@rebasepro/server-core 0.0.1-canary.4d4fb3e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -0
- package/README.md +40 -0
- package/build-errors.txt +52 -0
- package/coverage/clover.xml +3739 -0
- package/coverage/coverage-final.json +31 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +266 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
- package/coverage/lcov-report/src/api/errors.ts.html +472 -0
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
- package/coverage/lcov-report/src/api/graphql/index.html +116 -0
- package/coverage/lcov-report/src/api/index.html +176 -0
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
- package/coverage/lcov-report/src/api/rest/index.html +131 -0
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
- package/coverage/lcov-report/src/api/server.ts.html +823 -0
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
- package/coverage/lcov-report/src/auth/index.html +176 -0
- package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
- package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
- package/coverage/lcov-report/src/auth/password.ts.html +310 -0
- package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
- package/coverage/lcov-report/src/collections/index.html +116 -0
- package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
- package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
- package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
- package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
- package/coverage/lcov-report/src/db/index.html +146 -0
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
- package/coverage/lcov-report/src/db/services/index.html +176 -0
- package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
- package/coverage/lcov-report/src/services/index.html +131 -0
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
- package/coverage/lcov-report/src/storage/index.html +161 -0
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
- package/coverage/lcov-report/src/storage/types.ts.html +451 -0
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov.info +7179 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +36 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-BeMqpmfQ.js +239 -0
- package/dist/index-BeMqpmfQ.js.map +1 -0
- package/dist/index-bl4J3lNb.js +55823 -0
- package/dist/index-bl4J3lNb.js.map +1 -0
- package/dist/index.es.js +58 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +56062 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
- package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/server-core/src/api/errors.d.ts +35 -0
- package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
- package/dist/server-core/src/api/graphql/index.d.ts +1 -0
- package/dist/server-core/src/api/index.d.ts +9 -0
- package/dist/server-core/src/api/openapi-generator.d.ts +2 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
- package/dist/server-core/src/api/rest/index.d.ts +1 -0
- package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
- package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
- package/dist/server-core/src/api/server.d.ts +40 -0
- package/dist/server-core/src/api/types.d.ts +90 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +7 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +20 -0
- package/dist/server-core/src/auth/index.d.ts +12 -0
- package/dist/server-core/src/auth/interfaces.d.ts +270 -0
- package/dist/server-core/src/auth/jwt.d.ts +42 -0
- package/dist/server-core/src/auth/middleware.d.ts +56 -0
- package/dist/server-core/src/auth/password.d.ts +22 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +17 -0
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/server-core/src/collections/loader.d.ts +5 -0
- package/dist/server-core/src/db/interfaces.d.ts +18 -0
- package/dist/server-core/src/email/index.d.ts +6 -0
- package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
- package/dist/server-core/src/email/templates.d.ts +33 -0
- package/dist/server-core/src/email/types.d.ts +110 -0
- package/dist/server-core/src/functions/function-loader.d.ts +17 -0
- package/dist/server-core/src/functions/function-routes.d.ts +10 -0
- package/dist/server-core/src/functions/index.d.ts +3 -0
- package/dist/server-core/src/history/history-routes.d.ts +23 -0
- package/dist/server-core/src/history/index.d.ts +1 -0
- package/dist/server-core/src/index.d.ts +24 -0
- package/dist/server-core/src/init.d.ts +49 -0
- package/dist/server-core/src/serve-spa.d.ts +30 -0
- package/dist/server-core/src/services/driver-registry.d.ts +78 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
- package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
- package/dist/server-core/src/storage/index.d.ts +18 -0
- package/dist/server-core/src/storage/routes.d.ts +38 -0
- package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
- package/dist/server-core/src/storage/types.d.ts +91 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -0
- package/dist/server-core/src/utils/sql.d.ts +27 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/index.d.ts +17 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +9 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +22 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/history_diff.log +385 -0
- package/jest.config.cjs +16 -0
- package/package.json +86 -0
- package/scratch.ts +8 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +57 -0
- package/src/api/errors.ts +155 -0
- package/src/api/graphql/graphql-schema-generator.ts +334 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +160 -0
- package/src/api/rest/api-generator.ts +466 -0
- package/src/api/rest/index.ts +2 -0
- package/src/api/rest/query-parser.ts +155 -0
- package/src/api/schema-editor-routes.ts +39 -0
- package/src/api/server.ts +245 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +488 -0
- package/src/auth/google-oauth.ts +60 -0
- package/src/auth/index.ts +21 -0
- package/src/auth/interfaces.ts +316 -0
- package/src/auth/jwt.ts +164 -0
- package/src/auth/middleware.ts +235 -0
- package/src/auth/password.ts +75 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +730 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +17 -0
- package/src/email/smtp-email-service.ts +88 -0
- package/src/email/templates.ts +301 -0
- package/src/email/types.ts +112 -0
- package/src/functions/function-loader.ts +91 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +128 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +56 -0
- package/src/init.ts +309 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/storage/LocalStorageController.ts +368 -0
- package/src/storage/S3StorageController.ts +295 -0
- package/src/storage/index.ts +32 -0
- package/src/storage/routes.ts +247 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +122 -0
- package/src/types/index.ts +27 -0
- package/src/utils/logging.ts +35 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +591 -0
- package/test/api-generator.test.ts +458 -0
- package/test/ast-schema-editor.test.ts +61 -0
- package/test/auth-middleware-hono.test.ts +321 -0
- package/test/auth-routes.test.ts +868 -0
- package/test/driver-registry.test.ts +280 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +150 -0
- package/test/jwt-security.test.ts +173 -0
- package/test/jwt.test.ts +311 -0
- package/test/middleware.test.ts +295 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +258 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/storage-local.test.ts +278 -0
- package/test/storage-registry.test.ts +280 -0
- package/test/storage-routes.test.ts +218 -0
- package/test/storage-s3.test.ts +301 -0
- package/test-ast.ts +28 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +78 -0
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +46 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { ApiError } from "../api/errors";
|
|
3
|
+
import { randomBytes, createHash } from "crypto";
|
|
4
|
+
import type { AuthRepository } from "./interfaces";
|
|
5
|
+
import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
|
|
6
|
+
import { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
|
|
7
|
+
import { verifyGoogleIdToken, isGoogleOAuthConfigured } from "./google-oauth";
|
|
8
|
+
import { requireAuth } from "./middleware";
|
|
9
|
+
import { EmailService, EmailConfig } from "../email";
|
|
10
|
+
import { getPasswordResetTemplate, getEmailVerificationTemplate } from "../email/templates";
|
|
11
|
+
import { HonoEnv } from "../api/types";
|
|
12
|
+
import { defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shared configuration for auth and admin route factories.
|
|
17
|
+
*/
|
|
18
|
+
export interface AuthModuleConfig {
|
|
19
|
+
authRepo: AuthRepository;
|
|
20
|
+
emailService?: EmailService;
|
|
21
|
+
emailConfig?: EmailConfig;
|
|
22
|
+
/** Allow new user registration (default: false). First user can always register for bootstrap. */
|
|
23
|
+
allowRegistration?: boolean;
|
|
24
|
+
/** Default role ID to assign to new users (default: none). The first user always gets "admin". */
|
|
25
|
+
defaultRole?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Helper to build standard auth response output
|
|
30
|
+
*/
|
|
31
|
+
function buildAuthResponse(
|
|
32
|
+
user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null },
|
|
33
|
+
roleIds: string[],
|
|
34
|
+
accessToken: string,
|
|
35
|
+
refreshToken: string
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
user: {
|
|
39
|
+
uid: user.id,
|
|
40
|
+
email: user.email,
|
|
41
|
+
displayName: user.displayName ?? null,
|
|
42
|
+
photoURL: user.photoUrl ?? null,
|
|
43
|
+
roles: roleIds
|
|
44
|
+
},
|
|
45
|
+
tokens: {
|
|
46
|
+
accessToken,
|
|
47
|
+
refreshToken,
|
|
48
|
+
accessTokenExpiresAt: getAccessTokenExpiry()
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate a secure random token
|
|
55
|
+
*/
|
|
56
|
+
function generateSecureToken(): string {
|
|
57
|
+
return randomBytes(40).toString("hex");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hash a token for database storage
|
|
62
|
+
*/
|
|
63
|
+
function hashToken(token: string): string {
|
|
64
|
+
return createHash("sha256").update(token).digest("hex");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get password reset token expiry (1 hour from now)
|
|
69
|
+
*/
|
|
70
|
+
function getPasswordResetExpiry(): Date {
|
|
71
|
+
return new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
75
|
+
const router = new Hono<HonoEnv>();
|
|
76
|
+
const authRepo = config.authRepo;
|
|
77
|
+
const { emailService, emailConfig, allowRegistration = false } = config;
|
|
78
|
+
|
|
79
|
+
// ── Zod input schemas ──────────────────────────────────────────────
|
|
80
|
+
const registerSchema = z.object({
|
|
81
|
+
email: z.string().email("Invalid email address").max(255),
|
|
82
|
+
password: z.string().min(1, "Password is required").max(128),
|
|
83
|
+
displayName: z.string().max(255).optional()
|
|
84
|
+
});
|
|
85
|
+
const loginSchema = z.object({
|
|
86
|
+
email: z.string().email("Invalid email address").max(255),
|
|
87
|
+
password: z.string().min(1, "Password is required").max(128)
|
|
88
|
+
});
|
|
89
|
+
const googleSchema = z.object({
|
|
90
|
+
idToken: z.string().min(1, "ID token is required")
|
|
91
|
+
});
|
|
92
|
+
const forgotPasswordSchema = z.object({
|
|
93
|
+
email: z.string().email("Invalid email address").max(255)
|
|
94
|
+
});
|
|
95
|
+
const resetPasswordSchema = z.object({
|
|
96
|
+
token: z.string().min(1, "Token is required"),
|
|
97
|
+
password: z.string().min(1, "Password is required").max(128)
|
|
98
|
+
});
|
|
99
|
+
const changePasswordSchema = z.object({
|
|
100
|
+
oldPassword: z.string().min(1, "Old password is required").max(128),
|
|
101
|
+
newPassword: z.string().min(1, "New password is required").max(128)
|
|
102
|
+
});
|
|
103
|
+
const refreshSchema = z.object({
|
|
104
|
+
refreshToken: z.string().min(1, "Refresh token is required")
|
|
105
|
+
});
|
|
106
|
+
const logoutSchema = z.object({
|
|
107
|
+
refreshToken: z.string().optional()
|
|
108
|
+
});
|
|
109
|
+
const updateProfileSchema = z.object({
|
|
110
|
+
displayName: z.string().max(255).optional(),
|
|
111
|
+
photoURL: z.string().url().max(2048).optional()
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/** Parse a Zod schema against the request body, throwing ApiError on failure */
|
|
115
|
+
function parseBody<T>(schema: z.ZodSchema<T>, body: unknown): T {
|
|
116
|
+
const result = schema.safeParse(body);
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
const messages = result.error.errors.map(e => `${e.path.join(".")}: ${e.message}`).join(". ");
|
|
119
|
+
throw ApiError.badRequest(messages, "INVALID_INPUT");
|
|
120
|
+
}
|
|
121
|
+
return result.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if email service is configured
|
|
126
|
+
*/
|
|
127
|
+
function isEmailConfigured(): boolean {
|
|
128
|
+
return !!(emailService && emailService.isConfigured());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if registration is allowed (always allow first user for bootstrap)
|
|
133
|
+
*/
|
|
134
|
+
async function isRegistrationAllowed(): Promise<boolean> {
|
|
135
|
+
if (allowRegistration) return true;
|
|
136
|
+
// Always allow first user registration for bootstrap
|
|
137
|
+
const allUsers = await authRepo.listUsers();
|
|
138
|
+
return allUsers.length === 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* POST /auth/register
|
|
143
|
+
* Create a new account with email/password
|
|
144
|
+
*/
|
|
145
|
+
router.post("/register", defaultAuthLimiter, async (c) => {
|
|
146
|
+
const { email, password, displayName } = parseBody(registerSchema, await c.req.json());
|
|
147
|
+
|
|
148
|
+
// Check if registration is allowed
|
|
149
|
+
const registrationAllowed = await isRegistrationAllowed();
|
|
150
|
+
if (!registrationAllowed) {
|
|
151
|
+
throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate password strength
|
|
155
|
+
const passwordValidation = validatePasswordStrength(password);
|
|
156
|
+
if (!passwordValidation.valid) {
|
|
157
|
+
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if email already exists
|
|
161
|
+
const existingUser = await authRepo.getUserByEmail(email);
|
|
162
|
+
if (existingUser) {
|
|
163
|
+
throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create user
|
|
167
|
+
const passwordHash = await hashPassword(password);
|
|
168
|
+
const user = await authRepo.createUser({
|
|
169
|
+
email: email.toLowerCase(),
|
|
170
|
+
passwordHash,
|
|
171
|
+
displayName: displayName || undefined,
|
|
172
|
+
provider: "email"
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Check if this is the first user - make them admin
|
|
176
|
+
const allUsers = await authRepo.listUsers();
|
|
177
|
+
const isFirstUser = allUsers.length === 1;
|
|
178
|
+
if (isFirstUser) {
|
|
179
|
+
await authRepo.assignDefaultRole(user.id, "admin");
|
|
180
|
+
} else if (config.defaultRole) {
|
|
181
|
+
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Generate tokens
|
|
185
|
+
const roles = await authRepo.getUserRoles(user.id);
|
|
186
|
+
const roleIds = roles.map(r => r.id);
|
|
187
|
+
const accessToken = generateAccessToken(user.id, roleIds);
|
|
188
|
+
const refreshToken = generateRefreshToken();
|
|
189
|
+
|
|
190
|
+
// Store refresh token
|
|
191
|
+
const userAgent = c.req.header("user-agent") || "unknown";
|
|
192
|
+
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
193
|
+
|
|
194
|
+
await authRepo.createRefreshToken(
|
|
195
|
+
user.id,
|
|
196
|
+
hashRefreshToken(refreshToken),
|
|
197
|
+
getRefreshTokenExpiry(),
|
|
198
|
+
userAgent,
|
|
199
|
+
ipAddress
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* POST /auth/login
|
|
207
|
+
* Login with email/password
|
|
208
|
+
*/
|
|
209
|
+
router.post("/login", defaultAuthLimiter, async (c) => {
|
|
210
|
+
const { email, password } = parseBody(loginSchema, await c.req.json());
|
|
211
|
+
|
|
212
|
+
const user = await authRepo.getUserByEmail(email);
|
|
213
|
+
if (!user) {
|
|
214
|
+
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!user.passwordHash) {
|
|
218
|
+
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const isValidPassword = await verifyPassword(password, user.passwordHash);
|
|
222
|
+
if (!isValidPassword) {
|
|
223
|
+
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Generate tokens
|
|
227
|
+
const roles = await authRepo.getUserRoles(user.id);
|
|
228
|
+
const roleIds = roles.map(r => r.id);
|
|
229
|
+
|
|
230
|
+
const accessToken = generateAccessToken(user.id, roleIds);
|
|
231
|
+
const refreshToken = generateRefreshToken();
|
|
232
|
+
|
|
233
|
+
// Store refresh token
|
|
234
|
+
const userAgent = c.req.header("user-agent") || "unknown";
|
|
235
|
+
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
236
|
+
|
|
237
|
+
await authRepo.createRefreshToken(
|
|
238
|
+
user.id,
|
|
239
|
+
hashRefreshToken(refreshToken),
|
|
240
|
+
getRefreshTokenExpiry(),
|
|
241
|
+
userAgent,
|
|
242
|
+
ipAddress
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* POST /auth/google
|
|
250
|
+
* Login/register with Google ID token
|
|
251
|
+
*/
|
|
252
|
+
router.post("/google", defaultAuthLimiter, async (c) => {
|
|
253
|
+
const { idToken } = parseBody(googleSchema, await c.req.json());
|
|
254
|
+
|
|
255
|
+
if (!isGoogleOAuthConfigured()) {
|
|
256
|
+
throw ApiError.serviceUnavailable("Google login not configured", "NOT_CONFIGURED");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const googleUser = await verifyGoogleIdToken(idToken);
|
|
260
|
+
if (!googleUser) {
|
|
261
|
+
throw ApiError.unauthorized("Invalid Google token", "INVALID_TOKEN");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Find or create user
|
|
265
|
+
let user = await authRepo.getUserByGoogleId(googleUser.googleId);
|
|
266
|
+
|
|
267
|
+
if (!user) {
|
|
268
|
+
// Check if email exists (link accounts)
|
|
269
|
+
user = await authRepo.getUserByEmail(googleUser.email);
|
|
270
|
+
|
|
271
|
+
if (user) {
|
|
272
|
+
// Link Google to existing account
|
|
273
|
+
await authRepo.updateUser(user.id, { googleId: googleUser.googleId });
|
|
274
|
+
} else {
|
|
275
|
+
// Create new user
|
|
276
|
+
user = await authRepo.createUser({
|
|
277
|
+
email: googleUser.email.toLowerCase(),
|
|
278
|
+
displayName: googleUser.displayName || undefined,
|
|
279
|
+
photoUrl: googleUser.photoUrl || undefined,
|
|
280
|
+
provider: "google",
|
|
281
|
+
googleId: googleUser.googleId
|
|
282
|
+
});
|
|
283
|
+
// Check if this is the first user - make them admin
|
|
284
|
+
const allUsers = await authRepo.listUsers();
|
|
285
|
+
const isFirstUser = allUsers.length === 1;
|
|
286
|
+
if (isFirstUser) {
|
|
287
|
+
await authRepo.assignDefaultRole(user.id, "admin");
|
|
288
|
+
} else if (config.defaultRole) {
|
|
289
|
+
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
// Update profile info from Google
|
|
294
|
+
await authRepo.updateUser(user.id, {
|
|
295
|
+
displayName: googleUser.displayName || user.displayName || undefined,
|
|
296
|
+
photoUrl: googleUser.photoUrl || user.photoUrl || undefined
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Generate tokens
|
|
301
|
+
const roles = await authRepo.getUserRoles(user.id);
|
|
302
|
+
const roleIds = roles.map(r => r.id);
|
|
303
|
+
const accessToken = generateAccessToken(user.id, roleIds);
|
|
304
|
+
const refreshToken = generateRefreshToken();
|
|
305
|
+
|
|
306
|
+
// Store refresh token
|
|
307
|
+
const userAgent = c.req.header("user-agent") || "unknown";
|
|
308
|
+
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
309
|
+
|
|
310
|
+
await authRepo.createRefreshToken(
|
|
311
|
+
user.id,
|
|
312
|
+
hashRefreshToken(refreshToken),
|
|
313
|
+
getRefreshTokenExpiry(),
|
|
314
|
+
userAgent,
|
|
315
|
+
ipAddress
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* POST /auth/forgot-password
|
|
323
|
+
* Request password reset email
|
|
324
|
+
*/
|
|
325
|
+
router.post("/forgot-password", strictAuthLimiter, async (c) => {
|
|
326
|
+
const { email } = parseBody(forgotPasswordSchema, await c.req.json());
|
|
327
|
+
|
|
328
|
+
// Check if email service is configured
|
|
329
|
+
if (!isEmailConfigured()) {
|
|
330
|
+
throw ApiError.serviceUnavailable("Email service not configured. Password reset is not available.", "EMAIL_NOT_CONFIGURED");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Always return success (security: don't reveal if email exists)
|
|
334
|
+
// But only send email if user exists
|
|
335
|
+
const user = await authRepo.getUserByEmail(email);
|
|
336
|
+
|
|
337
|
+
if (user) {
|
|
338
|
+
// Generate reset token
|
|
339
|
+
const token = generateSecureToken();
|
|
340
|
+
const tokenHash = hashToken(token);
|
|
341
|
+
const expiresAt = getPasswordResetExpiry();
|
|
342
|
+
|
|
343
|
+
await authRepo.createPasswordResetToken(user.id, tokenHash, expiresAt);
|
|
344
|
+
|
|
345
|
+
// Build reset URL
|
|
346
|
+
const baseUrl = emailConfig?.resetPasswordUrl || "";
|
|
347
|
+
const resetUrl = `${baseUrl}/reset-password?token=${token}`;
|
|
348
|
+
|
|
349
|
+
// Get email template
|
|
350
|
+
const appName = emailConfig?.appName || "Rebase";
|
|
351
|
+
const templateFn = emailConfig?.templates?.passwordReset;
|
|
352
|
+
const emailContent = templateFn
|
|
353
|
+
? templateFn(resetUrl, { email: user.email, displayName: user.displayName })
|
|
354
|
+
: getPasswordResetTemplate(resetUrl, { email: user.email, displayName: user.displayName }, appName);
|
|
355
|
+
|
|
356
|
+
// Send email
|
|
357
|
+
try {
|
|
358
|
+
await emailService!.send({
|
|
359
|
+
to: user.email,
|
|
360
|
+
subject: emailContent.subject,
|
|
361
|
+
html: emailContent.html,
|
|
362
|
+
text: emailContent.text
|
|
363
|
+
});
|
|
364
|
+
} catch (emailError: unknown) {
|
|
365
|
+
console.error("Failed to send password reset email:", emailError instanceof Error ? emailError.message : emailError);
|
|
366
|
+
// Don't reveal email sending failure to client
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Always return success
|
|
371
|
+
return c.json({
|
|
372
|
+
success: true,
|
|
373
|
+
message: "If an account with that email exists, a password reset link has been sent."
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* POST /auth/reset-password
|
|
379
|
+
* Reset password using token
|
|
380
|
+
*/
|
|
381
|
+
router.post("/reset-password", strictAuthLimiter, async (c) => {
|
|
382
|
+
const { token, password } = parseBody(resetPasswordSchema, await c.req.json());
|
|
383
|
+
|
|
384
|
+
// Validate password strength
|
|
385
|
+
const passwordValidation = validatePasswordStrength(password);
|
|
386
|
+
if (!passwordValidation.valid) {
|
|
387
|
+
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Find valid token
|
|
391
|
+
const tokenHash = hashToken(token);
|
|
392
|
+
const storedToken = await authRepo.findValidPasswordResetToken(tokenHash);
|
|
393
|
+
|
|
394
|
+
if (!storedToken) {
|
|
395
|
+
throw ApiError.badRequest("Invalid or expired reset token", "INVALID_TOKEN");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Update password
|
|
399
|
+
const passwordHash = await hashPassword(password);
|
|
400
|
+
await authRepo.updatePassword(storedToken.userId, passwordHash);
|
|
401
|
+
|
|
402
|
+
// Mark token as used
|
|
403
|
+
await authRepo.markPasswordResetTokenUsed(tokenHash);
|
|
404
|
+
|
|
405
|
+
// Invalidate all refresh tokens (security: log out all sessions)
|
|
406
|
+
await authRepo.deleteAllRefreshTokensForUser(storedToken.userId);
|
|
407
|
+
|
|
408
|
+
return c.json({ success: true, message: "Password has been reset successfully" });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* POST /auth/change-password
|
|
413
|
+
* Change password for authenticated user
|
|
414
|
+
*/
|
|
415
|
+
router.post("/change-password", requireAuth, async (c) => {
|
|
416
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
417
|
+
if (!userCtx) {
|
|
418
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const { oldPassword, newPassword } = parseBody(changePasswordSchema, await c.req.json());
|
|
422
|
+
|
|
423
|
+
// Get user
|
|
424
|
+
const user = await authRepo.getUserById(userCtx.userId);
|
|
425
|
+
if (!user || !user.passwordHash) {
|
|
426
|
+
throw ApiError.badRequest("Cannot change password for this account", "INVALID_ACCOUNT");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Verify old password
|
|
430
|
+
const isValidOldPassword = await verifyPassword(oldPassword, user.passwordHash);
|
|
431
|
+
if (!isValidOldPassword) {
|
|
432
|
+
throw ApiError.unauthorized("Current password is incorrect", "INVALID_CREDENTIALS");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Validate new password strength
|
|
436
|
+
const passwordValidation = validatePasswordStrength(newPassword);
|
|
437
|
+
if (!passwordValidation.valid) {
|
|
438
|
+
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Update password
|
|
442
|
+
const passwordHash = await hashPassword(newPassword);
|
|
443
|
+
await authRepo.updatePassword(user.id, passwordHash);
|
|
444
|
+
|
|
445
|
+
// Invalidate all refresh tokens (security: log out all sessions)
|
|
446
|
+
await authRepo.deleteAllRefreshTokensForUser(user.id);
|
|
447
|
+
|
|
448
|
+
return c.json({ success: true, message: "Password has been changed successfully" });
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* POST /auth/send-verification
|
|
453
|
+
* Send email verification link (authenticated)
|
|
454
|
+
*/
|
|
455
|
+
router.post("/send-verification", requireAuth, async (c) => {
|
|
456
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
457
|
+
if (!userCtx) {
|
|
458
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Check if email service is configured
|
|
462
|
+
if (!isEmailConfigured()) {
|
|
463
|
+
throw ApiError.serviceUnavailable("Email service not configured. Email verification is not available.", "EMAIL_NOT_CONFIGURED");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const user = await authRepo.getUserById(userCtx.userId);
|
|
467
|
+
if (!user) {
|
|
468
|
+
throw ApiError.notFound("User not found");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (user.emailVerified) {
|
|
472
|
+
throw ApiError.badRequest("Email is already verified", "ALREADY_VERIFIED");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Generate verification token
|
|
476
|
+
const token = generateSecureToken();
|
|
477
|
+
|
|
478
|
+
// Store hashed token in user record (raw token goes in the email URL)
|
|
479
|
+
await authRepo.setVerificationToken(user.id, hashToken(token));
|
|
480
|
+
|
|
481
|
+
// Build verification URL
|
|
482
|
+
const baseUrl = emailConfig?.verifyEmailUrl || "";
|
|
483
|
+
const verifyUrl = `${baseUrl}/verify-email?token=${token}`;
|
|
484
|
+
|
|
485
|
+
// Get email template
|
|
486
|
+
const appName = emailConfig?.appName || "Rebase";
|
|
487
|
+
const templateFn = emailConfig?.templates?.emailVerification;
|
|
488
|
+
const emailContent = templateFn
|
|
489
|
+
? templateFn(verifyUrl, { email: user.email, displayName: user.displayName })
|
|
490
|
+
: getEmailVerificationTemplate(verifyUrl, { email: user.email, displayName: user.displayName }, appName);
|
|
491
|
+
|
|
492
|
+
// Send email
|
|
493
|
+
await emailService!.send({
|
|
494
|
+
to: user.email,
|
|
495
|
+
subject: emailContent.subject,
|
|
496
|
+
html: emailContent.html,
|
|
497
|
+
text: emailContent.text
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
return c.json({ success: true, message: "Verification email sent" });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* GET /auth/verify-email
|
|
505
|
+
* Verify email address using token
|
|
506
|
+
*/
|
|
507
|
+
router.get("/verify-email", async (c) => {
|
|
508
|
+
const token = c.req.query("token");
|
|
509
|
+
|
|
510
|
+
if (!token) {
|
|
511
|
+
throw ApiError.badRequest("Verification token is required", "INVALID_INPUT");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Find user by hashed verification token
|
|
515
|
+
const user = await authRepo.getUserByVerificationToken(hashToken(token));
|
|
516
|
+
if (!user) {
|
|
517
|
+
throw ApiError.badRequest("Invalid or expired verification token", "INVALID_TOKEN");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Mark email as verified
|
|
521
|
+
await authRepo.setEmailVerified(user.id, true);
|
|
522
|
+
|
|
523
|
+
return c.json({ success: true, message: "Email verified successfully" });
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* POST /auth/refresh
|
|
528
|
+
* Refresh access token using refresh token
|
|
529
|
+
*/
|
|
530
|
+
router.post("/refresh", async (c) => {
|
|
531
|
+
const { refreshToken } = parseBody(refreshSchema, await c.req.json());
|
|
532
|
+
|
|
533
|
+
const tokenHash = hashRefreshToken(refreshToken);
|
|
534
|
+
const storedToken = await authRepo.findRefreshTokenByHash(tokenHash);
|
|
535
|
+
|
|
536
|
+
if (!storedToken) {
|
|
537
|
+
throw ApiError.unauthorized("Invalid refresh token", "INVALID_TOKEN");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (new Date() > storedToken.expiresAt) {
|
|
541
|
+
await authRepo.deleteRefreshToken(tokenHash);
|
|
542
|
+
throw ApiError.unauthorized("Refresh token expired", "TOKEN_EXPIRED");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Generate new tokens
|
|
546
|
+
const roles = await authRepo.getUserRoles(storedToken.userId);
|
|
547
|
+
const roleIds = roles.map(r => r.id);
|
|
548
|
+
|
|
549
|
+
const newAccessToken = generateAccessToken(storedToken.userId, roleIds);
|
|
550
|
+
const newRefreshToken = generateRefreshToken();
|
|
551
|
+
|
|
552
|
+
// Rotate refresh token (delete old, create new)
|
|
553
|
+
const userAgent = c.req.header("user-agent") || "unknown";
|
|
554
|
+
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
555
|
+
|
|
556
|
+
await authRepo.deleteRefreshToken(tokenHash);
|
|
557
|
+
await authRepo.createRefreshToken(
|
|
558
|
+
storedToken.userId,
|
|
559
|
+
hashRefreshToken(newRefreshToken),
|
|
560
|
+
getRefreshTokenExpiry(),
|
|
561
|
+
userAgent,
|
|
562
|
+
ipAddress
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
return c.json({
|
|
566
|
+
tokens: {
|
|
567
|
+
accessToken: newAccessToken,
|
|
568
|
+
refreshToken: newRefreshToken,
|
|
569
|
+
accessTokenExpiresAt: getAccessTokenExpiry()
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* POST /auth/logout
|
|
576
|
+
* Invalidate refresh token
|
|
577
|
+
*/
|
|
578
|
+
router.post("/logout", async (c) => {
|
|
579
|
+
const { refreshToken } = parseBody(logoutSchema, await c.req.json());
|
|
580
|
+
|
|
581
|
+
if (refreshToken) {
|
|
582
|
+
const tokenHash = hashRefreshToken(refreshToken);
|
|
583
|
+
await authRepo.deleteRefreshToken(tokenHash);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return c.json({ success: true });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* GET /auth/sessions
|
|
591
|
+
* Get active refresh tokens (sessions) for the current user
|
|
592
|
+
*/
|
|
593
|
+
router.get("/sessions", requireAuth, async (c) => {
|
|
594
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
595
|
+
if (!userCtx) {
|
|
596
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const currentRefreshToken = c.req.header("x-refresh-token") as string;
|
|
600
|
+
const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
|
|
601
|
+
|
|
602
|
+
const sessions = await authRepo.listRefreshTokensForUser(userCtx.userId);
|
|
603
|
+
|
|
604
|
+
const mappedSessions = sessions.map(s => ({
|
|
605
|
+
id: s.id,
|
|
606
|
+
userAgent: s.userAgent,
|
|
607
|
+
ipAddress: s.ipAddress,
|
|
608
|
+
createdAt: s.createdAt,
|
|
609
|
+
isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
|
|
610
|
+
}));
|
|
611
|
+
|
|
612
|
+
return c.json({ sessions: mappedSessions });
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* DELETE /auth/sessions
|
|
617
|
+
* Delete all refresh tokens for the current user (remote logout every device)
|
|
618
|
+
*/
|
|
619
|
+
router.delete("/sessions", requireAuth, async (c) => {
|
|
620
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
621
|
+
if (!userCtx) {
|
|
622
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
|
|
626
|
+
return c.json({ success: true, message: "All sessions revoked successfully" });
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* DELETE /auth/sessions/:id
|
|
631
|
+
* Delete a specific refresh token (remote logout)
|
|
632
|
+
*/
|
|
633
|
+
router.delete("/sessions/:id", requireAuth, async (c) => {
|
|
634
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
635
|
+
if (!userCtx) {
|
|
636
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const id = c.req.param("id");
|
|
640
|
+
if (!id) {
|
|
641
|
+
throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
await authRepo.deleteRefreshTokenById(id, userCtx.userId);
|
|
645
|
+
return c.json({ success: true, message: "Session revoked successfully" });
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* GET /auth/me
|
|
650
|
+
* Get current authenticated user
|
|
651
|
+
*/
|
|
652
|
+
router.get("/me", requireAuth, async (c) => {
|
|
653
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
654
|
+
if (!userCtx) {
|
|
655
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
659
|
+
if (!result) {
|
|
660
|
+
throw ApiError.notFound("User not found");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return c.json({
|
|
664
|
+
user: {
|
|
665
|
+
uid: result.user.id,
|
|
666
|
+
email: result.user.email,
|
|
667
|
+
displayName: result.user.displayName,
|
|
668
|
+
photoURL: result.user.photoUrl,
|
|
669
|
+
emailVerified: result.user.emailVerified,
|
|
670
|
+
roles: result.roles.map(r => r.id)
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* PATCH /auth/me
|
|
677
|
+
* Update current authenticated user profile
|
|
678
|
+
*/
|
|
679
|
+
router.patch("/me", requireAuth, async (c) => {
|
|
680
|
+
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
681
|
+
if (!userCtx) {
|
|
682
|
+
throw ApiError.unauthorized("Not authenticated");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
|
|
686
|
+
|
|
687
|
+
const updatedUser = await authRepo.updateUser(userCtx.userId, {
|
|
688
|
+
displayName: displayName !== undefined ? displayName : undefined,
|
|
689
|
+
photoUrl: photoURL !== undefined ? photoURL : undefined,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
if (!updatedUser) {
|
|
693
|
+
throw ApiError.notFound("User not found");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
697
|
+
if (!result) {
|
|
698
|
+
throw ApiError.notFound("User not found");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return c.json({
|
|
702
|
+
user: {
|
|
703
|
+
uid: result.user.id,
|
|
704
|
+
email: result.user.email,
|
|
705
|
+
displayName: result.user.displayName,
|
|
706
|
+
photoURL: result.user.photoUrl,
|
|
707
|
+
emailVerified: result.user.emailVerified,
|
|
708
|
+
roles: result.roles.map(r => r.id)
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* GET /auth/config
|
|
715
|
+
* Get public auth configuration (for frontend to know what's available)
|
|
716
|
+
*/
|
|
717
|
+
router.get("/config", defaultAuthLimiter, async (c) => {
|
|
718
|
+
const allUsers = await authRepo.listUsers();
|
|
719
|
+
const needsSetup = allUsers.length === 0;
|
|
720
|
+
const registrationAllowed = needsSetup || allowRegistration;
|
|
721
|
+
return c.json({
|
|
722
|
+
needsSetup,
|
|
723
|
+
registrationEnabled: registrationAllowed,
|
|
724
|
+
googleEnabled: isGoogleOAuthConfigured(),
|
|
725
|
+
emailServiceEnabled: isEmailConfigured()
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
return router;
|
|
730
|
+
}
|