@rebasepro/server-core 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49931 -55
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +28475 -34569
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
- package/dist/server-core/src/api/types.d.ts +1 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
- 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 +5 -11
- package/dist/server-core/src/auth/index.d.ts +13 -2
- package/dist/server-core/src/auth/interfaces.d.ts +45 -6
- package/dist/server-core/src/auth/jwt.d.ts +1 -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 +36 -11
- package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
- package/dist/server-core/src/auth/routes.d.ts +13 -3
- 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/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/email/index.d.ts +2 -2
- package/dist/server-core/src/email/templates.d.ts +9 -0
- package/dist/server-core/src/email/types.d.ts +20 -23
- package/dist/server-core/src/history/history-routes.d.ts +2 -2
- package/dist/server-core/src/index.d.ts +5 -0
- package/dist/server-core/src/init.d.ts +112 -2
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
- package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
- package/dist/server-core/src/storage/index.d.ts +18 -11
- package/dist/server-core/src/storage/routes.d.ts +3 -3
- package/dist/server-core/src/storage/types.d.ts +28 -16
- 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/request-logger.d.ts +19 -0
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/package.json +84 -84
- package/scratch.ts +6 -5
- package/src/api/ast-schema-editor.ts +11 -11
- package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
- package/src/api/errors.ts +38 -14
- package/src/api/graphql/graphql-schema-generator.ts +5 -3
- package/src/api/openapi-generator.ts +613 -58
- package/src/api/rest/api-generator.ts +17 -11
- package/src/api/rest/query-parser.ts +1 -1
- package/src/api/schema-editor-routes.ts +2 -0
- package/src/api/server.ts +8 -5
- package/src/api/types.ts +1 -1
- package/src/auth/admin-routes.ts +58 -17
- 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 +34 -46
- package/src/auth/index.ts +15 -2
- package/src/auth/interfaces.ts +54 -7
- package/src/auth/jwt.ts +22 -5
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +194 -45
- package/src/auth/password.ts +3 -1
- package/src/auth/rate-limiter.ts +4 -4
- package/src/auth/routes.ts +197 -139
- 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/collections/loader.ts +6 -6
- 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 +2 -2
- package/src/email/index.ts +3 -2
- package/src/email/smtp-email-service.ts +5 -2
- package/src/email/templates.ts +93 -6
- package/src/email/types.ts +17 -24
- package/src/functions/function-loader.ts +34 -6
- package/src/functions/function-routes.ts +2 -2
- package/src/history/history-routes.ts +4 -3
- package/src/index.ts +10 -0
- package/src/init.ts +475 -57
- package/src/services/driver-registry.ts +1 -1
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +50 -53
- package/src/storage/S3StorageController.ts +49 -46
- package/src/storage/index.ts +27 -16
- package/src/storage/routes.ts +76 -59
- package/src/storage/storage-registry.ts +5 -5
- package/src/storage/types.ts +44 -32
- package/src/types/index.ts +4 -4
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +5 -2
- package/src/utils/request-logger.ts +66 -0
- package/test/admin-routes.test.ts +114 -65
- package/test/api-generator.test.ts +73 -30
- package/test/ast-schema-editor.test.ts +5 -3
- package/test/auth-middleware-hono.test.ts +268 -33
- package/test/auth-routes.test.ts +325 -146
- package/test/driver-registry.test.ts +4 -2
- package/test/error-propagation.test.ts +226 -0
- package/test/errors.test.ts +12 -7
- package/test/jwt-security.test.ts +18 -9
- package/test/jwt.test.ts +26 -13
- package/test/middleware.test.ts +13 -8
- package/test/query-parser.test.ts +13 -8
- package/test/rate-limiter.test.ts +7 -7
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +36 -43
- package/test/storage-registry.test.ts +38 -36
- package/test/storage-routes.test.ts +9 -5
- package/test/storage-s3.test.ts +51 -48
- package/test.ts +6 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +7 -5
- package/dist/index-BeMqpmfQ.js +0 -239
- package/dist/index-BeMqpmfQ.js.map +0 -1
- package/dist/index-bl4J3lNb.js +0 -55823
- package/dist/index-bl4J3lNb.js.map +0 -1
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +0 -46
package/src/auth/routes.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { ApiError } from "../api/errors";
|
|
2
|
+
import { ApiError, errorHandler } from "../api/errors";
|
|
3
3
|
import { randomBytes, createHash } from "crypto";
|
|
4
|
-
import type { AuthRepository } from "./interfaces";
|
|
4
|
+
import type { AuthRepository, OAuthProvider } from "./interfaces";
|
|
5
5
|
import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
|
|
6
6
|
import { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
|
|
7
|
-
import { verifyGoogleIdToken, isGoogleOAuthConfigured } from "./google-oauth";
|
|
8
7
|
import { requireAuth } from "./middleware";
|
|
9
8
|
import { EmailService, EmailConfig } from "../email";
|
|
10
|
-
import { getPasswordResetTemplate, getEmailVerificationTemplate } from "../email/templates";
|
|
9
|
+
import { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmailTemplate } from "../email/templates";
|
|
11
10
|
import { HonoEnv } from "../api/types";
|
|
12
11
|
import { defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";
|
|
13
12
|
import { z } from "zod";
|
|
@@ -19,10 +18,20 @@ export interface AuthModuleConfig {
|
|
|
19
18
|
authRepo: AuthRepository;
|
|
20
19
|
emailService?: EmailService;
|
|
21
20
|
emailConfig?: EmailConfig;
|
|
22
|
-
/** Allow new user registration (default: false).
|
|
21
|
+
/** Allow new user registration (default: false). */
|
|
23
22
|
allowRegistration?: boolean;
|
|
24
|
-
/** Default role ID to assign to new users (default: none).
|
|
23
|
+
/** Default role ID to assign to new users (default: none). Must NOT be "admin". */
|
|
25
24
|
defaultRole?: string;
|
|
25
|
+
/** Optional array of OAuth providers */
|
|
26
|
+
oauthProviders?: OAuthProvider[];
|
|
27
|
+
/** When true, blocks all self-registration regardless of `allowRegistration`. */
|
|
28
|
+
disableSelfRegistration?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Callback that checks if bootstrap has already been completed.
|
|
31
|
+
* Used by GET /auth/config to report `needsSetup` status.
|
|
32
|
+
* When not provided, falls back to checking if any users exist.
|
|
33
|
+
*/
|
|
34
|
+
isBootstrapCompleted?: () => Promise<boolean>;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
@@ -72,7 +81,17 @@ function getPasswordResetExpiry(): Date {
|
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
84
|
+
if (config.defaultRole === "admin") {
|
|
85
|
+
throw new Error("CRITICAL SECURITY ERROR: defaultRole cannot be 'admin'. Administrative privilege escalation via registration is strictly forbidden. Use the POST /admin/bootstrap endpoint to promote the initial administrator.");
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
const router = new Hono<HonoEnv>();
|
|
89
|
+
|
|
90
|
+
// Attach Rebase error handler to ensure ApiError exceptions are correctly
|
|
91
|
+
// formatted instead of caught by Hono's default error handler.
|
|
92
|
+
// Hono's onError does NOT propagate from parent to child routers.
|
|
93
|
+
router.onError(errorHandler);
|
|
94
|
+
|
|
76
95
|
const authRepo = config.authRepo;
|
|
77
96
|
const { emailService, emailConfig, allowRegistration = false } = config;
|
|
78
97
|
|
|
@@ -86,9 +105,6 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
86
105
|
email: z.string().email("Invalid email address").max(255),
|
|
87
106
|
password: z.string().min(1, "Password is required").max(128)
|
|
88
107
|
});
|
|
89
|
-
const googleSchema = z.object({
|
|
90
|
-
idToken: z.string().min(1, "ID token is required")
|
|
91
|
-
});
|
|
92
108
|
const forgotPasswordSchema = z.object({
|
|
93
109
|
email: z.string().email("Invalid email address").max(255)
|
|
94
110
|
});
|
|
@@ -129,13 +145,55 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
/**
|
|
132
|
-
* Check if registration is allowed
|
|
148
|
+
* Check if registration is allowed.
|
|
149
|
+
* Registration is only allowed when explicitly enabled via `allowRegistration`.
|
|
150
|
+
* First-user bootstrap must use POST /admin/bootstrap instead.
|
|
151
|
+
*/
|
|
152
|
+
function isRegistrationAllowed(): boolean {
|
|
153
|
+
if (config.disableSelfRegistration) return false;
|
|
154
|
+
return !!allowRegistration;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Send welcome email to a newly registered user (fire-and-forget).
|
|
159
|
+
*/
|
|
160
|
+
function sendWelcomeEmail(user: { email: string; displayName?: string | null }) {
|
|
161
|
+
if (!isEmailConfigured()) return;
|
|
162
|
+
const appName = emailConfig?.appName || "Rebase";
|
|
163
|
+
const loginUrl = emailConfig?.resetPasswordUrl || ""; // reuse base URL → the login / app page
|
|
164
|
+
const templateFn = emailConfig?.templates?.welcomeEmail;
|
|
165
|
+
const emailContent = templateFn
|
|
166
|
+
? templateFn(user, appName)
|
|
167
|
+
: getWelcomeEmailTemplate(user, appName, loginUrl ? `${loginUrl}/app` : undefined);
|
|
168
|
+
|
|
169
|
+
emailService!.send({
|
|
170
|
+
to: user.email,
|
|
171
|
+
subject: emailContent.subject,
|
|
172
|
+
html: emailContent.html,
|
|
173
|
+
text: emailContent.text
|
|
174
|
+
}).catch(err => {
|
|
175
|
+
console.error("Failed to send welcome email:", err instanceof Error ? err.message : err);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Helper to generate and store session tokens
|
|
133
181
|
*/
|
|
134
|
-
async function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
182
|
+
async function createSessionAndTokens(userId: string, userAgent: string, ipAddress: string) {
|
|
183
|
+
const roles = await authRepo.getUserRoles(userId);
|
|
184
|
+
const roleIds = roles.map(r => r.id);
|
|
185
|
+
const accessToken = generateAccessToken(userId, roleIds);
|
|
186
|
+
const refreshToken = generateRefreshToken();
|
|
187
|
+
|
|
188
|
+
await authRepo.createRefreshToken(
|
|
189
|
+
userId,
|
|
190
|
+
hashRefreshToken(refreshToken),
|
|
191
|
+
getRefreshTokenExpiry(),
|
|
192
|
+
userAgent,
|
|
193
|
+
ipAddress
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return { roleIds, accessToken, refreshToken };
|
|
139
197
|
}
|
|
140
198
|
|
|
141
199
|
/**
|
|
@@ -145,9 +203,13 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
145
203
|
router.post("/register", defaultAuthLimiter, async (c) => {
|
|
146
204
|
const { email, password, displayName } = parseBody(registerSchema, await c.req.json());
|
|
147
205
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
206
|
+
// Hard kill switch — blocks registration regardless of allowRegistration
|
|
207
|
+
if (config.disableSelfRegistration) {
|
|
208
|
+
throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if registration is allowed (no bypass for empty databases)
|
|
212
|
+
if (!isRegistrationAllowed()) {
|
|
151
213
|
throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
152
214
|
}
|
|
153
215
|
|
|
@@ -168,37 +230,24 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
168
230
|
const user = await authRepo.createUser({
|
|
169
231
|
email: email.toLowerCase(),
|
|
170
232
|
passwordHash,
|
|
171
|
-
displayName: displayName || undefined
|
|
172
|
-
provider: "email"
|
|
233
|
+
displayName: displayName || undefined
|
|
173
234
|
});
|
|
174
235
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
const isFirstUser = allUsers.length === 1;
|
|
178
|
-
if (isFirstUser) {
|
|
179
|
-
await authRepo.assignDefaultRole(user.id, "admin");
|
|
180
|
-
} else if (config.defaultRole) {
|
|
236
|
+
// Assign configured default role (never auto-assign admin via registration)
|
|
237
|
+
if (config.defaultRole) {
|
|
181
238
|
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
182
239
|
}
|
|
183
240
|
|
|
184
|
-
|
|
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(
|
|
241
|
+
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
195
242
|
user.id,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
userAgent,
|
|
199
|
-
ipAddress
|
|
243
|
+
c.req.header("user-agent") || "unknown",
|
|
244
|
+
c.req.header("x-forwarded-for") || "unknown"
|
|
200
245
|
);
|
|
201
246
|
|
|
247
|
+
// Send welcome email (fire-and-forget, don't block registration)
|
|
248
|
+
sendWelcomeEmail({ email: user.email,
|
|
249
|
+
displayName: user.displayName });
|
|
250
|
+
|
|
202
251
|
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
|
|
203
252
|
});
|
|
204
253
|
|
|
@@ -223,100 +272,81 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
223
272
|
throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
|
|
224
273
|
}
|
|
225
274
|
|
|
226
|
-
|
|
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(
|
|
275
|
+
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
238
276
|
user.id,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
userAgent,
|
|
242
|
-
ipAddress
|
|
277
|
+
c.req.header("user-agent") || "unknown",
|
|
278
|
+
c.req.header("x-forwarded-for") || "unknown"
|
|
243
279
|
);
|
|
244
280
|
|
|
245
281
|
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
|
|
246
282
|
});
|
|
247
283
|
|
|
248
284
|
/**
|
|
249
|
-
*
|
|
250
|
-
* Login/register with Google ID token
|
|
285
|
+
* Dynamically mount OAuth provider routes
|
|
251
286
|
*/
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
if (config.oauthProviders && config.oauthProviders.length > 0) {
|
|
288
|
+
for (const provider of config.oauthProviders) {
|
|
289
|
+
router.post(`/${provider.id}`, defaultAuthLimiter, async (c) => {
|
|
290
|
+
const payload = parseBody(provider.schema, await c.req.json());
|
|
291
|
+
|
|
292
|
+
const externalUser = await provider.verify(payload);
|
|
293
|
+
if (!externalUser) {
|
|
294
|
+
throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "INVALID_TOKEN");
|
|
295
|
+
}
|
|
258
296
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
297
|
+
// Find or create user
|
|
298
|
+
let user = await authRepo.getUserByIdentity(provider.id, externalUser.providerId);
|
|
299
|
+
|
|
300
|
+
if (!user) {
|
|
301
|
+
// Check if email exists (link accounts)
|
|
302
|
+
user = await authRepo.getUserByEmail(externalUser.email);
|
|
303
|
+
|
|
304
|
+
if (user) {
|
|
305
|
+
// Link Provider to existing account
|
|
306
|
+
await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
|
|
307
|
+
|
|
308
|
+
// Optional: Update profile info from external provider if empty
|
|
309
|
+
await authRepo.updateUser(user.id, {
|
|
310
|
+
displayName: user.displayName || externalUser.displayName || undefined,
|
|
311
|
+
photoUrl: user.photoUrl || externalUser.photoUrl || undefined
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
// Create new user
|
|
315
|
+
user = await authRepo.createUser({
|
|
316
|
+
email: externalUser.email.toLowerCase(),
|
|
317
|
+
displayName: externalUser.displayName || undefined,
|
|
318
|
+
photoUrl: externalUser.photoUrl || undefined
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
|
|
322
|
+
|
|
323
|
+
// Assign configured default role (never auto-assign admin via registration)
|
|
324
|
+
if (config.defaultRole) {
|
|
325
|
+
await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Send welcome email for new OAuth users (fire-and-forget)
|
|
329
|
+
sendWelcomeEmail({ email: user.email,
|
|
330
|
+
displayName: user.displayName });
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
// Update profile info from external provider
|
|
334
|
+
await authRepo.updateUser(user.id, {
|
|
335
|
+
displayName: externalUser.displayName || user.displayName || undefined,
|
|
336
|
+
photoUrl: externalUser.photoUrl || user.photoUrl || undefined
|
|
337
|
+
});
|
|
338
|
+
}
|
|
263
339
|
|
|
264
|
-
|
|
265
|
-
|
|
340
|
+
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
341
|
+
user.id,
|
|
342
|
+
c.req.header("user-agent") || "unknown",
|
|
343
|
+
c.req.header("x-forwarded-for") || "unknown"
|
|
344
|
+
);
|
|
266
345
|
|
|
267
|
-
|
|
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
|
|
346
|
+
return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
|
|
297
347
|
});
|
|
298
348
|
}
|
|
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
|
-
});
|
|
349
|
+
}
|
|
320
350
|
|
|
321
351
|
/**
|
|
322
352
|
* POST /auth/forgot-password
|
|
@@ -350,8 +380,10 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
350
380
|
const appName = emailConfig?.appName || "Rebase";
|
|
351
381
|
const templateFn = emailConfig?.templates?.passwordReset;
|
|
352
382
|
const emailContent = templateFn
|
|
353
|
-
? templateFn(resetUrl, { email: user.email,
|
|
354
|
-
|
|
383
|
+
? templateFn(resetUrl, { email: user.email,
|
|
384
|
+
displayName: user.displayName })
|
|
385
|
+
: getPasswordResetTemplate(resetUrl, { email: user.email,
|
|
386
|
+
displayName: user.displayName }, appName);
|
|
355
387
|
|
|
356
388
|
// Send email
|
|
357
389
|
try {
|
|
@@ -405,7 +437,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
405
437
|
// Invalidate all refresh tokens (security: log out all sessions)
|
|
406
438
|
await authRepo.deleteAllRefreshTokensForUser(storedToken.userId);
|
|
407
439
|
|
|
408
|
-
return c.json({ success: true,
|
|
440
|
+
return c.json({ success: true,
|
|
441
|
+
message: "Password has been reset successfully" });
|
|
409
442
|
});
|
|
410
443
|
|
|
411
444
|
/**
|
|
@@ -445,7 +478,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
445
478
|
// Invalidate all refresh tokens (security: log out all sessions)
|
|
446
479
|
await authRepo.deleteAllRefreshTokensForUser(user.id);
|
|
447
480
|
|
|
448
|
-
return c.json({ success: true,
|
|
481
|
+
return c.json({ success: true,
|
|
482
|
+
message: "Password has been changed successfully" });
|
|
449
483
|
});
|
|
450
484
|
|
|
451
485
|
/**
|
|
@@ -486,8 +520,10 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
486
520
|
const appName = emailConfig?.appName || "Rebase";
|
|
487
521
|
const templateFn = emailConfig?.templates?.emailVerification;
|
|
488
522
|
const emailContent = templateFn
|
|
489
|
-
? templateFn(verifyUrl, { email: user.email,
|
|
490
|
-
|
|
523
|
+
? templateFn(verifyUrl, { email: user.email,
|
|
524
|
+
displayName: user.displayName })
|
|
525
|
+
: getEmailVerificationTemplate(verifyUrl, { email: user.email,
|
|
526
|
+
displayName: user.displayName }, appName);
|
|
491
527
|
|
|
492
528
|
// Send email
|
|
493
529
|
await emailService!.send({
|
|
@@ -497,7 +533,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
497
533
|
text: emailContent.text
|
|
498
534
|
});
|
|
499
535
|
|
|
500
|
-
return c.json({ success: true,
|
|
536
|
+
return c.json({ success: true,
|
|
537
|
+
message: "Verification email sent" });
|
|
501
538
|
});
|
|
502
539
|
|
|
503
540
|
/**
|
|
@@ -520,7 +557,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
520
557
|
// Mark email as verified
|
|
521
558
|
await authRepo.setEmailVerified(user.id, true);
|
|
522
559
|
|
|
523
|
-
return c.json({ success: true,
|
|
560
|
+
return c.json({ success: true,
|
|
561
|
+
message: "Email verified successfully" });
|
|
524
562
|
});
|
|
525
563
|
|
|
526
564
|
/**
|
|
@@ -552,7 +590,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
552
590
|
// Rotate refresh token (delete old, create new)
|
|
553
591
|
const userAgent = c.req.header("user-agent") || "unknown";
|
|
554
592
|
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
555
|
-
|
|
593
|
+
|
|
556
594
|
await authRepo.deleteRefreshToken(tokenHash);
|
|
557
595
|
await authRepo.createRefreshToken(
|
|
558
596
|
storedToken.userId,
|
|
@@ -623,7 +661,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
623
661
|
}
|
|
624
662
|
|
|
625
663
|
await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
|
|
626
|
-
return c.json({ success: true,
|
|
664
|
+
return c.json({ success: true,
|
|
665
|
+
message: "All sessions revoked successfully" });
|
|
627
666
|
});
|
|
628
667
|
|
|
629
668
|
/**
|
|
@@ -642,7 +681,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
642
681
|
}
|
|
643
682
|
|
|
644
683
|
await authRepo.deleteRefreshTokenById(id, userCtx.userId);
|
|
645
|
-
return c.json({ success: true,
|
|
684
|
+
return c.json({ success: true,
|
|
685
|
+
message: "Session revoked successfully" });
|
|
646
686
|
});
|
|
647
687
|
|
|
648
688
|
/**
|
|
@@ -686,7 +726,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
686
726
|
|
|
687
727
|
const updatedUser = await authRepo.updateUser(userCtx.userId, {
|
|
688
728
|
displayName: displayName !== undefined ? displayName : undefined,
|
|
689
|
-
photoUrl: photoURL !== undefined ? photoURL : undefined
|
|
729
|
+
photoUrl: photoURL !== undefined ? photoURL : undefined
|
|
690
730
|
});
|
|
691
731
|
|
|
692
732
|
if (!updatedUser) {
|
|
@@ -715,14 +755,32 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
|
|
|
715
755
|
* Get public auth configuration (for frontend to know what's available)
|
|
716
756
|
*/
|
|
717
757
|
router.get("/config", defaultAuthLimiter, async (c) => {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
758
|
+
// Determine if setup is needed using the persistent bootstrap flag
|
|
759
|
+
// when available, falling back to user-count check for backward compat.
|
|
760
|
+
let needsSetup: boolean;
|
|
761
|
+
if (config.isBootstrapCompleted) {
|
|
762
|
+
needsSetup = !(await config.isBootstrapCompleted());
|
|
763
|
+
} else {
|
|
764
|
+
const allUsers = await authRepo.listUsers();
|
|
765
|
+
needsSetup = allUsers.length === 0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Registration is allowed when explicitly enabled OR during initial setup
|
|
769
|
+
const registrationAllowed = needsSetup || !!allowRegistration;
|
|
770
|
+
|
|
771
|
+
// Build a dynamic map of enabled providers for frontend discovery.
|
|
772
|
+
// Also maintain legacy boolean fields for backward compatibility.
|
|
773
|
+
const enabledProviders = (config.oauthProviders || []).map(p => p.id);
|
|
774
|
+
|
|
721
775
|
return c.json({
|
|
722
776
|
needsSetup,
|
|
723
777
|
registrationEnabled: registrationAllowed,
|
|
724
|
-
|
|
725
|
-
|
|
778
|
+
// Legacy fields (kept for backward compat)
|
|
779
|
+
googleEnabled: enabledProviders.includes("google"),
|
|
780
|
+
linkedinEnabled: enabledProviders.includes("linkedin"),
|
|
781
|
+
emailServiceEnabled: isEmailConfigured(),
|
|
782
|
+
// New: complete list of available OAuth providers
|
|
783
|
+
enabledProviders
|
|
726
784
|
});
|
|
727
785
|
});
|
|
728
786
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Slack OAuth Provider integration (OAuth 2.0 / "Sign in with Slack").
|
|
6
|
+
* Uses the OpenID Connect flow with the "openid,email,profile" scopes.
|
|
7
|
+
*/
|
|
8
|
+
export function createSlackProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
9
|
+
return {
|
|
10
|
+
id: "slack",
|
|
11
|
+
schema: z.object({
|
|
12
|
+
code: z.string().min(1, "Auth code is required"),
|
|
13
|
+
redirectUri: z.string().url("Valid redirect URI is required")
|
|
14
|
+
}),
|
|
15
|
+
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
16
|
+
try {
|
|
17
|
+
const tokenResponse = await fetch("https://slack.com/api/openid.connect.token", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
20
|
+
body: new URLSearchParams({
|
|
21
|
+
client_id: config.clientId,
|
|
22
|
+
client_secret: config.clientSecret,
|
|
23
|
+
code: payload.code,
|
|
24
|
+
redirect_uri: payload.redirectUri,
|
|
25
|
+
grant_type: "authorization_code"
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!tokenResponse.ok) {
|
|
30
|
+
console.error("Failed to get Slack access token:", await tokenResponse.text());
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tokenData = await tokenResponse.json() as { ok: boolean; access_token?: string; error?: string };
|
|
35
|
+
if (!tokenData.ok || !tokenData.access_token) {
|
|
36
|
+
console.error("Slack token exchange failed:", tokenData.error);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const profileResponse = await fetch("https://slack.com/api/openid.connect.userInfo", {
|
|
41
|
+
headers: { "Authorization": `Bearer ${tokenData.access_token}` }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!profileResponse.ok) {
|
|
45
|
+
console.error("Failed to get Slack user info:", await profileResponse.text());
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const p = await profileResponse.json() as {
|
|
50
|
+
ok: boolean; sub: string; name?: string;
|
|
51
|
+
email?: string; picture?: string; email_verified?: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!p.ok || !p.email) {
|
|
55
|
+
console.error("Slack user has no email");
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
providerId: p.sub,
|
|
61
|
+
email: p.email,
|
|
62
|
+
displayName: p.name || null,
|
|
63
|
+
photoUrl: p.picture || null
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Slack OAuth error:", error);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Spotify OAuth Provider integration.
|
|
6
|
+
* Uses the authorization code flow with the "user-read-email" scope.
|
|
7
|
+
*/
|
|
8
|
+
export function createSpotifyProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
9
|
+
return {
|
|
10
|
+
id: "spotify",
|
|
11
|
+
schema: z.object({
|
|
12
|
+
code: z.string().min(1, "Auth code is required"),
|
|
13
|
+
redirectUri: z.string().url("Valid redirect URI is required")
|
|
14
|
+
}),
|
|
15
|
+
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
16
|
+
try {
|
|
17
|
+
const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
|
|
18
|
+
|
|
19
|
+
const tokenResponse = await fetch("https://accounts.spotify.com/api/token", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
23
|
+
"Authorization": `Basic ${basicAuth}`
|
|
24
|
+
},
|
|
25
|
+
body: new URLSearchParams({
|
|
26
|
+
grant_type: "authorization_code",
|
|
27
|
+
code: payload.code,
|
|
28
|
+
redirect_uri: payload.redirectUri
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!tokenResponse.ok) {
|
|
33
|
+
console.error("Failed to get Spotify access token:", await tokenResponse.text());
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tokenData = await tokenResponse.json() as { access_token: string };
|
|
38
|
+
|
|
39
|
+
const profileResponse = await fetch("https://api.spotify.com/v1/me", {
|
|
40
|
+
headers: { "Authorization": `Bearer ${tokenData.access_token}` }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!profileResponse.ok) {
|
|
44
|
+
console.error("Failed to get Spotify user info:", await profileResponse.text());
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const p = await profileResponse.json() as {
|
|
49
|
+
id: string; display_name?: string | null;
|
|
50
|
+
email?: string; images?: Array<{ url: string }>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (!p.email) { console.error("Spotify user has no email"); return null; }
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
providerId: p.id,
|
|
57
|
+
email: p.email,
|
|
58
|
+
displayName: p.display_name || null,
|
|
59
|
+
photoUrl: p.images?.[0]?.url || null
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Spotify OAuth error:", error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|