diff options
Diffstat (limited to 'backend/api/src/userdefs.zig')
| -rw-r--r-- | backend/api/src/userdefs.zig | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/backend/api/src/userdefs.zig b/backend/api/src/userdefs.zig new file mode 100644 index 0000000..e43b680 --- /dev/null +++ b/backend/api/src/userdefs.zig @@ -0,0 +1,322 @@ +const z = @import( "std" ); +const u = struct { + usingnamespace @import( "util.zig" ); + usingnamespace @import( "user.zig" ); +}; +const db = @import( "db.zig" ); +const net = @import( "net-util.zig" ); +const zap = @import( "zap" ); +const jwt = @import( "jwt" ); + +const alloc = u.alloc; +const memeql = z.mem.eql; + +pub const ArenaAllocator = z.heap.ArenaAllocator; +pub const ErrorRes = net.ErrorResponse; +pub const Status = zap.StatusCode; +pub const ArrayList = z.ArrayList; +pub const Request = zap.Request; +pub const JWT = jwt.JWT; + +pub threadlocal var dbi: *db.Interface = undefined; + +pub const LoginToken = struct { + uuid: []const u8, + exp: i64, + + pub const @"getty.db" = u.@"json.ignore.unknown"; +}; + +pub const AuthToken = struct { + uuid: []const u8, + email: []const u8, + exp: i64, + iat: i64, + + pub const @"getty.db" = u.@"json.ignore.unknown"; +}; + +pub const ApiToken = struct { + uuid: []const u8, + exp: i64, + iat: i64, + + pub const @"getty.db" = u.@"json.ignore.unknown"; +}; + +pub const SubData = struct { + endTime: i64, + plan: []const u8, + reminded: ?bool = false +}; + +///layout must follow database structure +pub fn UserEntryTemplate( comptime token_type: type, comptime sub_type: type ) type { + return struct { + uuid: []const u8, + email: []const u8, + tokens: token_type, + login_token: []const u8, + token_resetdate: i64, + api_tokens: token_type, + created_at: i64, + subscription_data: sub_type + }; +} + +pub const UserDbEntry = UserEntryTemplate( []const u8, []const u8 ); +pub const UserEntry = u.MergeTypes( + UserEntryTemplate( [][]const u8, SubData ), + struct { db_entry: ?db.Row(UserDbEntry) = null } +); + +///row should not create any new allocations after this call +pub fn dbToUser( row: *db.Row(UserDbEntry) ) !UserEntry { + const a = row.alloc.allocator(); + const tokens = try u.jsonParseAlloc( [][]const u8, row.v.tokens, a ); + const api_tokens = try u.jsonParseAlloc( [][]const u8, row.v.api_tokens, a ); + const sub_data = try u.jsonParseAlloc( SubData, row.v.subscription_data, a ); + + var ret: UserEntry = undefined; + const ret_slice= @as( *[ @sizeOf(@TypeOf(row.v)) ]u8, @ptrCast( &ret ) ); + const row_slice= @as( *const [ @sizeOf(@TypeOf(row.v)) ]u8, @ptrCast( &row.v ) ); + @memcpy( ret_slice, row_slice ); + + ret.tokens = tokens.v; + ret.api_tokens = api_tokens.v; + ret.subscription_data = sub_data.v; + ret.db_entry = row.*; + return ret; +} + +pub fn userToDb( entry: UserEntry, arena: *ArenaAllocator ) !UserDbEntry { + const a = arena.allocator(); + + const tokens = try u.jsonStringifyAlloc( entry.tokens, a ); + const api_tokens = try u.jsonStringifyAlloc( entry.api_tokens, a ); + const sub_data = try u.jsonStringifyAlloc( entry.subscription_data, a ); + + var ret: UserDbEntry = undefined; + const ret_slice = @as( *[ @sizeOf(@TypeOf(ret)) ]u8, @ptrCast( &ret ) ); + const entry_slice = @as( *const[ @sizeOf(@TypeOf(ret)) ]u8, @ptrCast( &entry ) ); + @memcpy( ret_slice, entry_slice ); + + ret.tokens = tokens; + ret.api_tokens = api_tokens; + ret.subscription_data = sub_data; + return ret; +} + +pub fn entryToRow( entry: UserDbEntry, arena: *ArenaAllocator ) !db.Row( UserDbEntry ) { + var row: db.Row( UserDbEntry ) = undefined; + row.alloc = arena.*; + row.v = entry; + + const a = arena.allocator(); + + row.v.uuid = try a.dupe( u8, entry.uuid ); + row.v.email = try a.dupe( u8, entry.email ); + row.v.tokens = try a.dupe( u8, entry.tokens ); + row.v.api_tokens = try a.dupe( u8, entry.api_tokens ); + row.v.login_token = try a.dupe( u8, entry.login_token ); + row.v.subscription_data = try a.dupe( u8, entry.subscription_data ); + + return row; +} + +///freed by caller +pub fn generateApiToken( uuid: []const u8 ) ![]const u8 { + return net.encodeJWT( ApiToken{ + .uuid = uuid, + .exp = z.time.timestamp() + 10 * 365 * 24 * 60 * 60, // 10 years + .iat = z.time.timestamp(), + } ); +} + +///freed by caller +pub fn generateLoginToken( uuid: []const u8 ) ![]const u8 { + return net.encodeJWT( LoginToken{ + .uuid = uuid, + .exp = z.time.timestamp() + 120 * 60, // 2 hours + } ); +} + +///freed by caller +pub fn generateAuthToken( uuid: []const u8, email: []const u8 ) ![]const u8 { + return net.encodeJWT( AuthToken{ + .uuid = uuid, + .email = email, + .exp = z.time.timestamp() + 30 * 24 * 60 * 60, // 30 days + .iat = z.time.timestamp(), + } ); +} + +///returns parsed token +pub fn verifyToken( comptime t: type, token: []const u8 ) !JWT(t) { + comptime if( !@hasField( t, "uuid" ) ) + return error.InvalidTokenType; + + var decoded = try net.parseJWT( t, token ); + errdefer decoded.deinit(); + const uuid = decoded.claims.uuid; + + if( @hasField( t, "exp" ) ) { + const exp = @field( decoded.claims, "exp" ); + if( exp < z.time.timestamp() ) + return error.TokenExpired; + } + if( @hasField( t, "iat" ) ) { //sqlite is so fast that doing this probably wont matter much + const iat = decoded.claims.iat; + const user = u.getEntry( uuid ) catch { + return error.InvalidToken; + }; defer user.db_entry.?.deinit(); + + if( iat < user.token_resetdate ) + return error.TokenExpired; + + for( user.tokens ) |tok| { + if( memeql( u8, token, tok ) ) + return decoded; + } + + for( user.api_tokens ) |tok| { + if( memeql( u8, token, tok ) ) + return decoded; + } + + return error.InvalidToken; + } + + return decoded; +} + +///sends an invalid response if the token is invalid +pub fn tokenFromGetReq( r: Request ) !JWT(LoginToken) { + var token = r.getParamStr( alloc, "token", false ) catch { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "token invalid or expired" } ); + return error.MissingToken; + } orelse { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "token missing" } ); + return error.MissingToken; + }; defer token.deinit(); + + const decoded = verifyToken( LoginToken, token.str ) catch { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "token invalid or expired" } ); + return error.TokenExpired; + }; + + return decoded; +} + +///sends an invalid response if the token is invalid +pub fn tokenFromPostReq( r: Request ) !JWT(AuthToken) { + if( r.body == null ) { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "body missing" } ); + return error.MissingToken; + } + + const json = u.jsonParse( struct{ + token: []const u8, + pub const @"getty.db" = u.@"json.ignore.unknown"; + }, r.body.? ) catch |e| { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid json" } ); + return e; + }; defer json.deinit(); + + const token = json.v.token; + const decoded = verifyToken( AuthToken, token ) catch { + net.sendJson( r, .unauthorized, ErrorRes{ .msg = "invalid token" } ); + return error.Invalidtoken; + }; + + return decoded; +} + +///sends an invalid response if the token is invalid +pub fn apiTokenFromGetReq( r: Request ) !JWT(ApiToken) { + var token = r.getParamStr( alloc, "token", false ) catch { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "token invalid or expired" } ); + return error.MissingToken; + } orelse { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "token missing" } ); + return error.MissingToken; + }; defer token.deinit(); + + const decoded = verifyToken( ApiToken, token.str ) catch { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "token invalid or expired" } ); + return error.TokenExpired; + }; + + return decoded; +} + +///sends an invalid response if the token is invalid +pub fn apiTokenFromPostReq( r: Request ) !JWT(ApiToken) { + if( r.body == null ) { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "body missing" } ); + return error.MissingToken; + } + + const json = u.jsonParse( struct{ + token: []const u8, + pub const @"getty.db" = u.@"json.ignore.unknown"; + }, r.body.? ) catch |e| { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid json" } ); + return e; + }; defer json.deinit(); + + const token = json.v.token; + const decoded = verifyToken( ApiToken, token ) catch { + net.sendJson( r, .unauthorized, ErrorRes{ .msg = "invalid token" } ); + return error.Invalidtoken; + }; + + return decoded; +} + +///returns the uuid from either the auth token or api token, depending on provided. +///for use with api-accessible routes e.g. chat, generate. +///returned string must be freed by caller +///sends an invalid response if the token is invalid +pub fn uuidFromApiOrAuthToken( r: Request ) ![]const u8 { + if( r.body == null ) { + net.sendJson( r, .bad_request, ErrorRes{ .msg = "body missing" } ); + return error.InvalidToken; + } + + const TokenRes = struct { + token: []const u8, + pub const @"getty.db" = u.@"json.ignore.unknown"; + }; + + const auth_token: ?u.JsonResponse(TokenRes) = u.jsonParse( TokenRes, r.body.? ) catch |e| br: { + z.debug.print( "error parsing auth token: {s} {any} {any}\n", .{ r.body.?, e, @errorReturnTrace() } ); + break :br null; + }; + if( auth_token ) |token| { + defer token.deinit(); + var decoded: ?JWT(ApiToken) = verifyToken( ApiToken, token.v.token ) catch |e| br: { + z.debug.print( "error verifying auth token: {s} {any} {any}\n", .{ r.body.?, e, @errorReturnTrace() } ); + break :br null; + }; + if( decoded ) |*d| { // da d + defer d.deinit(); + return alloc.dupe( u8, d.claims.uuid ) catch return ""; + } + } + + net.sendJson( r, .unauthorized, ErrorRes{ .msg = "token missing or expired" } ); + return error.InvalidToken; +} + +///checks if the db is initialized on current thread +///if not initializes it +pub fn checkDbOnThread() !void { + db.init( "../data/users.sqlite" ) catch |e| { + if( e == error.DbiAlreadyInitialized ) + return; + return e; + }; + + dbi = &db.dbi; +} |
