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-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-22 04:19:08 +00:00
pub const RegistrationInfo = struct {
username : [ ] const u8 ,
password : [ ] const u8 ,
email : ? [ ] const u8 ,
2022-07-27 05:30:52 +00:00
invite_code : ? [ ] const u8 ,
2022-07-22 04:19:08 +00:00
} ;
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-27 06:03:27 +00:00
pub const InviteOptions = struct {
name : [ ] const u8 = " " ,
max_uses : ? i64 = null ,
lifetime : ? i64 = null , // unix seconds, TODO make a TimeSpan type
2022-08-02 04:33:23 +00:00
to_community : ? [ ] const u8 ,
2022-07-27 06:03:27 +00:00
} ;
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-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 {
if ( self . community_id ! = null ) {
return error . NotAdminHost ;
}
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
pub fn createInvite ( self : * Self , options : InviteOptions ) ! models . Invite {
const id = Uuid . randV4 ( prng . random ( ) ) ;
2022-08-02 04:33:23 +00:00
const user = try self . getAuthenticatedUser ( ) ;
// Users can only make invites to their own community, unless they
// are system users
const community_id = if ( options . to_community ) | host | blk : {
2022-09-08 02:01:24 +00:00
const desired_community = ( try self . db . execRow (
2022-09-05 07:03:31 +00:00
& . { Uuid } ,
" SELECT id FROM community WHERE host = ? " ,
. { host } ,
null ,
) ) orelse return error . CommunityNotFound ;
if ( user . community_id ! = null and ! Uuid . eql ( desired_community [ 0 ] , user . community_id . ? ) ) {
2022-08-02 04:33:23 +00:00
return error . WrongCommunity ;
}
2022-09-05 07:03:31 +00:00
break : blk desired_community [ 0 ] ;
2022-08-02 04:33:23 +00:00
} else null ;
2022-09-05 07:03:31 +00:00
if ( user . community_id ! = null and community_id = = null ) {
2022-08-02 04:33:23 +00:00
return error . WrongCommunity ;
}
2022-07-27 05:02:09 +00:00
var code : [ invite_code_len ] u8 = undefined ;
std . crypto . random . bytes ( & code ) ;
var code_str = try self . arena . allocator ( ) . alloc ( u8 , invite_code_str_len ) ;
_ = std . base64 . url_safe . Encoder . encode ( code_str , & code ) ;
const now = DateTime . now ( ) ;
const expires_at = if ( options . lifetime ) | lifetime | DateTime {
. seconds_since_epoch = lifetime + now . seconds_since_epoch ,
} else null ;
const invite = models . Invite {
. id = id ,
. name = try self . arena . allocator ( ) . dupe ( u8 , options . name ) ,
2022-08-02 04:33:23 +00:00
. created_by = user . id ,
2022-07-27 05:02:09 +00:00
. invite_code = code_str ,
2022-08-02 04:33:23 +00:00
. to_community = community_id ,
2022-07-27 05:02:09 +00:00
. max_uses = options . max_uses ,
. created_at = now ,
. expires_at = expires_at ,
} ;
try self . db . insert ( models . Invite , invite ) ;
return invite ;
}
2022-07-26 02:07:05 +00:00
} ;
}