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; }