summaryrefslogtreecommitdiff
path: root/backend/api/src/userdefs.zig
diff options
context:
space:
mode:
Diffstat (limited to 'backend/api/src/userdefs.zig')
-rw-r--r--backend/api/src/userdefs.zig322
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;
+}