@rebasepro/server-core 0.0.1-canary.09e5ec5
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 +56 -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 +58 -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 +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49934 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +49968 -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 +16 -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 +16 -0
- package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
- package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
- package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/index.d.ts +23 -0
- package/dist/server-core/src/auth/interfaces.d.ts +309 -0
- package/dist/server-core/src/auth/jwt.d.ts +43 -0
- package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
- package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
- package/dist/server-core/src/auth/middleware.d.ts +81 -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 +27 -0
- package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -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/cron/cron-loader.d.ts +17 -0
- package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
- package/dist/server-core/src/cron/cron-store.d.ts +32 -0
- package/dist/server-core/src/cron/index.d.ts +6 -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 +42 -0
- package/dist/server-core/src/email/types.d.ts +107 -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 +29 -0
- package/dist/server-core/src/init.d.ts +159 -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/singleton.d.ts +35 -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 +25 -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 +103 -0
- package/dist/server-core/src/types/index.d.ts +11 -0
- package/dist/server-core/src/utils/dev-port.d.ts +35 -0
- package/dist/server-core/src/utils/logger.d.ts +31 -0
- package/dist/server-core/src/utils/logging.d.ts +9 -0
- package/dist/server-core/src/utils/request-logger.d.ts +19 -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 +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -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/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -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 +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -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 +10 -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 +23 -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 +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -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 +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -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 +9 -0
- package/src/api/ast-schema-editor.ts +289 -0
- package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
- package/src/api/errors.ts +179 -0
- package/src/api/graphql/graphql-schema-generator.ts +336 -0
- package/src/api/graphql/index.ts +2 -0
- package/src/api/index.ts +11 -0
- package/src/api/openapi-generator.ts +715 -0
- package/src/api/rest/api-generator.ts +472 -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 +41 -0
- package/src/api/server.ts +248 -0
- package/src/api/types.ts +90 -0
- package/src/auth/admin-routes.ts +529 -0
- package/src/auth/apple-oauth.ts +130 -0
- package/src/auth/bitbucket-oauth.ts +82 -0
- package/src/auth/discord-oauth.ts +83 -0
- package/src/auth/facebook-oauth.ts +72 -0
- package/src/auth/github-oauth.ts +110 -0
- package/src/auth/gitlab-oauth.ts +70 -0
- package/src/auth/google-oauth.ts +48 -0
- package/src/auth/index.ts +34 -0
- package/src/auth/interfaces.ts +363 -0
- package/src/auth/jwt.ts +181 -0
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +384 -0
- package/src/auth/password.ts +77 -0
- package/src/auth/rate-limiter.ts +129 -0
- package/src/auth/routes.ts +788 -0
- package/src/auth/slack-oauth.ts +71 -0
- package/src/auth/spotify-oauth.ts +67 -0
- package/src/auth/twitter-oauth.ts +120 -0
- package/src/bootstrappers/index.ts +1 -0
- package/src/collections/BackendCollectionRegistry.ts +20 -0
- package/src/collections/loader.ts +49 -0
- package/src/cron/cron-loader.ts +89 -0
- package/src/cron/cron-routes.test.ts +265 -0
- package/src/cron/cron-routes.ts +85 -0
- package/src/cron/cron-scheduler.test.ts +421 -0
- package/src/cron/cron-scheduler.ts +413 -0
- package/src/cron/cron-store.ts +163 -0
- package/src/cron/index.ts +6 -0
- package/src/db/interfaces.ts +60 -0
- package/src/email/index.ts +18 -0
- package/src/email/smtp-email-service.ts +91 -0
- package/src/email/templates.ts +388 -0
- package/src/email/types.ts +105 -0
- package/src/functions/function-loader.ts +119 -0
- package/src/functions/function-routes.ts +31 -0
- package/src/functions/index.ts +3 -0
- package/src/history/history-routes.ts +129 -0
- package/src/history/index.ts +2 -0
- package/src/index.ts +66 -0
- package/src/init.ts +727 -0
- package/src/serve-spa.ts +81 -0
- package/src/services/driver-registry.ts +182 -0
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +365 -0
- package/src/storage/S3StorageController.ts +298 -0
- package/src/storage/index.ts +43 -0
- package/src/storage/routes.ts +264 -0
- package/src/storage/storage-registry.ts +187 -0
- package/src/storage/types.ts +134 -0
- package/src/types/index.ts +27 -0
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +38 -0
- package/src/utils/request-logger.ts +66 -0
- package/src/utils/sql.ts +38 -0
- package/test/admin-routes.test.ts +640 -0
- package/test/api-generator.test.ts +501 -0
- package/test/ast-schema-editor.test.ts +63 -0
- package/test/auth-middleware-hono.test.ts +556 -0
- package/test/auth-routes.test.ts +1047 -0
- package/test/driver-registry.test.ts +282 -0
- package/test/error-propagation.test.ts +226 -0
- package/test/errors-hono.test.ts +133 -0
- package/test/errors.test.ts +155 -0
- package/test/jwt-security.test.ts +182 -0
- package/test/jwt.test.ts +324 -0
- package/test/middleware.test.ts +300 -0
- package/test/password.test.ts +165 -0
- package/test/query-parser.test.ts +263 -0
- package/test/rate-limiter.test.ts +102 -0
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +271 -0
- package/test/storage-registry.test.ts +282 -0
- package/test/storage-routes.test.ts +222 -0
- package/test/storage-s3.test.ts +304 -0
- package/test-ast.ts +28 -0
- package/test.ts +6 -0
- package/test_output.txt +1133 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +80 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authentication Abstraction Interfaces
|
|
5
|
+
*
|
|
6
|
+
* These interfaces define the contracts for authentication-related operations.
|
|
7
|
+
* Implementations can use different databases (PostgreSQL, MongoDB, etc.) to
|
|
8
|
+
* store user, role, and token data.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* User data structure
|
|
13
|
+
*/
|
|
14
|
+
export interface UserData {
|
|
15
|
+
id: string;
|
|
16
|
+
email: string;
|
|
17
|
+
passwordHash?: string | null;
|
|
18
|
+
displayName?: string | null;
|
|
19
|
+
photoUrl?: string | null;
|
|
20
|
+
emailVerified: boolean;
|
|
21
|
+
emailVerificationToken?: string | null;
|
|
22
|
+
emailVerificationSentAt?: Date | null;
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
updatedAt: Date;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Data for creating a new user
|
|
29
|
+
*/
|
|
30
|
+
export interface CreateUserData {
|
|
31
|
+
email: string;
|
|
32
|
+
passwordHash?: string;
|
|
33
|
+
displayName?: string;
|
|
34
|
+
photoUrl?: string;
|
|
35
|
+
emailVerified?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* User Identity Data (OAuth accounts linked to user)
|
|
40
|
+
*/
|
|
41
|
+
export interface UserIdentityData {
|
|
42
|
+
id: string;
|
|
43
|
+
userId: string;
|
|
44
|
+
provider: string;
|
|
45
|
+
providerId: string;
|
|
46
|
+
profileData?: Record<string, unknown> | null;
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
updatedAt: Date;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Standardized profile data returned by an OAuth provider verification payload
|
|
53
|
+
*/
|
|
54
|
+
export interface OAuthProviderProfile {
|
|
55
|
+
providerId: string;
|
|
56
|
+
email: string;
|
|
57
|
+
displayName?: string | null;
|
|
58
|
+
photoUrl?: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pluggable OAuth Provider integration strategy
|
|
63
|
+
*/
|
|
64
|
+
export interface OAuthProvider<T = unknown> {
|
|
65
|
+
/** The identifier of the provider (e.g. "github", "google") */
|
|
66
|
+
id: string;
|
|
67
|
+
|
|
68
|
+
/** Zod schema validating the expected request payload (e.g. { code: string }) */
|
|
69
|
+
schema: z.ZodSchema<T>;
|
|
70
|
+
|
|
71
|
+
/** Verify external tokens/codes and return a standardized user profile */
|
|
72
|
+
verify: (payload: T) => Promise<OAuthProviderProfile | null>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Role data structure
|
|
77
|
+
*/
|
|
78
|
+
export interface RoleData {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
isAdmin: boolean;
|
|
82
|
+
defaultPermissions: {
|
|
83
|
+
read?: boolean;
|
|
84
|
+
create?: boolean;
|
|
85
|
+
edit?: boolean;
|
|
86
|
+
delete?: boolean;
|
|
87
|
+
} | null;
|
|
88
|
+
collectionPermissions: Record<string, {
|
|
89
|
+
read?: boolean;
|
|
90
|
+
create?: boolean;
|
|
91
|
+
edit?: boolean;
|
|
92
|
+
delete?: boolean;
|
|
93
|
+
}> | null;
|
|
94
|
+
config: Record<string, unknown> | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Data for creating a new role
|
|
99
|
+
*/
|
|
100
|
+
export interface CreateRoleData {
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
isAdmin?: boolean;
|
|
104
|
+
defaultPermissions?: RoleData["defaultPermissions"];
|
|
105
|
+
collectionPermissions?: RoleData["collectionPermissions"];
|
|
106
|
+
config?: RoleData["config"];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Refresh token info
|
|
111
|
+
*/
|
|
112
|
+
export interface RefreshTokenInfo {
|
|
113
|
+
id: string;
|
|
114
|
+
userId: string;
|
|
115
|
+
tokenHash: string;
|
|
116
|
+
expiresAt: Date;
|
|
117
|
+
createdAt: Date;
|
|
118
|
+
userAgent?: string | null;
|
|
119
|
+
ipAddress?: string | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Password reset token info
|
|
124
|
+
*/
|
|
125
|
+
export interface PasswordResetTokenInfo {
|
|
126
|
+
userId: string;
|
|
127
|
+
expiresAt: Date;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// AUTH REPOSITORY INTERFACES
|
|
132
|
+
// =============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Options for paginated user listing
|
|
136
|
+
*/
|
|
137
|
+
export interface ListUsersOptions {
|
|
138
|
+
/** Max results per page (default 25) */
|
|
139
|
+
limit?: number;
|
|
140
|
+
/** Number of results to skip (default 0) */
|
|
141
|
+
offset?: number;
|
|
142
|
+
/** Search term — matches against email and displayName (case-insensitive) */
|
|
143
|
+
search?: string;
|
|
144
|
+
/** Field to sort by (default "createdAt") */
|
|
145
|
+
orderBy?: string;
|
|
146
|
+
/** Sort direction (default "desc") */
|
|
147
|
+
orderDir?: "asc" | "desc";
|
|
148
|
+
/** Filter by role ID */
|
|
149
|
+
roleId?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Result of a paginated user listing
|
|
154
|
+
*/
|
|
155
|
+
export interface PaginatedUsersResult {
|
|
156
|
+
users: UserData[];
|
|
157
|
+
/** Total number of users matching the filters (ignoring limit/offset) */
|
|
158
|
+
total: number;
|
|
159
|
+
limit: number;
|
|
160
|
+
offset: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Abstract user repository interface.
|
|
165
|
+
* Handles all user-related database operations.
|
|
166
|
+
*/
|
|
167
|
+
export interface UserRepository {
|
|
168
|
+
/**
|
|
169
|
+
* Create a new user
|
|
170
|
+
*/
|
|
171
|
+
createUser(data: CreateUserData): Promise<UserData>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get a user by ID
|
|
175
|
+
*/
|
|
176
|
+
getUserById(id: string): Promise<UserData | null>;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get a user by email
|
|
180
|
+
*/
|
|
181
|
+
getUserByEmail(email: string): Promise<UserData | null>;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get a user by an OAuth identity
|
|
185
|
+
*/
|
|
186
|
+
getUserByIdentity(provider: string, providerId: string): Promise<UserData | null>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get all identities linked to a user
|
|
190
|
+
*/
|
|
191
|
+
getUserIdentities(userId: string): Promise<UserIdentityData[]>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Link a new OAuth identity to a user
|
|
195
|
+
*/
|
|
196
|
+
linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Update a user
|
|
200
|
+
*/
|
|
201
|
+
updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Delete a user
|
|
205
|
+
*/
|
|
206
|
+
deleteUser(id: string): Promise<void>;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* List all users (unbounded — use listUsersPaginated for large datasets)
|
|
210
|
+
*/
|
|
211
|
+
listUsers(): Promise<UserData[]>;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* List users with server-side pagination, search, and sorting.
|
|
215
|
+
*/
|
|
216
|
+
listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult>;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Update user's password hash
|
|
220
|
+
*/
|
|
221
|
+
updatePassword(id: string, passwordHash: string): Promise<void>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Set email verification status
|
|
225
|
+
*/
|
|
226
|
+
setEmailVerified(id: string, verified: boolean): Promise<void>;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Set email verification token
|
|
230
|
+
*/
|
|
231
|
+
setVerificationToken(id: string, token: string | null): Promise<void>;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Find user by email verification token
|
|
235
|
+
*/
|
|
236
|
+
getUserByVerificationToken(token: string): Promise<UserData | null>;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get roles for a user
|
|
240
|
+
*/
|
|
241
|
+
getUserRoles(userId: string): Promise<RoleData[]>;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get role IDs for a user
|
|
245
|
+
*/
|
|
246
|
+
getUserRoleIds(userId: string): Promise<string[]>;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Set roles for a user (replaces existing roles)
|
|
250
|
+
*/
|
|
251
|
+
setUserRoles(userId: string, roleIds: string[]): Promise<void>;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Assign a specific role to a new user
|
|
255
|
+
*/
|
|
256
|
+
assignDefaultRole(userId: string, roleId: string): Promise<void>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get user with their roles
|
|
260
|
+
*/
|
|
261
|
+
getUserWithRoles(userId: string): Promise<{ user: UserData; roles: RoleData[] } | null>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Abstract role repository interface.
|
|
266
|
+
* Handles all role-related database operations.
|
|
267
|
+
*/
|
|
268
|
+
export interface RoleRepository {
|
|
269
|
+
/**
|
|
270
|
+
* Get a role by ID
|
|
271
|
+
*/
|
|
272
|
+
getRoleById(id: string): Promise<RoleData | null>;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* List all roles
|
|
276
|
+
*/
|
|
277
|
+
listRoles(): Promise<RoleData[]>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create a new role
|
|
281
|
+
*/
|
|
282
|
+
createRole(data: CreateRoleData): Promise<RoleData>;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Update a role
|
|
286
|
+
*/
|
|
287
|
+
updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Delete a role
|
|
291
|
+
*/
|
|
292
|
+
deleteRole(id: string): Promise<void>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Abstract token repository interface.
|
|
297
|
+
* Handles refresh tokens and password reset tokens.
|
|
298
|
+
*/
|
|
299
|
+
export interface TokenRepository {
|
|
300
|
+
// Refresh tokens
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Create a new refresh token
|
|
304
|
+
*/
|
|
305
|
+
createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Find a refresh token by hash
|
|
309
|
+
*/
|
|
310
|
+
findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Delete a refresh token by hash
|
|
314
|
+
*/
|
|
315
|
+
deleteRefreshToken(tokenHash: string): Promise<void>;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Delete all refresh tokens for a user
|
|
319
|
+
*/
|
|
320
|
+
deleteAllRefreshTokensForUser(userId: string): Promise<void>;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* List all refresh tokens for a user
|
|
324
|
+
*/
|
|
325
|
+
listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]>;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Delete a specific refresh token by its primary key ID
|
|
329
|
+
*/
|
|
330
|
+
deleteRefreshTokenById(id: string, userId: string): Promise<void>;
|
|
331
|
+
|
|
332
|
+
// Password reset tokens
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create a password reset token
|
|
336
|
+
*/
|
|
337
|
+
createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Find a valid (not expired, not used) password reset token by hash
|
|
341
|
+
*/
|
|
342
|
+
findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Mark a password reset token as used
|
|
346
|
+
*/
|
|
347
|
+
markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Delete all password reset tokens for a user
|
|
351
|
+
*/
|
|
352
|
+
deleteAllPasswordResetTokensForUser(userId: string): Promise<void>;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Clean up expired tokens
|
|
356
|
+
*/
|
|
357
|
+
deleteExpiredTokens(): Promise<void>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Combined auth repository interface for convenience
|
|
362
|
+
*/
|
|
363
|
+
export interface AuthRepository extends UserRepository, RoleRepository, TokenRepository { }
|
package/src/auth/jwt.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
|
|
4
|
+
export interface JwtConfig {
|
|
5
|
+
secret: string;
|
|
6
|
+
accessExpiresIn?: string;
|
|
7
|
+
refreshExpiresIn?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AccessTokenPayload {
|
|
11
|
+
userId: string;
|
|
12
|
+
roles: string[];
|
|
13
|
+
uid?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let jwtConfig: JwtConfig = {
|
|
17
|
+
secret: "",
|
|
18
|
+
accessExpiresIn: "1h",
|
|
19
|
+
refreshExpiresIn: "30d"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configure JWT settings - call this during initialization.
|
|
24
|
+
* Validates the secret strength to prevent deployment with default/weak secrets.
|
|
25
|
+
*/
|
|
26
|
+
export function configureJwt(config: JwtConfig): void {
|
|
27
|
+
// Reject obviously weak/default secrets
|
|
28
|
+
const weakSecrets = new Set([
|
|
29
|
+
"secret",
|
|
30
|
+
"jwt-secret",
|
|
31
|
+
"jwt_secret",
|
|
32
|
+
"your-secret",
|
|
33
|
+
"your-super-secret-jwt-key-change-in-production",
|
|
34
|
+
"super-secret-jwt-key-change-in-production",
|
|
35
|
+
"change-me",
|
|
36
|
+
"changeme",
|
|
37
|
+
"password",
|
|
38
|
+
"test",
|
|
39
|
+
"mysecret",
|
|
40
|
+
"my-secret",
|
|
41
|
+
"my_secret",
|
|
42
|
+
"example-secret",
|
|
43
|
+
"please-change-me",
|
|
44
|
+
"replace-this-with-a-real-secret",
|
|
45
|
+
"default-secret"
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
if (!config.secret || config.secret.length < 32) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"JWT secret is too short. Must be at least 32 characters. " +
|
|
51
|
+
"Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (weakSecrets.has(config.secret.toLowerCase())) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"JWT secret is a known default/weak value. Please use a strong, randomly generated secret. " +
|
|
58
|
+
"Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
jwtConfig = {
|
|
63
|
+
...jwtConfig,
|
|
64
|
+
...config
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate an access token (short-lived, 1 hour by default)
|
|
70
|
+
*/
|
|
71
|
+
export function generateAccessToken(userId: string, roles: string[]): string {
|
|
72
|
+
if (!jwtConfig.secret) {
|
|
73
|
+
throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const payload: AccessTokenPayload = { userId,
|
|
77
|
+
roles };
|
|
78
|
+
|
|
79
|
+
return jwt.sign(payload, jwtConfig.secret, {
|
|
80
|
+
expiresIn: jwtConfig.accessExpiresIn as jwt.SignOptions["expiresIn"],
|
|
81
|
+
algorithm: "HS256"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the expiration time of an access token in milliseconds from now
|
|
87
|
+
*/
|
|
88
|
+
export function getAccessTokenExpiryMs(): number {
|
|
89
|
+
const duration = jwtConfig.accessExpiresIn || "1h";
|
|
90
|
+
const match = duration.match(/^(\d+)([dhms])$/);
|
|
91
|
+
|
|
92
|
+
if (!match) {
|
|
93
|
+
// Default to 1 hour
|
|
94
|
+
return 60 * 60 * 1000;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const value = parseInt(match[1], 10);
|
|
98
|
+
const unit = match[2];
|
|
99
|
+
|
|
100
|
+
switch (unit) {
|
|
101
|
+
case "d": return value * 24 * 60 * 60 * 1000;
|
|
102
|
+
case "h": return value * 60 * 60 * 1000;
|
|
103
|
+
case "m": return value * 60 * 1000;
|
|
104
|
+
case "s": return value * 1000;
|
|
105
|
+
default: return 60 * 60 * 1000;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the expiration timestamp for an access token
|
|
111
|
+
*/
|
|
112
|
+
export function getAccessTokenExpiry(): number {
|
|
113
|
+
return Date.now() + getAccessTokenExpiryMs();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Verify and decode an access token
|
|
118
|
+
*/
|
|
119
|
+
export function verifyAccessToken(token: string): AccessTokenPayload | null {
|
|
120
|
+
if (!jwtConfig.secret) {
|
|
121
|
+
throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const decoded = jwt.verify(token, jwtConfig.secret, { algorithms: ["HS256"] }) as { userId?: string; uid?: string; sub?: string; roles?: string[] };
|
|
126
|
+
const id = decoded.userId || decoded.uid || decoded.sub;
|
|
127
|
+
if (!id) {
|
|
128
|
+
console.error("[JWT] Verification failed: missing id in payload", decoded);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
userId: id,
|
|
134
|
+
roles: decoded.roles || []
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("[JWT] Verification failed:", error, "Token start:", token.substring(0, 15));
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate a random refresh token (long-lived, 30 days by default)
|
|
144
|
+
*/
|
|
145
|
+
export function generateRefreshToken(): string {
|
|
146
|
+
return randomBytes(40).toString("hex");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Hash a refresh token for database storage (don't store raw tokens)
|
|
151
|
+
*/
|
|
152
|
+
export function hashRefreshToken(token: string): string {
|
|
153
|
+
return createHash("sha256").update(token).digest("hex");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Calculate refresh token expiration date
|
|
158
|
+
*/
|
|
159
|
+
export function getRefreshTokenExpiry(): Date {
|
|
160
|
+
const duration = jwtConfig.refreshExpiresIn || "30d";
|
|
161
|
+
const match = duration.match(/^(\d+)([dhms])$/);
|
|
162
|
+
|
|
163
|
+
if (!match) {
|
|
164
|
+
// Default to 30 days
|
|
165
|
+
return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const value = parseInt(match[1], 10);
|
|
169
|
+
const unit = match[2];
|
|
170
|
+
|
|
171
|
+
let ms: number;
|
|
172
|
+
switch (unit) {
|
|
173
|
+
case "d": ms = value * 24 * 60 * 60 * 1000; break;
|
|
174
|
+
case "h": ms = value * 60 * 60 * 1000; break;
|
|
175
|
+
case "m": ms = value * 60 * 1000; break;
|
|
176
|
+
case "s": ms = value * 1000; break;
|
|
177
|
+
default: ms = 30 * 24 * 60 * 60 * 1000;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return new Date(Date.now() + ms);
|
|
181
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export interface LinkedinUserInfo {
|
|
5
|
+
linkedinId: string;
|
|
6
|
+
email: string;
|
|
7
|
+
displayName: string | null;
|
|
8
|
+
photoUrl: string | null;
|
|
9
|
+
emailVerified: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a LinkedIn OAuth Provider integration
|
|
14
|
+
*/
|
|
15
|
+
export function createLinkedinProvider(config: { clientId: string, clientSecret: string }): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
16
|
+
return {
|
|
17
|
+
id: "linkedin",
|
|
18
|
+
schema: z.object({
|
|
19
|
+
code: z.string().min(1, "Auth code is required"),
|
|
20
|
+
redirectUri: z.string().url("Valid redirect URI is required")
|
|
21
|
+
}),
|
|
22
|
+
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
23
|
+
try {
|
|
24
|
+
// Exchange code for access token
|
|
25
|
+
const tokenResponse = await fetch("https://www.linkedin.com/oauth/v2/accessToken", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
29
|
+
},
|
|
30
|
+
body: new URLSearchParams({
|
|
31
|
+
grant_type: "authorization_code",
|
|
32
|
+
code: payload.code,
|
|
33
|
+
redirect_uri: payload.redirectUri,
|
|
34
|
+
client_id: config.clientId,
|
|
35
|
+
client_secret: config.clientSecret
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!tokenResponse.ok) {
|
|
40
|
+
const errBody = await tokenResponse.text();
|
|
41
|
+
console.error("Failed to get LinkedIn access token:", errBody);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tokenData = await tokenResponse.json() as { access_token: string; id_token?: string };
|
|
46
|
+
const accessToken = tokenData.access_token;
|
|
47
|
+
|
|
48
|
+
// Fetch User Info using OIDC userinfo endpoint
|
|
49
|
+
const profileResponse = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
50
|
+
headers: {
|
|
51
|
+
"Authorization": `Bearer ${accessToken}`
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!profileResponse.ok) {
|
|
56
|
+
const errBody = await profileResponse.text();
|
|
57
|
+
console.error("Failed to get LinkedIn user info:", errBody);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const profileData = await profileResponse.json() as {
|
|
62
|
+
sub: string;
|
|
63
|
+
email: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
picture?: string;
|
|
66
|
+
email_verified?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
providerId: profileData.sub,
|
|
71
|
+
email: profileData.email,
|
|
72
|
+
displayName: profileData.name || null,
|
|
73
|
+
photoUrl: profileData.picture || null
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("LinkedIn OAuth error:", error);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Microsoft / Entra ID (Azure AD) OAuth Provider integration.
|
|
6
|
+
*
|
|
7
|
+
* Supports both personal Microsoft accounts and work/school (Azure AD) accounts
|
|
8
|
+
* via the "common" tenant endpoint. Uses the authorization code flow.
|
|
9
|
+
*/
|
|
10
|
+
export function createMicrosoftProvider(config: {
|
|
11
|
+
clientId: string;
|
|
12
|
+
clientSecret: string;
|
|
13
|
+
/** Tenant ID. Defaults to "common" which allows both personal and organizational accounts. */
|
|
14
|
+
tenantId?: string;
|
|
15
|
+
}): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
16
|
+
const tenantId = config.tenantId || "common";
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
id: "microsoft",
|
|
20
|
+
schema: z.object({
|
|
21
|
+
code: z.string().min(1, "Auth code is required"),
|
|
22
|
+
redirectUri: z.string().url("Valid redirect URI is required")
|
|
23
|
+
}),
|
|
24
|
+
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
25
|
+
try {
|
|
26
|
+
// Exchange code for access token
|
|
27
|
+
const tokenResponse = await fetch(
|
|
28
|
+
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
|
29
|
+
{
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
32
|
+
body: new URLSearchParams({
|
|
33
|
+
client_id: config.clientId,
|
|
34
|
+
client_secret: config.clientSecret,
|
|
35
|
+
code: payload.code,
|
|
36
|
+
redirect_uri: payload.redirectUri,
|
|
37
|
+
grant_type: "authorization_code",
|
|
38
|
+
scope: "openid profile email User.Read"
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!tokenResponse.ok) {
|
|
44
|
+
console.error("Failed to get Microsoft access token:", await tokenResponse.text());
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tokenData = await tokenResponse.json() as { access_token: string };
|
|
49
|
+
const accessToken = tokenData.access_token;
|
|
50
|
+
|
|
51
|
+
// Fetch user profile from Microsoft Graph
|
|
52
|
+
const profileResponse = await fetch("https://graph.microsoft.com/v1.0/me", {
|
|
53
|
+
headers: { "Authorization": `Bearer ${accessToken}` }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!profileResponse.ok) {
|
|
57
|
+
console.error("Failed to get Microsoft user info:", await profileResponse.text());
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const profileData = await profileResponse.json() as {
|
|
62
|
+
id: string;
|
|
63
|
+
displayName?: string | null;
|
|
64
|
+
mail?: string | null;
|
|
65
|
+
userPrincipalName?: string | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const email = profileData.mail || profileData.userPrincipalName;
|
|
69
|
+
if (!email) {
|
|
70
|
+
console.error("Microsoft user has no email");
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Attempt to fetch profile photo URL (Graph returns binary, not a URL).
|
|
75
|
+
// We skip this and let the frontend use the Microsoft Graph photo endpoint.
|
|
76
|
+
return {
|
|
77
|
+
providerId: profileData.id,
|
|
78
|
+
email,
|
|
79
|
+
displayName: profileData.displayName || null,
|
|
80
|
+
photoUrl: null
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Microsoft OAuth error:", error);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|