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-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-07-13 03:40:48 +00:00
pub fn CreateInfo ( comptime T : type ) type {
const t_fields = std . meta . fields ( T ) ;
var fields : [ t_fields . len - 1 ] std . builtin . Type . StructField = undefined ;
var count = 0 ;
inline for ( t_fields ) | f | {
if ( std . mem . eql ( u8 , f . name , " id " ) ) continue ;
fields [ count ] = f ;
count + = 1 ;
}
return @Type ( . { . Struct = . {
. layout = . Auto ,
. fields = & fields ,
. decls = & [ 0 ] std . builtin . Type . Declaration { } ,
. is_tuple = false ,
} } ) ;
}
2022-07-13 04:16:33 +00:00
fn reify ( comptime T : type , id : Uuid , val : CreateInfo ( T ) ) T {
2022-07-13 03:40:48 +00:00
var result : T = undefined ;
result . id = id ;
inline for ( std . meta . fields ( CreateInfo ( T ) ) ) | f | {
@field ( result , f . name ) = @field ( val , f . name ) ;
}
return result ;
}
2022-07-26 02:07:05 +00:00
pub const NoteCreateInfo = struct {
2022-07-18 06:11:42 +00:00
content : [ ] const u8 ,
} ;
2022-08-02 04:33:23 +00:00
pub const Scheme = models . Community . Scheme ;
pub const CommunityCreateOptions = struct {
name : [ ] const u8 ,
host : [ ] const u8 ,
} ;
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-08-02 04:33:23 +00:00
community_host : ? [ ] 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-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-07-30 06:14:42 +00:00
pub fn init ( alloc : std . mem . Allocator , cfg : Config ) ! ApiSource {
2022-07-13 14:42:30 +00:00
var my_db = try db . Database . init ( ) ;
{
2022-09-05 07:03:31 +00:00
const row = try my_db . execRow2 (
& . { Uuid } ,
" SELECT id FROM user WHERE username = ? " ,
. { " heartles " } ,
null ,
) ;
std . log . debug ( " {s} " , . { row . ? [ 0 ] } ) ;
2022-07-13 14:42:30 +00:00
}
2022-07-26 02:07:05 +00:00
return ApiSource {
2022-07-13 14:42:30 +00:00
//.db = try db.Database.init(),
. db = my_db ,
2022-07-22 04:19:08 +00:00
. internal_alloc = alloc ,
2022-07-30 06:14:42 +00:00
. config = cfg ,
2022-07-13 03:40:48 +00:00
} ;
2022-07-17 23:21:03 +00:00
}
2022-08-02 06:35:56 +00:00
pub fn connectUnauthorized ( self : * ApiSource , host : ? [ ] const u8 , alloc : std . mem . Allocator ) ! Conn {
const community_id = blk : {
if ( host ) | h | {
2022-09-05 07:03:31 +00:00
const result = try self . db . execRow2 ( & . { Uuid } , " SELECT id FROM community WHERE host = ? " , . { h } , null ) ;
if ( result ) | r | break : blk r [ 0 ] ;
2022-08-02 06:35:56 +00:00
}
break : blk null ;
} ;
2022-07-26 02:07:05 +00:00
return Conn {
. db = self . db ,
. internal_alloc = self . internal_alloc ,
. as_user = null ,
2022-08-02 06:35:56 +00:00
. on_community = community_id ,
2022-07-26 02:07:05 +00:00
. arena = std . heap . ArenaAllocator . init ( alloc ) ,
} ;
}
2022-08-02 06:35:56 +00:00
pub fn connectToken ( self : * ApiSource , host : ? [ ] const u8 , token : [ ] const u8 , alloc : std . mem . Allocator ) ! Conn {
var conn = try self . connectUnauthorized ( host , alloc ) ;
errdefer conn . close ( ) ;
2022-07-25 00:04:44 +00:00
const decoded_len = std . base64 . standard . Decoder . calcSizeForSlice ( token ) catch return error . InvalidToken ;
if ( decoded_len ! = token_len ) return error . InvalidToken ;
2022-07-17 23:21:03 +00:00
2022-07-25 00:04:44 +00:00
var decoded : [ token_len ] u8 = undefined ;
std . base64 . standard . Decoder . decode ( & decoded , token ) catch return error . InvalidToken ;
2022-07-17 23:21:03 +00:00
2022-07-25 00:04:44 +00:00
var hash : models . ByteArray ( models . Token . hash_len ) = undefined ;
models . Token . HashFn . hash ( & decoded , & hash . data , . { } ) ;
2022-08-02 06:35:56 +00:00
const db_token = ( try self . db . getBy ( models . Token , . hash , hash , conn . arena . allocator ( ) ) ) orelse return error . InvalidToken ;
2022-09-05 07:03:31 +00:00
//const token_result = (try self.db.execRow2(
//&.{Uuid},
//"SELECT id FROM token WHERE hash = ?",
//.{hash},
//null,
//)) orelse return error.InvalidToken;
//conn.as_user = token_result[0];
2022-08-02 06:35:56 +00:00
conn . as_user = db_token . user_id ;
2022-07-17 23:21:03 +00:00
2022-08-02 06:35:56 +00:00
return conn ;
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
as_user : ? Uuid ,
2022-08-02 06:35:56 +00:00
on_community : ? 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-08-02 04:33:23 +00:00
fn getAuthenticatedUser ( self : * Self ) ! models . User {
if ( self . as_user ) | id | {
const user = try self . db . getBy ( models . User , . id , id , self . arena . allocator ( ) ) ;
if ( user = = null ) return error . NotAuthorized ;
return user . ? ;
} else {
return error . NotAuthorized ;
}
}
2022-07-30 05:46:00 +00:00
fn getAuthenticatedLocalUser ( self : * Self ) ! models . LocalUser {
2022-07-26 02:07:05 +00:00
if ( self . as_user ) | user_id | {
2022-07-30 05:46:00 +00:00
const local_user = try self . db . getBy ( models . LocalUser , . user_id , user_id , self . arena . allocator ( ) ) ;
if ( local_user = = null ) return error . NotAuthorized ;
2022-07-13 05:35:39 +00:00
2022-07-26 02:07:05 +00:00
return local_user . ? ;
} else {
2022-07-30 05:46:00 +00:00
return error . NotAuthorized ;
2022-07-26 02:07:05 +00:00
}
2022-07-16 19:30:47 +00:00
}
2022-07-13 04:56:47 +00:00
2022-07-26 02:07:05 +00:00
fn getAuthenticatedActor ( self : * Self ) ! models . Actor {
2022-07-30 05:46:00 +00:00
return if ( self . as_user ) | user_id |
( try self . db . getBy ( models . Actor , . user_id , user_id , self . arena . allocator ( ) ) ) orelse error . NotAuthorized
else
error . NotAuthorized ;
2022-07-22 04:19:08 +00:00
}
2022-07-26 02:07:05 +00:00
pub fn createNote ( self : * Self , info : NoteCreateInfo ) ! models . Note {
const id = Uuid . randV4 ( prng . random ( ) ) ;
2022-07-30 05:46:00 +00:00
const actor = try self . getAuthenticatedActor ( ) ;
2022-07-22 04:19:08 +00:00
2022-07-26 02:07:05 +00:00
const note = models . Note {
. id = id ,
2022-07-30 05:46:00 +00:00
. author_id = actor . user_id ,
2022-07-26 02:07:05 +00:00
. content = info . content ,
2022-07-22 04:19:08 +00:00
2022-07-26 02:07:05 +00:00
. created_at = DateTime . now ( ) ,
} ;
try self . db . insert ( models . Note , note ) ;
2022-07-22 04:19:08 +00:00
2022-07-26 02:07:05 +00:00
return note ;
}
2022-07-13 04:56:47 +00:00
2022-07-13 14:42:30 +00:00
//pub fn getNote(self: *Self, id: Uuid) !?models.Note {
pub fn getNote ( self : * Self , id : Uuid ) ! ? [ ] const u8 {
const row = try self . db . execRow ( " select content from note where id = ? " , . { id } , & . { [ ] const u8 } , self . arena . allocator ( ) ) ;
if ( row ) | results | {
return results [ 0 ] ;
} else return null ;
//return self.db.getBy(models.Note, .id, id, self.arena.allocator());
2022-07-26 02:07:05 +00:00
}
2022-07-24 05:19:32 +00:00
2022-07-30 05:46:00 +00:00
pub fn getActor ( self : * Self , user_id : Uuid ) ! ? models . Actor {
return self . db . getBy ( models . Actor , . user_id , user_id , self . arena . allocator ( ) ) ;
2022-07-26 02:07:05 +00:00
}
2022-07-25 00:04:44 +00:00
2022-07-26 02:07:05 +00:00
pub fn getActorByHandle ( self : * Self , handle : [ ] const u8 ) ! ? models . Actor {
2022-07-30 05:46:00 +00:00
const user = ( try self . db . getBy ( models . User , . username , handle , self . arena . allocator ( ) ) ) orelse return null ;
return self . db . getBy ( models . Actor , . user_id , user . id , self . arena . allocator ( ) ) ;
2022-07-26 02:07:05 +00:00
}
2022-07-24 05:19:32 +00:00
2022-07-26 02:07:05 +00:00
pub fn react ( self : * Self , note_id : Uuid ) ! void {
const id = Uuid . randV4 ( prng . random ( ) ) ;
2022-07-30 05:46:00 +00:00
const actor = try self . getAuthenticatedActor ( ) ;
try self . db . insert ( models . Reaction , . { . id = id , . note_id = note_id , . reactor_id = actor . user_id , . created_at = DateTime . now ( ) } ) ;
2022-07-26 02:07:05 +00:00
}
2022-07-24 05:19:32 +00:00
2022-07-26 02:07:05 +00:00
pub fn listReacts ( self : * Self , note_id : Uuid ) ! [ ] models . Reaction {
return try self . db . getWhereEq ( models . Reaction , . note_id , note_id , self . arena . allocator ( ) ) ;
}
2022-07-13 04:56:47 +00:00
2022-08-02 04:33:23 +00:00
pub fn createCommunity ( self : * Self , info : CommunityCreateOptions ) ! models . Community {
2022-08-02 05:18:54 +00:00
const scheme_len = firstIndexOf ( info . host , ':' ) orelse return error . InvalidHost ;
const scheme_str = info . host [ 0 . . scheme_len ] ;
const scheme = std . meta . stringToEnum ( models . Community . Scheme , scheme_str ) orelse return error . UnsupportedScheme ;
const host = blk : {
// host must be in the format "{scheme}://{host}"
if ( info . host . len < = scheme_len + ( " :// " ) . len or
info . host [ scheme_len ] ! = ':' or
info . host [ scheme_len + 1 ] ! = '/' or
info . host [ scheme_len + 2 ] ! = '/' ) return error . InvalidHost ;
const host = info . host [ scheme_len + 3 . . ] ;
// community cannot use non-default ports (except for testing)
// NOTE: Do not add, say localhost and localhost:80 or bugs may happen.
// Avoid using non-default ports unless a test can't be conducted without it.
if ( firstIndexOf ( host , ':' ) ! = null and builtin . mode ! = . Debug ) return error . InvalidHost ;
// community cannot be hosted on a path
if ( firstIndexOf ( host , '/' ) ! = null ) return error . InvalidHost ;
break : blk host ;
} ;
2022-08-02 04:33:23 +00:00
const id = Uuid . randV4 ( prng . random ( ) ) ;
const now = DateTime . now ( ) ;
// Require TLS on production builds
2022-08-02 05:18:54 +00:00
if ( scheme ! = . https and builtin . mode ! = . Debug ) return error . UnsupportedScheme ;
2022-08-02 04:33:23 +00:00
const community = models . Community {
. id = id ,
. created_at = now ,
. name = info . name ,
2022-08-02 05:18:54 +00:00
. host = host ,
. scheme = scheme ,
2022-08-02 04:33:23 +00:00
} ;
try self . db . insert ( models . Community , community ) ;
return community ;
}
pub fn getCommunity ( self : * Self , host : [ ] const u8 ) ! ? models . Community {
return try self . db . getBy ( models . Community , . host , host , self . arena . allocator ( ) ) ;
}
2022-07-26 02:07:05 +00:00
pub fn register ( self : * Self , info : RegistrationInfo ) ! models . Actor {
const user_id = Uuid . randV4 ( prng . random ( ) ) ;
2022-07-27 05:30:52 +00:00
// TODO: lock for transaction
2022-07-26 02:07:05 +00:00
2022-09-05 07:03:31 +00:00
// TODO: not community aware :(
if ( try self . db . execRow2 ( & . { } , " SELECT 1 FROM user WHERE username = ? " , . { info . username } , null ) ! = null ) {
//if (try self.db.existsWhereEq(models.User, .username, info.username)) {
2022-07-26 02:07:05 +00:00
return error . UsernameUnavailable ;
}
2022-07-27 05:30:52 +00:00
const now = DateTime . now ( ) ;
const invite_id = if ( info . invite_code ) | invite_code | blk : {
2022-09-05 07:03:31 +00:00
// TODO have this query also check for time-based expiration
const result = ( try self . db . execRow2 (
& . { Uuid , ? DateTime } ,
\\SELECT invite.id, invite.expires_at
\\FROM invite
\\ LEFT OUTER JOIN local_user ON invite.id = local_user.invite_id
\\WHERE invite.invite_code = ?
\\GROUP BY invite.id
\\HAVING
\\ (invite.max_uses IS NULL OR invite.max_uses > COUNT(local_user.user_id))
\\
,
. { invite_code } ,
null ,
) ) orelse return error . InvalidInvite ;
const expired = if ( result [ 1 ] ) | expires_at | now . seconds_since_epoch > expires_at . seconds_since_epoch else false ;
if ( expired ) return error . InvalidInvite ;
//const invite = (try self.db.getBy(models.Invite, .invite_code, invite_code, self.arena.allocator())) orelse return error.InvalidInvite;
//const invite = (try self.db.getBy(models.Invite, .invite_code, invite_code, self.arena.allocator())) orelse return error.InvalidInvite;
//const uses = try self.db.countWhereEq(models.LocalUser, .invite_id, invite.id);
//const uses_left = if (invite.max_uses) |max_uses| uses < max_uses else true;
//const expired = if (invite.expires_at) |expires_at| now.seconds_since_epoch > expires_at.seconds_since_epoch else false;
//if (!uses_left or expired) return error.InvalidInvite;
2022-07-27 05:30:52 +00:00
// TODO: increment uses
2022-09-05 07:03:31 +00:00
break : blk result [ 0 ] ;
2022-07-27 05:30:52 +00:00
} else null ;
2022-07-26 02:07:05 +00:00
// use internal alloc because necessary buffer is *big*
var buf : [ pw_hash_buf_size ] u8 = undefined ;
const hash = try PwHash . strHash ( info . password , . { . allocator = self . internal_alloc , . params = pw_hash_params , . encoding = pw_hash_encoding } , & buf ) ;
2022-08-02 04:33:23 +00:00
const community_id = if ( info . community_host ) | host | blk : {
2022-07-13 14:42:30 +00:00
//const id_tuple = (try self.db.execRow("select id from community where host = '?'", host, &.{Uuid}, self.arena.allocator())) orelse return error.CommunityNotFound;
2022-09-05 07:03:31 +00:00
const community_result = ( try self . db . execRow2 (
& . { Uuid } ,
" SELECT id FROM community WHERE host = ? " ,
. { host } ,
null ,
) ) orelse return error . CommunityNotFound ;
//const community = (try self.db.getBy(models.Community, .host, host, self.arena.allocator())) orelse return error.CommunityNotFound;
break : blk community_result [ 0 ] ;
2022-07-13 14:42:30 +00:00
//break :blk id_tuple[0];
2022-08-02 04:33:23 +00:00
} else null ;
2022-07-30 05:46:00 +00:00
const user = models . User {
2022-07-26 02:07:05 +00:00
. id = user_id ,
. username = info . username ,
2022-07-30 05:46:00 +00:00
. created_at = now ,
2022-08-02 04:33:23 +00:00
. community_id = community_id ,
2022-07-30 05:46:00 +00:00
} ;
const actor = models . Actor {
. user_id = user_id ,
. public_id = " abc " , // TODO
} ;
const local_user = models . LocalUser {
. user_id = user_id ,
2022-07-26 02:07:05 +00:00
. email = info . email ,
2022-07-27 05:30:52 +00:00
. invite_id = invite_id ,
2022-07-26 02:07:05 +00:00
. hashed_password = hash ,
. password_changed_at = now ,
} ;
2022-07-30 05:46:00 +00:00
try self . db . insert ( models . User , user ) ;
2022-07-26 02:07:05 +00:00
try self . db . insert ( models . Actor , actor ) ;
2022-07-30 05:46:00 +00:00
try self . db . insert ( models . LocalUser , local_user ) ;
2022-07-26 02:07:05 +00:00
return actor ;
}
2022-07-16 18:41:09 +00:00
2022-07-26 02:07:05 +00:00
pub fn login ( self : * Self , username : [ ] const u8 , password : [ ] const u8 ) ! LoginResult {
// TODO: This gives away the existence of a user through a timing side channel. is that acceptable?
2022-09-05 07:03:31 +00:00
//const user_info = (try self.db.getBy(models.User, .username, username, self.arena.allocator())) orelse return error.InvalidLogin;
//const local_user_info = (try self.db.getBy(models.LocalUser, .user_id, user_info.id, self.arena.allocator())) orelse return error.InvalidLogin;
const user_info = ( try self . db . execRow2 (
& . { Uuid , [ ] const u8 } ,
\\SELECT user.id, local_user.hashed_password
\\FROM user JOIN local_user ON local_user.user_id = user.id
\\WHERE user.username = ?
,
. { username } ,
self . arena . allocator ( ) ,
) ) orelse return error . InvalidLogin ;
const user_id = user_info [ 0 ] ;
const hashed_password = user_info [ 1 ] ;
2022-07-26 02:07:05 +00:00
//defer free(self.arena.allocator(), user_info);
2022-07-19 07:07:01 +00:00
2022-07-26 02:07:05 +00:00
const Hash = std . crypto . pwhash . scrypt ;
2022-09-05 07:03:31 +00:00
Hash . strVerify ( hashed_password , password , . { . allocator = self . internal_alloc } ) catch | err | switch ( err ) {
2022-07-26 02:07:05 +00:00
error . PasswordVerificationFailed = > return error . InvalidLogin ,
else = > return err ,
} ;
2022-07-21 05:26:13 +00:00
2022-09-05 07:03:31 +00:00
const token = try self . createToken ( user_id ) ;
2022-07-19 07:07:01 +00:00
2022-07-26 02:07:05 +00:00
var token_enc : [ token_str_len ] u8 = undefined ;
_ = std . base64 . standard . Encoder . encode ( & token_enc , & token . value ) ;
return LoginResult {
2022-09-05 07:03:31 +00:00
. user_id = user_id ,
2022-07-26 02:07:05 +00:00
. token = token_enc ,
. issued_at = token . info . issued_at ,
} ;
}
const TokenResult = struct {
info : models . Token ,
value : [ token_len ] u8 ,
} ;
2022-07-30 05:46:00 +00:00
fn createToken ( self : * Self , user_id : Uuid ) ! TokenResult {
2022-07-26 02:07:05 +00:00
var token : [ token_len ] u8 = undefined ;
std . crypto . random . bytes ( & token ) ;
var hash : [ models . Token . hash_len ] u8 = undefined ;
models . Token . HashFn . hash ( & token , & hash , . { } ) ;
const db_token = models . Token {
. id = Uuid . randV4 ( prng . random ( ) ) ,
. hash = . { . data = hash } ,
2022-07-30 05:46:00 +00:00
. user_id = user_id ,
2022-07-26 02:07:05 +00:00
. issued_at = DateTime . now ( ) ,
} ;
2022-09-05 07:03:31 +00:00
try self . db . insert2 ( " token " , db_token ) ;
2022-07-26 02:07:05 +00:00
return TokenResult {
. info = db_token ,
. value = token ,
} ;
}
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-05 07:03:31 +00:00
const desired_community = ( try self . db . execRow2 (
& . { 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 ;
}
pub fn getInvite ( self : * Self , id : Uuid ) ! ? models . Invite {
return self . db . getBy ( models . Invite , . id , id , self . arena . allocator ( ) ) ;
}
2022-07-26 02:07:05 +00:00
} ;
}