@rebasepro/server-core 0.7.0 → 0.9.0
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/README.md +2 -2
- package/dist/api/ast-schema-editor.d.ts +21 -0
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/api/errors.d.ts +53 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/logs-routes.d.ts +37 -0
- package/dist/api/openapi-generator.d.ts +16 -0
- package/dist/api/rest/api-generator.d.ts +68 -0
- package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
- package/dist/api/rest/query-parser.d.ts +6 -0
- package/dist/api/schema-editor-routes.d.ts +3 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/auth/adapter-middleware.d.ts +43 -0
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
- package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/auth/api-keys/api-key-store.d.ts +35 -0
- package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
- package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
- package/dist/auth/apple-oauth.d.ts +30 -0
- package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
- package/dist/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/auth/builtin-auth-adapter.d.ts +55 -0
- package/dist/auth/cookie-utils.d.ts +32 -0
- package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
- package/dist/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/auth/discord-oauth.d.ts +14 -0
- package/dist/auth/facebook-oauth.d.ts +14 -0
- package/dist/auth/github-oauth.d.ts +15 -0
- package/dist/auth/gitlab-oauth.d.ts +13 -0
- package/dist/auth/google-oauth.d.ts +47 -0
- package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
- package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
- package/dist/auth/jwt.d.ts +67 -0
- package/dist/auth/linkedin-oauth.d.ts +18 -0
- package/dist/auth/magic-link-routes.d.ts +32 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +7 -0
- package/dist/auth/mfa.d.ts +49 -0
- package/dist/auth/microsoft-oauth.d.ts +16 -0
- package/dist/auth/middleware.d.ts +158 -0
- package/dist/auth/password.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/rls-scope.d.ts +31 -0
- package/dist/auth/routes.d.ts +66 -0
- package/dist/auth/session-routes.d.ts +29 -0
- package/dist/auth/slack-oauth.d.ts +12 -0
- package/dist/auth/spotify-oauth.d.ts +12 -0
- package/dist/auth/twitter-oauth.d.ts +18 -0
- package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/collections/loader.d.ts +5 -0
- package/dist/cron/cron-loader.d.ts +18 -0
- package/dist/cron/cron-routes.d.ts +14 -0
- package/dist/cron/cron-scheduler.d.ts +106 -0
- package/dist/cron/cron-store.d.ts +32 -0
- package/dist/cron/define-cron.d.ts +28 -0
- package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
- package/dist/db/interfaces.d.ts +18 -0
- package/dist/dynamic-import-Dvh-K5fl.js +22 -0
- package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
- package/dist/email/index.d.ts +6 -0
- package/dist/email/smtp-email-service.d.ts +30 -0
- package/dist/email/templates.d.ts +50 -0
- package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
- package/dist/env.d.ts +102 -0
- package/dist/functions/define-function.d.ts +55 -0
- package/dist/functions/function-loader.d.ts +18 -0
- package/dist/functions/function-routes.d.ts +10 -0
- package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
- package/dist/history/history-routes.d.ts +23 -0
- package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.es.js +18184 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +48629 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +65 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/init.d.ts +344 -0
- package/dist/jwt-BwIn8xmk.js +4200 -0
- package/dist/jwt-BwIn8xmk.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-DnYXB-Wd.js +162 -0
- package/dist/ms-DnYXB-Wd.js.map +1 -0
- package/dist/openapi-generator-Z9oYWLf_.js +586 -0
- package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
- package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
- package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
- package/dist/serve-spa.d.ts +36 -0
- package/dist/services/driver-registry.d.ts +78 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/services/webhook-service.d.ts +29 -0
- package/dist/singleton.d.ts +51 -0
- package/dist/src-B4OLmNVa.js +437 -0
- package/dist/src-B4OLmNVa.js.map +1 -0
- package/dist/src-B5WkP1Rv.js +24778 -0
- package/dist/src-B5WkP1Rv.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/LocalStorageController.d.ts +46 -0
- package/dist/storage/S3StorageController.d.ts +40 -0
- package/dist/storage/image-transform.d.ts +50 -0
- package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
- package/dist/storage/routes.d.ts +70 -0
- package/dist/storage/storage-registry.d.ts +78 -0
- package/dist/storage/tus-handler.d.ts +53 -0
- package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/dev-port.d.ts +45 -0
- package/dist/utils/dynamic-import.d.ts +25 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logging.d.ts +9 -0
- package/dist/utils/request-id.d.ts +4 -0
- package/dist/utils/request-logger.d.ts +19 -0
- package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
- package/package.json +49 -22
- package/coverage/clover.xml +0 -3739
- package/coverage/coverage-final.json +0 -31
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
- package/coverage/lcov-report/src/api/errors.ts.html +0 -472
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
- package/coverage/lcov-report/src/api/graphql/index.html +0 -116
- package/coverage/lcov-report/src/api/index.html +0 -176
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
- package/coverage/lcov-report/src/api/rest/index.html +0 -131
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
- package/coverage/lcov-report/src/api/server.ts.html +0 -823
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
- package/coverage/lcov-report/src/auth/index.html +0 -176
- package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
- package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
- package/coverage/lcov-report/src/auth/password.ts.html +0 -310
- package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
- package/coverage/lcov-report/src/collections/index.html +0 -116
- package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
- package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
- package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
- package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
- package/coverage/lcov-report/src/db/index.html +0 -146
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
- package/coverage/lcov-report/src/db/services/index.html +0 -176
- package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
- package/coverage/lcov-report/src/index.html +0 -116
- package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
- package/coverage/lcov-report/src/services/index.html +0 -131
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
- package/coverage/lcov-report/src/storage/index.html +0 -161
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
- package/coverage/lcov-report/src/storage/types.ts.html +0 -451
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
- package/coverage/lcov-report/src/utils/index.html +0 -116
- package/coverage/lcov.info +0 -7179
- package/jest.config.cjs +0 -19
- package/src/api/ast-schema-editor.ts +0 -289
- package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
- package/src/api/errors.ts +0 -278
- package/src/api/graphql/graphql-schema-generator.ts +0 -383
- package/src/api/graphql/index.ts +0 -2
- package/src/api/index.ts +0 -11
- package/src/api/logs-routes.ts +0 -132
- package/src/api/openapi-generator.ts +0 -727
- package/src/api/rest/api-generator-count.test.ts +0 -126
- package/src/api/rest/api-generator.ts +0 -747
- package/src/api/rest/query-parser.ts +0 -300
- package/src/api/schema-editor-routes.ts +0 -41
- package/src/api/server.ts +0 -271
- package/src/api/types.ts +0 -104
- package/src/auth/adapter-middleware.ts +0 -114
- package/src/auth/admin-roles-route.ts +0 -36
- package/src/auth/admin-user-ops.ts +0 -236
- package/src/auth/admin-users-route.ts +0 -302
- package/src/auth/api-keys/api-key-middleware.ts +0 -133
- package/src/auth/api-keys/api-key-routes.ts +0 -193
- package/src/auth/api-keys/api-key-store.ts +0 -340
- package/src/auth/apple-oauth.ts +0 -122
- package/src/auth/bitbucket-oauth.ts +0 -84
- package/src/auth/builtin-auth-adapter.ts +0 -380
- package/src/auth/custom-auth-adapter.ts +0 -86
- package/src/auth/discord-oauth.ts +0 -85
- package/src/auth/facebook-oauth.ts +0 -74
- package/src/auth/github-oauth.ts +0 -112
- package/src/auth/gitlab-oauth.ts +0 -72
- package/src/auth/google-oauth.ts +0 -223
- package/src/auth/jwt.ts +0 -208
- package/src/auth/linkedin-oauth.ts +0 -83
- package/src/auth/magic-link-routes.ts +0 -167
- package/src/auth/mfa-crypto.ts +0 -91
- package/src/auth/mfa-routes.ts +0 -323
- package/src/auth/mfa.ts +0 -161
- package/src/auth/microsoft-oauth.ts +0 -90
- package/src/auth/middleware.ts +0 -369
- package/src/auth/password.ts +0 -77
- package/src/auth/rate-limiter.ts +0 -225
- package/src/auth/reset-password-admin.ts +0 -160
- package/src/auth/rls-scope.ts +0 -58
- package/src/auth/routes.ts +0 -802
- package/src/auth/session-routes.ts +0 -353
- package/src/auth/slack-oauth.ts +0 -73
- package/src/auth/spotify-oauth.ts +0 -69
- package/src/auth/twitter-oauth.ts +0 -128
- package/src/collections/BackendCollectionRegistry.ts +0 -20
- package/src/collections/loader.ts +0 -50
- package/src/cron/cron-loader.ts +0 -84
- package/src/cron/cron-routes.test.ts +0 -265
- package/src/cron/cron-routes.ts +0 -85
- package/src/cron/cron-scheduler.test.ts +0 -559
- package/src/cron/cron-scheduler.ts +0 -581
- package/src/cron/cron-store.ts +0 -166
- package/src/db/interfaces.ts +0 -60
- package/src/email/index.ts +0 -19
- package/src/email/smtp-email-service.ts +0 -123
- package/src/email/templates.ts +0 -470
- package/src/env.ts +0 -227
- package/src/functions/function-loader.ts +0 -116
- package/src/functions/function-routes.ts +0 -31
- package/src/history/history-routes.ts +0 -129
- package/src/index.ts +0 -70
- package/src/init/docs.ts +0 -47
- package/src/init/health.ts +0 -37
- package/src/init/middlewares.ts +0 -61
- package/src/init/shutdown.ts +0 -56
- package/src/init/storage.ts +0 -57
- package/src/init.ts +0 -1041
- package/src/serve-spa.ts +0 -89
- package/src/services/driver-registry.ts +0 -183
- package/src/services/routed-realtime-service.ts +0 -113
- package/src/services/webhook-service.ts +0 -155
- package/src/singleton.ts +0 -70
- package/src/storage/LocalStorageController.ts +0 -389
- package/src/storage/S3StorageController.ts +0 -300
- package/src/storage/image-transform.ts +0 -218
- package/src/storage/routes.ts +0 -368
- package/src/storage/storage-registry.ts +0 -188
- package/src/storage/tus-handler.ts +0 -315
- package/src/types/index.ts +0 -27
- package/src/utils/dev-port.ts +0 -196
- package/src/utils/logger.ts +0 -143
- package/src/utils/logging.ts +0 -40
- package/src/utils/request-id.ts +0 -40
- package/src/utils/request-logger.ts +0 -72
- package/test/api-generator.test.ts +0 -588
- package/test/api-key-permission-guard.test.ts +0 -144
- package/test/ast-schema-editor.test.ts +0 -89
- package/test/auth-middleware-hono.test.ts +0 -556
- package/test/auth-routes.test.ts +0 -1098
- package/test/backend-hooks-data.test.ts +0 -477
- package/test/custom-auth-adapter.test.ts +0 -189
- package/test/driver-registry.test.ts +0 -282
- package/test/email-templates.test.ts +0 -174
- package/test/env.test.ts +0 -128
- package/test/error-propagation.test.ts +0 -226
- package/test/errors-hono.test.ts +0 -133
- package/test/errors.test.ts +0 -155
- package/test/function-loader.test.ts +0 -127
- package/test/graphql-schema-generator.test.ts +0 -554
- package/test/jwt-security.test.ts +0 -182
- package/test/jwt.test.ts +0 -326
- package/test/mfa.test.ts +0 -197
- package/test/middleware.test.ts +0 -305
- package/test/multi-datasource-routing.test.ts +0 -113
- package/test/password.test.ts +0 -165
- package/test/query-parser.test.ts +0 -233
- package/test/rate-limiter.test.ts +0 -102
- package/test/reset-password-admin.test.ts +0 -113
- package/test/routed-realtime-service.test.ts +0 -86
- package/test/safe-compare.test.ts +0 -66
- package/test/singleton.test.ts +0 -59
- package/test/smtp-email-service.test.ts +0 -169
- package/test/storage-local.test.ts +0 -271
- package/test/storage-registry.test.ts +0 -282
- package/test/storage-routes.test.ts +0 -222
- package/test/storage-s3.test.ts +0 -304
- package/test/transform-auth-response.test.ts +0 -305
- package/test/webhook-service.test.ts +0 -260
- package/tsconfig.json +0 -54
- package/tsconfig.prod.json +0 -23
- package/vite.config.ts +0 -86
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
4
|
-
import { ApiError } from "../api/errors";
|
|
5
|
-
import { HonoEnv } from "../api/types";
|
|
6
|
-
import { requireAuth } from "./middleware";
|
|
7
|
-
import { logger } from "../utils/logger";
|
|
8
|
-
import { strictAuthLimiter, defaultAuthLimiter } from "./rate-limiter";
|
|
9
|
-
import { hashRefreshToken } from "./jwt";
|
|
10
|
-
import type { AuthModuleConfig } from "./routes";
|
|
11
|
-
import { resolveAuthHooks } from "./auth-hooks";
|
|
12
|
-
import type { CreateUserData } from "./interfaces";
|
|
13
|
-
import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
|
|
14
|
-
|
|
15
|
-
interface SessionRoutesConfig {
|
|
16
|
-
router: Hono<HonoEnv>;
|
|
17
|
-
config: AuthModuleConfig;
|
|
18
|
-
ops: ReturnType<typeof resolveAuthHooks>;
|
|
19
|
-
parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;
|
|
20
|
-
buildAuthResponse: (
|
|
21
|
-
user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
|
|
22
|
-
roleIds: string[],
|
|
23
|
-
accessToken: string,
|
|
24
|
-
refreshToken: string
|
|
25
|
-
) => unknown;
|
|
26
|
-
createSessionAndTokens: (userId: string, userAgent: string, ipAddress: string) => Promise<{
|
|
27
|
-
roleIds: string[];
|
|
28
|
-
accessToken: string;
|
|
29
|
-
refreshToken: string;
|
|
30
|
-
}>;
|
|
31
|
-
applyTransformHook: (
|
|
32
|
-
response: AuthResponsePayload,
|
|
33
|
-
method: TransformAuthResponseContext["method"],
|
|
34
|
-
request: Request,
|
|
35
|
-
userId: string
|
|
36
|
-
) => Promise<AuthResponsePayload>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function mountSessionRoutes(opts: SessionRoutesConfig): void {
|
|
40
|
-
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = opts;
|
|
41
|
-
const authRepo = config.authRepo;
|
|
42
|
-
|
|
43
|
-
const logoutSchema = z.object({
|
|
44
|
-
refreshToken: z.string().optional()
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const linkSchema = z.object({
|
|
48
|
-
email: z.string().email("Invalid email address").max(255),
|
|
49
|
-
password: z.string().min(1, "Password is required").max(128)
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const updateProfileSchema = z.object({
|
|
53
|
-
displayName: z.string().max(255).optional(),
|
|
54
|
-
photoURL: z.string().url().max(2048).optional()
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const isEmailConfigured = () => !!(config.emailService && config.emailService.isConfigured());
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* POST /auth/logout
|
|
61
|
-
*/
|
|
62
|
-
router.post("/logout", async (c) => {
|
|
63
|
-
const { refreshToken } = parseBody(logoutSchema, await c.req.json());
|
|
64
|
-
|
|
65
|
-
if (refreshToken) {
|
|
66
|
-
const tokenHash = hashRefreshToken(refreshToken);
|
|
67
|
-
await authRepo.deleteRefreshToken(tokenHash);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Call afterLogout hook (fire-and-forget)
|
|
71
|
-
// Extract userId from the access token if present
|
|
72
|
-
const authHeader = c.req.header("authorization");
|
|
73
|
-
if (ops.afterLogout && authHeader?.startsWith("Bearer ")) {
|
|
74
|
-
const { verifyAccessToken } = await import("./jwt");
|
|
75
|
-
const payload = verifyAccessToken(authHeader.substring(7));
|
|
76
|
-
if (payload) {
|
|
77
|
-
ops.afterLogout(payload.userId).catch((err) => {
|
|
78
|
-
logger.error("[AuthHooks] afterLogout error", {
|
|
79
|
-
error: err instanceof Error ? err.message : err
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return c.json({ success: true });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* GET /auth/sessions
|
|
90
|
-
* Get active refresh tokens (sessions) for the current user
|
|
91
|
-
*/
|
|
92
|
-
router.get("/sessions", requireAuth, async (c) => {
|
|
93
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
94
|
-
if (!userCtx) {
|
|
95
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const currentRefreshToken = c.req.header("x-refresh-token") as string;
|
|
99
|
-
const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
|
|
100
|
-
|
|
101
|
-
const sessions = await authRepo.listRefreshTokensForUser(userCtx.userId);
|
|
102
|
-
|
|
103
|
-
const mappedSessions = sessions.map((s) => ({
|
|
104
|
-
id: s.id,
|
|
105
|
-
userAgent: s.userAgent,
|
|
106
|
-
ipAddress: s.ipAddress,
|
|
107
|
-
createdAt: s.createdAt,
|
|
108
|
-
isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
|
|
109
|
-
}));
|
|
110
|
-
|
|
111
|
-
return c.json({ sessions: mappedSessions });
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* DELETE /auth/sessions
|
|
116
|
-
* Delete all refresh tokens for the current user (remote logout every device)
|
|
117
|
-
*/
|
|
118
|
-
router.delete("/sessions", requireAuth, async (c) => {
|
|
119
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
120
|
-
if (!userCtx) {
|
|
121
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
|
|
125
|
-
return c.json({
|
|
126
|
-
success: true,
|
|
127
|
-
message: "All sessions revoked successfully"
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* DELETE /auth/sessions/:id
|
|
133
|
-
* Delete a specific refresh token (remote logout)
|
|
134
|
-
*/
|
|
135
|
-
router.delete("/sessions/:id", requireAuth, async (c) => {
|
|
136
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
137
|
-
if (!userCtx) {
|
|
138
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const id = c.req.param("id");
|
|
142
|
-
if (!id) {
|
|
143
|
-
throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
await authRepo.deleteRefreshTokenById(id, userCtx.userId);
|
|
147
|
-
return c.json({
|
|
148
|
-
success: true,
|
|
149
|
-
message: "Session revoked successfully"
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* GET /auth/me
|
|
155
|
-
* Get current authenticated user
|
|
156
|
-
*/
|
|
157
|
-
router.get("/me", requireAuth, async (c) => {
|
|
158
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
159
|
-
if (!userCtx) {
|
|
160
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
164
|
-
if (!result) {
|
|
165
|
-
throw ApiError.notFound("User not found");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return c.json({
|
|
169
|
-
user: {
|
|
170
|
-
uid: result.user.id,
|
|
171
|
-
email: result.user.email,
|
|
172
|
-
displayName: result.user.displayName,
|
|
173
|
-
photoURL: result.user.photoUrl,
|
|
174
|
-
emailVerified: result.user.emailVerified,
|
|
175
|
-
roles: result.roles.map((r) => r.id),
|
|
176
|
-
metadata: result.user.metadata ?? {}
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* PATCH /auth/me
|
|
183
|
-
* Update current authenticated user profile
|
|
184
|
-
*/
|
|
185
|
-
router.patch("/me", requireAuth, async (c) => {
|
|
186
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
187
|
-
if (!userCtx) {
|
|
188
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
|
|
192
|
-
|
|
193
|
-
const updatedUser = await authRepo.updateUser(userCtx.userId, {
|
|
194
|
-
displayName: displayName !== undefined ? displayName : undefined,
|
|
195
|
-
photoUrl: photoURL !== undefined ? photoURL : undefined
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
if (!updatedUser) {
|
|
199
|
-
throw ApiError.notFound("User not found");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const result = await authRepo.getUserWithRoles(userCtx.userId);
|
|
203
|
-
if (!result) {
|
|
204
|
-
throw ApiError.notFound("User not found");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return c.json({
|
|
208
|
-
user: {
|
|
209
|
-
uid: result.user.id,
|
|
210
|
-
email: result.user.email,
|
|
211
|
-
displayName: result.user.displayName,
|
|
212
|
-
photoURL: result.user.photoUrl,
|
|
213
|
-
emailVerified: result.user.emailVerified,
|
|
214
|
-
roles: result.roles.map((r) => r.id),
|
|
215
|
-
metadata: result.user.metadata ?? {}
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* GET /auth/config
|
|
222
|
-
* Get public auth configuration
|
|
223
|
-
*/
|
|
224
|
-
router.get("/config", defaultAuthLimiter, async (c) => {
|
|
225
|
-
let needsSetup: boolean;
|
|
226
|
-
if (config.isBootstrapCompleted) {
|
|
227
|
-
needsSetup = !(await config.isBootstrapCompleted());
|
|
228
|
-
} else {
|
|
229
|
-
const allUsers = await authRepo.listUsers();
|
|
230
|
-
needsSetup = allUsers.length === 0;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const registrationAllowed = needsSetup || !!config.allowRegistration;
|
|
234
|
-
const enabledProviders = (config.oauthProviders || []).map((p) => p.id);
|
|
235
|
-
|
|
236
|
-
return c.json({
|
|
237
|
-
needsSetup,
|
|
238
|
-
registrationEnabled: registrationAllowed,
|
|
239
|
-
emailServiceEnabled: isEmailConfigured(),
|
|
240
|
-
magicLinkEnabled: !!config.enableMagicLink && isEmailConfigured(),
|
|
241
|
-
enabledProviders
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* POST /auth/anonymous
|
|
247
|
-
* Create an anonymous user with temporary credentials
|
|
248
|
-
*/
|
|
249
|
-
router.post("/anonymous", strictAuthLimiter, async (c) => {
|
|
250
|
-
const anonId = randomBytes(16).toString("hex");
|
|
251
|
-
const anonEmail = `anon_${anonId.slice(0, 8)}@anonymous.local`;
|
|
252
|
-
|
|
253
|
-
let createData: CreateUserData = {
|
|
254
|
-
email: anonEmail,
|
|
255
|
-
emailVerified: false,
|
|
256
|
-
isAnonymous: true
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
if (ops.beforeUserCreate) {
|
|
260
|
-
createData = await ops.beforeUserCreate(createData);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const user = await authRepo.createUser(createData);
|
|
264
|
-
|
|
265
|
-
// Assign default role (follow register route pattern, but never auto-admin)
|
|
266
|
-
if (config.defaultRole) {
|
|
267
|
-
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
271
|
-
user.id,
|
|
272
|
-
c.req.header("user-agent") || "unknown",
|
|
273
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
// Fire afterUserCreate hook
|
|
277
|
-
if (ops.afterUserCreate) {
|
|
278
|
-
ops.afterUserCreate(user).catch((err) => {
|
|
279
|
-
logger.error("[AuthHooks] afterUserCreate error", {
|
|
280
|
-
error: err instanceof Error ? err.message : err
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Fire onAuthenticated hook
|
|
286
|
-
if (ops.onAuthenticated) {
|
|
287
|
-
ops.onAuthenticated(user, "anonymous").catch((err) => {
|
|
288
|
-
logger.error("[AuthHooks] onAuthenticated error", {
|
|
289
|
-
error: err instanceof Error ? err.message : err
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken) as AuthResponsePayload;
|
|
295
|
-
const finalResponse = await applyTransformHook(authResponse, "anonymous", c.req.raw, user.id);
|
|
296
|
-
return c.json(finalResponse, 201);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* POST /auth/anonymous/link
|
|
301
|
-
* Upgrade an anonymous user to a permanent account with email/password
|
|
302
|
-
*/
|
|
303
|
-
router.post("/anonymous/link", requireAuth, async (c) => {
|
|
304
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
305
|
-
if (!userCtx) {
|
|
306
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const user = await authRepo.getUserById(userCtx.userId);
|
|
310
|
-
if (!user?.isAnonymous) {
|
|
311
|
-
throw ApiError.badRequest("User is not anonymous", "NOT_ANONYMOUS");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const { email, password } = parseBody(linkSchema, await c.req.json());
|
|
315
|
-
|
|
316
|
-
// Validate password strength
|
|
317
|
-
const passwordValidation = ops.validatePasswordStrength(password);
|
|
318
|
-
if (!passwordValidation.valid) {
|
|
319
|
-
throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Check if email is already taken
|
|
323
|
-
const existingUser = await authRepo.getUserByEmail(email.toLowerCase());
|
|
324
|
-
if (existingUser) {
|
|
325
|
-
throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Hash password
|
|
329
|
-
const passwordHash = await ops.hashPassword(password);
|
|
330
|
-
|
|
331
|
-
// Update user: set email, password, remove anonymous flag
|
|
332
|
-
const updatedUser = await authRepo.updateUser(user.id, {
|
|
333
|
-
email: email.toLowerCase(),
|
|
334
|
-
passwordHash,
|
|
335
|
-
isAnonymous: false
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
if (!updatedUser) {
|
|
339
|
-
throw ApiError.notFound("User not found");
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Generate new tokens with updated identity
|
|
343
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
344
|
-
user.id,
|
|
345
|
-
c.req.header("user-agent") || "unknown",
|
|
346
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
const authResponse = buildAuthResponse(updatedUser, roleIds, accessToken, refreshToken) as AuthResponsePayload;
|
|
350
|
-
const finalResponse = await applyTransformHook(authResponse, "anonymous", c.req.raw, user.id);
|
|
351
|
-
return c.json(finalResponse);
|
|
352
|
-
});
|
|
353
|
-
}
|
package/src/auth/slack-oauth.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { logger } from "../utils/logger";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a Slack OAuth Provider integration (OAuth 2.0 / "Sign in with Slack").
|
|
7
|
-
* Uses the OpenID Connect flow with the "openid,email,profile" scopes.
|
|
8
|
-
*/
|
|
9
|
-
export function createSlackProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
10
|
-
return {
|
|
11
|
-
id: "slack",
|
|
12
|
-
schema: z.object({
|
|
13
|
-
code: z.string().min(1, "Auth code is required"),
|
|
14
|
-
redirectUri: z.string().url("Valid redirect URI is required")
|
|
15
|
-
}),
|
|
16
|
-
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
17
|
-
try {
|
|
18
|
-
const tokenResponse = await fetch("https://slack.com/api/openid.connect.token", {
|
|
19
|
-
method: "POST",
|
|
20
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
21
|
-
body: new URLSearchParams({
|
|
22
|
-
client_id: config.clientId,
|
|
23
|
-
client_secret: config.clientSecret,
|
|
24
|
-
code: payload.code,
|
|
25
|
-
redirect_uri: payload.redirectUri,
|
|
26
|
-
grant_type: "authorization_code"
|
|
27
|
-
})
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (!tokenResponse.ok) {
|
|
31
|
-
logger.error("Failed to get Slack access token", { detail: await tokenResponse.text() });
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const tokenData = await tokenResponse.json() as { ok: boolean; access_token?: string; error?: string };
|
|
36
|
-
if (!tokenData.ok || !tokenData.access_token) {
|
|
37
|
-
logger.error("Slack token exchange failed", { detail: tokenData.error });
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const profileResponse = await fetch("https://slack.com/api/openid.connect.userInfo", {
|
|
42
|
-
headers: { "Authorization": `Bearer ${tokenData.access_token}` }
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (!profileResponse.ok) {
|
|
46
|
-
logger.error("Failed to get Slack user info", { detail: await profileResponse.text() });
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const p = await profileResponse.json() as {
|
|
51
|
-
ok: boolean; sub: string; name?: string;
|
|
52
|
-
email?: string; picture?: string; email_verified?: boolean;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
if (!p.ok || !p.email) {
|
|
56
|
-
logger.error("Slack user has no email");
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
providerId: p.sub,
|
|
62
|
-
email: p.email,
|
|
63
|
-
displayName: p.name || null,
|
|
64
|
-
photoUrl: p.picture || null,
|
|
65
|
-
emailVerified: p.email_verified === true
|
|
66
|
-
};
|
|
67
|
-
} catch (error) {
|
|
68
|
-
logger.error("Slack OAuth error", { error: error });
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { logger } from "../utils/logger";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a Spotify OAuth Provider integration.
|
|
7
|
-
* Uses the authorization code flow with the "user-read-email" scope.
|
|
8
|
-
*/
|
|
9
|
-
export function createSpotifyProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
10
|
-
return {
|
|
11
|
-
id: "spotify",
|
|
12
|
-
schema: z.object({
|
|
13
|
-
code: z.string().min(1, "Auth code is required"),
|
|
14
|
-
redirectUri: z.string().url("Valid redirect URI is required")
|
|
15
|
-
}),
|
|
16
|
-
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
17
|
-
try {
|
|
18
|
-
const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
|
|
19
|
-
|
|
20
|
-
const tokenResponse = await fetch("https://accounts.spotify.com/api/token", {
|
|
21
|
-
method: "POST",
|
|
22
|
-
headers: {
|
|
23
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
24
|
-
"Authorization": `Basic ${basicAuth}`
|
|
25
|
-
},
|
|
26
|
-
body: new URLSearchParams({
|
|
27
|
-
grant_type: "authorization_code",
|
|
28
|
-
code: payload.code,
|
|
29
|
-
redirect_uri: payload.redirectUri
|
|
30
|
-
})
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
if (!tokenResponse.ok) {
|
|
34
|
-
logger.error("Failed to get Spotify access token", { detail: await tokenResponse.text() });
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const tokenData = await tokenResponse.json() as { access_token: string };
|
|
39
|
-
|
|
40
|
-
const profileResponse = await fetch("https://api.spotify.com/v1/me", {
|
|
41
|
-
headers: { "Authorization": `Bearer ${tokenData.access_token}` }
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (!profileResponse.ok) {
|
|
45
|
-
logger.error("Failed to get Spotify user info", { detail: await profileResponse.text() });
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const p = await profileResponse.json() as {
|
|
50
|
-
id: string; display_name?: string | null;
|
|
51
|
-
email?: string; images?: Array<{ url: string }>;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
if (!p.email) { logger.error("Spotify user has no email"); return null; }
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
providerId: p.id,
|
|
58
|
-
email: p.email,
|
|
59
|
-
displayName: p.display_name || null,
|
|
60
|
-
photoUrl: p.images?.[0]?.url || null,
|
|
61
|
-
emailVerified: true
|
|
62
|
-
};
|
|
63
|
-
} catch (error) {
|
|
64
|
-
logger.error("Spotify OAuth error", { error: error });
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { logger } from "../utils/logger";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a Twitter/X OAuth 2.0 Provider integration.
|
|
7
|
-
*
|
|
8
|
-
* Uses OAuth 2.0 with PKCE (authorization code flow). The frontend must include
|
|
9
|
-
* the PKCE `code_verifier` when sending the authorization code.
|
|
10
|
-
*
|
|
11
|
-
* Twitter API v2 requires the "tweet.read" and "users.read" scopes at minimum,
|
|
12
|
-
* plus "offline.access" if refresh tokens are needed on Twitter's side.
|
|
13
|
-
*/
|
|
14
|
-
export function createTwitterProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<{
|
|
15
|
-
code: string;
|
|
16
|
-
redirectUri: string;
|
|
17
|
-
codeVerifier: string;
|
|
18
|
-
}> {
|
|
19
|
-
return {
|
|
20
|
-
id: "twitter",
|
|
21
|
-
schema: z.object({
|
|
22
|
-
code: z.string().min(1, "Auth code is required"),
|
|
23
|
-
redirectUri: z.string().url("Valid redirect URI is required"),
|
|
24
|
-
codeVerifier: z.string().min(1, "PKCE code verifier is required")
|
|
25
|
-
}),
|
|
26
|
-
verify: async (payload: {
|
|
27
|
-
code: string;
|
|
28
|
-
redirectUri: string;
|
|
29
|
-
codeVerifier: string;
|
|
30
|
-
}): Promise<OAuthProviderProfile | null> => {
|
|
31
|
-
try {
|
|
32
|
-
// Twitter OAuth 2.0 uses Basic auth for the token endpoint
|
|
33
|
-
const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
|
|
34
|
-
|
|
35
|
-
// Exchange code for access token
|
|
36
|
-
const tokenResponse = await fetch("https://api.twitter.com/2/oauth2/token", {
|
|
37
|
-
method: "POST",
|
|
38
|
-
headers: {
|
|
39
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
40
|
-
"Authorization": `Basic ${basicAuth}`
|
|
41
|
-
},
|
|
42
|
-
body: new URLSearchParams({
|
|
43
|
-
code: payload.code,
|
|
44
|
-
grant_type: "authorization_code",
|
|
45
|
-
redirect_uri: payload.redirectUri,
|
|
46
|
-
code_verifier: payload.codeVerifier
|
|
47
|
-
})
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (!tokenResponse.ok) {
|
|
51
|
-
logger.error("Failed to get Twitter access token", { detail: await tokenResponse.text() });
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const tokenData = await tokenResponse.json() as { access_token: string };
|
|
56
|
-
const accessToken = tokenData.access_token;
|
|
57
|
-
|
|
58
|
-
// Fetch user profile from Twitter API v2
|
|
59
|
-
const profileResponse = await fetch(
|
|
60
|
-
"https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url",
|
|
61
|
-
{
|
|
62
|
-
headers: { "Authorization": `Bearer ${accessToken}` }
|
|
63
|
-
}
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
if (!profileResponse.ok) {
|
|
67
|
-
logger.error("Failed to get Twitter user info", { detail: await profileResponse.text() });
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const profileResult = await profileResponse.json() as {
|
|
72
|
-
data: {
|
|
73
|
-
id: string;
|
|
74
|
-
name: string;
|
|
75
|
-
username: string;
|
|
76
|
-
profile_image_url?: string;
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const profileData = profileResult.data;
|
|
81
|
-
|
|
82
|
-
// Twitter does NOT expose email via API v2 by default.
|
|
83
|
-
// It requires elevated access. If we can't get an email, we
|
|
84
|
-
// generate a placeholder email — the user can update it later.
|
|
85
|
-
// For apps with elevated access, fetch from v1.1 endpoint.
|
|
86
|
-
let email: string | null = null;
|
|
87
|
-
let emailVerified = false;
|
|
88
|
-
try {
|
|
89
|
-
const emailResponse = await fetch(
|
|
90
|
-
"https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
|
|
91
|
-
{
|
|
92
|
-
headers: { "Authorization": `Bearer ${accessToken}` }
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
if (emailResponse.ok) {
|
|
96
|
-
const emailData = await emailResponse.json() as { email?: string };
|
|
97
|
-
email = emailData.email || null;
|
|
98
|
-
if (email) {
|
|
99
|
-
emailVerified = true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
} catch {
|
|
103
|
-
// Elevated access not available — fall through
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!email) {
|
|
107
|
-
// Construct a deterministic placeholder email from the Twitter user ID.
|
|
108
|
-
// This allows the account to be created and linked; the user should
|
|
109
|
-
// update their email through the profile settings.
|
|
110
|
-
email = `${profileData.id}@twitter.placeholder.rebase`;
|
|
111
|
-
// Placeholder emails are NOT verified — prevents auto-linking to existing accounts
|
|
112
|
-
emailVerified = false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
providerId: profileData.id,
|
|
117
|
-
email,
|
|
118
|
-
displayName: profileData.name || profileData.username || null,
|
|
119
|
-
photoUrl: profileData.profile_image_url?.replace("_normal", "_400x400") || null,
|
|
120
|
-
emailVerified
|
|
121
|
-
};
|
|
122
|
-
} catch (error) {
|
|
123
|
-
logger.error("Twitter OAuth error", { error: error });
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { CollectionRegistry } from "@rebasepro/common";
|
|
2
|
-
import { CollectionRegistryInterface } from "../db/interfaces";
|
|
3
|
-
import { EntityCollection } from "@rebasepro/types";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Backend-agnostic collection registry.
|
|
7
|
-
* Satisfies CollectionRegistryInterface through inheritance from CollectionRegistry.
|
|
8
|
-
*/
|
|
9
|
-
export class BackendCollectionRegistry extends CollectionRegistry implements CollectionRegistryInterface {
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get the available relation keys for a given collection path.
|
|
13
|
-
* Maps from the collection's relation property names to the relation names.
|
|
14
|
-
*/
|
|
15
|
-
getRelationKeysForCollection(collectionPath: string): string[] {
|
|
16
|
-
const collection = this.getCollectionByPath(collectionPath) as (EntityCollection & { relations?: { relationName?: string, localKey?: string }[] }) | undefined;
|
|
17
|
-
if (!collection?.relations) return [];
|
|
18
|
-
return collection.relations.map(r => (r.relationName || r.localKey || "") as string).filter(Boolean);
|
|
19
|
-
}
|
|
20
|
-
}
|