2022-07-13 03:40:48 +00:00
const std = @import ( " std " ) ;
2022-07-13 04:16:33 +00:00
const util = @import ( " util " ) ;
2022-08-02 04:33:23 +00:00
const builtin = @import ( " builtin " ) ;
2022-07-13 03:40:48 +00:00
const db = @import ( " ./db.zig " ) ;
2022-07-24 09:01:17 +00:00
const models = @import ( " ./db/models.zig " ) ;
2022-07-18 07:37:10 +00:00
pub const DateTime = util . DateTime ;
2022-07-13 04:16:33 +00:00
pub const Uuid = util . Uuid ;
2022-07-30 06:14:42 +00:00
const Config = @import ( " ./main.zig " ) . Config ;
2022-07-13 03:40:48 +00:00
2022-07-22 04:19:08 +00:00
const PwHash = std . crypto . pwhash . scrypt ;
const pw_hash_params = PwHash . Params . interactive ;
const pw_hash_encoding = . phc ;
const pw_hash_buf_size = 128 ;
2022-07-24 05:19:32 +00:00
const token_len = 20 ;
2022-07-25 00:04:44 +00:00
const token_str_len = std . base64 . standard . Encoder . calcSize ( token_len ) ;
2022-07-24 05:19:32 +00:00
2022-07-27 05:02:09 +00:00
const invite_code_len = 16 ;
const invite_code_str_len = std . base64 . url_safe . Encoder . calcSize ( invite_code_len ) ;
2022-09-07 23:14:52 +00:00
const services = struct {
const communities = @import ( " ./api/communities.zig " ) ;
const users = @import ( " ./api/users.zig " ) ;
const auth = @import ( " ./api/auth.zig " ) ;
2022-09-08 05:10:58 +00:00
const invites = @import ( " ./api/invites.zig " ) ;
} ;
2022-09-08 06:56:29 +00:00
pub const RegistrationRequest = struct {
username : [ ] const u8 ,
password : [ ] const u8 ,
invite_code : [ ] const u8 ,
email : ? [ ] const u8 ,
} ;
2022-09-08 05:10:58 +00:00
pub const InviteRequest = struct {
pub const Type = services . invites . InviteType ;
name : ? [ ] const u8 = null ,
expires_at : ? DateTime = null , // TODO: Change this to lifespan
max_uses : ? usize = null ,
invite_type : Type = . user , // must be user unless the creator is an admin
to_community : ? [ ] const u8 = null , // only valid on admin community
2022-09-07 23:14:52 +00:00
} ;
2022-07-16 18:44:46 +00:00
// Frees an api struct and its fields allocated from alloc
pub fn free ( alloc : std . mem . Allocator , val : anytype ) void {
2022-07-19 09:22:19 +00:00
switch ( @typeInfo ( @TypeOf ( val ) ) ) {
. Pointer = > | ptr_info | switch ( ptr_info . size ) {
. One = > {
free ( alloc , val . * ) ;
alloc . destroy ( val ) ;
} ,
. Slice = > {
for ( val ) | elem | free ( alloc , elem ) ;
alloc . free ( val ) ;
} ,
else = > unreachable ,
} ,
. Struct = > inline for ( std . meta . fields ( @TypeOf ( val ) ) ) | f | free ( alloc , @field ( val , f . name ) ) ,
. Array = > for ( val ) | elem | free ( alloc , elem ) ,
. Optional = > if ( val ) | opt | free ( alloc , opt ) ,
. Bool , . Int , . Float , . Enum = > { } ,
else = > unreachable ,
2022-07-16 18:44:46 +00:00
}
}
2022-08-02 05:18:54 +00:00
pub fn firstIndexOf ( str : [ ] const u8 , ch : u8 ) ? usize {
for ( str ) | c , i | {
if ( c = = ch ) return i ;
}
return null ;
}
2022-08-02 04:33:23 +00:00
pub const Scheme = models . Community . Scheme ;
2022-07-26 02:07:05 +00:00
pub const LoginResult = struct {
user_id : Uuid ,
token : [ token_str_len ] u8 ,
issued_at : DateTime ,
} ;
2022-07-25 00:18:25 +00:00
threadlocal var prng : std . rand . DefaultPrng = undefined ;
pub fn initThreadPrng ( seed : u64 ) void {
prng = std . rand . DefaultPrng . init ( seed + % std . Thread . getCurrentId ( ) ) ;
}
2022-09-07 23:14:52 +00:00
pub fn getRandom ( ) std . rand . Random {
return prng . random ( ) ;
}
2022-09-05 08:52:49 +00:00
// Returned slice points into buf
fn hashPassword ( password : [ ] const u8 , alloc : std . mem . Allocator , buf : * [ pw_hash_buf_size ] u8 ) ! [ ] const u8 {
return PwHash . strHash ( password , . { . allocator = alloc , . params = pw_hash_params , . encoding = pw_hash_encoding } , buf ) ;
}
2022-07-26 02:07:05 +00:00
pub const ApiSource = struct {
2022-07-15 07:27:27 +00:00
db : db . Database ,
2022-07-22 04:19:08 +00:00
internal_alloc : std . mem . Allocator ,
2022-07-30 06:14:42 +00:00
config : Config ,
2022-07-15 00:58:08 +00:00
2022-07-26 02:07:05 +00:00
pub const Conn = ApiConn ( db . Database ) ;
2022-09-05 08:52:49 +00:00
const root_username = " root " ;
const root_password_envvar = " CLUSTER_ROOT_PASSWORD " ;
2022-07-30 06:14:42 +00:00
pub fn init ( alloc : std . mem . Allocator , cfg : Config ) ! ApiSource {
2022-09-05 08:52:49 +00:00
var self = ApiSource {
. db = try db . Database . init ( ) ,
. internal_alloc = alloc ,
. config = cfg ,
} ;
2022-07-13 14:42:30 +00:00
2022-09-07 23:14:52 +00:00
if ( ( try services . users . lookupByUsername ( & self . db , root_username , null ) ) = = null ) {
2022-09-05 08:52:49 +00:00
std . log . info ( " No cluster root user detected. Creating... " , . { } ) ;
const root_password = std . os . getenv ( root_password_envvar ) orelse {
std . log . err (
2022-09-07 23:14:52 +00:00
" No root user created and no password specified. Please provide the password for the root user by the ${s} environment variable for initial startup. This only needs to be done once " ,
2022-09-05 08:52:49 +00:00
. { root_password_envvar } ,
) ;
@panic ( " No root password provided " ) ;
} ;
2022-09-05 07:03:31 +00:00
2022-09-07 23:14:52 +00:00
_ = try services . users . create ( & self . db , root_username , root_password , null , . { } , alloc ) ;
2022-07-13 14:42:30 +00:00
}
2022-09-05 08:52:49 +00:00
return self ;
2022-07-17 23:21:03 +00:00
}
2022-09-07 23:14:52 +00:00
fn getCommunityFromHost ( self : * ApiSource , host : [ ] const u8 ) ! ? Uuid {
2022-09-08 02:01:24 +00:00
if ( try self . db . execRow (
2022-09-05 10:33:54 +00:00
& . { Uuid } ,
" SELECT id FROM community WHERE host = ? " ,
. { host } ,
null ,
2022-09-07 23:14:52 +00:00
) ) | result | return result [ 0 ] ;
// Test for cluster admin community
if ( util . ciutf8 . eql ( self . config . cluster_host , host ) ) {
return null ;
}
return error . NoCommunity ;
}
pub fn connectUnauthorized ( self : * ApiSource , host : [ ] const u8 , alloc : std . mem . Allocator ) ! Conn {
const community_id = try self . getCommunityFromHost ( host ) ;
2022-09-05 08:52:49 +00:00
2022-07-26 02:07:05 +00:00
return Conn {
. db = self . db ,
. internal_alloc = self . internal_alloc ,
2022-09-07 23:14:52 +00:00
. user_id = null ,
. community_id = community_id ,
2022-07-26 02:07:05 +00:00
. arena = std . heap . ArenaAllocator . init ( alloc ) ,
} ;
}
2022-09-05 08:52:49 +00:00
pub fn connectToken ( self : * ApiSource , host : [ ] const u8 , token : [ ] const u8 , alloc : std . mem . Allocator ) ! Conn {
2022-09-07 23:14:52 +00:00
const community_id = try self . getCommunityFromHost ( host ) ;
2022-07-17 23:21:03 +00:00
2022-09-07 23:14:52 +00:00
const token_info = try services . auth . tokens . verify ( & self . db , token , community_id ) ;
2022-07-17 23:21:03 +00:00
2022-09-07 23:14:52 +00:00
return Conn {
. db = self . db ,
. internal_alloc = self . internal_alloc ,
. user_id = token_info . user_id ,
. community_id = community_id ,
. arena = std . heap . ArenaAllocator . init ( alloc ) ,
} ;
2022-07-13 03:40:48 +00:00
}
2022-07-26 02:07:05 +00:00
} ;
2022-07-13 03:40:48 +00:00
2022-07-26 02:07:05 +00:00
fn ApiConn ( comptime DbConn : type ) type {
return struct {
const Self = @This ( ) ;
2022-07-18 07:37:10 +00:00
2022-07-26 02:07:05 +00:00
db : DbConn ,
internal_alloc : std . mem . Allocator , // used *only* for large, internal buffers
2022-09-07 23:14:52 +00:00
user_id : ? Uuid ,
community_id : ? Uuid ,
2022-07-26 02:07:05 +00:00
arena : std . heap . ArenaAllocator ,
2022-07-18 06:11:42 +00:00
2022-07-26 02:07:05 +00:00
pub fn close ( self : * Self ) void {
self . arena . deinit ( ) ;
}
2022-07-18 06:11:42 +00:00
2022-09-08 05:10:58 +00:00
fn isAdmin ( self : * Self ) bool {
// TODO
return self . user_id ! = null and self . community_id = = null ;
}
2022-07-26 02:07:05 +00:00
pub fn login ( self : * Self , username : [ ] const u8 , password : [ ] const u8 ) ! LoginResult {
2022-09-07 23:14:52 +00:00
const user_id = ( try services . users . lookupByUsername ( & self . db , username , self . community_id ) ) orelse return error . InvalidLogin ;
try services . auth . passwords . verify ( & self . db , user_id , password , self . internal_alloc ) ;
2022-07-21 05:26:13 +00:00
2022-09-07 23:14:52 +00:00
const token = try services . auth . tokens . create ( & self . db , user_id ) ;
2022-07-26 02:07:05 +00:00
return LoginResult {
2022-09-05 07:03:31 +00:00
. user_id = user_id ,
2022-09-07 23:14:52 +00:00
. token = token . value ,
2022-07-26 02:07:05 +00:00
. issued_at = token . info . issued_at ,
} ;
}
2022-09-05 10:33:54 +00:00
const TokenInfo = struct {
username : [ ] const u8 ,
} ;
pub fn getTokenInfo ( self : * Self ) ! TokenInfo {
2022-09-07 23:14:52 +00:00
if ( self . user_id ) | user_id | {
2022-09-08 02:01:24 +00:00
const result = ( try self . db . execRow (
2022-09-05 10:33:54 +00:00
& . { [ ] const u8 } ,
" SELECT username FROM user WHERE id = ? " ,
. { user_id } ,
self . arena . allocator ( ) ,
) ) orelse {
return error . UserNotFound ;
} ;
return TokenInfo { . username = result [ 0 ] } ;
}
return error . Unauthorized ;
}
2022-09-07 23:14:52 +00:00
pub fn createCommunity ( self : * Self , origin : [ ] const u8 ) ! services . communities . Community {
2022-09-08 05:10:58 +00:00
if ( ! self . isAdmin ( ) ) {
return error . PermissionDenied ;
2022-09-07 23:14:52 +00:00
}
2022-07-26 02:07:05 +00:00
2022-09-07 23:14:52 +00:00
return services . communities . create ( & self . db , origin , null ) ;
2022-07-26 02:07:05 +00:00
}
2022-07-27 05:02:09 +00:00
2022-09-08 05:10:58 +00:00
pub fn createInvite ( self : * Self , options : InviteRequest ) ! services . invites . Invite {
// Only logged in users can make invites
const user_id = self . user_id orelse return error . PermissionDenied ;
2022-07-27 05:02:09 +00:00
2022-08-02 04:33:23 +00:00
const community_id = if ( options . to_community ) | host | blk : {
2022-09-08 05:10:58 +00:00
// You can only specify a different community if you're on the admin domain
if ( self . community_id ! = null ) return error . WrongCommunity ;
2022-07-27 05:02:09 +00:00
2022-09-08 05:10:58 +00:00
// Only admins can invite on the admin domain
if ( ! self . isAdmin ( ) ) return error . PermissionDenied ;
2022-07-27 05:02:09 +00:00
2022-09-08 05:10:58 +00:00
break : blk ( try services . communities . getByHost ( & self . db , host , self . arena . allocator ( ) ) ) . id ;
} else self . community_id ;
2022-07-27 05:02:09 +00:00
2022-09-08 05:10:58 +00:00
// Users can only make user invites
if ( options . invite_type ! = . user and ! self . isAdmin ( ) ) return error . PermissionDenied ;
2022-07-27 05:02:09 +00:00
2022-09-08 05:10:58 +00:00
return try services . invites . create ( & self . db , user_id , community_id , . {
. name = options . name ,
. expires_at = options . expires_at ,
2022-07-27 05:02:09 +00:00
. max_uses = options . max_uses ,
2022-09-08 05:10:58 +00:00
. invite_type = options . invite_type ,
} , self . arena . allocator ( ) ) ;
2022-07-27 05:02:09 +00:00
}
2022-09-08 06:56:29 +00:00
pub fn register ( self : * Self , request : RegistrationRequest ) ! services . users . User {
std . log . debug ( " registering user {s} with code {s} " , . { request . username , request . invite_code } ) ;
const invite = try services . invites . getByCode ( & self . db , request . invite_code , self . arena . allocator ( ) ) ;
if ( ! Uuid . eql ( invite . to_community , self . community_id ) ) return error . NotFound ;
if ( invite . max_uses ! = null and invite . times_used > = invite . max_uses . ? ) return error . InviteExpired ;
if ( invite . expires_at ! = null and DateTime . now ( ) . isAfter ( invite . expires_at . ? ) ) return error . InviteExpired ;
if ( self . community_id = = null ) @panic ( " Unimplmented " ) ;
const user_id = try services . users . create ( & self . db , request . username , request . password , self . community_id , . { . invite_id = invite . id , . email = request . email } , self . internal_alloc ) ;
switch ( invite . invite_type ) {
. user = > { } ,
. system = > @panic ( " System user invites unimplemented " ) ,
. community_owner = > {
try services . communities . transferOwnership ( & self . db , self . community_id . ? , user_id ) ;
} ,
}
return services . users . get ( & self . db , user_id , self . arena . allocator ( ) ) catch | err | switch ( err ) {
error . NotFound = > error . Unexpected ,
else = > err ,
} ;
}
2022-07-26 02:07:05 +00:00
} ;
}