@spfn/auth 0.2.0-beta.5 → 0.2.0-beta.50
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 +831 -198
- package/dist/{dto-Bb2qFUO6.d.ts → authenticate-eucncHxN.d.ts} +452 -161
- package/dist/config.d.ts +176 -44
- package/dist/config.js +99 -35
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +30 -2
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +287 -113
- package/dist/index.js +59 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +235 -13
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/client.d.ts +28 -0
- package/dist/nextjs/client.js +80 -0
- package/dist/nextjs/client.js.map +1 -0
- package/dist/nextjs/server.d.ts +90 -2
- package/dist/nextjs/server.js +146 -21
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +828 -416
- package/dist/server.js +1405 -592
- package/dist/server.js.map +1 -1
- package/migrations/0001_smooth_the_fury.sql +3 -0
- package/migrations/0002_deep_iceman.sql +11 -0
- package/migrations/0003_perfect_deathbird.sql +3 -0
- package/migrations/0004_concerned_rawhide_kid.sql +5 -0
- package/migrations/meta/0001_snapshot.json +1660 -0
- package/migrations/meta/0002_snapshot.json +1660 -0
- package/migrations/meta/0003_snapshot.json +1689 -0
- package/migrations/meta/0004_snapshot.json +1721 -0
- package/migrations/meta/_journal.json +28 -0
- package/package.json +13 -9
package/dist/server.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
-
}) : x)(function(x) {
|
|
6
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
-
});
|
|
9
3
|
var __esm = (fn, res) => function __init() {
|
|
10
4
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
5
|
};
|
|
@@ -1365,7 +1359,7 @@ var init_literal2 = __esm({
|
|
|
1365
1359
|
});
|
|
1366
1360
|
|
|
1367
1361
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/boolean/boolean.mjs
|
|
1368
|
-
function
|
|
1362
|
+
function Boolean2(options) {
|
|
1369
1363
|
return CreateType({ [Kind]: "Boolean", type: "boolean" }, options);
|
|
1370
1364
|
}
|
|
1371
1365
|
var init_boolean = __esm({
|
|
@@ -1447,7 +1441,7 @@ var init_string2 = __esm({
|
|
|
1447
1441
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/template-literal/syntax.mjs
|
|
1448
1442
|
function* FromUnion(syntax) {
|
|
1449
1443
|
const trim = syntax.trim().replace(/"|'/g, "");
|
|
1450
|
-
return trim === "boolean" ? yield
|
|
1444
|
+
return trim === "boolean" ? yield Boolean2() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt() : trim === "string" ? yield String2() : yield (() => {
|
|
1451
1445
|
const literals = trim.split("|").map((literal) => Literal(literal.trim()));
|
|
1452
1446
|
return literals.length === 0 ? Never() : literals.length === 1 ? literals[0] : UnionEvaluated(literals);
|
|
1453
1447
|
})();
|
|
@@ -4250,7 +4244,7 @@ __export(type_exports3, {
|
|
|
4250
4244
|
AsyncIterator: () => AsyncIterator,
|
|
4251
4245
|
Awaited: () => Awaited,
|
|
4252
4246
|
BigInt: () => BigInt,
|
|
4253
|
-
Boolean: () =>
|
|
4247
|
+
Boolean: () => Boolean2,
|
|
4254
4248
|
Capitalize: () => Capitalize,
|
|
4255
4249
|
Composite: () => Composite,
|
|
4256
4250
|
Const: () => Const,
|
|
@@ -4570,7 +4564,7 @@ var init_roles = __esm({
|
|
|
4570
4564
|
});
|
|
4571
4565
|
|
|
4572
4566
|
// src/server/entities/users.ts
|
|
4573
|
-
import { text as text2, check, boolean as boolean2, index as index2 } from "drizzle-orm/pg-core";
|
|
4567
|
+
import { text as text2, check, boolean as boolean2, index as index2, uuid } from "drizzle-orm/pg-core";
|
|
4574
4568
|
import { id as id2, timestamps as timestamps2, enumText, utcTimestamp, foreignKey } from "@spfn/core/db";
|
|
4575
4569
|
import { sql } from "drizzle-orm";
|
|
4576
4570
|
var users;
|
|
@@ -4585,6 +4579,9 @@ var init_users = __esm({
|
|
|
4585
4579
|
{
|
|
4586
4580
|
// Identity
|
|
4587
4581
|
id: id2(),
|
|
4582
|
+
// Public-facing UUID (for URLs, external APIs)
|
|
4583
|
+
// Never expose internal bigserial ID externally
|
|
4584
|
+
publicId: uuid("public_id").notNull().unique().defaultRandom(),
|
|
4588
4585
|
// Email address (unique identifier)
|
|
4589
4586
|
// Used for: login, password reset, notifications
|
|
4590
4587
|
email: text2("email").unique(),
|
|
@@ -4592,6 +4589,9 @@ var init_users = __esm({
|
|
|
4592
4589
|
// Format: +[country code][number] (e.g., +821012345678)
|
|
4593
4590
|
// Used for: SMS login, 2FA, notifications
|
|
4594
4591
|
phone: text2("phone").unique(),
|
|
4592
|
+
// Username (unique, optional)
|
|
4593
|
+
// Used for: display, mention, public profile URL
|
|
4594
|
+
username: text2("username").unique(),
|
|
4595
4595
|
// Authentication
|
|
4596
4596
|
// Bcrypt password hash ($2b$10$[salt][hash], 60 chars)
|
|
4597
4597
|
// Nullable to support OAuth-only accounts
|
|
@@ -4629,8 +4629,10 @@ var init_users = __esm({
|
|
|
4629
4629
|
sql`${table.email} IS NOT NULL OR ${table.phone} IS NOT NULL`
|
|
4630
4630
|
),
|
|
4631
4631
|
// Indexes for query optimization
|
|
4632
|
+
index2("users_public_id_idx").on(table.publicId),
|
|
4632
4633
|
index2("users_email_idx").on(table.email),
|
|
4633
4634
|
index2("users_phone_idx").on(table.phone),
|
|
4635
|
+
index2("users_username_idx").on(table.username),
|
|
4634
4636
|
index2("users_status_idx").on(table.status),
|
|
4635
4637
|
index2("users_role_id_idx").on(table.roleId)
|
|
4636
4638
|
]
|
|
@@ -4655,8 +4657,8 @@ var init_user_profiles = __esm({
|
|
|
4655
4657
|
// Foreign key to users table
|
|
4656
4658
|
userId: foreignKey2("user", () => users.id).unique(),
|
|
4657
4659
|
// Display Information
|
|
4658
|
-
// Display name shown in UI (
|
|
4659
|
-
displayName: text3("display_name")
|
|
4660
|
+
// Display name shown in UI (optional)
|
|
4661
|
+
displayName: text3("display_name"),
|
|
4660
4662
|
// First name (optional)
|
|
4661
4663
|
firstName: text3("first_name"),
|
|
4662
4664
|
// Last name (optional)
|
|
@@ -5297,6 +5299,27 @@ var init_user_permissions = __esm({
|
|
|
5297
5299
|
}
|
|
5298
5300
|
});
|
|
5299
5301
|
|
|
5302
|
+
// src/server/entities/auth-metadata.ts
|
|
5303
|
+
import { text as text10, timestamp } from "drizzle-orm/pg-core";
|
|
5304
|
+
var authMetadata;
|
|
5305
|
+
var init_auth_metadata = __esm({
|
|
5306
|
+
"src/server/entities/auth-metadata.ts"() {
|
|
5307
|
+
"use strict";
|
|
5308
|
+
init_schema4();
|
|
5309
|
+
authMetadata = authSchema.table(
|
|
5310
|
+
"auth_metadata",
|
|
5311
|
+
{
|
|
5312
|
+
// Metadata key (primary key)
|
|
5313
|
+
key: text10("key").primaryKey(),
|
|
5314
|
+
// Metadata value
|
|
5315
|
+
value: text10("value").notNull(),
|
|
5316
|
+
// Last updated timestamp
|
|
5317
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
5318
|
+
}
|
|
5319
|
+
);
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
5322
|
+
|
|
5300
5323
|
// src/server/entities/index.ts
|
|
5301
5324
|
var init_entities = __esm({
|
|
5302
5325
|
"src/server/entities/index.ts"() {
|
|
@@ -5312,6 +5335,7 @@ var init_entities = __esm({
|
|
|
5312
5335
|
init_permissions();
|
|
5313
5336
|
init_role_permissions();
|
|
5314
5337
|
init_user_permissions();
|
|
5338
|
+
init_auth_metadata();
|
|
5315
5339
|
}
|
|
5316
5340
|
});
|
|
5317
5341
|
|
|
@@ -5349,6 +5373,22 @@ var init_users_repository = __esm({
|
|
|
5349
5373
|
const result = await this.readDb.select().from(users).where(eq(users.phone, phone)).limit(1);
|
|
5350
5374
|
return result[0] ?? null;
|
|
5351
5375
|
}
|
|
5376
|
+
/**
|
|
5377
|
+
* 사용자명으로 사용자 조회
|
|
5378
|
+
* Read replica 사용
|
|
5379
|
+
*/
|
|
5380
|
+
async findByUsername(username) {
|
|
5381
|
+
const result = await this.readDb.select().from(users).where(eq(users.username, username)).limit(1);
|
|
5382
|
+
return result[0] ?? null;
|
|
5383
|
+
}
|
|
5384
|
+
/**
|
|
5385
|
+
* Public ID(UUID)로 사용자 조회
|
|
5386
|
+
* Read replica 사용
|
|
5387
|
+
*/
|
|
5388
|
+
async findByPublicId(publicId) {
|
|
5389
|
+
const result = await this.readDb.select().from(users).where(eq(users.publicId, publicId)).limit(1);
|
|
5390
|
+
return result[0] ?? null;
|
|
5391
|
+
}
|
|
5352
5392
|
/**
|
|
5353
5393
|
* 이메일 또는 전화번호로 사용자 조회
|
|
5354
5394
|
* Read replica 사용
|
|
@@ -5361,6 +5401,28 @@ var init_users_repository = __esm({
|
|
|
5361
5401
|
}
|
|
5362
5402
|
return null;
|
|
5363
5403
|
}
|
|
5404
|
+
/**
|
|
5405
|
+
* ID로 사용자 + Role 조회 (leftJoin)
|
|
5406
|
+
* Read replica 사용
|
|
5407
|
+
*
|
|
5408
|
+
* roleId가 null인 유저는 role: null 반환
|
|
5409
|
+
*/
|
|
5410
|
+
async findByIdWithRole(id11) {
|
|
5411
|
+
const result = await this.readDb.select({
|
|
5412
|
+
user: users,
|
|
5413
|
+
roleName: roles.name,
|
|
5414
|
+
roleDisplayName: roles.displayName,
|
|
5415
|
+
rolePriority: roles.priority
|
|
5416
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id11)).limit(1);
|
|
5417
|
+
const row = result[0];
|
|
5418
|
+
if (!row) {
|
|
5419
|
+
return null;
|
|
5420
|
+
}
|
|
5421
|
+
return {
|
|
5422
|
+
user: row.user,
|
|
5423
|
+
role: row.roleName ? { name: row.roleName, displayName: row.roleDisplayName, priority: row.rolePriority } : null
|
|
5424
|
+
};
|
|
5425
|
+
}
|
|
5364
5426
|
/**
|
|
5365
5427
|
* 사용자 생성
|
|
5366
5428
|
* Write primary 사용
|
|
@@ -5467,7 +5529,9 @@ var init_users_repository = __esm({
|
|
|
5467
5529
|
async fetchMinimalUserData(userId) {
|
|
5468
5530
|
const user = await this.readDb.select({
|
|
5469
5531
|
id: users.id,
|
|
5532
|
+
publicId: users.publicId,
|
|
5470
5533
|
email: users.email,
|
|
5534
|
+
username: users.username,
|
|
5471
5535
|
emailVerifiedAt: users.emailVerifiedAt,
|
|
5472
5536
|
phoneVerifiedAt: users.phoneVerifiedAt
|
|
5473
5537
|
}).from(users).where(eq(users.id, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
@@ -5476,7 +5540,9 @@ var init_users_repository = __esm({
|
|
|
5476
5540
|
}
|
|
5477
5541
|
return {
|
|
5478
5542
|
userId: user.id,
|
|
5543
|
+
publicId: user.publicId,
|
|
5479
5544
|
email: user.email,
|
|
5545
|
+
username: user.username,
|
|
5480
5546
|
isEmailVerified: !!user.emailVerifiedAt,
|
|
5481
5547
|
isPhoneVerified: !!user.phoneVerifiedAt
|
|
5482
5548
|
};
|
|
@@ -5491,7 +5557,9 @@ var init_users_repository = __esm({
|
|
|
5491
5557
|
async fetchFullUserData(userId) {
|
|
5492
5558
|
const user = await this.readDb.select({
|
|
5493
5559
|
id: users.id,
|
|
5560
|
+
publicId: users.publicId,
|
|
5494
5561
|
email: users.email,
|
|
5562
|
+
username: users.username,
|
|
5495
5563
|
emailVerifiedAt: users.emailVerifiedAt,
|
|
5496
5564
|
phoneVerifiedAt: users.phoneVerifiedAt,
|
|
5497
5565
|
lastLoginAt: users.lastLoginAt,
|
|
@@ -5503,7 +5571,9 @@ var init_users_repository = __esm({
|
|
|
5503
5571
|
}
|
|
5504
5572
|
return {
|
|
5505
5573
|
userId: user.id,
|
|
5574
|
+
publicId: user.publicId,
|
|
5506
5575
|
email: user.email,
|
|
5576
|
+
username: user.username,
|
|
5507
5577
|
isEmailVerified: !!user.emailVerifiedAt,
|
|
5508
5578
|
isPhoneVerified: !!user.phoneVerifiedAt,
|
|
5509
5579
|
lastLoginAt: user.lastLoginAt,
|
|
@@ -6087,6 +6157,13 @@ var init_user_profiles_repository = __esm({
|
|
|
6087
6157
|
const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id, id11)).limit(1);
|
|
6088
6158
|
return result[0] ?? null;
|
|
6089
6159
|
}
|
|
6160
|
+
/**
|
|
6161
|
+
* User ID로 locale만 조회 (경량)
|
|
6162
|
+
*/
|
|
6163
|
+
async findLocaleByUserId(userId) {
|
|
6164
|
+
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1);
|
|
6165
|
+
return result[0]?.locale || "en";
|
|
6166
|
+
}
|
|
6090
6167
|
/**
|
|
6091
6168
|
* User ID로 프로필 조회
|
|
6092
6169
|
*/
|
|
@@ -6260,16 +6337,16 @@ var init_invitations_repository = __esm({
|
|
|
6260
6337
|
/**
|
|
6261
6338
|
* 초대 상태 업데이트
|
|
6262
6339
|
*/
|
|
6263
|
-
async updateStatus(id11, status,
|
|
6340
|
+
async updateStatus(id11, status, timestamp2) {
|
|
6264
6341
|
const updates = {
|
|
6265
6342
|
status,
|
|
6266
6343
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6267
6344
|
};
|
|
6268
|
-
if (
|
|
6345
|
+
if (timestamp2) {
|
|
6269
6346
|
if (status === "accepted") {
|
|
6270
|
-
updates.acceptedAt =
|
|
6347
|
+
updates.acceptedAt = timestamp2;
|
|
6271
6348
|
} else if (status === "cancelled") {
|
|
6272
|
-
updates.cancelledAt =
|
|
6349
|
+
updates.cancelledAt = timestamp2;
|
|
6273
6350
|
}
|
|
6274
6351
|
}
|
|
6275
6352
|
const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id, id11)).returning();
|
|
@@ -6413,6 +6490,133 @@ var init_invitations_repository = __esm({
|
|
|
6413
6490
|
}
|
|
6414
6491
|
});
|
|
6415
6492
|
|
|
6493
|
+
// src/server/repositories/social-accounts.repository.ts
|
|
6494
|
+
import { eq as eq10, and as and7 } from "drizzle-orm";
|
|
6495
|
+
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
6496
|
+
var SocialAccountsRepository, socialAccountsRepository;
|
|
6497
|
+
var init_social_accounts_repository = __esm({
|
|
6498
|
+
"src/server/repositories/social-accounts.repository.ts"() {
|
|
6499
|
+
"use strict";
|
|
6500
|
+
init_entities();
|
|
6501
|
+
SocialAccountsRepository = class extends BaseRepository10 {
|
|
6502
|
+
/**
|
|
6503
|
+
* provider와 providerUserId로 소셜 계정 조회
|
|
6504
|
+
* Read replica 사용
|
|
6505
|
+
*/
|
|
6506
|
+
async findByProviderAndProviderId(provider, providerUserId) {
|
|
6507
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6508
|
+
and7(
|
|
6509
|
+
eq10(userSocialAccounts.provider, provider),
|
|
6510
|
+
eq10(userSocialAccounts.providerUserId, providerUserId)
|
|
6511
|
+
)
|
|
6512
|
+
).limit(1);
|
|
6513
|
+
return result[0] ?? null;
|
|
6514
|
+
}
|
|
6515
|
+
/**
|
|
6516
|
+
* userId로 모든 소셜 계정 조회
|
|
6517
|
+
* Read replica 사용
|
|
6518
|
+
*/
|
|
6519
|
+
async findByUserId(userId) {
|
|
6520
|
+
return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
|
|
6521
|
+
}
|
|
6522
|
+
/**
|
|
6523
|
+
* userId와 provider로 소셜 계정 조회
|
|
6524
|
+
* Read replica 사용
|
|
6525
|
+
*/
|
|
6526
|
+
async findByUserIdAndProvider(userId, provider) {
|
|
6527
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6528
|
+
and7(
|
|
6529
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6530
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6531
|
+
)
|
|
6532
|
+
).limit(1);
|
|
6533
|
+
return result[0] ?? null;
|
|
6534
|
+
}
|
|
6535
|
+
/**
|
|
6536
|
+
* 소셜 계정 생성
|
|
6537
|
+
* Write primary 사용
|
|
6538
|
+
*/
|
|
6539
|
+
async create(data) {
|
|
6540
|
+
return await this._create(userSocialAccounts, {
|
|
6541
|
+
...data,
|
|
6542
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
6543
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6546
|
+
/**
|
|
6547
|
+
* 토큰 정보 업데이트
|
|
6548
|
+
* Write primary 사용
|
|
6549
|
+
*/
|
|
6550
|
+
async updateTokens(id11, data) {
|
|
6551
|
+
const result = await this.db.update(userSocialAccounts).set({
|
|
6552
|
+
...data,
|
|
6553
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6554
|
+
}).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6555
|
+
return result[0] ?? null;
|
|
6556
|
+
}
|
|
6557
|
+
/**
|
|
6558
|
+
* 소셜 계정 삭제
|
|
6559
|
+
* Write primary 사용
|
|
6560
|
+
*/
|
|
6561
|
+
async deleteById(id11) {
|
|
6562
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6563
|
+
return result[0] ?? null;
|
|
6564
|
+
}
|
|
6565
|
+
/**
|
|
6566
|
+
* userId와 provider로 소셜 계정 삭제
|
|
6567
|
+
* Write primary 사용
|
|
6568
|
+
*/
|
|
6569
|
+
async deleteByUserIdAndProvider(userId, provider) {
|
|
6570
|
+
const result = await this.db.delete(userSocialAccounts).where(
|
|
6571
|
+
and7(
|
|
6572
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6573
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6574
|
+
)
|
|
6575
|
+
).returning();
|
|
6576
|
+
return result[0] ?? null;
|
|
6577
|
+
}
|
|
6578
|
+
};
|
|
6579
|
+
socialAccountsRepository = new SocialAccountsRepository();
|
|
6580
|
+
}
|
|
6581
|
+
});
|
|
6582
|
+
|
|
6583
|
+
// src/server/repositories/auth-metadata.repository.ts
|
|
6584
|
+
import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
|
|
6585
|
+
import { eq as eq11 } from "drizzle-orm";
|
|
6586
|
+
var AuthMetadataRepository, authMetadataRepository;
|
|
6587
|
+
var init_auth_metadata_repository = __esm({
|
|
6588
|
+
"src/server/repositories/auth-metadata.repository.ts"() {
|
|
6589
|
+
"use strict";
|
|
6590
|
+
init_auth_metadata();
|
|
6591
|
+
AuthMetadataRepository = class extends BaseRepository11 {
|
|
6592
|
+
/**
|
|
6593
|
+
* 키로 값 조회
|
|
6594
|
+
*/
|
|
6595
|
+
async get(key) {
|
|
6596
|
+
const result = await this.readDb.select().from(authMetadata).where(eq11(authMetadata.key, key)).limit(1);
|
|
6597
|
+
return result[0]?.value ?? null;
|
|
6598
|
+
}
|
|
6599
|
+
/**
|
|
6600
|
+
* 키-값 저장 (upsert)
|
|
6601
|
+
*/
|
|
6602
|
+
async set(key, value) {
|
|
6603
|
+
await this.db.insert(authMetadata).values({
|
|
6604
|
+
key,
|
|
6605
|
+
value,
|
|
6606
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6607
|
+
}).onConflictDoUpdate({
|
|
6608
|
+
target: authMetadata.key,
|
|
6609
|
+
set: {
|
|
6610
|
+
value,
|
|
6611
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6612
|
+
}
|
|
6613
|
+
});
|
|
6614
|
+
}
|
|
6615
|
+
};
|
|
6616
|
+
authMetadataRepository = new AuthMetadataRepository();
|
|
6617
|
+
}
|
|
6618
|
+
});
|
|
6619
|
+
|
|
6416
6620
|
// src/server/repositories/index.ts
|
|
6417
6621
|
var init_repositories = __esm({
|
|
6418
6622
|
"src/server/repositories/index.ts"() {
|
|
@@ -6426,6 +6630,8 @@ var init_repositories = __esm({
|
|
|
6426
6630
|
init_user_permissions_repository();
|
|
6427
6631
|
init_user_profiles_repository();
|
|
6428
6632
|
init_invitations_repository();
|
|
6633
|
+
init_social_accounts_repository();
|
|
6634
|
+
init_auth_metadata_repository();
|
|
6429
6635
|
}
|
|
6430
6636
|
});
|
|
6431
6637
|
|
|
@@ -6552,7 +6758,7 @@ var init_role_service = __esm({
|
|
|
6552
6758
|
import "@spfn/auth/config";
|
|
6553
6759
|
|
|
6554
6760
|
// src/server/routes/index.ts
|
|
6555
|
-
import { defineRouter as
|
|
6761
|
+
import { defineRouter as defineRouter5 } from "@spfn/core/route";
|
|
6556
6762
|
|
|
6557
6763
|
// src/server/routes/auth/index.ts
|
|
6558
6764
|
init_schema3();
|
|
@@ -6675,12 +6881,24 @@ function getAuth(c) {
|
|
|
6675
6881
|
}
|
|
6676
6882
|
return c.get("auth");
|
|
6677
6883
|
}
|
|
6884
|
+
function getOptionalAuth(c) {
|
|
6885
|
+
if ("raw" in c && c.raw) {
|
|
6886
|
+
return c.raw.get("auth");
|
|
6887
|
+
}
|
|
6888
|
+
return c.get("auth");
|
|
6889
|
+
}
|
|
6678
6890
|
function getUser(c) {
|
|
6679
6891
|
return getAuth(c).user;
|
|
6680
6892
|
}
|
|
6681
6893
|
function getUserId(c) {
|
|
6682
6894
|
return getAuth(c).userId;
|
|
6683
6895
|
}
|
|
6896
|
+
function getRole(c) {
|
|
6897
|
+
return getAuth(c).role;
|
|
6898
|
+
}
|
|
6899
|
+
function getLocale(c) {
|
|
6900
|
+
return getAuth(c).locale;
|
|
6901
|
+
}
|
|
6684
6902
|
function getKeyId(c) {
|
|
6685
6903
|
return getAuth(c).keyId;
|
|
6686
6904
|
}
|
|
@@ -6690,7 +6908,7 @@ init_types();
|
|
|
6690
6908
|
|
|
6691
6909
|
// src/server/services/auth.service.ts
|
|
6692
6910
|
init_repositories();
|
|
6693
|
-
import { ValidationError } from "@spfn/core/errors";
|
|
6911
|
+
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
6694
6912
|
import {
|
|
6695
6913
|
InvalidCredentialsError,
|
|
6696
6914
|
AccountDisabledError,
|
|
@@ -6701,9 +6919,10 @@ import {
|
|
|
6701
6919
|
} from "@spfn/auth/errors";
|
|
6702
6920
|
|
|
6703
6921
|
// src/server/services/verification.service.ts
|
|
6704
|
-
import { env as
|
|
6922
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
6705
6923
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
6706
6924
|
import jwt2 from "jsonwebtoken";
|
|
6925
|
+
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
6707
6926
|
|
|
6708
6927
|
// src/server/logger.ts
|
|
6709
6928
|
import { logger as rootLogger } from "@spfn/core/logger";
|
|
@@ -6713,8 +6932,10 @@ var authLogger = {
|
|
|
6713
6932
|
interceptor: {
|
|
6714
6933
|
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
6715
6934
|
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
6716
|
-
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation")
|
|
6935
|
+
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
6936
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
6717
6937
|
},
|
|
6938
|
+
session: rootLogger.child("@spfn/auth:session"),
|
|
6718
6939
|
service: rootLogger.child("@spfn/auth:service"),
|
|
6719
6940
|
setup: rootLogger.child("@spfn/auth:setup"),
|
|
6720
6941
|
email: rootLogger.child("@spfn/auth:email"),
|
|
@@ -6723,410 +6944,6 @@ var authLogger = {
|
|
|
6723
6944
|
|
|
6724
6945
|
// src/server/services/verification.service.ts
|
|
6725
6946
|
init_repositories();
|
|
6726
|
-
|
|
6727
|
-
// src/server/services/sms/provider.ts
|
|
6728
|
-
var currentProvider = null;
|
|
6729
|
-
var fallbackProvider = {
|
|
6730
|
-
name: "fallback",
|
|
6731
|
-
sendSMS: async (params) => {
|
|
6732
|
-
authLogger.sms.debug("DEV MODE - SMS not actually sent", {
|
|
6733
|
-
phone: params.phone,
|
|
6734
|
-
message: params.message,
|
|
6735
|
-
purpose: params.purpose || "N/A"
|
|
6736
|
-
});
|
|
6737
|
-
return {
|
|
6738
|
-
success: true,
|
|
6739
|
-
messageId: "dev-mode-no-actual-sms"
|
|
6740
|
-
};
|
|
6741
|
-
}
|
|
6742
|
-
};
|
|
6743
|
-
function registerSMSProvider(provider) {
|
|
6744
|
-
currentProvider = provider;
|
|
6745
|
-
authLogger.sms.info("Registered SMS provider", { name: provider.name });
|
|
6746
|
-
}
|
|
6747
|
-
function getSMSProvider() {
|
|
6748
|
-
return currentProvider || fallbackProvider;
|
|
6749
|
-
}
|
|
6750
|
-
async function sendSMS(params) {
|
|
6751
|
-
const provider = getSMSProvider();
|
|
6752
|
-
return await provider.sendSMS(params);
|
|
6753
|
-
}
|
|
6754
|
-
|
|
6755
|
-
// src/server/services/sms/aws-sns.provider.ts
|
|
6756
|
-
import { env as env3 } from "@spfn/auth/config";
|
|
6757
|
-
function isValidE164Phone(phone) {
|
|
6758
|
-
const e164Regex = /^\+[1-9]\d{1,14}$/;
|
|
6759
|
-
return e164Regex.test(phone);
|
|
6760
|
-
}
|
|
6761
|
-
function createAWSSNSProvider() {
|
|
6762
|
-
try {
|
|
6763
|
-
const { SNSClient, PublishCommand } = __require("@aws-sdk/client-sns");
|
|
6764
|
-
return {
|
|
6765
|
-
name: "aws-sns",
|
|
6766
|
-
sendSMS: async (params) => {
|
|
6767
|
-
const { phone, message, purpose } = params;
|
|
6768
|
-
if (!isValidE164Phone(phone)) {
|
|
6769
|
-
return {
|
|
6770
|
-
success: false,
|
|
6771
|
-
error: "Invalid phone number format. Must be E.164 format (e.g., +821012345678)"
|
|
6772
|
-
};
|
|
6773
|
-
}
|
|
6774
|
-
if (!env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID) {
|
|
6775
|
-
return {
|
|
6776
|
-
success: false,
|
|
6777
|
-
error: "AWS SNS credentials not configured. Set SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID environment variable."
|
|
6778
|
-
};
|
|
6779
|
-
}
|
|
6780
|
-
try {
|
|
6781
|
-
const config = {
|
|
6782
|
-
region: env3.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
|
|
6783
|
-
};
|
|
6784
|
-
if (env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID && env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY) {
|
|
6785
|
-
config.credentials = {
|
|
6786
|
-
accessKeyId: env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID,
|
|
6787
|
-
secretAccessKey: env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY
|
|
6788
|
-
};
|
|
6789
|
-
}
|
|
6790
|
-
const client = new SNSClient(config);
|
|
6791
|
-
const command = new PublishCommand({
|
|
6792
|
-
PhoneNumber: phone,
|
|
6793
|
-
Message: message,
|
|
6794
|
-
MessageAttributes: {
|
|
6795
|
-
"AWS.SNS.SMS.SMSType": {
|
|
6796
|
-
DataType: "String",
|
|
6797
|
-
StringValue: "Transactional"
|
|
6798
|
-
// For OTP codes
|
|
6799
|
-
},
|
|
6800
|
-
...env3.SPFN_AUTH_AWS_SNS_SENDER_ID && {
|
|
6801
|
-
"AWS.SNS.SMS.SenderID": {
|
|
6802
|
-
DataType: "String",
|
|
6803
|
-
StringValue: env3.SPFN_AUTH_AWS_SNS_SENDER_ID
|
|
6804
|
-
}
|
|
6805
|
-
}
|
|
6806
|
-
}
|
|
6807
|
-
});
|
|
6808
|
-
const response = await client.send(command);
|
|
6809
|
-
authLogger.sms.info("SMS sent via AWS SNS", {
|
|
6810
|
-
phone,
|
|
6811
|
-
messageId: response.MessageId,
|
|
6812
|
-
purpose: purpose || "N/A"
|
|
6813
|
-
});
|
|
6814
|
-
return {
|
|
6815
|
-
success: true,
|
|
6816
|
-
messageId: response.MessageId
|
|
6817
|
-
};
|
|
6818
|
-
} catch (error) {
|
|
6819
|
-
const err = error;
|
|
6820
|
-
authLogger.sms.error("Failed to send SMS via AWS SNS", {
|
|
6821
|
-
phone,
|
|
6822
|
-
error: err.message
|
|
6823
|
-
});
|
|
6824
|
-
return {
|
|
6825
|
-
success: false,
|
|
6826
|
-
error: err.message || "Failed to send SMS via AWS SNS"
|
|
6827
|
-
};
|
|
6828
|
-
}
|
|
6829
|
-
}
|
|
6830
|
-
};
|
|
6831
|
-
} catch (error) {
|
|
6832
|
-
return null;
|
|
6833
|
-
}
|
|
6834
|
-
}
|
|
6835
|
-
var awsSNSProvider = createAWSSNSProvider();
|
|
6836
|
-
|
|
6837
|
-
// src/server/services/sms/index.ts
|
|
6838
|
-
if (awsSNSProvider) {
|
|
6839
|
-
registerSMSProvider(awsSNSProvider);
|
|
6840
|
-
}
|
|
6841
|
-
|
|
6842
|
-
// src/server/services/email/provider.ts
|
|
6843
|
-
var currentProvider2 = null;
|
|
6844
|
-
var fallbackProvider2 = {
|
|
6845
|
-
name: "fallback",
|
|
6846
|
-
sendEmail: async (params) => {
|
|
6847
|
-
authLogger.email.debug("DEV MODE - Email not actually sent", {
|
|
6848
|
-
to: params.to,
|
|
6849
|
-
subject: params.subject,
|
|
6850
|
-
purpose: params.purpose || "N/A",
|
|
6851
|
-
textPreview: params.text?.substring(0, 100) || "N/A"
|
|
6852
|
-
});
|
|
6853
|
-
return {
|
|
6854
|
-
success: true,
|
|
6855
|
-
messageId: "dev-mode-no-actual-email"
|
|
6856
|
-
};
|
|
6857
|
-
}
|
|
6858
|
-
};
|
|
6859
|
-
function registerEmailProvider(provider) {
|
|
6860
|
-
currentProvider2 = provider;
|
|
6861
|
-
authLogger.email.info("Registered email provider", { name: provider.name });
|
|
6862
|
-
}
|
|
6863
|
-
function getEmailProvider() {
|
|
6864
|
-
return currentProvider2 || fallbackProvider2;
|
|
6865
|
-
}
|
|
6866
|
-
async function sendEmail(params) {
|
|
6867
|
-
const provider = getEmailProvider();
|
|
6868
|
-
return await provider.sendEmail(params);
|
|
6869
|
-
}
|
|
6870
|
-
|
|
6871
|
-
// src/server/services/email/aws-ses.provider.ts
|
|
6872
|
-
import { env as env4 } from "@spfn/auth/config";
|
|
6873
|
-
function isValidEmail(email) {
|
|
6874
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6875
|
-
return emailRegex.test(email);
|
|
6876
|
-
}
|
|
6877
|
-
function createAWSSESProvider() {
|
|
6878
|
-
try {
|
|
6879
|
-
const { SESClient, SendEmailCommand } = __require("@aws-sdk/client-ses");
|
|
6880
|
-
return {
|
|
6881
|
-
name: "aws-ses",
|
|
6882
|
-
sendEmail: async (params) => {
|
|
6883
|
-
const { to, subject, text: text10, html, purpose } = params;
|
|
6884
|
-
if (!isValidEmail(to)) {
|
|
6885
|
-
return {
|
|
6886
|
-
success: false,
|
|
6887
|
-
error: "Invalid email address format"
|
|
6888
|
-
};
|
|
6889
|
-
}
|
|
6890
|
-
if (!env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID) {
|
|
6891
|
-
return {
|
|
6892
|
-
success: false,
|
|
6893
|
-
error: "AWS SES credentials not configured. Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable."
|
|
6894
|
-
};
|
|
6895
|
-
}
|
|
6896
|
-
if (!env4.SPFN_AUTH_AWS_SES_FROM_EMAIL) {
|
|
6897
|
-
return {
|
|
6898
|
-
success: false,
|
|
6899
|
-
error: "AWS SES sender email not configured. Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable."
|
|
6900
|
-
};
|
|
6901
|
-
}
|
|
6902
|
-
try {
|
|
6903
|
-
const config = {
|
|
6904
|
-
region: env4.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
|
|
6905
|
-
};
|
|
6906
|
-
if (env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID && env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY) {
|
|
6907
|
-
config.credentials = {
|
|
6908
|
-
accessKeyId: env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID,
|
|
6909
|
-
secretAccessKey: env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY
|
|
6910
|
-
};
|
|
6911
|
-
}
|
|
6912
|
-
const client = new SESClient(config);
|
|
6913
|
-
const body = {};
|
|
6914
|
-
if (text10) {
|
|
6915
|
-
body.Text = {
|
|
6916
|
-
Charset: "UTF-8",
|
|
6917
|
-
Data: text10
|
|
6918
|
-
};
|
|
6919
|
-
}
|
|
6920
|
-
if (html) {
|
|
6921
|
-
body.Html = {
|
|
6922
|
-
Charset: "UTF-8",
|
|
6923
|
-
Data: html
|
|
6924
|
-
};
|
|
6925
|
-
}
|
|
6926
|
-
const command = new SendEmailCommand({
|
|
6927
|
-
Source: env4.SPFN_AUTH_AWS_SES_FROM_EMAIL,
|
|
6928
|
-
Destination: {
|
|
6929
|
-
ToAddresses: [to]
|
|
6930
|
-
},
|
|
6931
|
-
Message: {
|
|
6932
|
-
Subject: {
|
|
6933
|
-
Charset: "UTF-8",
|
|
6934
|
-
Data: subject
|
|
6935
|
-
},
|
|
6936
|
-
Body: body
|
|
6937
|
-
}
|
|
6938
|
-
});
|
|
6939
|
-
const response = await client.send(command);
|
|
6940
|
-
authLogger.email.info("Email sent via AWS SES", {
|
|
6941
|
-
to,
|
|
6942
|
-
messageId: response.MessageId,
|
|
6943
|
-
purpose: purpose || "N/A"
|
|
6944
|
-
});
|
|
6945
|
-
return {
|
|
6946
|
-
success: true,
|
|
6947
|
-
messageId: response.MessageId
|
|
6948
|
-
};
|
|
6949
|
-
} catch (error) {
|
|
6950
|
-
const err = error;
|
|
6951
|
-
authLogger.email.error("Failed to send email via AWS SES", {
|
|
6952
|
-
to,
|
|
6953
|
-
error: err.message
|
|
6954
|
-
});
|
|
6955
|
-
return {
|
|
6956
|
-
success: false,
|
|
6957
|
-
error: err.message || "Failed to send email via AWS SES"
|
|
6958
|
-
};
|
|
6959
|
-
}
|
|
6960
|
-
}
|
|
6961
|
-
};
|
|
6962
|
-
} catch (error) {
|
|
6963
|
-
return null;
|
|
6964
|
-
}
|
|
6965
|
-
}
|
|
6966
|
-
var awsSESProvider = createAWSSESProvider();
|
|
6967
|
-
|
|
6968
|
-
// src/server/services/email/index.ts
|
|
6969
|
-
if (awsSESProvider) {
|
|
6970
|
-
registerEmailProvider(awsSESProvider);
|
|
6971
|
-
}
|
|
6972
|
-
|
|
6973
|
-
// src/server/services/email/templates/verification-code.ts
|
|
6974
|
-
function getSubject(purpose) {
|
|
6975
|
-
switch (purpose) {
|
|
6976
|
-
case "registration":
|
|
6977
|
-
return "Verify your email address";
|
|
6978
|
-
case "login":
|
|
6979
|
-
return "Your login verification code";
|
|
6980
|
-
case "password_reset":
|
|
6981
|
-
return "Reset your password";
|
|
6982
|
-
default:
|
|
6983
|
-
return "Your verification code";
|
|
6984
|
-
}
|
|
6985
|
-
}
|
|
6986
|
-
function getPurposeText(purpose) {
|
|
6987
|
-
switch (purpose) {
|
|
6988
|
-
case "registration":
|
|
6989
|
-
return "complete your registration";
|
|
6990
|
-
case "login":
|
|
6991
|
-
return "verify your identity";
|
|
6992
|
-
case "password_reset":
|
|
6993
|
-
return "reset your password";
|
|
6994
|
-
default:
|
|
6995
|
-
return "verify your identity";
|
|
6996
|
-
}
|
|
6997
|
-
}
|
|
6998
|
-
function generateText(params) {
|
|
6999
|
-
const { code, expiresInMinutes = 5 } = params;
|
|
7000
|
-
return `Your verification code is: ${code}
|
|
7001
|
-
|
|
7002
|
-
This code will expire in ${expiresInMinutes} minutes.
|
|
7003
|
-
|
|
7004
|
-
If you didn't request this code, please ignore this email.`;
|
|
7005
|
-
}
|
|
7006
|
-
function generateHTML(params) {
|
|
7007
|
-
const { code, purpose, expiresInMinutes = 5, appName } = params;
|
|
7008
|
-
const purposeText = getPurposeText(purpose);
|
|
7009
|
-
return `<!DOCTYPE html>
|
|
7010
|
-
<html>
|
|
7011
|
-
<head>
|
|
7012
|
-
<meta charset="utf-8">
|
|
7013
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7014
|
-
<title>Verification Code</title>
|
|
7015
|
-
</head>
|
|
7016
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f5f5f5;">
|
|
7017
|
-
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
|
|
7018
|
-
<h1 style="color: white; margin: 0; font-size: 24px;">${appName ? appName : "Verification Code"}</h1>
|
|
7019
|
-
</div>
|
|
7020
|
-
<div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
|
|
7021
|
-
<p style="margin-bottom: 20px; font-size: 16px;">
|
|
7022
|
-
Please use the following verification code to ${purposeText}:
|
|
7023
|
-
</p>
|
|
7024
|
-
<div style="background: #f8f9fa; padding: 25px; border-radius: 8px; text-align: center; margin: 25px 0; border: 2px dashed #dee2e6;">
|
|
7025
|
-
<span style="font-size: 36px; font-weight: bold; letter-spacing: 10px; color: #333; font-family: 'Courier New', monospace;">${code}</span>
|
|
7026
|
-
</div>
|
|
7027
|
-
<p style="color: #666; font-size: 14px; margin-top: 20px; text-align: center;">
|
|
7028
|
-
<strong>This code will expire in ${expiresInMinutes} minutes.</strong>
|
|
7029
|
-
</p>
|
|
7030
|
-
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
|
|
7031
|
-
<p style="color: #999; font-size: 12px; text-align: center; margin: 0;">
|
|
7032
|
-
If you didn't request this code, please ignore this email.
|
|
7033
|
-
</p>
|
|
7034
|
-
</div>
|
|
7035
|
-
<div style="text-align: center; padding: 20px; color: #999; font-size: 11px;">
|
|
7036
|
-
<p style="margin: 0;">This is an automated message. Please do not reply.</p>
|
|
7037
|
-
</div>
|
|
7038
|
-
</body>
|
|
7039
|
-
</html>`;
|
|
7040
|
-
}
|
|
7041
|
-
function verificationCodeTemplate(params) {
|
|
7042
|
-
return {
|
|
7043
|
-
subject: getSubject(params.purpose),
|
|
7044
|
-
text: generateText(params),
|
|
7045
|
-
html: generateHTML(params)
|
|
7046
|
-
};
|
|
7047
|
-
}
|
|
7048
|
-
|
|
7049
|
-
// src/server/services/email/templates/registry.ts
|
|
7050
|
-
var customTemplates = {};
|
|
7051
|
-
function registerEmailTemplates(templates) {
|
|
7052
|
-
customTemplates = { ...customTemplates, ...templates };
|
|
7053
|
-
authLogger.email.info("Registered custom email templates", {
|
|
7054
|
-
templates: Object.keys(templates)
|
|
7055
|
-
});
|
|
7056
|
-
}
|
|
7057
|
-
function getVerificationCodeTemplate(params) {
|
|
7058
|
-
if (customTemplates.verificationCode) {
|
|
7059
|
-
return customTemplates.verificationCode(params);
|
|
7060
|
-
}
|
|
7061
|
-
return verificationCodeTemplate(params);
|
|
7062
|
-
}
|
|
7063
|
-
function getWelcomeTemplate(params) {
|
|
7064
|
-
if (customTemplates.welcome) {
|
|
7065
|
-
return customTemplates.welcome(params);
|
|
7066
|
-
}
|
|
7067
|
-
return {
|
|
7068
|
-
subject: params.appName ? `Welcome to ${params.appName}!` : "Welcome!",
|
|
7069
|
-
text: `Welcome! Your account has been created successfully.`,
|
|
7070
|
-
html: `
|
|
7071
|
-
<!DOCTYPE html>
|
|
7072
|
-
<html>
|
|
7073
|
-
<head><meta charset="utf-8"></head>
|
|
7074
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
|
|
7075
|
-
<h1>Welcome${params.appName ? ` to ${params.appName}` : ""}!</h1>
|
|
7076
|
-
<p>Your account has been created successfully.</p>
|
|
7077
|
-
</body>
|
|
7078
|
-
</html>`
|
|
7079
|
-
};
|
|
7080
|
-
}
|
|
7081
|
-
function getPasswordResetTemplate(params) {
|
|
7082
|
-
if (customTemplates.passwordReset) {
|
|
7083
|
-
return customTemplates.passwordReset(params);
|
|
7084
|
-
}
|
|
7085
|
-
const expires = params.expiresInMinutes || 30;
|
|
7086
|
-
return {
|
|
7087
|
-
subject: "Reset your password",
|
|
7088
|
-
text: `Click this link to reset your password: ${params.resetLink}
|
|
7089
|
-
|
|
7090
|
-
This link will expire in ${expires} minutes.`,
|
|
7091
|
-
html: `
|
|
7092
|
-
<!DOCTYPE html>
|
|
7093
|
-
<html>
|
|
7094
|
-
<head><meta charset="utf-8"></head>
|
|
7095
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
|
|
7096
|
-
<h1>Reset Your Password</h1>
|
|
7097
|
-
<p>Click the button below to reset your password:</p>
|
|
7098
|
-
<a href="${params.resetLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Reset Password</a>
|
|
7099
|
-
<p style="color: #666; margin-top: 20px;">This link will expire in ${expires} minutes.</p>
|
|
7100
|
-
</body>
|
|
7101
|
-
</html>`
|
|
7102
|
-
};
|
|
7103
|
-
}
|
|
7104
|
-
function getInvitationTemplate(params) {
|
|
7105
|
-
if (customTemplates.invitation) {
|
|
7106
|
-
return customTemplates.invitation(params);
|
|
7107
|
-
}
|
|
7108
|
-
const appName = params.appName || "our platform";
|
|
7109
|
-
const inviterText = params.inviterName ? `${params.inviterName} has invited you` : "You have been invited";
|
|
7110
|
-
const roleText = params.roleName ? ` as ${params.roleName}` : "";
|
|
7111
|
-
return {
|
|
7112
|
-
subject: `You're invited to join ${appName}`,
|
|
7113
|
-
text: `${inviterText} to join ${appName}${roleText}.
|
|
7114
|
-
|
|
7115
|
-
Click here to accept: ${params.inviteLink}`,
|
|
7116
|
-
html: `
|
|
7117
|
-
<!DOCTYPE html>
|
|
7118
|
-
<html>
|
|
7119
|
-
<head><meta charset="utf-8"></head>
|
|
7120
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
|
|
7121
|
-
<h1>You're Invited!</h1>
|
|
7122
|
-
<p>${inviterText} to join <strong>${appName}</strong>${roleText}.</p>
|
|
7123
|
-
<a href="${params.inviteLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Accept Invitation</a>
|
|
7124
|
-
</body>
|
|
7125
|
-
</html>`
|
|
7126
|
-
};
|
|
7127
|
-
}
|
|
7128
|
-
|
|
7129
|
-
// src/server/services/verification.service.ts
|
|
7130
6947
|
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
7131
6948
|
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
7132
6949
|
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
@@ -7170,7 +6987,7 @@ async function markCodeAsUsed(codeId) {
|
|
|
7170
6987
|
await verificationCodesRepository.markAsUsed(codeId);
|
|
7171
6988
|
}
|
|
7172
6989
|
function createVerificationToken(payload) {
|
|
7173
|
-
return jwt2.sign(payload,
|
|
6990
|
+
return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7174
6991
|
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
7175
6992
|
issuer: "spfn-auth",
|
|
7176
6993
|
audience: "spfn-client"
|
|
@@ -7178,7 +6995,7 @@ function createVerificationToken(payload) {
|
|
|
7178
6995
|
}
|
|
7179
6996
|
function validateVerificationToken(token) {
|
|
7180
6997
|
try {
|
|
7181
|
-
const decoded = jwt2.verify(token,
|
|
6998
|
+
const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7182
6999
|
issuer: "spfn-auth",
|
|
7183
7000
|
audience: "spfn-client"
|
|
7184
7001
|
});
|
|
@@ -7192,17 +7009,14 @@ function validateVerificationToken(token) {
|
|
|
7192
7009
|
}
|
|
7193
7010
|
}
|
|
7194
7011
|
async function sendVerificationEmail(email, code, purpose) {
|
|
7195
|
-
const { subject, text: text10, html } = getVerificationCodeTemplate({
|
|
7196
|
-
code,
|
|
7197
|
-
purpose,
|
|
7198
|
-
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
7199
|
-
});
|
|
7200
7012
|
const result = await sendEmail({
|
|
7201
7013
|
to: email,
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7014
|
+
template: "verification-code",
|
|
7015
|
+
data: {
|
|
7016
|
+
code,
|
|
7017
|
+
purpose,
|
|
7018
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
7019
|
+
}
|
|
7206
7020
|
});
|
|
7207
7021
|
if (!result.success) {
|
|
7208
7022
|
authLogger.email.error("Failed to send verification email", {
|
|
@@ -7213,11 +7027,13 @@ async function sendVerificationEmail(email, code, purpose) {
|
|
|
7213
7027
|
}
|
|
7214
7028
|
}
|
|
7215
7029
|
async function sendVerificationSMS(phone, code, purpose) {
|
|
7216
|
-
const message = `Your verification code is: ${code}`;
|
|
7217
7030
|
const result = await sendSMS({
|
|
7218
|
-
phone,
|
|
7219
|
-
|
|
7220
|
-
|
|
7031
|
+
to: phone,
|
|
7032
|
+
template: "verification-code",
|
|
7033
|
+
data: {
|
|
7034
|
+
code,
|
|
7035
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
7036
|
+
}
|
|
7221
7037
|
});
|
|
7222
7038
|
if (!result.success) {
|
|
7223
7039
|
authLogger.sms.error("Failed to send verification SMS", {
|
|
@@ -7318,6 +7134,9 @@ async function revokeKeyService(params) {
|
|
|
7318
7134
|
|
|
7319
7135
|
// src/server/services/user.service.ts
|
|
7320
7136
|
init_repositories();
|
|
7137
|
+
import { ValidationError } from "@spfn/core/errors";
|
|
7138
|
+
import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
7139
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
7321
7140
|
async function getUserByIdService(userId) {
|
|
7322
7141
|
return await usersRepository.findById(userId);
|
|
7323
7142
|
}
|
|
@@ -7333,6 +7152,108 @@ async function updateLastLoginService(userId) {
|
|
|
7333
7152
|
async function updateUserService(userId, updates) {
|
|
7334
7153
|
await usersRepository.updateById(userId, updates);
|
|
7335
7154
|
}
|
|
7155
|
+
function getReservedUsernames() {
|
|
7156
|
+
const raw = env4.SPFN_AUTH_RESERVED_USERNAMES ?? "";
|
|
7157
|
+
if (!raw) {
|
|
7158
|
+
return /* @__PURE__ */ new Set();
|
|
7159
|
+
}
|
|
7160
|
+
return new Set(
|
|
7161
|
+
raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
7162
|
+
);
|
|
7163
|
+
}
|
|
7164
|
+
function isReservedUsername(username) {
|
|
7165
|
+
return getReservedUsernames().has(username.toLowerCase());
|
|
7166
|
+
}
|
|
7167
|
+
function validateUsernameLength(username) {
|
|
7168
|
+
const min = env4.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
|
|
7169
|
+
const max = env4.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
|
|
7170
|
+
if (username.length < min) {
|
|
7171
|
+
throw new ValidationError({
|
|
7172
|
+
message: `Username must be at least ${min} characters`,
|
|
7173
|
+
details: { minLength: min, actual: username.length }
|
|
7174
|
+
});
|
|
7175
|
+
}
|
|
7176
|
+
if (username.length > max) {
|
|
7177
|
+
throw new ValidationError({
|
|
7178
|
+
message: `Username must be at most ${max} characters`,
|
|
7179
|
+
details: { maxLength: max, actual: username.length }
|
|
7180
|
+
});
|
|
7181
|
+
}
|
|
7182
|
+
}
|
|
7183
|
+
async function checkUsernameAvailableService(username) {
|
|
7184
|
+
validateUsernameLength(username);
|
|
7185
|
+
if (isReservedUsername(username)) {
|
|
7186
|
+
return false;
|
|
7187
|
+
}
|
|
7188
|
+
const existing = await usersRepository.findByUsername(username);
|
|
7189
|
+
return !existing;
|
|
7190
|
+
}
|
|
7191
|
+
async function updateUsernameService(userId, username) {
|
|
7192
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7193
|
+
if (username !== null) {
|
|
7194
|
+
validateUsernameLength(username);
|
|
7195
|
+
if (isReservedUsername(username)) {
|
|
7196
|
+
throw new ReservedUsernameError({ username });
|
|
7197
|
+
}
|
|
7198
|
+
const existing = await usersRepository.findByUsername(username);
|
|
7199
|
+
if (existing && existing.id !== userIdNum) {
|
|
7200
|
+
throw new UsernameAlreadyTakenError({ username });
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
return await usersRepository.updateById(userIdNum, { username });
|
|
7204
|
+
}
|
|
7205
|
+
|
|
7206
|
+
// src/server/events/index.ts
|
|
7207
|
+
init_esm();
|
|
7208
|
+
import { defineEvent } from "@spfn/core/event";
|
|
7209
|
+
var AuthProviderSchema = Type.Union([
|
|
7210
|
+
Type.Literal("email"),
|
|
7211
|
+
Type.Literal("phone"),
|
|
7212
|
+
Type.Literal("google")
|
|
7213
|
+
]);
|
|
7214
|
+
var authLoginEvent = defineEvent(
|
|
7215
|
+
"auth.login",
|
|
7216
|
+
Type.Object({
|
|
7217
|
+
userId: Type.String(),
|
|
7218
|
+
provider: AuthProviderSchema,
|
|
7219
|
+
email: Type.Optional(Type.String()),
|
|
7220
|
+
phone: Type.Optional(Type.String())
|
|
7221
|
+
})
|
|
7222
|
+
);
|
|
7223
|
+
var authRegisterEvent = defineEvent(
|
|
7224
|
+
"auth.register",
|
|
7225
|
+
Type.Object({
|
|
7226
|
+
userId: Type.String(),
|
|
7227
|
+
provider: AuthProviderSchema,
|
|
7228
|
+
email: Type.Optional(Type.String()),
|
|
7229
|
+
phone: Type.Optional(Type.String()),
|
|
7230
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
|
|
7231
|
+
})
|
|
7232
|
+
);
|
|
7233
|
+
var invitationCreatedEvent = defineEvent(
|
|
7234
|
+
"auth.invitation.created",
|
|
7235
|
+
Type.Object({
|
|
7236
|
+
invitationId: Type.String(),
|
|
7237
|
+
email: Type.String(),
|
|
7238
|
+
token: Type.String(),
|
|
7239
|
+
roleId: Type.Number(),
|
|
7240
|
+
invitedBy: Type.String(),
|
|
7241
|
+
expiresAt: Type.String(),
|
|
7242
|
+
isResend: Type.Boolean(),
|
|
7243
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
|
|
7244
|
+
})
|
|
7245
|
+
);
|
|
7246
|
+
var invitationAcceptedEvent = defineEvent(
|
|
7247
|
+
"auth.invitation.accepted",
|
|
7248
|
+
Type.Object({
|
|
7249
|
+
invitationId: Type.String(),
|
|
7250
|
+
email: Type.String(),
|
|
7251
|
+
userId: Type.String(),
|
|
7252
|
+
roleId: Type.Number(),
|
|
7253
|
+
invitedBy: Type.String(),
|
|
7254
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
|
|
7255
|
+
})
|
|
7256
|
+
);
|
|
7336
7257
|
|
|
7337
7258
|
// src/server/services/auth.service.ts
|
|
7338
7259
|
async function checkAccountExistsService(params) {
|
|
@@ -7349,7 +7270,7 @@ async function checkAccountExistsService(params) {
|
|
|
7349
7270
|
identifierType = "phone";
|
|
7350
7271
|
user = await usersRepository.findByPhone(phone);
|
|
7351
7272
|
} else {
|
|
7352
|
-
throw new
|
|
7273
|
+
throw new ValidationError2({ message: "Either email or phone must be provided" });
|
|
7353
7274
|
}
|
|
7354
7275
|
return {
|
|
7355
7276
|
exists: !!user,
|
|
@@ -7358,7 +7279,7 @@ async function checkAccountExistsService(params) {
|
|
|
7358
7279
|
};
|
|
7359
7280
|
}
|
|
7360
7281
|
async function registerService(params) {
|
|
7361
|
-
const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm } = params;
|
|
7282
|
+
const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
7362
7283
|
const tokenPayload = validateVerificationToken(verificationToken);
|
|
7363
7284
|
if (!tokenPayload) {
|
|
7364
7285
|
throw new InvalidVerificationTokenError();
|
|
@@ -7400,17 +7321,26 @@ async function registerService(params) {
|
|
|
7400
7321
|
fingerprint,
|
|
7401
7322
|
algorithm
|
|
7402
7323
|
});
|
|
7403
|
-
|
|
7324
|
+
const result = {
|
|
7404
7325
|
userId: String(newUser.id),
|
|
7326
|
+
publicId: newUser.publicId,
|
|
7405
7327
|
email: newUser.email || void 0,
|
|
7406
7328
|
phone: newUser.phone || void 0
|
|
7407
7329
|
};
|
|
7330
|
+
await authRegisterEvent.emit({
|
|
7331
|
+
userId: result.userId,
|
|
7332
|
+
provider: email ? "email" : "phone",
|
|
7333
|
+
email: result.email,
|
|
7334
|
+
phone: result.phone,
|
|
7335
|
+
metadata
|
|
7336
|
+
});
|
|
7337
|
+
return result;
|
|
7408
7338
|
}
|
|
7409
7339
|
async function loginService(params) {
|
|
7410
7340
|
const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
|
|
7411
7341
|
const user = await usersRepository.findByEmailOrPhone(email, phone);
|
|
7412
7342
|
if (!email && !phone) {
|
|
7413
|
-
throw new
|
|
7343
|
+
throw new ValidationError2({ message: "Either email or phone must be provided" });
|
|
7414
7344
|
}
|
|
7415
7345
|
if (!user || !user.passwordHash) {
|
|
7416
7346
|
throw new InvalidCredentialsError();
|
|
@@ -7437,12 +7367,20 @@ async function loginService(params) {
|
|
|
7437
7367
|
algorithm
|
|
7438
7368
|
});
|
|
7439
7369
|
await updateLastLoginService(user.id);
|
|
7440
|
-
|
|
7370
|
+
const result = {
|
|
7441
7371
|
userId: String(user.id),
|
|
7372
|
+
publicId: user.publicId,
|
|
7442
7373
|
email: user.email || void 0,
|
|
7443
7374
|
phone: user.phone || void 0,
|
|
7444
7375
|
passwordChangeRequired: user.passwordChangeRequired
|
|
7445
7376
|
};
|
|
7377
|
+
await authLoginEvent.emit({
|
|
7378
|
+
userId: result.userId,
|
|
7379
|
+
provider: email ? "email" : "phone",
|
|
7380
|
+
email: result.email,
|
|
7381
|
+
phone: result.phone
|
|
7382
|
+
});
|
|
7383
|
+
return result;
|
|
7446
7384
|
}
|
|
7447
7385
|
async function logoutService(params) {
|
|
7448
7386
|
const { userId, keyId } = params;
|
|
@@ -7460,12 +7398,12 @@ async function changePasswordService(params) {
|
|
|
7460
7398
|
} else {
|
|
7461
7399
|
const user = await usersRepository.findById(userId);
|
|
7462
7400
|
if (!user) {
|
|
7463
|
-
throw new
|
|
7401
|
+
throw new ValidationError2({ message: "User not found" });
|
|
7464
7402
|
}
|
|
7465
7403
|
passwordHash = user.passwordHash;
|
|
7466
7404
|
}
|
|
7467
7405
|
if (!passwordHash) {
|
|
7468
|
-
throw new
|
|
7406
|
+
throw new ValidationError2({ message: "No password set for this account" });
|
|
7469
7407
|
}
|
|
7470
7408
|
const isValid = await verifyPassword(currentPassword, passwordHash);
|
|
7471
7409
|
if (!isValid) {
|
|
@@ -7478,14 +7416,17 @@ async function changePasswordService(params) {
|
|
|
7478
7416
|
// src/server/services/rbac.service.ts
|
|
7479
7417
|
init_repositories();
|
|
7480
7418
|
init_rbac();
|
|
7419
|
+
import { createHash } from "crypto";
|
|
7481
7420
|
|
|
7482
7421
|
// src/server/lib/config.ts
|
|
7483
|
-
import { env as
|
|
7422
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7484
7423
|
var COOKIE_NAMES = {
|
|
7485
7424
|
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
7486
7425
|
SESSION: "spfn_session",
|
|
7487
7426
|
/** Current key ID (for key rotation) */
|
|
7488
|
-
SESSION_KEY_ID: "spfn_session_key_id"
|
|
7427
|
+
SESSION_KEY_ID: "spfn_session_key_id",
|
|
7428
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7429
|
+
OAUTH_PENDING: "spfn_oauth_pending"
|
|
7489
7430
|
};
|
|
7490
7431
|
function parseDuration(duration) {
|
|
7491
7432
|
if (typeof duration === "number") {
|
|
@@ -7530,7 +7471,7 @@ function getSessionTtl(override) {
|
|
|
7530
7471
|
if (globalConfig.sessionTtl !== void 0) {
|
|
7531
7472
|
return parseDuration(globalConfig.sessionTtl);
|
|
7532
7473
|
}
|
|
7533
|
-
const envTtl =
|
|
7474
|
+
const envTtl = env5.SPFN_AUTH_SESSION_TTL;
|
|
7534
7475
|
if (envTtl) {
|
|
7535
7476
|
return parseDuration(envTtl);
|
|
7536
7477
|
}
|
|
@@ -7538,6 +7479,33 @@ function getSessionTtl(override) {
|
|
|
7538
7479
|
}
|
|
7539
7480
|
|
|
7540
7481
|
// src/server/services/rbac.service.ts
|
|
7482
|
+
var RBAC_HASH_KEY = "rbac_config_hash";
|
|
7483
|
+
function computeConfigHash(allRoles, allPermissions, allMappings) {
|
|
7484
|
+
const payload = JSON.stringify({
|
|
7485
|
+
roles: allRoles.map((r) => ({ name: r.name, displayName: r.displayName, description: r.description, priority: r.priority, isSystem: r.isSystem, isBuiltin: r.isBuiltin })).sort((a, b) => a.name.localeCompare(b.name)),
|
|
7486
|
+
permissions: allPermissions.map((p) => ({ name: p.name, displayName: p.displayName, description: p.description, category: p.category, isSystem: p.isSystem, isBuiltin: p.isBuiltin })).sort((a, b) => a.name.localeCompare(b.name)),
|
|
7487
|
+
mappings: Object.keys(allMappings).sort().reduce((acc, key) => {
|
|
7488
|
+
acc[key] = [...allMappings[key]].sort();
|
|
7489
|
+
return acc;
|
|
7490
|
+
}, {})
|
|
7491
|
+
});
|
|
7492
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
7493
|
+
}
|
|
7494
|
+
function collectMappings(options) {
|
|
7495
|
+
const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
|
|
7496
|
+
if (options.rolePermissions) {
|
|
7497
|
+
for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
|
|
7498
|
+
if (allMappings[roleName]) {
|
|
7499
|
+
allMappings[roleName] = [
|
|
7500
|
+
.../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
|
|
7501
|
+
];
|
|
7502
|
+
} else {
|
|
7503
|
+
allMappings[roleName] = permNames;
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
return allMappings;
|
|
7508
|
+
}
|
|
7541
7509
|
async function initializeAuth(options = {}) {
|
|
7542
7510
|
authLogger.service.info("\u{1F510} Initializing RBAC system...");
|
|
7543
7511
|
if (options.sessionTtl !== void 0) {
|
|
@@ -7550,100 +7518,100 @@ async function initializeAuth(options = {}) {
|
|
|
7550
7518
|
...Object.values(BUILTIN_ROLES),
|
|
7551
7519
|
...options.roles || []
|
|
7552
7520
|
];
|
|
7553
|
-
for (const roleConfig of allRoles) {
|
|
7554
|
-
await upsertRole(roleConfig);
|
|
7555
|
-
}
|
|
7556
7521
|
const allPermissions = [
|
|
7557
7522
|
...Object.values(BUILTIN_PERMISSIONS),
|
|
7558
7523
|
...options.permissions || []
|
|
7559
7524
|
];
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
if (allMappings[roleName]) {
|
|
7567
|
-
allMappings[roleName] = [
|
|
7568
|
-
.../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
|
|
7569
|
-
];
|
|
7570
|
-
} else {
|
|
7571
|
-
allMappings[roleName] = permNames;
|
|
7572
|
-
}
|
|
7573
|
-
}
|
|
7574
|
-
}
|
|
7575
|
-
for (const [roleName, permNames] of Object.entries(allMappings)) {
|
|
7576
|
-
await assignPermissionsToRole(roleName, permNames);
|
|
7525
|
+
const allMappings = collectMappings(options);
|
|
7526
|
+
const configHash = computeConfigHash(allRoles, allPermissions, allMappings);
|
|
7527
|
+
const storedHash = await authMetadataRepository.get(RBAC_HASH_KEY);
|
|
7528
|
+
if (storedHash === configHash) {
|
|
7529
|
+
authLogger.service.info("\u2705 RBAC config unchanged, skipping initialization");
|
|
7530
|
+
return;
|
|
7577
7531
|
}
|
|
7532
|
+
authLogger.service.info("\u{1F504} RBAC config changed, applying updates...");
|
|
7533
|
+
const existingRoles = await rolesRepository.findAll();
|
|
7534
|
+
const existingPermissions = await permissionsRepository.findAll();
|
|
7535
|
+
const rolesByName = new Map(existingRoles.map((r) => [r.name, r]));
|
|
7536
|
+
const permsByName = new Map(existingPermissions.map((p) => [p.name, p]));
|
|
7537
|
+
await syncRoles(allRoles, rolesByName);
|
|
7538
|
+
await syncPermissions(allPermissions, permsByName);
|
|
7539
|
+
const updatedRoles = await rolesRepository.findAll();
|
|
7540
|
+
const updatedPermissions = await permissionsRepository.findAll();
|
|
7541
|
+
const updatedRolesByName = new Map(updatedRoles.map((r) => [r.name, r]));
|
|
7542
|
+
const updatedPermsByName = new Map(updatedPermissions.map((p) => [p.name, p]));
|
|
7543
|
+
await syncMappings(allMappings, updatedRolesByName, updatedPermsByName);
|
|
7544
|
+
await authMetadataRepository.set(RBAC_HASH_KEY, configHash);
|
|
7578
7545
|
authLogger.service.info("\u2705 RBAC initialization complete");
|
|
7579
7546
|
authLogger.service.info(`\u{1F4CA} Roles: ${allRoles.length}, Permissions: ${allPermissions.length}`);
|
|
7580
7547
|
authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
|
|
7581
7548
|
}
|
|
7582
|
-
async function
|
|
7583
|
-
const
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7549
|
+
async function syncRoles(configs, existingByName) {
|
|
7550
|
+
for (const config of configs) {
|
|
7551
|
+
const existing = existingByName.get(config.name);
|
|
7552
|
+
if (!existing) {
|
|
7553
|
+
await rolesRepository.create({
|
|
7554
|
+
name: config.name,
|
|
7555
|
+
displayName: config.displayName,
|
|
7556
|
+
description: config.description || null,
|
|
7557
|
+
priority: config.priority ?? 10,
|
|
7558
|
+
isSystem: config.isSystem ?? false,
|
|
7559
|
+
isBuiltin: config.isBuiltin ?? false,
|
|
7560
|
+
isActive: true
|
|
7561
|
+
});
|
|
7562
|
+
authLogger.service.info(` \u2705 Created role: ${config.name}`);
|
|
7563
|
+
} else {
|
|
7564
|
+
const updateData = {
|
|
7565
|
+
displayName: config.displayName,
|
|
7566
|
+
description: config.description || null
|
|
7567
|
+
};
|
|
7568
|
+
if (!existing.isBuiltin) {
|
|
7569
|
+
updateData.priority = config.priority ?? existing.priority;
|
|
7570
|
+
}
|
|
7571
|
+
await rolesRepository.updateById(existing.id, updateData);
|
|
7602
7572
|
}
|
|
7603
|
-
await rolesRepository.updateById(existing.id, updateData);
|
|
7604
|
-
}
|
|
7605
|
-
}
|
|
7606
|
-
async function upsertPermission(config) {
|
|
7607
|
-
const existing = await permissionsRepository.findByName(config.name);
|
|
7608
|
-
if (!existing) {
|
|
7609
|
-
await permissionsRepository.create({
|
|
7610
|
-
name: config.name,
|
|
7611
|
-
displayName: config.displayName,
|
|
7612
|
-
description: config.description || null,
|
|
7613
|
-
category: config.category || null,
|
|
7614
|
-
isSystem: config.isSystem ?? false,
|
|
7615
|
-
isBuiltin: config.isBuiltin ?? false,
|
|
7616
|
-
isActive: true,
|
|
7617
|
-
metadata: null
|
|
7618
|
-
});
|
|
7619
|
-
authLogger.service.info(` \u2705 Created permission: ${config.name}`);
|
|
7620
|
-
} else {
|
|
7621
|
-
await permissionsRepository.updateById(existing.id, {
|
|
7622
|
-
displayName: config.displayName,
|
|
7623
|
-
description: config.description || null,
|
|
7624
|
-
category: config.category || null
|
|
7625
|
-
});
|
|
7626
7573
|
}
|
|
7627
7574
|
}
|
|
7628
|
-
async function
|
|
7629
|
-
const
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7575
|
+
async function syncPermissions(configs, existingByName) {
|
|
7576
|
+
for (const config of configs) {
|
|
7577
|
+
const existing = existingByName.get(config.name);
|
|
7578
|
+
if (!existing) {
|
|
7579
|
+
await permissionsRepository.create({
|
|
7580
|
+
name: config.name,
|
|
7581
|
+
displayName: config.displayName,
|
|
7582
|
+
description: config.description || null,
|
|
7583
|
+
category: config.category || null,
|
|
7584
|
+
isSystem: config.isSystem ?? false,
|
|
7585
|
+
isBuiltin: config.isBuiltin ?? false,
|
|
7586
|
+
isActive: true,
|
|
7587
|
+
metadata: null
|
|
7588
|
+
});
|
|
7589
|
+
authLogger.service.info(` \u2705 Created permission: ${config.name}`);
|
|
7590
|
+
} else {
|
|
7591
|
+
await permissionsRepository.updateById(existing.id, {
|
|
7592
|
+
displayName: config.displayName,
|
|
7593
|
+
description: config.description || null,
|
|
7594
|
+
category: config.category || null
|
|
7595
|
+
});
|
|
7596
|
+
}
|
|
7638
7597
|
}
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
const
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
})
|
|
7645
|
-
|
|
7646
|
-
|
|
7598
|
+
}
|
|
7599
|
+
async function syncMappings(allMappings, rolesByName, permsByName) {
|
|
7600
|
+
for (const [roleName, permNames] of Object.entries(allMappings)) {
|
|
7601
|
+
const role = rolesByName.get(roleName);
|
|
7602
|
+
if (!role) {
|
|
7603
|
+
authLogger.service.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
|
|
7604
|
+
continue;
|
|
7605
|
+
}
|
|
7606
|
+
const existingMappings = await rolePermissionsRepository.findByRoleId(role.id);
|
|
7607
|
+
const existingPermIds = new Set(existingMappings.map((m) => m.permissionId));
|
|
7608
|
+
const newMappings = permNames.map((name) => permsByName.get(name)).filter((perm) => perm != null).filter((perm) => !existingPermIds.has(perm.id)).map((perm) => ({
|
|
7609
|
+
roleId: role.id,
|
|
7610
|
+
permissionId: perm.id
|
|
7611
|
+
}));
|
|
7612
|
+
if (newMappings.length > 0) {
|
|
7613
|
+
await rolePermissionsRepository.createMany(newMappings);
|
|
7614
|
+
}
|
|
7647
7615
|
}
|
|
7648
7616
|
}
|
|
7649
7617
|
|
|
@@ -7729,7 +7697,7 @@ function calculateExpiresAt(days = 7) {
|
|
|
7729
7697
|
return expiresAt;
|
|
7730
7698
|
}
|
|
7731
7699
|
async function createInvitation(params) {
|
|
7732
|
-
const { email, roleId, invitedBy, expiresInDays = 7, metadata } = params;
|
|
7700
|
+
const { email, roleId, invitedBy, expiresInDays = 7, expiresAt: expiresAtParam, metadata } = params;
|
|
7733
7701
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
7734
7702
|
if (!emailRegex.test(email)) {
|
|
7735
7703
|
throw new Error("Invalid email format");
|
|
@@ -7751,7 +7719,7 @@ async function createInvitation(params) {
|
|
|
7751
7719
|
throw new Error(`User with id ${invitedBy} not found`);
|
|
7752
7720
|
}
|
|
7753
7721
|
const token = generateInvitationToken();
|
|
7754
|
-
const expiresAt = calculateExpiresAt(expiresInDays);
|
|
7722
|
+
const expiresAt = expiresAtParam ?? calculateExpiresAt(expiresInDays);
|
|
7755
7723
|
const invitation = await invitationsRepository.create({
|
|
7756
7724
|
email,
|
|
7757
7725
|
token,
|
|
@@ -7761,7 +7729,16 @@ async function createInvitation(params) {
|
|
|
7761
7729
|
expiresAt,
|
|
7762
7730
|
metadata: metadata || null
|
|
7763
7731
|
});
|
|
7764
|
-
|
|
7732
|
+
await invitationCreatedEvent.emit({
|
|
7733
|
+
invitationId: String(invitation.id),
|
|
7734
|
+
email,
|
|
7735
|
+
token,
|
|
7736
|
+
roleId,
|
|
7737
|
+
invitedBy: String(invitedBy),
|
|
7738
|
+
expiresAt: expiresAt.toISOString(),
|
|
7739
|
+
isResend: false,
|
|
7740
|
+
metadata
|
|
7741
|
+
});
|
|
7765
7742
|
return invitation;
|
|
7766
7743
|
}
|
|
7767
7744
|
async function getInvitationByToken(token) {
|
|
@@ -7825,7 +7802,14 @@ async function acceptInvitation(params) {
|
|
|
7825
7802
|
"accepted",
|
|
7826
7803
|
/* @__PURE__ */ new Date()
|
|
7827
7804
|
);
|
|
7828
|
-
|
|
7805
|
+
await invitationAcceptedEvent.emit({
|
|
7806
|
+
invitationId: String(invitation.id),
|
|
7807
|
+
email: invitation.email,
|
|
7808
|
+
userId: String(newUser.id),
|
|
7809
|
+
roleId: Number(invitation.roleId),
|
|
7810
|
+
invitedBy: String(invitation.invitedBy),
|
|
7811
|
+
metadata: invitation.metadata
|
|
7812
|
+
});
|
|
7829
7813
|
return {
|
|
7830
7814
|
userId: newUser.id,
|
|
7831
7815
|
email: newUser.email,
|
|
@@ -7870,7 +7854,16 @@ async function resendInvitation(id11, expiresInDays = 7) {
|
|
|
7870
7854
|
if (!updated) {
|
|
7871
7855
|
throw new Error("Failed to update invitation");
|
|
7872
7856
|
}
|
|
7873
|
-
|
|
7857
|
+
await invitationCreatedEvent.emit({
|
|
7858
|
+
invitationId: String(invitation.id),
|
|
7859
|
+
email: invitation.email,
|
|
7860
|
+
token: invitation.token,
|
|
7861
|
+
roleId: Number(invitation.roleId),
|
|
7862
|
+
invitedBy: String(invitation.invitedBy),
|
|
7863
|
+
expiresAt: newExpiresAt.toISOString(),
|
|
7864
|
+
isResend: true,
|
|
7865
|
+
metadata: invitation.metadata
|
|
7866
|
+
});
|
|
7874
7867
|
return updated;
|
|
7875
7868
|
}
|
|
7876
7869
|
|
|
@@ -7884,6 +7877,7 @@ async function getAuthSessionService(userId) {
|
|
|
7884
7877
|
]);
|
|
7885
7878
|
return {
|
|
7886
7879
|
userId: user.userId,
|
|
7880
|
+
publicId: user.publicId,
|
|
7887
7881
|
email: user.email,
|
|
7888
7882
|
emailVerified: user.isEmailVerified,
|
|
7889
7883
|
phoneVerified: user.isPhoneVerified,
|
|
@@ -7891,6 +7885,38 @@ async function getAuthSessionService(userId) {
|
|
|
7891
7885
|
};
|
|
7892
7886
|
}
|
|
7893
7887
|
|
|
7888
|
+
// src/server/lib/one-time-token.ts
|
|
7889
|
+
import { SSETokenManager } from "@spfn/core/event/sse";
|
|
7890
|
+
var manager = null;
|
|
7891
|
+
function initOneTimeTokenManager(config) {
|
|
7892
|
+
if (manager) {
|
|
7893
|
+
manager.destroy();
|
|
7894
|
+
}
|
|
7895
|
+
manager = new SSETokenManager({
|
|
7896
|
+
ttl: config?.ttl
|
|
7897
|
+
});
|
|
7898
|
+
}
|
|
7899
|
+
function getOneTimeTokenManager() {
|
|
7900
|
+
if (!manager) {
|
|
7901
|
+
throw new Error(
|
|
7902
|
+
"OneTimeTokenManager not initialized. Ensure createAuthLifecycle() is configured in your server config."
|
|
7903
|
+
);
|
|
7904
|
+
}
|
|
7905
|
+
return manager;
|
|
7906
|
+
}
|
|
7907
|
+
|
|
7908
|
+
// src/server/services/one-time-token.service.ts
|
|
7909
|
+
async function issueOneTimeTokenService(userId) {
|
|
7910
|
+
const manager2 = getOneTimeTokenManager();
|
|
7911
|
+
const token = await manager2.issue(userId);
|
|
7912
|
+
const expiresAt = new Date(Date.now() + 3e4).toISOString();
|
|
7913
|
+
return { token, expiresAt };
|
|
7914
|
+
}
|
|
7915
|
+
async function verifyOneTimeTokenService(token) {
|
|
7916
|
+
const manager2 = getOneTimeTokenManager();
|
|
7917
|
+
return await manager2.verify(token);
|
|
7918
|
+
}
|
|
7919
|
+
|
|
7894
7920
|
// src/server/services/user-profile.service.ts
|
|
7895
7921
|
init_repositories();
|
|
7896
7922
|
async function getUserProfileService(userId) {
|
|
@@ -7901,7 +7927,9 @@ async function getUserProfileService(userId) {
|
|
|
7901
7927
|
]);
|
|
7902
7928
|
return {
|
|
7903
7929
|
userId: user.userId,
|
|
7930
|
+
publicId: user.publicId,
|
|
7904
7931
|
email: user.email,
|
|
7932
|
+
username: user.username,
|
|
7905
7933
|
emailVerified: user.isEmailVerified,
|
|
7906
7934
|
phoneVerified: user.isPhoneVerified,
|
|
7907
7935
|
lastLoginAt: user.lastLoginAt,
|
|
@@ -7910,6 +7938,12 @@ async function getUserProfileService(userId) {
|
|
|
7910
7938
|
profile
|
|
7911
7939
|
};
|
|
7912
7940
|
}
|
|
7941
|
+
async function updateLocaleService(userId, locale) {
|
|
7942
|
+
const userIdNum = Number(userId);
|
|
7943
|
+
const normalized = locale.trim() || "en";
|
|
7944
|
+
await userProfilesRepository.upsertByUserId(userIdNum, { locale: normalized });
|
|
7945
|
+
return { locale: normalized };
|
|
7946
|
+
}
|
|
7913
7947
|
function emptyToNull(value) {
|
|
7914
7948
|
if (value === "") {
|
|
7915
7949
|
return null;
|
|
@@ -7920,7 +7954,7 @@ async function updateUserProfileService(userId, params) {
|
|
|
7920
7954
|
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7921
7955
|
const updateData = {};
|
|
7922
7956
|
if (params.displayName !== void 0) {
|
|
7923
|
-
updateData.displayName = emptyToNull(params.displayName)
|
|
7957
|
+
updateData.displayName = emptyToNull(params.displayName);
|
|
7924
7958
|
}
|
|
7925
7959
|
if (params.firstName !== void 0) {
|
|
7926
7960
|
updateData.firstName = emptyToNull(params.firstName);
|
|
@@ -7961,15 +7995,355 @@ async function updateUserProfileService(userId, params) {
|
|
|
7961
7995
|
if (params.metadata !== void 0) {
|
|
7962
7996
|
updateData.metadata = params.metadata;
|
|
7963
7997
|
}
|
|
7964
|
-
const existing = await userProfilesRepository.findByUserId(userIdNum);
|
|
7965
|
-
if (!existing && !updateData.displayName) {
|
|
7966
|
-
updateData.displayName = "User";
|
|
7967
|
-
}
|
|
7968
7998
|
await userProfilesRepository.upsertByUserId(userIdNum, updateData);
|
|
7969
7999
|
const profile = await userProfilesRepository.fetchProfileData(userIdNum);
|
|
7970
8000
|
return profile;
|
|
7971
8001
|
}
|
|
7972
8002
|
|
|
8003
|
+
// src/server/services/oauth.service.ts
|
|
8004
|
+
init_repositories();
|
|
8005
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
8006
|
+
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
8007
|
+
|
|
8008
|
+
// src/server/lib/oauth/google.ts
|
|
8009
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
8010
|
+
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8011
|
+
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
8012
|
+
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
8013
|
+
function isGoogleOAuthEnabled() {
|
|
8014
|
+
return !!(env6.SPFN_AUTH_GOOGLE_CLIENT_ID && env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
8015
|
+
}
|
|
8016
|
+
function getGoogleOAuthConfig() {
|
|
8017
|
+
const clientId = env6.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
8018
|
+
const clientSecret = env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
8019
|
+
if (!clientId || !clientSecret) {
|
|
8020
|
+
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
8021
|
+
}
|
|
8022
|
+
const baseUrl = env6.NEXT_PUBLIC_SPFN_API_URL || env6.SPFN_API_URL;
|
|
8023
|
+
const redirectUri = env6.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
8024
|
+
return {
|
|
8025
|
+
clientId,
|
|
8026
|
+
clientSecret,
|
|
8027
|
+
redirectUri
|
|
8028
|
+
};
|
|
8029
|
+
}
|
|
8030
|
+
function getDefaultScopes() {
|
|
8031
|
+
const envScopes = env6.SPFN_AUTH_GOOGLE_SCOPES;
|
|
8032
|
+
if (envScopes) {
|
|
8033
|
+
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8034
|
+
}
|
|
8035
|
+
return ["email", "profile"];
|
|
8036
|
+
}
|
|
8037
|
+
function getGoogleAuthUrl(state, scopes) {
|
|
8038
|
+
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
8039
|
+
const config = getGoogleOAuthConfig();
|
|
8040
|
+
const params = new URLSearchParams({
|
|
8041
|
+
client_id: config.clientId,
|
|
8042
|
+
redirect_uri: config.redirectUri,
|
|
8043
|
+
response_type: "code",
|
|
8044
|
+
scope: resolvedScopes.join(" "),
|
|
8045
|
+
state,
|
|
8046
|
+
access_type: "offline",
|
|
8047
|
+
// refresh_token 받기 위해
|
|
8048
|
+
prompt: "consent"
|
|
8049
|
+
// 매번 동의 화면 표시 (refresh_token 보장)
|
|
8050
|
+
});
|
|
8051
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
8052
|
+
}
|
|
8053
|
+
async function exchangeCodeForTokens(code) {
|
|
8054
|
+
const config = getGoogleOAuthConfig();
|
|
8055
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
8056
|
+
method: "POST",
|
|
8057
|
+
headers: {
|
|
8058
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
8059
|
+
},
|
|
8060
|
+
body: new URLSearchParams({
|
|
8061
|
+
client_id: config.clientId,
|
|
8062
|
+
client_secret: config.clientSecret,
|
|
8063
|
+
redirect_uri: config.redirectUri,
|
|
8064
|
+
grant_type: "authorization_code",
|
|
8065
|
+
code
|
|
8066
|
+
})
|
|
8067
|
+
});
|
|
8068
|
+
if (!response.ok) {
|
|
8069
|
+
const error = await response.text();
|
|
8070
|
+
throw new Error(`Failed to exchange code for tokens: ${error}`);
|
|
8071
|
+
}
|
|
8072
|
+
return response.json();
|
|
8073
|
+
}
|
|
8074
|
+
async function getGoogleUserInfo(accessToken) {
|
|
8075
|
+
const response = await fetch(GOOGLE_USERINFO_URL, {
|
|
8076
|
+
headers: {
|
|
8077
|
+
Authorization: `Bearer ${accessToken}`
|
|
8078
|
+
}
|
|
8079
|
+
});
|
|
8080
|
+
if (!response.ok) {
|
|
8081
|
+
const error = await response.text();
|
|
8082
|
+
throw new Error(`Failed to get user info: ${error}`);
|
|
8083
|
+
}
|
|
8084
|
+
return response.json();
|
|
8085
|
+
}
|
|
8086
|
+
async function refreshAccessToken(refreshToken) {
|
|
8087
|
+
const config = getGoogleOAuthConfig();
|
|
8088
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
8089
|
+
method: "POST",
|
|
8090
|
+
headers: {
|
|
8091
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
8092
|
+
},
|
|
8093
|
+
body: new URLSearchParams({
|
|
8094
|
+
client_id: config.clientId,
|
|
8095
|
+
client_secret: config.clientSecret,
|
|
8096
|
+
refresh_token: refreshToken,
|
|
8097
|
+
grant_type: "refresh_token"
|
|
8098
|
+
})
|
|
8099
|
+
});
|
|
8100
|
+
if (!response.ok) {
|
|
8101
|
+
const error = await response.text();
|
|
8102
|
+
throw new Error(`Failed to refresh access token: ${error}`);
|
|
8103
|
+
}
|
|
8104
|
+
return response.json();
|
|
8105
|
+
}
|
|
8106
|
+
|
|
8107
|
+
// src/server/lib/oauth/state.ts
|
|
8108
|
+
import * as jose from "jose";
|
|
8109
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
8110
|
+
async function getStateKey() {
|
|
8111
|
+
const secret = env7.SPFN_AUTH_SESSION_SECRET;
|
|
8112
|
+
const encoder = new TextEncoder();
|
|
8113
|
+
const data = encoder.encode(`oauth-state:${secret}`);
|
|
8114
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
8115
|
+
return new Uint8Array(hashBuffer);
|
|
8116
|
+
}
|
|
8117
|
+
function generateNonce() {
|
|
8118
|
+
const array = new Uint8Array(16);
|
|
8119
|
+
crypto.getRandomValues(array);
|
|
8120
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
8121
|
+
}
|
|
8122
|
+
async function createOAuthState(params) {
|
|
8123
|
+
const key = await getStateKey();
|
|
8124
|
+
const state = {
|
|
8125
|
+
returnUrl: params.returnUrl,
|
|
8126
|
+
nonce: generateNonce(),
|
|
8127
|
+
provider: params.provider,
|
|
8128
|
+
publicKey: params.publicKey,
|
|
8129
|
+
keyId: params.keyId,
|
|
8130
|
+
fingerprint: params.fingerprint,
|
|
8131
|
+
algorithm: params.algorithm,
|
|
8132
|
+
metadata: params.metadata
|
|
8133
|
+
};
|
|
8134
|
+
const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
|
|
8135
|
+
return encodeURIComponent(jwe);
|
|
8136
|
+
}
|
|
8137
|
+
async function verifyOAuthState(encryptedState) {
|
|
8138
|
+
const key = await getStateKey();
|
|
8139
|
+
const jwe = decodeURIComponent(encryptedState);
|
|
8140
|
+
const { payload } = await jose.jwtDecrypt(jwe, key);
|
|
8141
|
+
return payload.state;
|
|
8142
|
+
}
|
|
8143
|
+
|
|
8144
|
+
// src/server/services/oauth.service.ts
|
|
8145
|
+
async function oauthStartService(params) {
|
|
8146
|
+
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
8147
|
+
if (provider === "google") {
|
|
8148
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8149
|
+
throw new ValidationError3({
|
|
8150
|
+
message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
|
|
8151
|
+
});
|
|
8152
|
+
}
|
|
8153
|
+
const state = await createOAuthState({
|
|
8154
|
+
provider: "google",
|
|
8155
|
+
returnUrl,
|
|
8156
|
+
publicKey,
|
|
8157
|
+
keyId,
|
|
8158
|
+
fingerprint,
|
|
8159
|
+
algorithm,
|
|
8160
|
+
metadata
|
|
8161
|
+
});
|
|
8162
|
+
const authUrl = getGoogleAuthUrl(state);
|
|
8163
|
+
return { authUrl };
|
|
8164
|
+
}
|
|
8165
|
+
throw new ValidationError3({
|
|
8166
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
8167
|
+
});
|
|
8168
|
+
}
|
|
8169
|
+
async function oauthCallbackService(params) {
|
|
8170
|
+
const { provider, code, state } = params;
|
|
8171
|
+
const stateData = await verifyOAuthState(state);
|
|
8172
|
+
if (stateData.provider !== provider) {
|
|
8173
|
+
throw new ValidationError3({
|
|
8174
|
+
message: "OAuth state provider mismatch"
|
|
8175
|
+
});
|
|
8176
|
+
}
|
|
8177
|
+
if (provider === "google") {
|
|
8178
|
+
return handleGoogleCallback(code, stateData);
|
|
8179
|
+
}
|
|
8180
|
+
throw new ValidationError3({
|
|
8181
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
8182
|
+
});
|
|
8183
|
+
}
|
|
8184
|
+
async function handleGoogleCallback(code, stateData) {
|
|
8185
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
8186
|
+
const googleUser = await getGoogleUserInfo(tokens.access_token);
|
|
8187
|
+
const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
|
|
8188
|
+
"google",
|
|
8189
|
+
googleUser.id
|
|
8190
|
+
);
|
|
8191
|
+
let userId;
|
|
8192
|
+
let isNewUser = false;
|
|
8193
|
+
if (existingSocialAccount) {
|
|
8194
|
+
userId = existingSocialAccount.userId;
|
|
8195
|
+
await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
|
|
8196
|
+
accessToken: tokens.access_token,
|
|
8197
|
+
refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
|
|
8198
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8199
|
+
});
|
|
8200
|
+
} else {
|
|
8201
|
+
const result = await createOrLinkUser(googleUser, tokens);
|
|
8202
|
+
userId = result.userId;
|
|
8203
|
+
isNewUser = result.isNewUser;
|
|
8204
|
+
}
|
|
8205
|
+
await registerPublicKeyService({
|
|
8206
|
+
userId,
|
|
8207
|
+
keyId: stateData.keyId,
|
|
8208
|
+
publicKey: stateData.publicKey,
|
|
8209
|
+
fingerprint: stateData.fingerprint,
|
|
8210
|
+
algorithm: stateData.algorithm
|
|
8211
|
+
});
|
|
8212
|
+
await updateLastLoginService(userId);
|
|
8213
|
+
const appUrl = env8.NEXT_PUBLIC_SPFN_APP_URL || env8.SPFN_APP_URL;
|
|
8214
|
+
const callbackPath = env8.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
8215
|
+
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
8216
|
+
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
8217
|
+
userId: String(userId),
|
|
8218
|
+
keyId: stateData.keyId,
|
|
8219
|
+
returnUrl: stateData.returnUrl,
|
|
8220
|
+
isNewUser: String(isNewUser)
|
|
8221
|
+
});
|
|
8222
|
+
const user = await usersRepository.findById(userId);
|
|
8223
|
+
const eventPayload = {
|
|
8224
|
+
userId: String(userId),
|
|
8225
|
+
provider: "google",
|
|
8226
|
+
email: user?.email || void 0,
|
|
8227
|
+
phone: user?.phone || void 0,
|
|
8228
|
+
metadata: stateData.metadata
|
|
8229
|
+
};
|
|
8230
|
+
if (isNewUser) {
|
|
8231
|
+
await authRegisterEvent.emit(eventPayload);
|
|
8232
|
+
} else {
|
|
8233
|
+
await authLoginEvent.emit(eventPayload);
|
|
8234
|
+
}
|
|
8235
|
+
return {
|
|
8236
|
+
redirectUrl,
|
|
8237
|
+
userId: String(userId),
|
|
8238
|
+
keyId: stateData.keyId,
|
|
8239
|
+
isNewUser
|
|
8240
|
+
};
|
|
8241
|
+
}
|
|
8242
|
+
async function createOrLinkUser(googleUser, tokens) {
|
|
8243
|
+
const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
|
|
8244
|
+
let userId;
|
|
8245
|
+
let isNewUser = false;
|
|
8246
|
+
if (existingUser) {
|
|
8247
|
+
if (!googleUser.verified_email) {
|
|
8248
|
+
throw new ValidationError3({
|
|
8249
|
+
message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
|
|
8250
|
+
});
|
|
8251
|
+
}
|
|
8252
|
+
userId = existingUser.id;
|
|
8253
|
+
if (!existingUser.emailVerifiedAt) {
|
|
8254
|
+
await usersRepository.updateById(existingUser.id, {
|
|
8255
|
+
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
8256
|
+
});
|
|
8257
|
+
}
|
|
8258
|
+
} else {
|
|
8259
|
+
const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
8260
|
+
const userRole = await getRoleByName3("user");
|
|
8261
|
+
if (!userRole) {
|
|
8262
|
+
throw new Error("Default user role not found. Run initializeAuth() first.");
|
|
8263
|
+
}
|
|
8264
|
+
const newUser = await usersRepository.create({
|
|
8265
|
+
email: googleUser.verified_email ? googleUser.email : null,
|
|
8266
|
+
phone: null,
|
|
8267
|
+
passwordHash: null,
|
|
8268
|
+
// OAuth 사용자는 비밀번호 없음
|
|
8269
|
+
passwordChangeRequired: false,
|
|
8270
|
+
roleId: userRole.id,
|
|
8271
|
+
status: "active",
|
|
8272
|
+
emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
|
|
8273
|
+
});
|
|
8274
|
+
userId = newUser.id;
|
|
8275
|
+
isNewUser = true;
|
|
8276
|
+
}
|
|
8277
|
+
await socialAccountsRepository.create({
|
|
8278
|
+
userId,
|
|
8279
|
+
provider: "google",
|
|
8280
|
+
providerUserId: googleUser.id,
|
|
8281
|
+
providerEmail: googleUser.email,
|
|
8282
|
+
accessToken: tokens.access_token,
|
|
8283
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
8284
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8285
|
+
});
|
|
8286
|
+
return { userId, isNewUser };
|
|
8287
|
+
}
|
|
8288
|
+
function buildRedirectUrl(baseUrl, params) {
|
|
8289
|
+
const url = new URL(baseUrl, "http://placeholder");
|
|
8290
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8291
|
+
url.searchParams.set(key, value);
|
|
8292
|
+
}
|
|
8293
|
+
if (baseUrl.startsWith("http")) {
|
|
8294
|
+
return url.toString();
|
|
8295
|
+
}
|
|
8296
|
+
return `${url.pathname}${url.search}`;
|
|
8297
|
+
}
|
|
8298
|
+
function buildOAuthErrorUrl(error) {
|
|
8299
|
+
const errorUrl = env8.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
8300
|
+
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
8301
|
+
}
|
|
8302
|
+
function isOAuthProviderEnabled(provider) {
|
|
8303
|
+
switch (provider) {
|
|
8304
|
+
case "google":
|
|
8305
|
+
return isGoogleOAuthEnabled();
|
|
8306
|
+
case "github":
|
|
8307
|
+
case "kakao":
|
|
8308
|
+
case "naver":
|
|
8309
|
+
return false;
|
|
8310
|
+
default:
|
|
8311
|
+
return false;
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
function getEnabledOAuthProviders() {
|
|
8315
|
+
const providers = [];
|
|
8316
|
+
if (isGoogleOAuthEnabled()) {
|
|
8317
|
+
providers.push("google");
|
|
8318
|
+
}
|
|
8319
|
+
return providers;
|
|
8320
|
+
}
|
|
8321
|
+
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
8322
|
+
async function getGoogleAccessToken(userId) {
|
|
8323
|
+
const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
|
|
8324
|
+
if (!account) {
|
|
8325
|
+
throw new ValidationError3({
|
|
8326
|
+
message: "No Google account linked. User must sign in with Google first."
|
|
8327
|
+
});
|
|
8328
|
+
}
|
|
8329
|
+
const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
|
|
8330
|
+
if (!isExpired && account.accessToken) {
|
|
8331
|
+
return account.accessToken;
|
|
8332
|
+
}
|
|
8333
|
+
if (!account.refreshToken) {
|
|
8334
|
+
throw new ValidationError3({
|
|
8335
|
+
message: "Google refresh token not available. User must re-authenticate with Google."
|
|
8336
|
+
});
|
|
8337
|
+
}
|
|
8338
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
8339
|
+
await socialAccountsRepository.updateTokens(account.id, {
|
|
8340
|
+
accessToken: tokens.access_token,
|
|
8341
|
+
refreshToken: tokens.refresh_token ?? account.refreshToken,
|
|
8342
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8343
|
+
});
|
|
8344
|
+
return tokens.access_token;
|
|
8345
|
+
}
|
|
8346
|
+
|
|
7973
8347
|
// src/server/routes/auth/index.ts
|
|
7974
8348
|
init_esm();
|
|
7975
8349
|
import { Transactional } from "@spfn/core/db";
|
|
@@ -8020,7 +8394,10 @@ var register = route.post("/_auth/register").input({
|
|
|
8020
8394
|
verificationToken: Type.String({
|
|
8021
8395
|
description: "Verification token obtained from /verify-code endpoint"
|
|
8022
8396
|
}),
|
|
8023
|
-
password: PasswordSchema
|
|
8397
|
+
password: PasswordSchema,
|
|
8398
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
8399
|
+
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
8400
|
+
}))
|
|
8024
8401
|
}, {
|
|
8025
8402
|
minProperties: 3,
|
|
8026
8403
|
// email/phone + verificationToken + password
|
|
@@ -8062,9 +8439,7 @@ var login = route.post("/_auth/login").input({
|
|
|
8062
8439
|
const { body } = await c.data();
|
|
8063
8440
|
return await loginService(body);
|
|
8064
8441
|
});
|
|
8065
|
-
var logout = route.post("/_auth/logout").
|
|
8066
|
-
body: Type.Object({})
|
|
8067
|
-
}).handler(async (c) => {
|
|
8442
|
+
var logout = route.post("/_auth/logout").handler(async (c) => {
|
|
8068
8443
|
const auth = getAuth(c);
|
|
8069
8444
|
if (!auth) {
|
|
8070
8445
|
return c.noContent();
|
|
@@ -8073,9 +8448,7 @@ var logout = route.post("/_auth/logout").input({
|
|
|
8073
8448
|
await logoutService({ userId: Number(userId), keyId });
|
|
8074
8449
|
return c.noContent();
|
|
8075
8450
|
});
|
|
8076
|
-
var rotateKey = route.post("/_auth/keys/rotate").
|
|
8077
|
-
body: Type.Object({})
|
|
8078
|
-
}).interceptor({
|
|
8451
|
+
var rotateKey = route.post("/_auth/keys/rotate").interceptor({
|
|
8079
8452
|
body: Type.Object({
|
|
8080
8453
|
publicKey: Type.String({ description: "New public key" }),
|
|
8081
8454
|
keyId: Type.String({ description: "New key identifier" }),
|
|
@@ -8117,6 +8490,10 @@ var getAuthSession = route.get("/_auth/session").handler(async (c) => {
|
|
|
8117
8490
|
const { userId } = getAuth(c);
|
|
8118
8491
|
return await getAuthSessionService(userId);
|
|
8119
8492
|
});
|
|
8493
|
+
var issueOneTimeToken = route.post("/_auth/tokens").handler(async (c) => {
|
|
8494
|
+
const { userId } = getAuth(c);
|
|
8495
|
+
return await issueOneTimeTokenService(userId);
|
|
8496
|
+
});
|
|
8120
8497
|
var authRouter = defineRouter({
|
|
8121
8498
|
checkAccountExists,
|
|
8122
8499
|
sendVerificationCode,
|
|
@@ -8126,7 +8503,8 @@ var authRouter = defineRouter({
|
|
|
8126
8503
|
logout,
|
|
8127
8504
|
rotateKey,
|
|
8128
8505
|
changePassword,
|
|
8129
|
-
getAuthSession
|
|
8506
|
+
getAuthSession,
|
|
8507
|
+
issueOneTimeToken
|
|
8130
8508
|
});
|
|
8131
8509
|
|
|
8132
8510
|
// src/server/routes/invitations/index.ts
|
|
@@ -8135,7 +8513,7 @@ import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
|
|
|
8135
8513
|
// src/server/middleware/authenticate.ts
|
|
8136
8514
|
import { defineMiddleware } from "@spfn/core/route";
|
|
8137
8515
|
import { UnauthorizedError } from "@spfn/core/errors";
|
|
8138
|
-
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2 } from "@spfn/auth/server";
|
|
8516
|
+
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2, userProfilesRepository as userProfilesRepository2 } from "@spfn/auth/server";
|
|
8139
8517
|
import {
|
|
8140
8518
|
InvalidTokenError,
|
|
8141
8519
|
TokenExpiredError,
|
|
@@ -8182,10 +8560,14 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8182
8560
|
}
|
|
8183
8561
|
throw new UnauthorizedError({ message: "Authentication failed" });
|
|
8184
8562
|
}
|
|
8185
|
-
const
|
|
8186
|
-
|
|
8563
|
+
const [result, locale] = await Promise.all([
|
|
8564
|
+
usersRepository2.findByIdWithRole(keyRecord.userId),
|
|
8565
|
+
userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
|
|
8566
|
+
]);
|
|
8567
|
+
if (!result) {
|
|
8187
8568
|
throw new UnauthorizedError({ message: "User not found" });
|
|
8188
8569
|
}
|
|
8570
|
+
const { user, role } = result;
|
|
8189
8571
|
if (user.status !== "active") {
|
|
8190
8572
|
throw new AccountDisabledError2({ status: user.status });
|
|
8191
8573
|
}
|
|
@@ -8193,7 +8575,9 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8193
8575
|
c.set("auth", {
|
|
8194
8576
|
user,
|
|
8195
8577
|
userId: String(user.id),
|
|
8196
|
-
keyId
|
|
8578
|
+
keyId,
|
|
8579
|
+
role: role?.name ?? null,
|
|
8580
|
+
locale
|
|
8197
8581
|
});
|
|
8198
8582
|
const method = c.req.method;
|
|
8199
8583
|
const path = c.req.path;
|
|
@@ -8208,6 +8592,55 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8208
8592
|
});
|
|
8209
8593
|
await next();
|
|
8210
8594
|
});
|
|
8595
|
+
var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
8596
|
+
const authHeader = c.req.header("Authorization");
|
|
8597
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
8598
|
+
await next();
|
|
8599
|
+
return;
|
|
8600
|
+
}
|
|
8601
|
+
const token = authHeader.substring(7);
|
|
8602
|
+
try {
|
|
8603
|
+
const decoded = decodeToken2(token);
|
|
8604
|
+
if (!decoded || !decoded.keyId) {
|
|
8605
|
+
await next();
|
|
8606
|
+
return;
|
|
8607
|
+
}
|
|
8608
|
+
const keyId = decoded.keyId;
|
|
8609
|
+
const keyRecord = await keysRepository2.findActiveByKeyId(keyId);
|
|
8610
|
+
if (!keyRecord) {
|
|
8611
|
+
await next();
|
|
8612
|
+
return;
|
|
8613
|
+
}
|
|
8614
|
+
if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
|
|
8615
|
+
await next();
|
|
8616
|
+
return;
|
|
8617
|
+
}
|
|
8618
|
+
verifyClientToken2(
|
|
8619
|
+
token,
|
|
8620
|
+
keyRecord.publicKey,
|
|
8621
|
+
keyRecord.algorithm
|
|
8622
|
+
);
|
|
8623
|
+
const [result, locale] = await Promise.all([
|
|
8624
|
+
usersRepository2.findByIdWithRole(keyRecord.userId),
|
|
8625
|
+
userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
|
|
8626
|
+
]);
|
|
8627
|
+
if (!result || result.user.status !== "active") {
|
|
8628
|
+
await next();
|
|
8629
|
+
return;
|
|
8630
|
+
}
|
|
8631
|
+
const { user, role } = result;
|
|
8632
|
+
keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
8633
|
+
c.set("auth", {
|
|
8634
|
+
user,
|
|
8635
|
+
userId: String(user.id),
|
|
8636
|
+
keyId,
|
|
8637
|
+
role: role?.name ?? null,
|
|
8638
|
+
locale
|
|
8639
|
+
});
|
|
8640
|
+
} catch {
|
|
8641
|
+
}
|
|
8642
|
+
await next();
|
|
8643
|
+
}, { skips: ["auth"] });
|
|
8211
8644
|
|
|
8212
8645
|
// src/server/middleware/require-permission.ts
|
|
8213
8646
|
import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
|
|
@@ -8273,7 +8706,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
8273
8706
|
|
|
8274
8707
|
// src/server/middleware/require-role.ts
|
|
8275
8708
|
import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
|
|
8276
|
-
import { getAuth as getAuth3,
|
|
8709
|
+
import { getAuth as getAuth3, authLogger as authLogger4 } from "@spfn/auth/server";
|
|
8277
8710
|
import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
|
|
8278
8711
|
import { InsufficientRoleError } from "@spfn/auth/errors";
|
|
8279
8712
|
var requireRole = defineMiddleware3(
|
|
@@ -8287,11 +8720,11 @@ var requireRole = defineMiddleware3(
|
|
|
8287
8720
|
});
|
|
8288
8721
|
throw new ForbiddenError2({ message: "Authentication required" });
|
|
8289
8722
|
}
|
|
8290
|
-
const { userId } = auth;
|
|
8291
|
-
|
|
8292
|
-
if (!allowed) {
|
|
8723
|
+
const { userId, role: userRole } = auth;
|
|
8724
|
+
if (!userRole || !roleNames.includes(userRole)) {
|
|
8293
8725
|
authLogger4.middleware.warn("Role check failed", {
|
|
8294
8726
|
userId,
|
|
8727
|
+
userRole,
|
|
8295
8728
|
requiredRoles: roleNames,
|
|
8296
8729
|
path: c.req.path
|
|
8297
8730
|
});
|
|
@@ -8299,6 +8732,7 @@ var requireRole = defineMiddleware3(
|
|
|
8299
8732
|
}
|
|
8300
8733
|
authLogger4.middleware.debug("Role check passed", {
|
|
8301
8734
|
userId,
|
|
8735
|
+
userRole,
|
|
8302
8736
|
roles: roleNames
|
|
8303
8737
|
});
|
|
8304
8738
|
await next();
|
|
@@ -8307,7 +8741,7 @@ var requireRole = defineMiddleware3(
|
|
|
8307
8741
|
|
|
8308
8742
|
// src/server/middleware/role-guard.ts
|
|
8309
8743
|
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
8310
|
-
import { getAuth as getAuth4,
|
|
8744
|
+
import { getAuth as getAuth4, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
8311
8745
|
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
8312
8746
|
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
8313
8747
|
var roleGuard = defineMiddleware4(
|
|
@@ -8324,8 +8758,7 @@ var roleGuard = defineMiddleware4(
|
|
|
8324
8758
|
});
|
|
8325
8759
|
throw new ForbiddenError3({ message: "Authentication required" });
|
|
8326
8760
|
}
|
|
8327
|
-
const { userId } = auth;
|
|
8328
|
-
const userRole = await getUserRole2(userId);
|
|
8761
|
+
const { userId, role: userRole } = auth;
|
|
8329
8762
|
if (deny && deny.length > 0) {
|
|
8330
8763
|
if (userRole && deny.includes(userRole)) {
|
|
8331
8764
|
authLogger5.middleware.warn("Role guard denied", {
|
|
@@ -8358,6 +8791,47 @@ var roleGuard = defineMiddleware4(
|
|
|
8358
8791
|
}
|
|
8359
8792
|
);
|
|
8360
8793
|
|
|
8794
|
+
// src/server/middleware/one-time-token-auth.ts
|
|
8795
|
+
import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
|
|
8796
|
+
import { UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
|
|
8797
|
+
import { usersRepository as usersRepository3, userProfilesRepository as userProfilesRepository3 } from "@spfn/auth/server";
|
|
8798
|
+
var oneTimeTokenAuth = defineMiddleware5("oneTimeTokenAuth", async (c, next) => {
|
|
8799
|
+
const token = c.req.query("token") ?? extractOTTHeader(c.req.header("Authorization"));
|
|
8800
|
+
if (!token) {
|
|
8801
|
+
throw new UnauthorizedError2({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
|
|
8802
|
+
}
|
|
8803
|
+
const userId = await verifyOneTimeTokenService(token);
|
|
8804
|
+
if (!userId) {
|
|
8805
|
+
throw new UnauthorizedError2({ message: "Invalid or expired one-time token" });
|
|
8806
|
+
}
|
|
8807
|
+
const [result, locale] = await Promise.all([
|
|
8808
|
+
usersRepository3.findByIdWithRole(Number(userId)),
|
|
8809
|
+
userProfilesRepository3.findLocaleByUserId(Number(userId))
|
|
8810
|
+
]);
|
|
8811
|
+
if (!result) {
|
|
8812
|
+
throw new UnauthorizedError2({ message: "User not found" });
|
|
8813
|
+
}
|
|
8814
|
+
const { user, role } = result;
|
|
8815
|
+
if (user.status !== "active") {
|
|
8816
|
+
throw new UnauthorizedError2({ message: "Account is not active" });
|
|
8817
|
+
}
|
|
8818
|
+
c.set("auth", {
|
|
8819
|
+
user,
|
|
8820
|
+
userId: String(user.id),
|
|
8821
|
+
keyId: "",
|
|
8822
|
+
// No key involved in OTT auth
|
|
8823
|
+
role: role?.name ?? null,
|
|
8824
|
+
locale
|
|
8825
|
+
});
|
|
8826
|
+
await next();
|
|
8827
|
+
}, { skips: ["auth"] });
|
|
8828
|
+
function extractOTTHeader(header) {
|
|
8829
|
+
if (!header || !header.startsWith("OTT ")) {
|
|
8830
|
+
return null;
|
|
8831
|
+
}
|
|
8832
|
+
return header.substring(4);
|
|
8833
|
+
}
|
|
8834
|
+
|
|
8361
8835
|
// src/server/routes/invitations/index.ts
|
|
8362
8836
|
init_types();
|
|
8363
8837
|
init_esm();
|
|
@@ -8433,6 +8907,10 @@ var createInvitation2 = route2.post("/_auth/invitations").input({
|
|
|
8433
8907
|
maximum: 30,
|
|
8434
8908
|
description: "Days until invitation expires (default: 7)"
|
|
8435
8909
|
})),
|
|
8910
|
+
expiresAt: Type.Optional(Type.String({
|
|
8911
|
+
format: "date-time",
|
|
8912
|
+
description: "Exact expiration timestamp (ISO 8601). Takes precedence over expiresInDays."
|
|
8913
|
+
})),
|
|
8436
8914
|
metadata: Type.Optional(Type.Any({
|
|
8437
8915
|
description: "Custom metadata (welcome message, department, etc.)"
|
|
8438
8916
|
}))
|
|
@@ -8445,6 +8923,7 @@ var createInvitation2 = route2.post("/_auth/invitations").input({
|
|
|
8445
8923
|
roleId: body.roleId,
|
|
8446
8924
|
invitedBy: Number(userId),
|
|
8447
8925
|
expiresInDays: body.expiresInDays,
|
|
8926
|
+
expiresAt: body.expiresAt ? new Date(body.expiresAt) : void 0,
|
|
8448
8927
|
metadata: body.metadata
|
|
8449
8928
|
});
|
|
8450
8929
|
const baseUrl = process.env.SPFN_API_URL || "http://localhost:8790";
|
|
@@ -8579,17 +9058,298 @@ var updateUserProfile = route3.patch("/_auth/users/profile").input({
|
|
|
8579
9058
|
const { body } = await c.data();
|
|
8580
9059
|
return await updateUserProfileService(userId, body);
|
|
8581
9060
|
});
|
|
9061
|
+
var checkUsername = route3.get("/_auth/users/username/check").input({
|
|
9062
|
+
query: Type.Object({
|
|
9063
|
+
username: Type.String({ minLength: 1 })
|
|
9064
|
+
})
|
|
9065
|
+
}).handler(async (c) => {
|
|
9066
|
+
const { query } = await c.data();
|
|
9067
|
+
return { available: await checkUsernameAvailableService(query.username) };
|
|
9068
|
+
});
|
|
9069
|
+
var updateUsername = route3.patch("/_auth/users/username").input({
|
|
9070
|
+
body: Type.Object({
|
|
9071
|
+
username: Type.Union([
|
|
9072
|
+
Type.String({ minLength: 1 }),
|
|
9073
|
+
Type.Null()
|
|
9074
|
+
], { description: "New username or null to clear" })
|
|
9075
|
+
})
|
|
9076
|
+
}).handler(async (c) => {
|
|
9077
|
+
const { userId } = getAuth(c);
|
|
9078
|
+
const { body } = await c.data();
|
|
9079
|
+
return await updateUsernameService(userId, body.username);
|
|
9080
|
+
});
|
|
9081
|
+
var updateLocale = route3.patch("/_auth/users/locale").input({
|
|
9082
|
+
body: Type.Object({
|
|
9083
|
+
locale: Type.String({ minLength: 1, description: "Locale code (e.g., en, ko, ja)" })
|
|
9084
|
+
})
|
|
9085
|
+
}).handler(async (c) => {
|
|
9086
|
+
const { userId } = getAuth(c);
|
|
9087
|
+
const { body } = await c.data();
|
|
9088
|
+
return await updateLocaleService(userId, body.locale);
|
|
9089
|
+
});
|
|
8582
9090
|
var userRouter = defineRouter3({
|
|
8583
9091
|
getUserProfile,
|
|
8584
|
-
updateUserProfile
|
|
9092
|
+
updateUserProfile,
|
|
9093
|
+
checkUsername,
|
|
9094
|
+
updateUsername,
|
|
9095
|
+
updateLocale
|
|
9096
|
+
});
|
|
9097
|
+
|
|
9098
|
+
// src/server/routes/oauth/index.ts
|
|
9099
|
+
init_esm();
|
|
9100
|
+
init_types();
|
|
9101
|
+
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
9102
|
+
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
9103
|
+
var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
9104
|
+
query: Type.Object({
|
|
9105
|
+
state: Type.String({
|
|
9106
|
+
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
9107
|
+
})
|
|
9108
|
+
})
|
|
9109
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
9110
|
+
const { query } = await c.data();
|
|
9111
|
+
if (!isGoogleOAuthEnabled()) {
|
|
9112
|
+
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
9113
|
+
}
|
|
9114
|
+
const authUrl = getGoogleAuthUrl(query.state);
|
|
9115
|
+
return c.redirect(authUrl);
|
|
9116
|
+
});
|
|
9117
|
+
var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
9118
|
+
query: Type.Object({
|
|
9119
|
+
code: Type.Optional(Type.String({
|
|
9120
|
+
description: "Authorization code from Google"
|
|
9121
|
+
})),
|
|
9122
|
+
state: Type.Optional(Type.String({
|
|
9123
|
+
description: "OAuth state parameter"
|
|
9124
|
+
})),
|
|
9125
|
+
error: Type.Optional(Type.String({
|
|
9126
|
+
description: "Error code from Google"
|
|
9127
|
+
})),
|
|
9128
|
+
error_description: Type.Optional(Type.String({
|
|
9129
|
+
description: "Error description from Google"
|
|
9130
|
+
}))
|
|
9131
|
+
})
|
|
9132
|
+
}).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
9133
|
+
const { query } = await c.data();
|
|
9134
|
+
if (query.error) {
|
|
9135
|
+
const errorMessage = query.error_description || query.error;
|
|
9136
|
+
return c.redirect(buildOAuthErrorUrl(errorMessage));
|
|
9137
|
+
}
|
|
9138
|
+
if (!query.code || !query.state) {
|
|
9139
|
+
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9140
|
+
}
|
|
9141
|
+
try {
|
|
9142
|
+
const result = await oauthCallbackService({
|
|
9143
|
+
provider: "google",
|
|
9144
|
+
code: query.code,
|
|
9145
|
+
state: query.state
|
|
9146
|
+
});
|
|
9147
|
+
return c.redirect(result.redirectUrl);
|
|
9148
|
+
} catch (err) {
|
|
9149
|
+
const message = err instanceof Error ? err.message : "OAuth callback failed";
|
|
9150
|
+
return c.redirect(buildOAuthErrorUrl(message));
|
|
9151
|
+
}
|
|
9152
|
+
});
|
|
9153
|
+
var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
9154
|
+
body: Type.Object({
|
|
9155
|
+
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
9156
|
+
description: "OAuth provider (google, github, kakao, naver)"
|
|
9157
|
+
}),
|
|
9158
|
+
returnUrl: Type.String({
|
|
9159
|
+
description: "URL to redirect after OAuth success"
|
|
9160
|
+
}),
|
|
9161
|
+
publicKey: Type.String({
|
|
9162
|
+
description: "Client public key (Base64 DER)"
|
|
9163
|
+
}),
|
|
9164
|
+
keyId: Type.String({
|
|
9165
|
+
description: "Key identifier (UUID)"
|
|
9166
|
+
}),
|
|
9167
|
+
fingerprint: Type.String({
|
|
9168
|
+
description: "Key fingerprint (SHA-256 hex)"
|
|
9169
|
+
}),
|
|
9170
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
|
|
9171
|
+
description: "Key algorithm (ES256 or RS256)"
|
|
9172
|
+
}),
|
|
9173
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
9174
|
+
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
9175
|
+
}))
|
|
9176
|
+
})
|
|
9177
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
9178
|
+
const { body } = await c.data();
|
|
9179
|
+
const result = await oauthStartService(body);
|
|
9180
|
+
return result;
|
|
9181
|
+
});
|
|
9182
|
+
var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
9183
|
+
return {
|
|
9184
|
+
providers: getEnabledOAuthProviders()
|
|
9185
|
+
};
|
|
9186
|
+
});
|
|
9187
|
+
var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
9188
|
+
body: Type.Object({
|
|
9189
|
+
returnUrl: Type.Optional(Type.String({
|
|
9190
|
+
description: "URL to redirect after OAuth success"
|
|
9191
|
+
})),
|
|
9192
|
+
state: Type.Optional(Type.String({
|
|
9193
|
+
description: "Encrypted OAuth state (injected by interceptor)"
|
|
9194
|
+
}))
|
|
9195
|
+
})
|
|
9196
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
9197
|
+
const { body } = await c.data();
|
|
9198
|
+
if (!isGoogleOAuthEnabled()) {
|
|
9199
|
+
throw new Error("Google OAuth is not configured");
|
|
9200
|
+
}
|
|
9201
|
+
if (!body.state) {
|
|
9202
|
+
throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
|
|
9203
|
+
}
|
|
9204
|
+
return { authUrl: getGoogleAuthUrl(body.state) };
|
|
9205
|
+
});
|
|
9206
|
+
var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
|
|
9207
|
+
body: Type.Object({
|
|
9208
|
+
userId: Type.String({ description: "User ID from OAuth callback" }),
|
|
9209
|
+
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
9210
|
+
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
9211
|
+
})
|
|
9212
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
9213
|
+
const { body } = await c.data();
|
|
9214
|
+
return {
|
|
9215
|
+
success: true,
|
|
9216
|
+
userId: body.userId,
|
|
9217
|
+
keyId: body.keyId,
|
|
9218
|
+
returnUrl: body.returnUrl || "/"
|
|
9219
|
+
};
|
|
9220
|
+
});
|
|
9221
|
+
var oauthRouter = defineRouter4({
|
|
9222
|
+
oauthGoogleStart,
|
|
9223
|
+
oauthGoogleCallback,
|
|
9224
|
+
oauthStart,
|
|
9225
|
+
oauthProviders,
|
|
9226
|
+
getGoogleOAuthUrl,
|
|
9227
|
+
oauthFinalize
|
|
9228
|
+
});
|
|
9229
|
+
|
|
9230
|
+
// src/server/routes/admin/index.ts
|
|
9231
|
+
init_esm();
|
|
9232
|
+
import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
|
|
9233
|
+
import { route as route5 } from "@spfn/core/route";
|
|
9234
|
+
var listRoles = route5.get("/_auth/admin/roles").input({
|
|
9235
|
+
query: Type.Object({
|
|
9236
|
+
includeInactive: Type.Optional(Type.Boolean({
|
|
9237
|
+
description: "Include inactive roles (default: false)"
|
|
9238
|
+
}))
|
|
9239
|
+
})
|
|
9240
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9241
|
+
const { query } = await c.data();
|
|
9242
|
+
const roles2 = await getAllRoles(query.includeInactive ?? false);
|
|
9243
|
+
return { roles: roles2 };
|
|
9244
|
+
});
|
|
9245
|
+
var createAdminRole = route5.post("/_auth/admin/roles").input({
|
|
9246
|
+
body: Type.Object({
|
|
9247
|
+
name: Type.String({ description: "Unique role name (slug)" }),
|
|
9248
|
+
displayName: Type.String({ description: "Human-readable role name" }),
|
|
9249
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
9250
|
+
priority: Type.Optional(Type.Number({ description: "Role priority (default: 10)" })),
|
|
9251
|
+
permissionIds: Type.Optional(Type.Array(
|
|
9252
|
+
Type.Number({ description: "Permission ID" }),
|
|
9253
|
+
{ description: "Permission IDs to assign" }
|
|
9254
|
+
))
|
|
9255
|
+
})
|
|
9256
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9257
|
+
const { body } = await c.data();
|
|
9258
|
+
const role = await createRole({
|
|
9259
|
+
name: body.name,
|
|
9260
|
+
displayName: body.displayName,
|
|
9261
|
+
description: body.description,
|
|
9262
|
+
priority: body.priority,
|
|
9263
|
+
permissionIds: body.permissionIds
|
|
9264
|
+
});
|
|
9265
|
+
return { role };
|
|
9266
|
+
});
|
|
9267
|
+
var updateAdminRole = route5.patch("/_auth/admin/roles/:id").input({
|
|
9268
|
+
params: Type.Object({
|
|
9269
|
+
id: Type.Number({ description: "Role ID" })
|
|
9270
|
+
}),
|
|
9271
|
+
body: Type.Object({
|
|
9272
|
+
displayName: Type.Optional(Type.String({ description: "Human-readable role name" })),
|
|
9273
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
9274
|
+
priority: Type.Optional(Type.Number({ description: "Role priority" })),
|
|
9275
|
+
isActive: Type.Optional(Type.Boolean({ description: "Active status" }))
|
|
9276
|
+
})
|
|
9277
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9278
|
+
const { params, body } = await c.data();
|
|
9279
|
+
const role = await updateRole(params.id, body);
|
|
9280
|
+
return { role };
|
|
9281
|
+
});
|
|
9282
|
+
var deleteAdminRole = route5.delete("/_auth/admin/roles/:id").input({
|
|
9283
|
+
params: Type.Object({
|
|
9284
|
+
id: Type.Number({ description: "Role ID" })
|
|
9285
|
+
})
|
|
9286
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9287
|
+
const { params } = await c.data();
|
|
9288
|
+
await deleteRole(params.id);
|
|
9289
|
+
return c.noContent();
|
|
9290
|
+
});
|
|
9291
|
+
var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
9292
|
+
params: Type.Object({
|
|
9293
|
+
userId: Type.Number({ description: "User ID" })
|
|
9294
|
+
}),
|
|
9295
|
+
body: Type.Object({
|
|
9296
|
+
roleId: Type.Number({ description: "New role ID to assign" })
|
|
9297
|
+
})
|
|
9298
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9299
|
+
const { params, body } = await c.data();
|
|
9300
|
+
const auth = getAuth(c);
|
|
9301
|
+
if (params.userId === Number(auth.userId)) {
|
|
9302
|
+
throw new ForbiddenError4({ message: "Cannot change your own role" });
|
|
9303
|
+
}
|
|
9304
|
+
const targetRole = await getUserRole(params.userId);
|
|
9305
|
+
if (targetRole === "superadmin") {
|
|
9306
|
+
throw new ForbiddenError4({ message: "Cannot modify superadmin role" });
|
|
9307
|
+
}
|
|
9308
|
+
await updateUserService(params.userId, { roleId: body.roleId });
|
|
9309
|
+
return { userId: params.userId, roleId: body.roleId };
|
|
8585
9310
|
});
|
|
8586
9311
|
|
|
8587
9312
|
// src/server/routes/index.ts
|
|
8588
|
-
var mainAuthRouter =
|
|
8589
|
-
//
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
9313
|
+
var mainAuthRouter = defineRouter5({
|
|
9314
|
+
// Auth routes
|
|
9315
|
+
checkAccountExists,
|
|
9316
|
+
sendVerificationCode,
|
|
9317
|
+
verifyCode,
|
|
9318
|
+
register,
|
|
9319
|
+
login,
|
|
9320
|
+
logout,
|
|
9321
|
+
rotateKey,
|
|
9322
|
+
changePassword,
|
|
9323
|
+
getAuthSession,
|
|
9324
|
+
// One-Time Token routes
|
|
9325
|
+
issueOneTimeToken,
|
|
9326
|
+
// OAuth routes
|
|
9327
|
+
oauthGoogleStart,
|
|
9328
|
+
oauthGoogleCallback,
|
|
9329
|
+
oauthStart,
|
|
9330
|
+
oauthProviders,
|
|
9331
|
+
getGoogleOAuthUrl,
|
|
9332
|
+
oauthFinalize,
|
|
9333
|
+
// Invitation routes
|
|
9334
|
+
getInvitation,
|
|
9335
|
+
acceptInvitation: acceptInvitation2,
|
|
9336
|
+
createInvitation: createInvitation2,
|
|
9337
|
+
listInvitations: listInvitations2,
|
|
9338
|
+
cancelInvitation: cancelInvitation2,
|
|
9339
|
+
resendInvitation: resendInvitation2,
|
|
9340
|
+
deleteInvitation: deleteInvitation2,
|
|
9341
|
+
// User routes
|
|
9342
|
+
getUserProfile,
|
|
9343
|
+
updateUserProfile,
|
|
9344
|
+
checkUsername,
|
|
9345
|
+
updateUsername,
|
|
9346
|
+
updateLocale,
|
|
9347
|
+
// Admin routes (superadmin only)
|
|
9348
|
+
listRoles,
|
|
9349
|
+
createAdminRole,
|
|
9350
|
+
updateAdminRole,
|
|
9351
|
+
deleteAdminRole,
|
|
9352
|
+
updateUserRole
|
|
8593
9353
|
});
|
|
8594
9354
|
|
|
8595
9355
|
// src/server.ts
|
|
@@ -8699,36 +9459,60 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
8699
9459
|
}
|
|
8700
9460
|
|
|
8701
9461
|
// src/server/lib/session.ts
|
|
8702
|
-
import * as
|
|
8703
|
-
import { env as
|
|
9462
|
+
import * as jose2 from "jose";
|
|
9463
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
8704
9464
|
import { env as coreEnv } from "@spfn/core/config";
|
|
8705
9465
|
async function getSessionSecretKey() {
|
|
8706
|
-
const secret =
|
|
9466
|
+
const secret = env9.SPFN_AUTH_SESSION_SECRET;
|
|
8707
9467
|
const encoder = new TextEncoder();
|
|
8708
9468
|
const data = encoder.encode(secret);
|
|
8709
9469
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
8710
9470
|
return new Uint8Array(hashBuffer);
|
|
8711
9471
|
}
|
|
9472
|
+
async function getSecretFingerprint() {
|
|
9473
|
+
const key = await getSessionSecretKey();
|
|
9474
|
+
const hash = await crypto.subtle.digest("SHA-256", key.buffer);
|
|
9475
|
+
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
9476
|
+
return hex.slice(0, 8);
|
|
9477
|
+
}
|
|
8712
9478
|
async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
|
|
8713
9479
|
const secret = await getSessionSecretKey();
|
|
8714
|
-
|
|
9480
|
+
const result = await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
|
|
9481
|
+
if (coreEnv.NODE_ENV !== "production") {
|
|
9482
|
+
const fingerprint = await getSecretFingerprint();
|
|
9483
|
+
authLogger.session.debug(`Sealed session`, {
|
|
9484
|
+
secretFingerprint: fingerprint,
|
|
9485
|
+
resultLength: result.length,
|
|
9486
|
+
resultPrefix: result.slice(0, 20)
|
|
9487
|
+
});
|
|
9488
|
+
}
|
|
9489
|
+
return result;
|
|
8715
9490
|
}
|
|
8716
9491
|
async function unsealSession(jwt4) {
|
|
8717
9492
|
try {
|
|
8718
9493
|
const secret = await getSessionSecretKey();
|
|
8719
|
-
const { payload } = await
|
|
9494
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
|
|
8720
9495
|
issuer: "spfn-auth",
|
|
8721
9496
|
audience: "spfn-client"
|
|
8722
9497
|
});
|
|
8723
9498
|
return payload.data;
|
|
8724
9499
|
} catch (err) {
|
|
8725
|
-
if (err instanceof
|
|
9500
|
+
if (err instanceof jose2.errors.JWTExpired) {
|
|
8726
9501
|
throw new Error("Session expired");
|
|
8727
9502
|
}
|
|
8728
|
-
if (err instanceof
|
|
9503
|
+
if (err instanceof jose2.errors.JWEDecryptionFailed) {
|
|
9504
|
+
if (coreEnv.NODE_ENV !== "production") {
|
|
9505
|
+
const fingerprint = await getSecretFingerprint();
|
|
9506
|
+
authLogger.session.warn(`JWE decryption failed`, {
|
|
9507
|
+
secretFingerprint: fingerprint,
|
|
9508
|
+
jwtLength: jwt4.length,
|
|
9509
|
+
jwtPrefix: jwt4.slice(0, 20),
|
|
9510
|
+
jwtSuffix: jwt4.slice(-10)
|
|
9511
|
+
});
|
|
9512
|
+
}
|
|
8729
9513
|
throw new Error("Invalid session");
|
|
8730
9514
|
}
|
|
8731
|
-
if (err instanceof
|
|
9515
|
+
if (err instanceof jose2.errors.JWTClaimValidationFailed) {
|
|
8732
9516
|
throw new Error("Session validation failed");
|
|
8733
9517
|
}
|
|
8734
9518
|
throw new Error("Failed to unseal session");
|
|
@@ -8737,7 +9521,7 @@ async function unsealSession(jwt4) {
|
|
|
8737
9521
|
async function getSessionInfo(jwt4) {
|
|
8738
9522
|
const secret = await getSessionSecretKey();
|
|
8739
9523
|
try {
|
|
8740
|
-
const { payload } = await
|
|
9524
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret);
|
|
8741
9525
|
return {
|
|
8742
9526
|
issuedAt: new Date(payload.iat * 1e3),
|
|
8743
9527
|
expiresAt: new Date(payload.exp * 1e3),
|
|
@@ -8746,7 +9530,7 @@ async function getSessionInfo(jwt4) {
|
|
|
8746
9530
|
};
|
|
8747
9531
|
} catch (err) {
|
|
8748
9532
|
if (coreEnv.NODE_ENV !== "production") {
|
|
8749
|
-
|
|
9533
|
+
authLogger.session.warn("Failed to get session info:", err instanceof Error ? err.message : "Unknown error");
|
|
8750
9534
|
}
|
|
8751
9535
|
return null;
|
|
8752
9536
|
}
|
|
@@ -8761,14 +9545,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
8761
9545
|
}
|
|
8762
9546
|
|
|
8763
9547
|
// src/server/setup.ts
|
|
8764
|
-
import { env as
|
|
9548
|
+
import { env as env10 } from "@spfn/auth/config";
|
|
8765
9549
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
8766
9550
|
init_repositories();
|
|
8767
9551
|
function parseAdminAccounts() {
|
|
8768
9552
|
const accounts = [];
|
|
8769
|
-
if (
|
|
9553
|
+
if (env10.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
8770
9554
|
try {
|
|
8771
|
-
const accountsJson =
|
|
9555
|
+
const accountsJson = env10.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
8772
9556
|
const parsed = JSON.parse(accountsJson);
|
|
8773
9557
|
if (!Array.isArray(parsed)) {
|
|
8774
9558
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -8795,11 +9579,11 @@ function parseAdminAccounts() {
|
|
|
8795
9579
|
return accounts;
|
|
8796
9580
|
}
|
|
8797
9581
|
}
|
|
8798
|
-
const adminEmails =
|
|
9582
|
+
const adminEmails = env10.SPFN_AUTH_ADMIN_EMAILS;
|
|
8799
9583
|
if (adminEmails) {
|
|
8800
9584
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
8801
|
-
const passwords = (
|
|
8802
|
-
const roles2 = (
|
|
9585
|
+
const passwords = (env10.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
9586
|
+
const roles2 = (env10.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
8803
9587
|
if (passwords.length !== emails.length) {
|
|
8804
9588
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
8805
9589
|
return accounts;
|
|
@@ -8821,8 +9605,8 @@ function parseAdminAccounts() {
|
|
|
8821
9605
|
}
|
|
8822
9606
|
return accounts;
|
|
8823
9607
|
}
|
|
8824
|
-
const adminEmail =
|
|
8825
|
-
const adminPassword =
|
|
9608
|
+
const adminEmail = env10.SPFN_AUTH_ADMIN_EMAIL;
|
|
9609
|
+
const adminPassword = env10.SPFN_AUTH_ADMIN_PASSWORD;
|
|
8826
9610
|
if (adminEmail && adminPassword) {
|
|
8827
9611
|
accounts.push({
|
|
8828
9612
|
email: adminEmail,
|
|
@@ -8892,14 +9676,18 @@ function createAuthLifecycle(options = {}) {
|
|
|
8892
9676
|
* Performs:
|
|
8893
9677
|
* 1. Ensures admin account exists (creates if missing)
|
|
8894
9678
|
* 2. Initializes RBAC system with built-in + custom roles/permissions
|
|
9679
|
+
* 3. Initializes one-time token manager
|
|
8895
9680
|
*/
|
|
8896
9681
|
afterInfrastructure: async () => {
|
|
8897
9682
|
await initializeAuth(options);
|
|
8898
9683
|
await ensureAdminExists();
|
|
9684
|
+
initOneTimeTokenManager(options.oneTimeToken);
|
|
8899
9685
|
}
|
|
8900
9686
|
};
|
|
8901
9687
|
}
|
|
8902
9688
|
export {
|
|
9689
|
+
AuthMetadataRepository,
|
|
9690
|
+
AuthProviderSchema,
|
|
8903
9691
|
COOKIE_NAMES,
|
|
8904
9692
|
EmailSchema,
|
|
8905
9693
|
INVITATION_STATUSES,
|
|
@@ -8912,6 +9700,7 @@ export {
|
|
|
8912
9700
|
RolePermissionsRepository,
|
|
8913
9701
|
RolesRepository,
|
|
8914
9702
|
SOCIAL_PROVIDERS,
|
|
9703
|
+
SocialAccountsRepository,
|
|
8915
9704
|
TargetTypeSchema,
|
|
8916
9705
|
USER_STATUSES,
|
|
8917
9706
|
UserPermissionsRepository,
|
|
@@ -8924,19 +9713,27 @@ export {
|
|
|
8924
9713
|
acceptInvitation,
|
|
8925
9714
|
addPermissionToRole,
|
|
8926
9715
|
authLogger,
|
|
9716
|
+
authLoginEvent,
|
|
9717
|
+
authMetadata,
|
|
9718
|
+
authMetadataRepository,
|
|
9719
|
+
authRegisterEvent,
|
|
8927
9720
|
mainAuthRouter as authRouter,
|
|
8928
9721
|
authSchema,
|
|
8929
9722
|
authenticate,
|
|
9723
|
+
buildOAuthErrorUrl,
|
|
8930
9724
|
cancelInvitation,
|
|
8931
9725
|
changePasswordService,
|
|
8932
9726
|
checkAccountExistsService,
|
|
9727
|
+
checkUsernameAvailableService,
|
|
8933
9728
|
configureAuth,
|
|
8934
9729
|
createAuthLifecycle,
|
|
8935
9730
|
createInvitation,
|
|
9731
|
+
createOAuthState,
|
|
8936
9732
|
createRole,
|
|
8937
9733
|
decodeToken,
|
|
8938
9734
|
deleteInvitation,
|
|
8939
9735
|
deleteRole,
|
|
9736
|
+
exchangeCodeForTokens,
|
|
8940
9737
|
expireOldInvitations,
|
|
8941
9738
|
generateClientToken,
|
|
8942
9739
|
generateKeyPair,
|
|
@@ -8947,12 +9744,19 @@ export {
|
|
|
8947
9744
|
getAuth,
|
|
8948
9745
|
getAuthConfig,
|
|
8949
9746
|
getAuthSessionService,
|
|
9747
|
+
getEnabledOAuthProviders,
|
|
9748
|
+
getGoogleAccessToken,
|
|
9749
|
+
getGoogleAuthUrl,
|
|
9750
|
+
getGoogleOAuthConfig,
|
|
9751
|
+
getGoogleUserInfo,
|
|
8950
9752
|
getInvitationByToken,
|
|
8951
|
-
getInvitationTemplate,
|
|
8952
9753
|
getInvitationWithDetails,
|
|
8953
9754
|
getKeyId,
|
|
8954
9755
|
getKeySize,
|
|
8955
|
-
|
|
9756
|
+
getLocale,
|
|
9757
|
+
getOneTimeTokenManager,
|
|
9758
|
+
getOptionalAuth,
|
|
9759
|
+
getRole,
|
|
8956
9760
|
getRoleByName,
|
|
8957
9761
|
getRolePermissions,
|
|
8958
9762
|
getSessionInfo,
|
|
@@ -8965,27 +9769,33 @@ export {
|
|
|
8965
9769
|
getUserPermissions,
|
|
8966
9770
|
getUserProfileService,
|
|
8967
9771
|
getUserRole,
|
|
8968
|
-
getVerificationCodeTemplate,
|
|
8969
|
-
getWelcomeTemplate,
|
|
8970
9772
|
hasAllPermissions,
|
|
8971
9773
|
hasAnyPermission,
|
|
8972
9774
|
hasAnyRole,
|
|
8973
9775
|
hasPermission,
|
|
8974
9776
|
hasRole,
|
|
8975
9777
|
hashPassword,
|
|
9778
|
+
initOneTimeTokenManager,
|
|
8976
9779
|
initializeAuth,
|
|
9780
|
+
invitationAcceptedEvent,
|
|
9781
|
+
invitationCreatedEvent,
|
|
8977
9782
|
invitationsRepository,
|
|
9783
|
+
isGoogleOAuthEnabled,
|
|
9784
|
+
isOAuthProviderEnabled,
|
|
9785
|
+
issueOneTimeTokenService,
|
|
8978
9786
|
keysRepository,
|
|
8979
9787
|
listInvitations,
|
|
8980
9788
|
loginService,
|
|
8981
9789
|
logoutService,
|
|
9790
|
+
oauthCallbackService,
|
|
9791
|
+
oauthStartService,
|
|
9792
|
+
oneTimeTokenAuth,
|
|
9793
|
+
optionalAuth,
|
|
8982
9794
|
parseDuration,
|
|
8983
9795
|
permissions,
|
|
8984
9796
|
permissionsRepository,
|
|
8985
|
-
|
|
8986
|
-
registerEmailTemplates,
|
|
9797
|
+
refreshAccessToken,
|
|
8987
9798
|
registerPublicKeyService,
|
|
8988
|
-
registerSMSProvider,
|
|
8989
9799
|
registerService,
|
|
8990
9800
|
removePermissionFromRole,
|
|
8991
9801
|
requireAnyPermission,
|
|
@@ -9000,17 +9810,18 @@ export {
|
|
|
9000
9810
|
rolesRepository,
|
|
9001
9811
|
rotateKeyService,
|
|
9002
9812
|
sealSession,
|
|
9003
|
-
sendEmail,
|
|
9004
|
-
sendSMS,
|
|
9005
9813
|
sendVerificationCodeService,
|
|
9006
9814
|
setRolePermissions,
|
|
9007
9815
|
shouldRefreshSession,
|
|
9008
9816
|
shouldRotateKey,
|
|
9817
|
+
socialAccountsRepository,
|
|
9009
9818
|
unsealSession,
|
|
9010
9819
|
updateLastLoginService,
|
|
9820
|
+
updateLocaleService,
|
|
9011
9821
|
updateRole,
|
|
9012
9822
|
updateUserProfileService,
|
|
9013
9823
|
updateUserService,
|
|
9824
|
+
updateUsernameService,
|
|
9014
9825
|
userInvitations,
|
|
9015
9826
|
userPermissions,
|
|
9016
9827
|
userPermissionsRepository,
|
|
@@ -9027,6 +9838,8 @@ export {
|
|
|
9027
9838
|
verifyClientToken,
|
|
9028
9839
|
verifyCodeService,
|
|
9029
9840
|
verifyKeyFingerprint,
|
|
9841
|
+
verifyOAuthState,
|
|
9842
|
+
verifyOneTimeTokenService,
|
|
9030
9843
|
verifyPassword,
|
|
9031
9844
|
verifyToken
|
|
9032
9845
|
};
|