@spfn/auth 0.2.0-beta.3 → 0.2.0-beta.31
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 +689 -180
- package/dist/{dto-CLYtuAom.d.ts → authenticate-Brx2N-Ip.d.ts} +413 -147
- package/dist/config.d.ts +100 -44
- package/dist/config.js +64 -35
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +16 -2
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +279 -100
- package/dist/index.js +47 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +202 -1
- 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 +89 -2
- package/dist/nextjs/server.js +147 -22
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +576 -360
- package/dist/server.js +1089 -484
- package/dist/server.js.map +1 -1
- package/migrations/0001_smooth_the_fury.sql +3 -0
- package/migrations/meta/0001_snapshot.json +1660 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +14 -10
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,
|
|
@@ -4592,6 +4586,9 @@ var init_users = __esm({
|
|
|
4592
4586
|
// Format: +[country code][number] (e.g., +821012345678)
|
|
4593
4587
|
// Used for: SMS login, 2FA, notifications
|
|
4594
4588
|
phone: text2("phone").unique(),
|
|
4589
|
+
// Username (unique, optional)
|
|
4590
|
+
// Used for: display, mention, public profile URL
|
|
4591
|
+
username: text2("username").unique(),
|
|
4595
4592
|
// Authentication
|
|
4596
4593
|
// Bcrypt password hash ($2b$10$[salt][hash], 60 chars)
|
|
4597
4594
|
// Nullable to support OAuth-only accounts
|
|
@@ -4631,6 +4628,7 @@ var init_users = __esm({
|
|
|
4631
4628
|
// Indexes for query optimization
|
|
4632
4629
|
index2("users_email_idx").on(table.email),
|
|
4633
4630
|
index2("users_phone_idx").on(table.phone),
|
|
4631
|
+
index2("users_username_idx").on(table.username),
|
|
4634
4632
|
index2("users_status_idx").on(table.status),
|
|
4635
4633
|
index2("users_role_id_idx").on(table.roleId)
|
|
4636
4634
|
]
|
|
@@ -5349,6 +5347,14 @@ var init_users_repository = __esm({
|
|
|
5349
5347
|
const result = await this.readDb.select().from(users).where(eq(users.phone, phone)).limit(1);
|
|
5350
5348
|
return result[0] ?? null;
|
|
5351
5349
|
}
|
|
5350
|
+
/**
|
|
5351
|
+
* 사용자명으로 사용자 조회
|
|
5352
|
+
* Read replica 사용
|
|
5353
|
+
*/
|
|
5354
|
+
async findByUsername(username) {
|
|
5355
|
+
const result = await this.readDb.select().from(users).where(eq(users.username, username)).limit(1);
|
|
5356
|
+
return result[0] ?? null;
|
|
5357
|
+
}
|
|
5352
5358
|
/**
|
|
5353
5359
|
* 이메일 또는 전화번호로 사용자 조회
|
|
5354
5360
|
* Read replica 사용
|
|
@@ -5361,6 +5367,28 @@ var init_users_repository = __esm({
|
|
|
5361
5367
|
}
|
|
5362
5368
|
return null;
|
|
5363
5369
|
}
|
|
5370
|
+
/**
|
|
5371
|
+
* ID로 사용자 + Role 조회 (leftJoin)
|
|
5372
|
+
* Read replica 사용
|
|
5373
|
+
*
|
|
5374
|
+
* roleId가 null인 유저는 role: null 반환
|
|
5375
|
+
*/
|
|
5376
|
+
async findByIdWithRole(id11) {
|
|
5377
|
+
const result = await this.readDb.select({
|
|
5378
|
+
user: users,
|
|
5379
|
+
roleName: roles.name,
|
|
5380
|
+
roleDisplayName: roles.displayName,
|
|
5381
|
+
rolePriority: roles.priority
|
|
5382
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id11)).limit(1);
|
|
5383
|
+
const row = result[0];
|
|
5384
|
+
if (!row) {
|
|
5385
|
+
return null;
|
|
5386
|
+
}
|
|
5387
|
+
return {
|
|
5388
|
+
user: row.user,
|
|
5389
|
+
role: row.roleName ? { name: row.roleName, displayName: row.roleDisplayName, priority: row.rolePriority } : null
|
|
5390
|
+
};
|
|
5391
|
+
}
|
|
5364
5392
|
/**
|
|
5365
5393
|
* 사용자 생성
|
|
5366
5394
|
* Write primary 사용
|
|
@@ -5468,6 +5496,7 @@ var init_users_repository = __esm({
|
|
|
5468
5496
|
const user = await this.readDb.select({
|
|
5469
5497
|
id: users.id,
|
|
5470
5498
|
email: users.email,
|
|
5499
|
+
username: users.username,
|
|
5471
5500
|
emailVerifiedAt: users.emailVerifiedAt,
|
|
5472
5501
|
phoneVerifiedAt: users.phoneVerifiedAt
|
|
5473
5502
|
}).from(users).where(eq(users.id, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
@@ -5477,6 +5506,7 @@ var init_users_repository = __esm({
|
|
|
5477
5506
|
return {
|
|
5478
5507
|
userId: user.id,
|
|
5479
5508
|
email: user.email,
|
|
5509
|
+
username: user.username,
|
|
5480
5510
|
isEmailVerified: !!user.emailVerifiedAt,
|
|
5481
5511
|
isPhoneVerified: !!user.phoneVerifiedAt
|
|
5482
5512
|
};
|
|
@@ -5492,6 +5522,7 @@ var init_users_repository = __esm({
|
|
|
5492
5522
|
const user = await this.readDb.select({
|
|
5493
5523
|
id: users.id,
|
|
5494
5524
|
email: users.email,
|
|
5525
|
+
username: users.username,
|
|
5495
5526
|
emailVerifiedAt: users.emailVerifiedAt,
|
|
5496
5527
|
phoneVerifiedAt: users.phoneVerifiedAt,
|
|
5497
5528
|
lastLoginAt: users.lastLoginAt,
|
|
@@ -5504,6 +5535,7 @@ var init_users_repository = __esm({
|
|
|
5504
5535
|
return {
|
|
5505
5536
|
userId: user.id,
|
|
5506
5537
|
email: user.email,
|
|
5538
|
+
username: user.username,
|
|
5507
5539
|
isEmailVerified: !!user.emailVerifiedAt,
|
|
5508
5540
|
isPhoneVerified: !!user.phoneVerifiedAt,
|
|
5509
5541
|
lastLoginAt: user.lastLoginAt,
|
|
@@ -6132,6 +6164,23 @@ var init_user_profiles_repository = __esm({
|
|
|
6132
6164
|
const result = await this.db.delete(userProfiles).where(eq8(userProfiles.userId, userId)).returning();
|
|
6133
6165
|
return result[0] ?? null;
|
|
6134
6166
|
}
|
|
6167
|
+
/**
|
|
6168
|
+
* 프로필 Upsert (by User ID)
|
|
6169
|
+
*
|
|
6170
|
+
* 프로필이 없으면 생성, 있으면 업데이트
|
|
6171
|
+
* 새로 생성 시 displayName은 필수 (없으면 'User'로 설정)
|
|
6172
|
+
*/
|
|
6173
|
+
async upsertByUserId(userId, data) {
|
|
6174
|
+
const existing = await this.findByUserId(userId);
|
|
6175
|
+
if (existing) {
|
|
6176
|
+
return await this.updateByUserId(userId, data);
|
|
6177
|
+
}
|
|
6178
|
+
return await this.create({
|
|
6179
|
+
userId,
|
|
6180
|
+
displayName: data.displayName || "User",
|
|
6181
|
+
...data
|
|
6182
|
+
});
|
|
6183
|
+
}
|
|
6135
6184
|
/**
|
|
6136
6185
|
* User ID로 프로필 데이터 조회 (formatted)
|
|
6137
6186
|
*
|
|
@@ -6151,6 +6200,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6151
6200
|
location: userProfiles.location,
|
|
6152
6201
|
company: userProfiles.company,
|
|
6153
6202
|
jobTitle: userProfiles.jobTitle,
|
|
6203
|
+
metadata: userProfiles.metadata,
|
|
6154
6204
|
createdAt: userProfiles.createdAt,
|
|
6155
6205
|
updatedAt: userProfiles.updatedAt
|
|
6156
6206
|
}).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
@@ -6170,6 +6220,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6170
6220
|
location: profile.location,
|
|
6171
6221
|
company: profile.company,
|
|
6172
6222
|
jobTitle: profile.jobTitle,
|
|
6223
|
+
metadata: profile.metadata,
|
|
6173
6224
|
createdAt: profile.createdAt,
|
|
6174
6225
|
updatedAt: profile.updatedAt
|
|
6175
6226
|
};
|
|
@@ -6394,6 +6445,96 @@ var init_invitations_repository = __esm({
|
|
|
6394
6445
|
}
|
|
6395
6446
|
});
|
|
6396
6447
|
|
|
6448
|
+
// src/server/repositories/social-accounts.repository.ts
|
|
6449
|
+
import { eq as eq10, and as and7 } from "drizzle-orm";
|
|
6450
|
+
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
6451
|
+
var SocialAccountsRepository, socialAccountsRepository;
|
|
6452
|
+
var init_social_accounts_repository = __esm({
|
|
6453
|
+
"src/server/repositories/social-accounts.repository.ts"() {
|
|
6454
|
+
"use strict";
|
|
6455
|
+
init_entities();
|
|
6456
|
+
SocialAccountsRepository = class extends BaseRepository10 {
|
|
6457
|
+
/**
|
|
6458
|
+
* provider와 providerUserId로 소셜 계정 조회
|
|
6459
|
+
* Read replica 사용
|
|
6460
|
+
*/
|
|
6461
|
+
async findByProviderAndProviderId(provider, providerUserId) {
|
|
6462
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6463
|
+
and7(
|
|
6464
|
+
eq10(userSocialAccounts.provider, provider),
|
|
6465
|
+
eq10(userSocialAccounts.providerUserId, providerUserId)
|
|
6466
|
+
)
|
|
6467
|
+
).limit(1);
|
|
6468
|
+
return result[0] ?? null;
|
|
6469
|
+
}
|
|
6470
|
+
/**
|
|
6471
|
+
* userId로 모든 소셜 계정 조회
|
|
6472
|
+
* Read replica 사용
|
|
6473
|
+
*/
|
|
6474
|
+
async findByUserId(userId) {
|
|
6475
|
+
return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
|
|
6476
|
+
}
|
|
6477
|
+
/**
|
|
6478
|
+
* userId와 provider로 소셜 계정 조회
|
|
6479
|
+
* Read replica 사용
|
|
6480
|
+
*/
|
|
6481
|
+
async findByUserIdAndProvider(userId, provider) {
|
|
6482
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6483
|
+
and7(
|
|
6484
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6485
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6486
|
+
)
|
|
6487
|
+
).limit(1);
|
|
6488
|
+
return result[0] ?? null;
|
|
6489
|
+
}
|
|
6490
|
+
/**
|
|
6491
|
+
* 소셜 계정 생성
|
|
6492
|
+
* Write primary 사용
|
|
6493
|
+
*/
|
|
6494
|
+
async create(data) {
|
|
6495
|
+
return await this._create(userSocialAccounts, {
|
|
6496
|
+
...data,
|
|
6497
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
6498
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6499
|
+
});
|
|
6500
|
+
}
|
|
6501
|
+
/**
|
|
6502
|
+
* 토큰 정보 업데이트
|
|
6503
|
+
* Write primary 사용
|
|
6504
|
+
*/
|
|
6505
|
+
async updateTokens(id11, data) {
|
|
6506
|
+
const result = await this.db.update(userSocialAccounts).set({
|
|
6507
|
+
...data,
|
|
6508
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6509
|
+
}).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6510
|
+
return result[0] ?? null;
|
|
6511
|
+
}
|
|
6512
|
+
/**
|
|
6513
|
+
* 소셜 계정 삭제
|
|
6514
|
+
* Write primary 사용
|
|
6515
|
+
*/
|
|
6516
|
+
async deleteById(id11) {
|
|
6517
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6518
|
+
return result[0] ?? null;
|
|
6519
|
+
}
|
|
6520
|
+
/**
|
|
6521
|
+
* userId와 provider로 소셜 계정 삭제
|
|
6522
|
+
* Write primary 사용
|
|
6523
|
+
*/
|
|
6524
|
+
async deleteByUserIdAndProvider(userId, provider) {
|
|
6525
|
+
const result = await this.db.delete(userSocialAccounts).where(
|
|
6526
|
+
and7(
|
|
6527
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6528
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6529
|
+
)
|
|
6530
|
+
).returning();
|
|
6531
|
+
return result[0] ?? null;
|
|
6532
|
+
}
|
|
6533
|
+
};
|
|
6534
|
+
socialAccountsRepository = new SocialAccountsRepository();
|
|
6535
|
+
}
|
|
6536
|
+
});
|
|
6537
|
+
|
|
6397
6538
|
// src/server/repositories/index.ts
|
|
6398
6539
|
var init_repositories = __esm({
|
|
6399
6540
|
"src/server/repositories/index.ts"() {
|
|
@@ -6407,6 +6548,7 @@ var init_repositories = __esm({
|
|
|
6407
6548
|
init_user_permissions_repository();
|
|
6408
6549
|
init_user_profiles_repository();
|
|
6409
6550
|
init_invitations_repository();
|
|
6551
|
+
init_social_accounts_repository();
|
|
6410
6552
|
}
|
|
6411
6553
|
});
|
|
6412
6554
|
|
|
@@ -6533,7 +6675,7 @@ var init_role_service = __esm({
|
|
|
6533
6675
|
import "@spfn/auth/config";
|
|
6534
6676
|
|
|
6535
6677
|
// src/server/routes/index.ts
|
|
6536
|
-
import { defineRouter as
|
|
6678
|
+
import { defineRouter as defineRouter5 } from "@spfn/core/route";
|
|
6537
6679
|
|
|
6538
6680
|
// src/server/routes/auth/index.ts
|
|
6539
6681
|
init_schema3();
|
|
@@ -6656,12 +6798,21 @@ function getAuth(c) {
|
|
|
6656
6798
|
}
|
|
6657
6799
|
return c.get("auth");
|
|
6658
6800
|
}
|
|
6801
|
+
function getOptionalAuth(c) {
|
|
6802
|
+
if ("raw" in c && c.raw) {
|
|
6803
|
+
return c.raw.get("auth");
|
|
6804
|
+
}
|
|
6805
|
+
return c.get("auth");
|
|
6806
|
+
}
|
|
6659
6807
|
function getUser(c) {
|
|
6660
6808
|
return getAuth(c).user;
|
|
6661
6809
|
}
|
|
6662
6810
|
function getUserId(c) {
|
|
6663
6811
|
return getAuth(c).userId;
|
|
6664
6812
|
}
|
|
6813
|
+
function getRole(c) {
|
|
6814
|
+
return getAuth(c).role;
|
|
6815
|
+
}
|
|
6665
6816
|
function getKeyId(c) {
|
|
6666
6817
|
return getAuth(c).keyId;
|
|
6667
6818
|
}
|
|
@@ -6682,9 +6833,10 @@ import {
|
|
|
6682
6833
|
} from "@spfn/auth/errors";
|
|
6683
6834
|
|
|
6684
6835
|
// src/server/services/verification.service.ts
|
|
6685
|
-
import { env as
|
|
6836
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
6686
6837
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
6687
6838
|
import jwt2 from "jsonwebtoken";
|
|
6839
|
+
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
6688
6840
|
|
|
6689
6841
|
// src/server/logger.ts
|
|
6690
6842
|
import { logger as rootLogger } from "@spfn/core/logger";
|
|
@@ -6694,7 +6846,8 @@ var authLogger = {
|
|
|
6694
6846
|
interceptor: {
|
|
6695
6847
|
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
6696
6848
|
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
6697
|
-
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation")
|
|
6849
|
+
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
6850
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
6698
6851
|
},
|
|
6699
6852
|
service: rootLogger.child("@spfn/auth:service"),
|
|
6700
6853
|
setup: rootLogger.child("@spfn/auth:setup"),
|
|
@@ -6704,410 +6857,6 @@ var authLogger = {
|
|
|
6704
6857
|
|
|
6705
6858
|
// src/server/services/verification.service.ts
|
|
6706
6859
|
init_repositories();
|
|
6707
|
-
|
|
6708
|
-
// src/server/services/sms/provider.ts
|
|
6709
|
-
var currentProvider = null;
|
|
6710
|
-
var fallbackProvider = {
|
|
6711
|
-
name: "fallback",
|
|
6712
|
-
sendSMS: async (params) => {
|
|
6713
|
-
authLogger.sms.debug("DEV MODE - SMS not actually sent", {
|
|
6714
|
-
phone: params.phone,
|
|
6715
|
-
message: params.message,
|
|
6716
|
-
purpose: params.purpose || "N/A"
|
|
6717
|
-
});
|
|
6718
|
-
return {
|
|
6719
|
-
success: true,
|
|
6720
|
-
messageId: "dev-mode-no-actual-sms"
|
|
6721
|
-
};
|
|
6722
|
-
}
|
|
6723
|
-
};
|
|
6724
|
-
function registerSMSProvider(provider) {
|
|
6725
|
-
currentProvider = provider;
|
|
6726
|
-
authLogger.sms.info("Registered SMS provider", { name: provider.name });
|
|
6727
|
-
}
|
|
6728
|
-
function getSMSProvider() {
|
|
6729
|
-
return currentProvider || fallbackProvider;
|
|
6730
|
-
}
|
|
6731
|
-
async function sendSMS(params) {
|
|
6732
|
-
const provider = getSMSProvider();
|
|
6733
|
-
return await provider.sendSMS(params);
|
|
6734
|
-
}
|
|
6735
|
-
|
|
6736
|
-
// src/server/services/sms/aws-sns.provider.ts
|
|
6737
|
-
import { env as env3 } from "@spfn/auth/config";
|
|
6738
|
-
function isValidE164Phone(phone) {
|
|
6739
|
-
const e164Regex = /^\+[1-9]\d{1,14}$/;
|
|
6740
|
-
return e164Regex.test(phone);
|
|
6741
|
-
}
|
|
6742
|
-
function createAWSSNSProvider() {
|
|
6743
|
-
try {
|
|
6744
|
-
const { SNSClient, PublishCommand } = __require("@aws-sdk/client-sns");
|
|
6745
|
-
return {
|
|
6746
|
-
name: "aws-sns",
|
|
6747
|
-
sendSMS: async (params) => {
|
|
6748
|
-
const { phone, message, purpose } = params;
|
|
6749
|
-
if (!isValidE164Phone(phone)) {
|
|
6750
|
-
return {
|
|
6751
|
-
success: false,
|
|
6752
|
-
error: "Invalid phone number format. Must be E.164 format (e.g., +821012345678)"
|
|
6753
|
-
};
|
|
6754
|
-
}
|
|
6755
|
-
if (!env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID) {
|
|
6756
|
-
return {
|
|
6757
|
-
success: false,
|
|
6758
|
-
error: "AWS SNS credentials not configured. Set SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID environment variable."
|
|
6759
|
-
};
|
|
6760
|
-
}
|
|
6761
|
-
try {
|
|
6762
|
-
const config = {
|
|
6763
|
-
region: env3.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
|
|
6764
|
-
};
|
|
6765
|
-
if (env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID && env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY) {
|
|
6766
|
-
config.credentials = {
|
|
6767
|
-
accessKeyId: env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID,
|
|
6768
|
-
secretAccessKey: env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY
|
|
6769
|
-
};
|
|
6770
|
-
}
|
|
6771
|
-
const client = new SNSClient(config);
|
|
6772
|
-
const command = new PublishCommand({
|
|
6773
|
-
PhoneNumber: phone,
|
|
6774
|
-
Message: message,
|
|
6775
|
-
MessageAttributes: {
|
|
6776
|
-
"AWS.SNS.SMS.SMSType": {
|
|
6777
|
-
DataType: "String",
|
|
6778
|
-
StringValue: "Transactional"
|
|
6779
|
-
// For OTP codes
|
|
6780
|
-
},
|
|
6781
|
-
...env3.SPFN_AUTH_AWS_SNS_SENDER_ID && {
|
|
6782
|
-
"AWS.SNS.SMS.SenderID": {
|
|
6783
|
-
DataType: "String",
|
|
6784
|
-
StringValue: env3.SPFN_AUTH_AWS_SNS_SENDER_ID
|
|
6785
|
-
}
|
|
6786
|
-
}
|
|
6787
|
-
}
|
|
6788
|
-
});
|
|
6789
|
-
const response = await client.send(command);
|
|
6790
|
-
authLogger.sms.info("SMS sent via AWS SNS", {
|
|
6791
|
-
phone,
|
|
6792
|
-
messageId: response.MessageId,
|
|
6793
|
-
purpose: purpose || "N/A"
|
|
6794
|
-
});
|
|
6795
|
-
return {
|
|
6796
|
-
success: true,
|
|
6797
|
-
messageId: response.MessageId
|
|
6798
|
-
};
|
|
6799
|
-
} catch (error) {
|
|
6800
|
-
const err = error;
|
|
6801
|
-
authLogger.sms.error("Failed to send SMS via AWS SNS", {
|
|
6802
|
-
phone,
|
|
6803
|
-
error: err.message
|
|
6804
|
-
});
|
|
6805
|
-
return {
|
|
6806
|
-
success: false,
|
|
6807
|
-
error: err.message || "Failed to send SMS via AWS SNS"
|
|
6808
|
-
};
|
|
6809
|
-
}
|
|
6810
|
-
}
|
|
6811
|
-
};
|
|
6812
|
-
} catch (error) {
|
|
6813
|
-
return null;
|
|
6814
|
-
}
|
|
6815
|
-
}
|
|
6816
|
-
var awsSNSProvider = createAWSSNSProvider();
|
|
6817
|
-
|
|
6818
|
-
// src/server/services/sms/index.ts
|
|
6819
|
-
if (awsSNSProvider) {
|
|
6820
|
-
registerSMSProvider(awsSNSProvider);
|
|
6821
|
-
}
|
|
6822
|
-
|
|
6823
|
-
// src/server/services/email/provider.ts
|
|
6824
|
-
var currentProvider2 = null;
|
|
6825
|
-
var fallbackProvider2 = {
|
|
6826
|
-
name: "fallback",
|
|
6827
|
-
sendEmail: async (params) => {
|
|
6828
|
-
authLogger.email.debug("DEV MODE - Email not actually sent", {
|
|
6829
|
-
to: params.to,
|
|
6830
|
-
subject: params.subject,
|
|
6831
|
-
purpose: params.purpose || "N/A",
|
|
6832
|
-
textPreview: params.text?.substring(0, 100) || "N/A"
|
|
6833
|
-
});
|
|
6834
|
-
return {
|
|
6835
|
-
success: true,
|
|
6836
|
-
messageId: "dev-mode-no-actual-email"
|
|
6837
|
-
};
|
|
6838
|
-
}
|
|
6839
|
-
};
|
|
6840
|
-
function registerEmailProvider(provider) {
|
|
6841
|
-
currentProvider2 = provider;
|
|
6842
|
-
authLogger.email.info("Registered email provider", { name: provider.name });
|
|
6843
|
-
}
|
|
6844
|
-
function getEmailProvider() {
|
|
6845
|
-
return currentProvider2 || fallbackProvider2;
|
|
6846
|
-
}
|
|
6847
|
-
async function sendEmail(params) {
|
|
6848
|
-
const provider = getEmailProvider();
|
|
6849
|
-
return await provider.sendEmail(params);
|
|
6850
|
-
}
|
|
6851
|
-
|
|
6852
|
-
// src/server/services/email/aws-ses.provider.ts
|
|
6853
|
-
import { env as env4 } from "@spfn/auth/config";
|
|
6854
|
-
function isValidEmail(email) {
|
|
6855
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6856
|
-
return emailRegex.test(email);
|
|
6857
|
-
}
|
|
6858
|
-
function createAWSSESProvider() {
|
|
6859
|
-
try {
|
|
6860
|
-
const { SESClient, SendEmailCommand } = __require("@aws-sdk/client-ses");
|
|
6861
|
-
return {
|
|
6862
|
-
name: "aws-ses",
|
|
6863
|
-
sendEmail: async (params) => {
|
|
6864
|
-
const { to, subject, text: text10, html, purpose } = params;
|
|
6865
|
-
if (!isValidEmail(to)) {
|
|
6866
|
-
return {
|
|
6867
|
-
success: false,
|
|
6868
|
-
error: "Invalid email address format"
|
|
6869
|
-
};
|
|
6870
|
-
}
|
|
6871
|
-
if (!env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID) {
|
|
6872
|
-
return {
|
|
6873
|
-
success: false,
|
|
6874
|
-
error: "AWS SES credentials not configured. Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable."
|
|
6875
|
-
};
|
|
6876
|
-
}
|
|
6877
|
-
if (!env4.SPFN_AUTH_AWS_SES_FROM_EMAIL) {
|
|
6878
|
-
return {
|
|
6879
|
-
success: false,
|
|
6880
|
-
error: "AWS SES sender email not configured. Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable."
|
|
6881
|
-
};
|
|
6882
|
-
}
|
|
6883
|
-
try {
|
|
6884
|
-
const config = {
|
|
6885
|
-
region: env4.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
|
|
6886
|
-
};
|
|
6887
|
-
if (env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID && env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY) {
|
|
6888
|
-
config.credentials = {
|
|
6889
|
-
accessKeyId: env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID,
|
|
6890
|
-
secretAccessKey: env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY
|
|
6891
|
-
};
|
|
6892
|
-
}
|
|
6893
|
-
const client = new SESClient(config);
|
|
6894
|
-
const body = {};
|
|
6895
|
-
if (text10) {
|
|
6896
|
-
body.Text = {
|
|
6897
|
-
Charset: "UTF-8",
|
|
6898
|
-
Data: text10
|
|
6899
|
-
};
|
|
6900
|
-
}
|
|
6901
|
-
if (html) {
|
|
6902
|
-
body.Html = {
|
|
6903
|
-
Charset: "UTF-8",
|
|
6904
|
-
Data: html
|
|
6905
|
-
};
|
|
6906
|
-
}
|
|
6907
|
-
const command = new SendEmailCommand({
|
|
6908
|
-
Source: env4.SPFN_AUTH_AWS_SES_FROM_EMAIL,
|
|
6909
|
-
Destination: {
|
|
6910
|
-
ToAddresses: [to]
|
|
6911
|
-
},
|
|
6912
|
-
Message: {
|
|
6913
|
-
Subject: {
|
|
6914
|
-
Charset: "UTF-8",
|
|
6915
|
-
Data: subject
|
|
6916
|
-
},
|
|
6917
|
-
Body: body
|
|
6918
|
-
}
|
|
6919
|
-
});
|
|
6920
|
-
const response = await client.send(command);
|
|
6921
|
-
authLogger.email.info("Email sent via AWS SES", {
|
|
6922
|
-
to,
|
|
6923
|
-
messageId: response.MessageId,
|
|
6924
|
-
purpose: purpose || "N/A"
|
|
6925
|
-
});
|
|
6926
|
-
return {
|
|
6927
|
-
success: true,
|
|
6928
|
-
messageId: response.MessageId
|
|
6929
|
-
};
|
|
6930
|
-
} catch (error) {
|
|
6931
|
-
const err = error;
|
|
6932
|
-
authLogger.email.error("Failed to send email via AWS SES", {
|
|
6933
|
-
to,
|
|
6934
|
-
error: err.message
|
|
6935
|
-
});
|
|
6936
|
-
return {
|
|
6937
|
-
success: false,
|
|
6938
|
-
error: err.message || "Failed to send email via AWS SES"
|
|
6939
|
-
};
|
|
6940
|
-
}
|
|
6941
|
-
}
|
|
6942
|
-
};
|
|
6943
|
-
} catch (error) {
|
|
6944
|
-
return null;
|
|
6945
|
-
}
|
|
6946
|
-
}
|
|
6947
|
-
var awsSESProvider = createAWSSESProvider();
|
|
6948
|
-
|
|
6949
|
-
// src/server/services/email/index.ts
|
|
6950
|
-
if (awsSESProvider) {
|
|
6951
|
-
registerEmailProvider(awsSESProvider);
|
|
6952
|
-
}
|
|
6953
|
-
|
|
6954
|
-
// src/server/services/email/templates/verification-code.ts
|
|
6955
|
-
function getSubject(purpose) {
|
|
6956
|
-
switch (purpose) {
|
|
6957
|
-
case "registration":
|
|
6958
|
-
return "Verify your email address";
|
|
6959
|
-
case "login":
|
|
6960
|
-
return "Your login verification code";
|
|
6961
|
-
case "password_reset":
|
|
6962
|
-
return "Reset your password";
|
|
6963
|
-
default:
|
|
6964
|
-
return "Your verification code";
|
|
6965
|
-
}
|
|
6966
|
-
}
|
|
6967
|
-
function getPurposeText(purpose) {
|
|
6968
|
-
switch (purpose) {
|
|
6969
|
-
case "registration":
|
|
6970
|
-
return "complete your registration";
|
|
6971
|
-
case "login":
|
|
6972
|
-
return "verify your identity";
|
|
6973
|
-
case "password_reset":
|
|
6974
|
-
return "reset your password";
|
|
6975
|
-
default:
|
|
6976
|
-
return "verify your identity";
|
|
6977
|
-
}
|
|
6978
|
-
}
|
|
6979
|
-
function generateText(params) {
|
|
6980
|
-
const { code, expiresInMinutes = 5 } = params;
|
|
6981
|
-
return `Your verification code is: ${code}
|
|
6982
|
-
|
|
6983
|
-
This code will expire in ${expiresInMinutes} minutes.
|
|
6984
|
-
|
|
6985
|
-
If you didn't request this code, please ignore this email.`;
|
|
6986
|
-
}
|
|
6987
|
-
function generateHTML(params) {
|
|
6988
|
-
const { code, purpose, expiresInMinutes = 5, appName } = params;
|
|
6989
|
-
const purposeText = getPurposeText(purpose);
|
|
6990
|
-
return `<!DOCTYPE html>
|
|
6991
|
-
<html>
|
|
6992
|
-
<head>
|
|
6993
|
-
<meta charset="utf-8">
|
|
6994
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6995
|
-
<title>Verification Code</title>
|
|
6996
|
-
</head>
|
|
6997
|
-
<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;">
|
|
6998
|
-
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
|
|
6999
|
-
<h1 style="color: white; margin: 0; font-size: 24px;">${appName ? appName : "Verification Code"}</h1>
|
|
7000
|
-
</div>
|
|
7001
|
-
<div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
|
|
7002
|
-
<p style="margin-bottom: 20px; font-size: 16px;">
|
|
7003
|
-
Please use the following verification code to ${purposeText}:
|
|
7004
|
-
</p>
|
|
7005
|
-
<div style="background: #f8f9fa; padding: 25px; border-radius: 8px; text-align: center; margin: 25px 0; border: 2px dashed #dee2e6;">
|
|
7006
|
-
<span style="font-size: 36px; font-weight: bold; letter-spacing: 10px; color: #333; font-family: 'Courier New', monospace;">${code}</span>
|
|
7007
|
-
</div>
|
|
7008
|
-
<p style="color: #666; font-size: 14px; margin-top: 20px; text-align: center;">
|
|
7009
|
-
<strong>This code will expire in ${expiresInMinutes} minutes.</strong>
|
|
7010
|
-
</p>
|
|
7011
|
-
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
|
|
7012
|
-
<p style="color: #999; font-size: 12px; text-align: center; margin: 0;">
|
|
7013
|
-
If you didn't request this code, please ignore this email.
|
|
7014
|
-
</p>
|
|
7015
|
-
</div>
|
|
7016
|
-
<div style="text-align: center; padding: 20px; color: #999; font-size: 11px;">
|
|
7017
|
-
<p style="margin: 0;">This is an automated message. Please do not reply.</p>
|
|
7018
|
-
</div>
|
|
7019
|
-
</body>
|
|
7020
|
-
</html>`;
|
|
7021
|
-
}
|
|
7022
|
-
function verificationCodeTemplate(params) {
|
|
7023
|
-
return {
|
|
7024
|
-
subject: getSubject(params.purpose),
|
|
7025
|
-
text: generateText(params),
|
|
7026
|
-
html: generateHTML(params)
|
|
7027
|
-
};
|
|
7028
|
-
}
|
|
7029
|
-
|
|
7030
|
-
// src/server/services/email/templates/registry.ts
|
|
7031
|
-
var customTemplates = {};
|
|
7032
|
-
function registerEmailTemplates(templates) {
|
|
7033
|
-
customTemplates = { ...customTemplates, ...templates };
|
|
7034
|
-
authLogger.email.info("Registered custom email templates", {
|
|
7035
|
-
templates: Object.keys(templates)
|
|
7036
|
-
});
|
|
7037
|
-
}
|
|
7038
|
-
function getVerificationCodeTemplate(params) {
|
|
7039
|
-
if (customTemplates.verificationCode) {
|
|
7040
|
-
return customTemplates.verificationCode(params);
|
|
7041
|
-
}
|
|
7042
|
-
return verificationCodeTemplate(params);
|
|
7043
|
-
}
|
|
7044
|
-
function getWelcomeTemplate(params) {
|
|
7045
|
-
if (customTemplates.welcome) {
|
|
7046
|
-
return customTemplates.welcome(params);
|
|
7047
|
-
}
|
|
7048
|
-
return {
|
|
7049
|
-
subject: params.appName ? `Welcome to ${params.appName}!` : "Welcome!",
|
|
7050
|
-
text: `Welcome! Your account has been created successfully.`,
|
|
7051
|
-
html: `
|
|
7052
|
-
<!DOCTYPE html>
|
|
7053
|
-
<html>
|
|
7054
|
-
<head><meta charset="utf-8"></head>
|
|
7055
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
|
|
7056
|
-
<h1>Welcome${params.appName ? ` to ${params.appName}` : ""}!</h1>
|
|
7057
|
-
<p>Your account has been created successfully.</p>
|
|
7058
|
-
</body>
|
|
7059
|
-
</html>`
|
|
7060
|
-
};
|
|
7061
|
-
}
|
|
7062
|
-
function getPasswordResetTemplate(params) {
|
|
7063
|
-
if (customTemplates.passwordReset) {
|
|
7064
|
-
return customTemplates.passwordReset(params);
|
|
7065
|
-
}
|
|
7066
|
-
const expires = params.expiresInMinutes || 30;
|
|
7067
|
-
return {
|
|
7068
|
-
subject: "Reset your password",
|
|
7069
|
-
text: `Click this link to reset your password: ${params.resetLink}
|
|
7070
|
-
|
|
7071
|
-
This link will expire in ${expires} minutes.`,
|
|
7072
|
-
html: `
|
|
7073
|
-
<!DOCTYPE html>
|
|
7074
|
-
<html>
|
|
7075
|
-
<head><meta charset="utf-8"></head>
|
|
7076
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
|
|
7077
|
-
<h1>Reset Your Password</h1>
|
|
7078
|
-
<p>Click the button below to reset your password:</p>
|
|
7079
|
-
<a href="${params.resetLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Reset Password</a>
|
|
7080
|
-
<p style="color: #666; margin-top: 20px;">This link will expire in ${expires} minutes.</p>
|
|
7081
|
-
</body>
|
|
7082
|
-
</html>`
|
|
7083
|
-
};
|
|
7084
|
-
}
|
|
7085
|
-
function getInvitationTemplate(params) {
|
|
7086
|
-
if (customTemplates.invitation) {
|
|
7087
|
-
return customTemplates.invitation(params);
|
|
7088
|
-
}
|
|
7089
|
-
const appName = params.appName || "our platform";
|
|
7090
|
-
const inviterText = params.inviterName ? `${params.inviterName} has invited you` : "You have been invited";
|
|
7091
|
-
const roleText = params.roleName ? ` as ${params.roleName}` : "";
|
|
7092
|
-
return {
|
|
7093
|
-
subject: `You're invited to join ${appName}`,
|
|
7094
|
-
text: `${inviterText} to join ${appName}${roleText}.
|
|
7095
|
-
|
|
7096
|
-
Click here to accept: ${params.inviteLink}`,
|
|
7097
|
-
html: `
|
|
7098
|
-
<!DOCTYPE html>
|
|
7099
|
-
<html>
|
|
7100
|
-
<head><meta charset="utf-8"></head>
|
|
7101
|
-
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
|
|
7102
|
-
<h1>You're Invited!</h1>
|
|
7103
|
-
<p>${inviterText} to join <strong>${appName}</strong>${roleText}.</p>
|
|
7104
|
-
<a href="${params.inviteLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Accept Invitation</a>
|
|
7105
|
-
</body>
|
|
7106
|
-
</html>`
|
|
7107
|
-
};
|
|
7108
|
-
}
|
|
7109
|
-
|
|
7110
|
-
// src/server/services/verification.service.ts
|
|
7111
6860
|
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
7112
6861
|
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
7113
6862
|
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
@@ -7151,7 +6900,7 @@ async function markCodeAsUsed(codeId) {
|
|
|
7151
6900
|
await verificationCodesRepository.markAsUsed(codeId);
|
|
7152
6901
|
}
|
|
7153
6902
|
function createVerificationToken(payload) {
|
|
7154
|
-
return jwt2.sign(payload,
|
|
6903
|
+
return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7155
6904
|
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
7156
6905
|
issuer: "spfn-auth",
|
|
7157
6906
|
audience: "spfn-client"
|
|
@@ -7159,7 +6908,7 @@ function createVerificationToken(payload) {
|
|
|
7159
6908
|
}
|
|
7160
6909
|
function validateVerificationToken(token) {
|
|
7161
6910
|
try {
|
|
7162
|
-
const decoded = jwt2.verify(token,
|
|
6911
|
+
const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7163
6912
|
issuer: "spfn-auth",
|
|
7164
6913
|
audience: "spfn-client"
|
|
7165
6914
|
});
|
|
@@ -7173,17 +6922,14 @@ function validateVerificationToken(token) {
|
|
|
7173
6922
|
}
|
|
7174
6923
|
}
|
|
7175
6924
|
async function sendVerificationEmail(email, code, purpose) {
|
|
7176
|
-
const { subject, text: text10, html } = getVerificationCodeTemplate({
|
|
7177
|
-
code,
|
|
7178
|
-
purpose,
|
|
7179
|
-
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
7180
|
-
});
|
|
7181
6925
|
const result = await sendEmail({
|
|
7182
6926
|
to: email,
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
6927
|
+
template: "verification-code",
|
|
6928
|
+
data: {
|
|
6929
|
+
code,
|
|
6930
|
+
purpose,
|
|
6931
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
6932
|
+
}
|
|
7187
6933
|
});
|
|
7188
6934
|
if (!result.success) {
|
|
7189
6935
|
authLogger.email.error("Failed to send verification email", {
|
|
@@ -7194,11 +6940,13 @@ async function sendVerificationEmail(email, code, purpose) {
|
|
|
7194
6940
|
}
|
|
7195
6941
|
}
|
|
7196
6942
|
async function sendVerificationSMS(phone, code, purpose) {
|
|
7197
|
-
const message = `Your verification code is: ${code}`;
|
|
7198
6943
|
const result = await sendSMS({
|
|
7199
|
-
phone,
|
|
7200
|
-
|
|
7201
|
-
|
|
6944
|
+
to: phone,
|
|
6945
|
+
template: "verification-code",
|
|
6946
|
+
data: {
|
|
6947
|
+
code,
|
|
6948
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
6949
|
+
}
|
|
7202
6950
|
});
|
|
7203
6951
|
if (!result.success) {
|
|
7204
6952
|
authLogger.sms.error("Failed to send verification SMS", {
|
|
@@ -7299,6 +7047,7 @@ async function revokeKeyService(params) {
|
|
|
7299
7047
|
|
|
7300
7048
|
// src/server/services/user.service.ts
|
|
7301
7049
|
init_repositories();
|
|
7050
|
+
import { UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
7302
7051
|
async function getUserByIdService(userId) {
|
|
7303
7052
|
return await usersRepository.findById(userId);
|
|
7304
7053
|
}
|
|
@@ -7314,6 +7063,47 @@ async function updateLastLoginService(userId) {
|
|
|
7314
7063
|
async function updateUserService(userId, updates) {
|
|
7315
7064
|
await usersRepository.updateById(userId, updates);
|
|
7316
7065
|
}
|
|
7066
|
+
async function checkUsernameAvailableService(username) {
|
|
7067
|
+
const existing = await usersRepository.findByUsername(username);
|
|
7068
|
+
return !existing;
|
|
7069
|
+
}
|
|
7070
|
+
async function updateUsernameService(userId, username) {
|
|
7071
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7072
|
+
if (username !== null) {
|
|
7073
|
+
const existing = await usersRepository.findByUsername(username);
|
|
7074
|
+
if (existing && existing.id !== userIdNum) {
|
|
7075
|
+
throw new UsernameAlreadyTakenError({ username });
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
return await usersRepository.updateById(userIdNum, { username });
|
|
7079
|
+
}
|
|
7080
|
+
|
|
7081
|
+
// src/server/events/index.ts
|
|
7082
|
+
init_esm();
|
|
7083
|
+
import { defineEvent } from "@spfn/core/event";
|
|
7084
|
+
var AuthProviderSchema = Type.Union([
|
|
7085
|
+
Type.Literal("email"),
|
|
7086
|
+
Type.Literal("phone"),
|
|
7087
|
+
Type.Literal("google")
|
|
7088
|
+
]);
|
|
7089
|
+
var authLoginEvent = defineEvent(
|
|
7090
|
+
"auth.login",
|
|
7091
|
+
Type.Object({
|
|
7092
|
+
userId: Type.String(),
|
|
7093
|
+
provider: AuthProviderSchema,
|
|
7094
|
+
email: Type.Optional(Type.String()),
|
|
7095
|
+
phone: Type.Optional(Type.String())
|
|
7096
|
+
})
|
|
7097
|
+
);
|
|
7098
|
+
var authRegisterEvent = defineEvent(
|
|
7099
|
+
"auth.register",
|
|
7100
|
+
Type.Object({
|
|
7101
|
+
userId: Type.String(),
|
|
7102
|
+
provider: AuthProviderSchema,
|
|
7103
|
+
email: Type.Optional(Type.String()),
|
|
7104
|
+
phone: Type.Optional(Type.String())
|
|
7105
|
+
})
|
|
7106
|
+
);
|
|
7317
7107
|
|
|
7318
7108
|
// src/server/services/auth.service.ts
|
|
7319
7109
|
async function checkAccountExistsService(params) {
|
|
@@ -7381,11 +7171,18 @@ async function registerService(params) {
|
|
|
7381
7171
|
fingerprint,
|
|
7382
7172
|
algorithm
|
|
7383
7173
|
});
|
|
7384
|
-
|
|
7174
|
+
const result = {
|
|
7385
7175
|
userId: String(newUser.id),
|
|
7386
7176
|
email: newUser.email || void 0,
|
|
7387
7177
|
phone: newUser.phone || void 0
|
|
7388
7178
|
};
|
|
7179
|
+
await authRegisterEvent.emit({
|
|
7180
|
+
userId: result.userId,
|
|
7181
|
+
provider: email ? "email" : "phone",
|
|
7182
|
+
email: result.email,
|
|
7183
|
+
phone: result.phone
|
|
7184
|
+
});
|
|
7185
|
+
return result;
|
|
7389
7186
|
}
|
|
7390
7187
|
async function loginService(params) {
|
|
7391
7188
|
const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
|
|
@@ -7418,12 +7215,19 @@ async function loginService(params) {
|
|
|
7418
7215
|
algorithm
|
|
7419
7216
|
});
|
|
7420
7217
|
await updateLastLoginService(user.id);
|
|
7421
|
-
|
|
7218
|
+
const result = {
|
|
7422
7219
|
userId: String(user.id),
|
|
7423
7220
|
email: user.email || void 0,
|
|
7424
7221
|
phone: user.phone || void 0,
|
|
7425
7222
|
passwordChangeRequired: user.passwordChangeRequired
|
|
7426
7223
|
};
|
|
7224
|
+
await authLoginEvent.emit({
|
|
7225
|
+
userId: result.userId,
|
|
7226
|
+
provider: email ? "email" : "phone",
|
|
7227
|
+
email: result.email,
|
|
7228
|
+
phone: result.phone
|
|
7229
|
+
});
|
|
7230
|
+
return result;
|
|
7427
7231
|
}
|
|
7428
7232
|
async function logoutService(params) {
|
|
7429
7233
|
const { userId, keyId } = params;
|
|
@@ -7461,12 +7265,14 @@ init_repositories();
|
|
|
7461
7265
|
init_rbac();
|
|
7462
7266
|
|
|
7463
7267
|
// src/server/lib/config.ts
|
|
7464
|
-
import { env as
|
|
7268
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
7465
7269
|
var COOKIE_NAMES = {
|
|
7466
7270
|
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
7467
7271
|
SESSION: "spfn_session",
|
|
7468
7272
|
/** Current key ID (for key rotation) */
|
|
7469
|
-
SESSION_KEY_ID: "spfn_session_key_id"
|
|
7273
|
+
SESSION_KEY_ID: "spfn_session_key_id",
|
|
7274
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7275
|
+
OAUTH_PENDING: "spfn_oauth_pending"
|
|
7470
7276
|
};
|
|
7471
7277
|
function parseDuration(duration) {
|
|
7472
7278
|
if (typeof duration === "number") {
|
|
@@ -7511,7 +7317,7 @@ function getSessionTtl(override) {
|
|
|
7511
7317
|
if (globalConfig.sessionTtl !== void 0) {
|
|
7512
7318
|
return parseDuration(globalConfig.sessionTtl);
|
|
7513
7319
|
}
|
|
7514
|
-
const envTtl =
|
|
7320
|
+
const envTtl = env4.SPFN_AUTH_SESSION_TTL;
|
|
7515
7321
|
if (envTtl) {
|
|
7516
7322
|
return parseDuration(envTtl);
|
|
7517
7323
|
}
|
|
@@ -7673,14 +7479,18 @@ async function hasAllPermissions(userId, permissionNames) {
|
|
|
7673
7479
|
const perms = await getUserPermissions(userId);
|
|
7674
7480
|
return permissionNames.every((p) => perms.includes(p));
|
|
7675
7481
|
}
|
|
7676
|
-
async function
|
|
7482
|
+
async function getUserRole(userId) {
|
|
7677
7483
|
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7678
7484
|
const user = await usersRepository.findById(userIdNum);
|
|
7679
7485
|
if (!user || !user.roleId) {
|
|
7680
|
-
return
|
|
7486
|
+
return null;
|
|
7681
7487
|
}
|
|
7682
7488
|
const role = await rolesRepository.findById(user.roleId);
|
|
7683
|
-
return role?.name
|
|
7489
|
+
return role?.name || null;
|
|
7490
|
+
}
|
|
7491
|
+
async function hasRole(userId, roleName) {
|
|
7492
|
+
const role = await getUserRole(userId);
|
|
7493
|
+
return role === roleName;
|
|
7684
7494
|
}
|
|
7685
7495
|
async function hasAnyRole(userId, roleNames) {
|
|
7686
7496
|
for (const roleName of roleNames) {
|
|
@@ -7879,6 +7689,7 @@ async function getUserProfileService(userId) {
|
|
|
7879
7689
|
return {
|
|
7880
7690
|
userId: user.userId,
|
|
7881
7691
|
email: user.email,
|
|
7692
|
+
username: user.username,
|
|
7882
7693
|
emailVerified: user.isEmailVerified,
|
|
7883
7694
|
phoneVerified: user.isPhoneVerified,
|
|
7884
7695
|
lastLoginAt: user.lastLoginAt,
|
|
@@ -7887,6 +7698,406 @@ async function getUserProfileService(userId) {
|
|
|
7887
7698
|
profile
|
|
7888
7699
|
};
|
|
7889
7700
|
}
|
|
7701
|
+
function emptyToNull(value) {
|
|
7702
|
+
if (value === "") {
|
|
7703
|
+
return null;
|
|
7704
|
+
}
|
|
7705
|
+
return value;
|
|
7706
|
+
}
|
|
7707
|
+
async function updateUserProfileService(userId, params) {
|
|
7708
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7709
|
+
const updateData = {};
|
|
7710
|
+
if (params.displayName !== void 0) {
|
|
7711
|
+
updateData.displayName = emptyToNull(params.displayName) || "User";
|
|
7712
|
+
}
|
|
7713
|
+
if (params.firstName !== void 0) {
|
|
7714
|
+
updateData.firstName = emptyToNull(params.firstName);
|
|
7715
|
+
}
|
|
7716
|
+
if (params.lastName !== void 0) {
|
|
7717
|
+
updateData.lastName = emptyToNull(params.lastName);
|
|
7718
|
+
}
|
|
7719
|
+
if (params.avatarUrl !== void 0) {
|
|
7720
|
+
updateData.avatarUrl = emptyToNull(params.avatarUrl);
|
|
7721
|
+
}
|
|
7722
|
+
if (params.bio !== void 0) {
|
|
7723
|
+
updateData.bio = emptyToNull(params.bio);
|
|
7724
|
+
}
|
|
7725
|
+
if (params.locale !== void 0) {
|
|
7726
|
+
updateData.locale = emptyToNull(params.locale) || "en";
|
|
7727
|
+
}
|
|
7728
|
+
if (params.timezone !== void 0) {
|
|
7729
|
+
updateData.timezone = emptyToNull(params.timezone) || "UTC";
|
|
7730
|
+
}
|
|
7731
|
+
if (params.dateOfBirth !== void 0) {
|
|
7732
|
+
updateData.dateOfBirth = emptyToNull(params.dateOfBirth);
|
|
7733
|
+
}
|
|
7734
|
+
if (params.gender !== void 0) {
|
|
7735
|
+
updateData.gender = emptyToNull(params.gender);
|
|
7736
|
+
}
|
|
7737
|
+
if (params.website !== void 0) {
|
|
7738
|
+
updateData.website = emptyToNull(params.website);
|
|
7739
|
+
}
|
|
7740
|
+
if (params.location !== void 0) {
|
|
7741
|
+
updateData.location = emptyToNull(params.location);
|
|
7742
|
+
}
|
|
7743
|
+
if (params.company !== void 0) {
|
|
7744
|
+
updateData.company = emptyToNull(params.company);
|
|
7745
|
+
}
|
|
7746
|
+
if (params.jobTitle !== void 0) {
|
|
7747
|
+
updateData.jobTitle = emptyToNull(params.jobTitle);
|
|
7748
|
+
}
|
|
7749
|
+
if (params.metadata !== void 0) {
|
|
7750
|
+
updateData.metadata = params.metadata;
|
|
7751
|
+
}
|
|
7752
|
+
const existing = await userProfilesRepository.findByUserId(userIdNum);
|
|
7753
|
+
if (!existing && !updateData.displayName) {
|
|
7754
|
+
updateData.displayName = "User";
|
|
7755
|
+
}
|
|
7756
|
+
await userProfilesRepository.upsertByUserId(userIdNum, updateData);
|
|
7757
|
+
const profile = await userProfilesRepository.fetchProfileData(userIdNum);
|
|
7758
|
+
return profile;
|
|
7759
|
+
}
|
|
7760
|
+
|
|
7761
|
+
// src/server/services/oauth.service.ts
|
|
7762
|
+
init_repositories();
|
|
7763
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
7764
|
+
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
7765
|
+
|
|
7766
|
+
// src/server/lib/oauth/google.ts
|
|
7767
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7768
|
+
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
7769
|
+
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
7770
|
+
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
7771
|
+
function isGoogleOAuthEnabled() {
|
|
7772
|
+
return !!(env5.SPFN_AUTH_GOOGLE_CLIENT_ID && env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
7773
|
+
}
|
|
7774
|
+
function getGoogleOAuthConfig() {
|
|
7775
|
+
const clientId = env5.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
7776
|
+
const clientSecret = env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
7777
|
+
if (!clientId || !clientSecret) {
|
|
7778
|
+
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
7779
|
+
}
|
|
7780
|
+
const baseUrl = env5.NEXT_PUBLIC_SPFN_API_URL || env5.SPFN_API_URL;
|
|
7781
|
+
const redirectUri = env5.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
7782
|
+
return {
|
|
7783
|
+
clientId,
|
|
7784
|
+
clientSecret,
|
|
7785
|
+
redirectUri
|
|
7786
|
+
};
|
|
7787
|
+
}
|
|
7788
|
+
function getDefaultScopes() {
|
|
7789
|
+
const envScopes = env5.SPFN_AUTH_GOOGLE_SCOPES;
|
|
7790
|
+
if (envScopes) {
|
|
7791
|
+
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7792
|
+
}
|
|
7793
|
+
return ["email", "profile"];
|
|
7794
|
+
}
|
|
7795
|
+
function getGoogleAuthUrl(state, scopes) {
|
|
7796
|
+
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
7797
|
+
const config = getGoogleOAuthConfig();
|
|
7798
|
+
const params = new URLSearchParams({
|
|
7799
|
+
client_id: config.clientId,
|
|
7800
|
+
redirect_uri: config.redirectUri,
|
|
7801
|
+
response_type: "code",
|
|
7802
|
+
scope: resolvedScopes.join(" "),
|
|
7803
|
+
state,
|
|
7804
|
+
access_type: "offline",
|
|
7805
|
+
// refresh_token 받기 위해
|
|
7806
|
+
prompt: "consent"
|
|
7807
|
+
// 매번 동의 화면 표시 (refresh_token 보장)
|
|
7808
|
+
});
|
|
7809
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
7810
|
+
}
|
|
7811
|
+
async function exchangeCodeForTokens(code) {
|
|
7812
|
+
const config = getGoogleOAuthConfig();
|
|
7813
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7814
|
+
method: "POST",
|
|
7815
|
+
headers: {
|
|
7816
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7817
|
+
},
|
|
7818
|
+
body: new URLSearchParams({
|
|
7819
|
+
client_id: config.clientId,
|
|
7820
|
+
client_secret: config.clientSecret,
|
|
7821
|
+
redirect_uri: config.redirectUri,
|
|
7822
|
+
grant_type: "authorization_code",
|
|
7823
|
+
code
|
|
7824
|
+
})
|
|
7825
|
+
});
|
|
7826
|
+
if (!response.ok) {
|
|
7827
|
+
const error = await response.text();
|
|
7828
|
+
throw new Error(`Failed to exchange code for tokens: ${error}`);
|
|
7829
|
+
}
|
|
7830
|
+
return response.json();
|
|
7831
|
+
}
|
|
7832
|
+
async function getGoogleUserInfo(accessToken) {
|
|
7833
|
+
const response = await fetch(GOOGLE_USERINFO_URL, {
|
|
7834
|
+
headers: {
|
|
7835
|
+
Authorization: `Bearer ${accessToken}`
|
|
7836
|
+
}
|
|
7837
|
+
});
|
|
7838
|
+
if (!response.ok) {
|
|
7839
|
+
const error = await response.text();
|
|
7840
|
+
throw new Error(`Failed to get user info: ${error}`);
|
|
7841
|
+
}
|
|
7842
|
+
return response.json();
|
|
7843
|
+
}
|
|
7844
|
+
async function refreshAccessToken(refreshToken) {
|
|
7845
|
+
const config = getGoogleOAuthConfig();
|
|
7846
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7847
|
+
method: "POST",
|
|
7848
|
+
headers: {
|
|
7849
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7850
|
+
},
|
|
7851
|
+
body: new URLSearchParams({
|
|
7852
|
+
client_id: config.clientId,
|
|
7853
|
+
client_secret: config.clientSecret,
|
|
7854
|
+
refresh_token: refreshToken,
|
|
7855
|
+
grant_type: "refresh_token"
|
|
7856
|
+
})
|
|
7857
|
+
});
|
|
7858
|
+
if (!response.ok) {
|
|
7859
|
+
const error = await response.text();
|
|
7860
|
+
throw new Error(`Failed to refresh access token: ${error}`);
|
|
7861
|
+
}
|
|
7862
|
+
return response.json();
|
|
7863
|
+
}
|
|
7864
|
+
|
|
7865
|
+
// src/server/lib/oauth/state.ts
|
|
7866
|
+
import * as jose from "jose";
|
|
7867
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
7868
|
+
async function getStateKey() {
|
|
7869
|
+
const secret = env6.SPFN_AUTH_SESSION_SECRET;
|
|
7870
|
+
const encoder = new TextEncoder();
|
|
7871
|
+
const data = encoder.encode(`oauth-state:${secret}`);
|
|
7872
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
7873
|
+
return new Uint8Array(hashBuffer);
|
|
7874
|
+
}
|
|
7875
|
+
function generateNonce() {
|
|
7876
|
+
const array = new Uint8Array(16);
|
|
7877
|
+
crypto.getRandomValues(array);
|
|
7878
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
7879
|
+
}
|
|
7880
|
+
async function createOAuthState(params) {
|
|
7881
|
+
const key = await getStateKey();
|
|
7882
|
+
const state = {
|
|
7883
|
+
returnUrl: params.returnUrl,
|
|
7884
|
+
nonce: generateNonce(),
|
|
7885
|
+
provider: params.provider,
|
|
7886
|
+
publicKey: params.publicKey,
|
|
7887
|
+
keyId: params.keyId,
|
|
7888
|
+
fingerprint: params.fingerprint,
|
|
7889
|
+
algorithm: params.algorithm
|
|
7890
|
+
};
|
|
7891
|
+
const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
|
|
7892
|
+
return encodeURIComponent(jwe);
|
|
7893
|
+
}
|
|
7894
|
+
async function verifyOAuthState(encryptedState) {
|
|
7895
|
+
const key = await getStateKey();
|
|
7896
|
+
const jwe = decodeURIComponent(encryptedState);
|
|
7897
|
+
const { payload } = await jose.jwtDecrypt(jwe, key);
|
|
7898
|
+
return payload.state;
|
|
7899
|
+
}
|
|
7900
|
+
|
|
7901
|
+
// src/server/services/oauth.service.ts
|
|
7902
|
+
async function oauthStartService(params) {
|
|
7903
|
+
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm } = params;
|
|
7904
|
+
if (provider === "google") {
|
|
7905
|
+
if (!isGoogleOAuthEnabled()) {
|
|
7906
|
+
throw new ValidationError2({
|
|
7907
|
+
message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
|
|
7908
|
+
});
|
|
7909
|
+
}
|
|
7910
|
+
const state = await createOAuthState({
|
|
7911
|
+
provider: "google",
|
|
7912
|
+
returnUrl,
|
|
7913
|
+
publicKey,
|
|
7914
|
+
keyId,
|
|
7915
|
+
fingerprint,
|
|
7916
|
+
algorithm
|
|
7917
|
+
});
|
|
7918
|
+
const authUrl = getGoogleAuthUrl(state);
|
|
7919
|
+
return { authUrl };
|
|
7920
|
+
}
|
|
7921
|
+
throw new ValidationError2({
|
|
7922
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7923
|
+
});
|
|
7924
|
+
}
|
|
7925
|
+
async function oauthCallbackService(params) {
|
|
7926
|
+
const { provider, code, state } = params;
|
|
7927
|
+
const stateData = await verifyOAuthState(state);
|
|
7928
|
+
if (stateData.provider !== provider) {
|
|
7929
|
+
throw new ValidationError2({
|
|
7930
|
+
message: "OAuth state provider mismatch"
|
|
7931
|
+
});
|
|
7932
|
+
}
|
|
7933
|
+
if (provider === "google") {
|
|
7934
|
+
return handleGoogleCallback(code, stateData);
|
|
7935
|
+
}
|
|
7936
|
+
throw new ValidationError2({
|
|
7937
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7938
|
+
});
|
|
7939
|
+
}
|
|
7940
|
+
async function handleGoogleCallback(code, stateData) {
|
|
7941
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
7942
|
+
const googleUser = await getGoogleUserInfo(tokens.access_token);
|
|
7943
|
+
const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
|
|
7944
|
+
"google",
|
|
7945
|
+
googleUser.id
|
|
7946
|
+
);
|
|
7947
|
+
let userId;
|
|
7948
|
+
let isNewUser = false;
|
|
7949
|
+
if (existingSocialAccount) {
|
|
7950
|
+
userId = existingSocialAccount.userId;
|
|
7951
|
+
await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
|
|
7952
|
+
accessToken: tokens.access_token,
|
|
7953
|
+
refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
|
|
7954
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
7955
|
+
});
|
|
7956
|
+
} else {
|
|
7957
|
+
const result = await createOrLinkUser(googleUser, tokens);
|
|
7958
|
+
userId = result.userId;
|
|
7959
|
+
isNewUser = result.isNewUser;
|
|
7960
|
+
}
|
|
7961
|
+
await registerPublicKeyService({
|
|
7962
|
+
userId,
|
|
7963
|
+
keyId: stateData.keyId,
|
|
7964
|
+
publicKey: stateData.publicKey,
|
|
7965
|
+
fingerprint: stateData.fingerprint,
|
|
7966
|
+
algorithm: stateData.algorithm
|
|
7967
|
+
});
|
|
7968
|
+
await updateLastLoginService(userId);
|
|
7969
|
+
const appUrl = env7.NEXT_PUBLIC_SPFN_APP_URL || env7.SPFN_APP_URL;
|
|
7970
|
+
const callbackPath = env7.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
7971
|
+
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
7972
|
+
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
7973
|
+
userId: String(userId),
|
|
7974
|
+
keyId: stateData.keyId,
|
|
7975
|
+
returnUrl: stateData.returnUrl,
|
|
7976
|
+
isNewUser: String(isNewUser)
|
|
7977
|
+
});
|
|
7978
|
+
const user = await usersRepository.findById(userId);
|
|
7979
|
+
const eventPayload = {
|
|
7980
|
+
userId: String(userId),
|
|
7981
|
+
provider: "google",
|
|
7982
|
+
email: user?.email || void 0,
|
|
7983
|
+
phone: user?.phone || void 0
|
|
7984
|
+
};
|
|
7985
|
+
if (isNewUser) {
|
|
7986
|
+
await authRegisterEvent.emit(eventPayload);
|
|
7987
|
+
} else {
|
|
7988
|
+
await authLoginEvent.emit(eventPayload);
|
|
7989
|
+
}
|
|
7990
|
+
return {
|
|
7991
|
+
redirectUrl,
|
|
7992
|
+
userId: String(userId),
|
|
7993
|
+
keyId: stateData.keyId,
|
|
7994
|
+
isNewUser
|
|
7995
|
+
};
|
|
7996
|
+
}
|
|
7997
|
+
async function createOrLinkUser(googleUser, tokens) {
|
|
7998
|
+
const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
|
|
7999
|
+
let userId;
|
|
8000
|
+
let isNewUser = false;
|
|
8001
|
+
if (existingUser) {
|
|
8002
|
+
if (!googleUser.verified_email) {
|
|
8003
|
+
throw new ValidationError2({
|
|
8004
|
+
message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
|
|
8005
|
+
});
|
|
8006
|
+
}
|
|
8007
|
+
userId = existingUser.id;
|
|
8008
|
+
if (!existingUser.emailVerifiedAt) {
|
|
8009
|
+
await usersRepository.updateById(existingUser.id, {
|
|
8010
|
+
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
8011
|
+
});
|
|
8012
|
+
}
|
|
8013
|
+
} else {
|
|
8014
|
+
const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
8015
|
+
const userRole = await getRoleByName3("user");
|
|
8016
|
+
if (!userRole) {
|
|
8017
|
+
throw new Error("Default user role not found. Run initializeAuth() first.");
|
|
8018
|
+
}
|
|
8019
|
+
const newUser = await usersRepository.create({
|
|
8020
|
+
email: googleUser.verified_email ? googleUser.email : null,
|
|
8021
|
+
phone: null,
|
|
8022
|
+
passwordHash: null,
|
|
8023
|
+
// OAuth 사용자는 비밀번호 없음
|
|
8024
|
+
passwordChangeRequired: false,
|
|
8025
|
+
roleId: userRole.id,
|
|
8026
|
+
status: "active",
|
|
8027
|
+
emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
|
|
8028
|
+
});
|
|
8029
|
+
userId = newUser.id;
|
|
8030
|
+
isNewUser = true;
|
|
8031
|
+
}
|
|
8032
|
+
await socialAccountsRepository.create({
|
|
8033
|
+
userId,
|
|
8034
|
+
provider: "google",
|
|
8035
|
+
providerUserId: googleUser.id,
|
|
8036
|
+
providerEmail: googleUser.email,
|
|
8037
|
+
accessToken: tokens.access_token,
|
|
8038
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
8039
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8040
|
+
});
|
|
8041
|
+
return { userId, isNewUser };
|
|
8042
|
+
}
|
|
8043
|
+
function buildRedirectUrl(baseUrl, params) {
|
|
8044
|
+
const url = new URL(baseUrl, "http://placeholder");
|
|
8045
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8046
|
+
url.searchParams.set(key, value);
|
|
8047
|
+
}
|
|
8048
|
+
if (baseUrl.startsWith("http")) {
|
|
8049
|
+
return url.toString();
|
|
8050
|
+
}
|
|
8051
|
+
return `${url.pathname}${url.search}`;
|
|
8052
|
+
}
|
|
8053
|
+
function buildOAuthErrorUrl(error) {
|
|
8054
|
+
const errorUrl = env7.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
8055
|
+
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
8056
|
+
}
|
|
8057
|
+
function isOAuthProviderEnabled(provider) {
|
|
8058
|
+
switch (provider) {
|
|
8059
|
+
case "google":
|
|
8060
|
+
return isGoogleOAuthEnabled();
|
|
8061
|
+
case "github":
|
|
8062
|
+
case "kakao":
|
|
8063
|
+
case "naver":
|
|
8064
|
+
return false;
|
|
8065
|
+
default:
|
|
8066
|
+
return false;
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
8069
|
+
function getEnabledOAuthProviders() {
|
|
8070
|
+
const providers = [];
|
|
8071
|
+
if (isGoogleOAuthEnabled()) {
|
|
8072
|
+
providers.push("google");
|
|
8073
|
+
}
|
|
8074
|
+
return providers;
|
|
8075
|
+
}
|
|
8076
|
+
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
8077
|
+
async function getGoogleAccessToken(userId) {
|
|
8078
|
+
const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
|
|
8079
|
+
if (!account) {
|
|
8080
|
+
throw new ValidationError2({
|
|
8081
|
+
message: "No Google account linked. User must sign in with Google first."
|
|
8082
|
+
});
|
|
8083
|
+
}
|
|
8084
|
+
const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
|
|
8085
|
+
if (!isExpired && account.accessToken) {
|
|
8086
|
+
return account.accessToken;
|
|
8087
|
+
}
|
|
8088
|
+
if (!account.refreshToken) {
|
|
8089
|
+
throw new ValidationError2({
|
|
8090
|
+
message: "Google refresh token not available. User must re-authenticate with Google."
|
|
8091
|
+
});
|
|
8092
|
+
}
|
|
8093
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
8094
|
+
await socialAccountsRepository.updateTokens(account.id, {
|
|
8095
|
+
accessToken: tokens.access_token,
|
|
8096
|
+
refreshToken: tokens.refresh_token ?? account.refreshToken,
|
|
8097
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8098
|
+
});
|
|
8099
|
+
return tokens.access_token;
|
|
8100
|
+
}
|
|
7890
8101
|
|
|
7891
8102
|
// src/server/routes/auth/index.ts
|
|
7892
8103
|
init_esm();
|
|
@@ -7980,9 +8191,7 @@ var login = route.post("/_auth/login").input({
|
|
|
7980
8191
|
const { body } = await c.data();
|
|
7981
8192
|
return await loginService(body);
|
|
7982
8193
|
});
|
|
7983
|
-
var logout = route.post("/_auth/logout").
|
|
7984
|
-
body: Type.Object({})
|
|
7985
|
-
}).handler(async (c) => {
|
|
8194
|
+
var logout = route.post("/_auth/logout").handler(async (c) => {
|
|
7986
8195
|
const auth = getAuth(c);
|
|
7987
8196
|
if (!auth) {
|
|
7988
8197
|
return c.noContent();
|
|
@@ -7991,9 +8200,7 @@ var logout = route.post("/_auth/logout").input({
|
|
|
7991
8200
|
await logoutService({ userId: Number(userId), keyId });
|
|
7992
8201
|
return c.noContent();
|
|
7993
8202
|
});
|
|
7994
|
-
var rotateKey = route.post("/_auth/keys/rotate").
|
|
7995
|
-
body: Type.Object({})
|
|
7996
|
-
}).interceptor({
|
|
8203
|
+
var rotateKey = route.post("/_auth/keys/rotate").interceptor({
|
|
7997
8204
|
body: Type.Object({
|
|
7998
8205
|
publicKey: Type.String({ description: "New public key" }),
|
|
7999
8206
|
keyId: Type.String({ description: "New key identifier" }),
|
|
@@ -8100,10 +8307,11 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8100
8307
|
}
|
|
8101
8308
|
throw new UnauthorizedError({ message: "Authentication failed" });
|
|
8102
8309
|
}
|
|
8103
|
-
const
|
|
8104
|
-
if (!
|
|
8310
|
+
const result = await usersRepository2.findByIdWithRole(keyRecord.userId);
|
|
8311
|
+
if (!result) {
|
|
8105
8312
|
throw new UnauthorizedError({ message: "User not found" });
|
|
8106
8313
|
}
|
|
8314
|
+
const { user, role } = result;
|
|
8107
8315
|
if (user.status !== "active") {
|
|
8108
8316
|
throw new AccountDisabledError2({ status: user.status });
|
|
8109
8317
|
}
|
|
@@ -8111,7 +8319,8 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8111
8319
|
c.set("auth", {
|
|
8112
8320
|
user,
|
|
8113
8321
|
userId: String(user.id),
|
|
8114
|
-
keyId
|
|
8322
|
+
keyId,
|
|
8323
|
+
role: role?.name ?? null
|
|
8115
8324
|
});
|
|
8116
8325
|
const method = c.req.method;
|
|
8117
8326
|
const path = c.req.path;
|
|
@@ -8126,6 +8335,51 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8126
8335
|
});
|
|
8127
8336
|
await next();
|
|
8128
8337
|
});
|
|
8338
|
+
var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
8339
|
+
const authHeader = c.req.header("Authorization");
|
|
8340
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
8341
|
+
await next();
|
|
8342
|
+
return;
|
|
8343
|
+
}
|
|
8344
|
+
const token = authHeader.substring(7);
|
|
8345
|
+
try {
|
|
8346
|
+
const decoded = decodeToken2(token);
|
|
8347
|
+
if (!decoded || !decoded.keyId) {
|
|
8348
|
+
await next();
|
|
8349
|
+
return;
|
|
8350
|
+
}
|
|
8351
|
+
const keyId = decoded.keyId;
|
|
8352
|
+
const keyRecord = await keysRepository2.findActiveByKeyId(keyId);
|
|
8353
|
+
if (!keyRecord) {
|
|
8354
|
+
await next();
|
|
8355
|
+
return;
|
|
8356
|
+
}
|
|
8357
|
+
if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
|
|
8358
|
+
await next();
|
|
8359
|
+
return;
|
|
8360
|
+
}
|
|
8361
|
+
verifyClientToken2(
|
|
8362
|
+
token,
|
|
8363
|
+
keyRecord.publicKey,
|
|
8364
|
+
keyRecord.algorithm
|
|
8365
|
+
);
|
|
8366
|
+
const result = await usersRepository2.findByIdWithRole(keyRecord.userId);
|
|
8367
|
+
if (!result || result.user.status !== "active") {
|
|
8368
|
+
await next();
|
|
8369
|
+
return;
|
|
8370
|
+
}
|
|
8371
|
+
const { user, role } = result;
|
|
8372
|
+
keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
8373
|
+
c.set("auth", {
|
|
8374
|
+
user,
|
|
8375
|
+
userId: String(user.id),
|
|
8376
|
+
keyId,
|
|
8377
|
+
role: role?.name ?? null
|
|
8378
|
+
});
|
|
8379
|
+
} catch {
|
|
8380
|
+
}
|
|
8381
|
+
await next();
|
|
8382
|
+
}, { skips: ["auth"] });
|
|
8129
8383
|
|
|
8130
8384
|
// src/server/middleware/require-permission.ts
|
|
8131
8385
|
import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
|
|
@@ -8191,7 +8445,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
8191
8445
|
|
|
8192
8446
|
// src/server/middleware/require-role.ts
|
|
8193
8447
|
import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
|
|
8194
|
-
import { getAuth as getAuth3,
|
|
8448
|
+
import { getAuth as getAuth3, authLogger as authLogger4 } from "@spfn/auth/server";
|
|
8195
8449
|
import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
|
|
8196
8450
|
import { InsufficientRoleError } from "@spfn/auth/errors";
|
|
8197
8451
|
var requireRole = defineMiddleware3(
|
|
@@ -8205,11 +8459,11 @@ var requireRole = defineMiddleware3(
|
|
|
8205
8459
|
});
|
|
8206
8460
|
throw new ForbiddenError2({ message: "Authentication required" });
|
|
8207
8461
|
}
|
|
8208
|
-
const { userId } = auth;
|
|
8209
|
-
|
|
8210
|
-
if (!allowed) {
|
|
8462
|
+
const { userId, role: userRole } = auth;
|
|
8463
|
+
if (!userRole || !roleNames.includes(userRole)) {
|
|
8211
8464
|
authLogger4.middleware.warn("Role check failed", {
|
|
8212
8465
|
userId,
|
|
8466
|
+
userRole,
|
|
8213
8467
|
requiredRoles: roleNames,
|
|
8214
8468
|
path: c.req.path
|
|
8215
8469
|
});
|
|
@@ -8217,12 +8471,65 @@ var requireRole = defineMiddleware3(
|
|
|
8217
8471
|
}
|
|
8218
8472
|
authLogger4.middleware.debug("Role check passed", {
|
|
8219
8473
|
userId,
|
|
8474
|
+
userRole,
|
|
8220
8475
|
roles: roleNames
|
|
8221
8476
|
});
|
|
8222
8477
|
await next();
|
|
8223
8478
|
}
|
|
8224
8479
|
);
|
|
8225
8480
|
|
|
8481
|
+
// src/server/middleware/role-guard.ts
|
|
8482
|
+
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
8483
|
+
import { getAuth as getAuth4, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
8484
|
+
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
8485
|
+
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
8486
|
+
var roleGuard = defineMiddleware4(
|
|
8487
|
+
"roleGuard",
|
|
8488
|
+
(options) => async (c, next) => {
|
|
8489
|
+
const { allow, deny } = options;
|
|
8490
|
+
if (!allow && !deny) {
|
|
8491
|
+
throw new Error("roleGuard requires at least one of: allow, deny");
|
|
8492
|
+
}
|
|
8493
|
+
const auth = getAuth4(c);
|
|
8494
|
+
if (!auth) {
|
|
8495
|
+
authLogger5.middleware.warn("Role guard failed: not authenticated", {
|
|
8496
|
+
path: c.req.path
|
|
8497
|
+
});
|
|
8498
|
+
throw new ForbiddenError3({ message: "Authentication required" });
|
|
8499
|
+
}
|
|
8500
|
+
const { userId, role: userRole } = auth;
|
|
8501
|
+
if (deny && deny.length > 0) {
|
|
8502
|
+
if (userRole && deny.includes(userRole)) {
|
|
8503
|
+
authLogger5.middleware.warn("Role guard denied", {
|
|
8504
|
+
userId,
|
|
8505
|
+
userRole,
|
|
8506
|
+
deniedRoles: deny,
|
|
8507
|
+
path: c.req.path
|
|
8508
|
+
});
|
|
8509
|
+
throw new InsufficientRoleError2({ requiredRoles: allow || [] });
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
if (allow && allow.length > 0) {
|
|
8513
|
+
if (!userRole || !allow.includes(userRole)) {
|
|
8514
|
+
authLogger5.middleware.warn("Role guard failed: role not allowed", {
|
|
8515
|
+
userId,
|
|
8516
|
+
userRole,
|
|
8517
|
+
allowedRoles: allow,
|
|
8518
|
+
path: c.req.path
|
|
8519
|
+
});
|
|
8520
|
+
throw new InsufficientRoleError2({ requiredRoles: allow });
|
|
8521
|
+
}
|
|
8522
|
+
}
|
|
8523
|
+
authLogger5.middleware.debug("Role guard passed", {
|
|
8524
|
+
userId,
|
|
8525
|
+
userRole,
|
|
8526
|
+
allow,
|
|
8527
|
+
deny
|
|
8528
|
+
});
|
|
8529
|
+
await next();
|
|
8530
|
+
}
|
|
8531
|
+
);
|
|
8532
|
+
|
|
8226
8533
|
// src/server/routes/invitations/index.ts
|
|
8227
8534
|
init_types();
|
|
8228
8535
|
init_esm();
|
|
@@ -8416,21 +8723,301 @@ var invitationRouter = defineRouter2({
|
|
|
8416
8723
|
});
|
|
8417
8724
|
|
|
8418
8725
|
// src/server/routes/users/index.ts
|
|
8726
|
+
init_esm();
|
|
8419
8727
|
import { defineRouter as defineRouter3, route as route3 } from "@spfn/core/route";
|
|
8420
8728
|
var getUserProfile = route3.get("/_auth/users/profile").handler(async (c) => {
|
|
8421
8729
|
const { userId } = getAuth(c);
|
|
8422
8730
|
return await getUserProfileService(userId);
|
|
8423
8731
|
});
|
|
8732
|
+
var updateUserProfile = route3.patch("/_auth/users/profile").input({
|
|
8733
|
+
body: Type.Object({
|
|
8734
|
+
displayName: Type.Optional(Type.String({ description: "Display name shown in UI" })),
|
|
8735
|
+
firstName: Type.Optional(Type.String({ description: "First name" })),
|
|
8736
|
+
lastName: Type.Optional(Type.String({ description: "Last name" })),
|
|
8737
|
+
avatarUrl: Type.Optional(Type.String({ description: "Avatar/profile picture URL" })),
|
|
8738
|
+
bio: Type.Optional(Type.String({ description: "Short bio/description" })),
|
|
8739
|
+
locale: Type.Optional(Type.String({ description: "Locale/language preference (e.g., en, ko)" })),
|
|
8740
|
+
timezone: Type.Optional(Type.String({ description: "Timezone (e.g., Asia/Seoul)" })),
|
|
8741
|
+
dateOfBirth: Type.Optional(Type.String({ description: "Date of birth (YYYY-MM-DD)" })),
|
|
8742
|
+
gender: Type.Optional(Type.String({ description: "Gender" })),
|
|
8743
|
+
website: Type.Optional(Type.String({ description: "Personal or professional website" })),
|
|
8744
|
+
location: Type.Optional(Type.String({ description: "Location (city, country, etc.)" })),
|
|
8745
|
+
company: Type.Optional(Type.String({ description: "Company name" })),
|
|
8746
|
+
jobTitle: Type.Optional(Type.String({ description: "Job title" })),
|
|
8747
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Additional metadata" }))
|
|
8748
|
+
})
|
|
8749
|
+
}).handler(async (c) => {
|
|
8750
|
+
const { userId } = getAuth(c);
|
|
8751
|
+
const { body } = await c.data();
|
|
8752
|
+
return await updateUserProfileService(userId, body);
|
|
8753
|
+
});
|
|
8754
|
+
var checkUsername = route3.get("/_auth/users/username/check").input({
|
|
8755
|
+
query: Type.Object({
|
|
8756
|
+
username: Type.String({ minLength: 1 })
|
|
8757
|
+
})
|
|
8758
|
+
}).handler(async (c) => {
|
|
8759
|
+
const { query } = await c.data();
|
|
8760
|
+
return { available: await checkUsernameAvailableService(query.username) };
|
|
8761
|
+
});
|
|
8762
|
+
var updateUsername = route3.patch("/_auth/users/username").input({
|
|
8763
|
+
body: Type.Object({
|
|
8764
|
+
username: Type.Union([
|
|
8765
|
+
Type.String({ minLength: 1 }),
|
|
8766
|
+
Type.Null()
|
|
8767
|
+
], { description: "New username or null to clear" })
|
|
8768
|
+
})
|
|
8769
|
+
}).handler(async (c) => {
|
|
8770
|
+
const { userId } = getAuth(c);
|
|
8771
|
+
const { body } = await c.data();
|
|
8772
|
+
return await updateUsernameService(userId, body.username);
|
|
8773
|
+
});
|
|
8424
8774
|
var userRouter = defineRouter3({
|
|
8425
|
-
getUserProfile
|
|
8775
|
+
getUserProfile,
|
|
8776
|
+
updateUserProfile,
|
|
8777
|
+
checkUsername,
|
|
8778
|
+
updateUsername
|
|
8779
|
+
});
|
|
8780
|
+
|
|
8781
|
+
// src/server/routes/oauth/index.ts
|
|
8782
|
+
init_esm();
|
|
8783
|
+
init_types();
|
|
8784
|
+
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
8785
|
+
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
8786
|
+
var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
8787
|
+
query: Type.Object({
|
|
8788
|
+
state: Type.String({
|
|
8789
|
+
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
8790
|
+
})
|
|
8791
|
+
})
|
|
8792
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8793
|
+
const { query } = await c.data();
|
|
8794
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8795
|
+
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
8796
|
+
}
|
|
8797
|
+
const authUrl = getGoogleAuthUrl(query.state);
|
|
8798
|
+
return c.redirect(authUrl);
|
|
8799
|
+
});
|
|
8800
|
+
var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
8801
|
+
query: Type.Object({
|
|
8802
|
+
code: Type.Optional(Type.String({
|
|
8803
|
+
description: "Authorization code from Google"
|
|
8804
|
+
})),
|
|
8805
|
+
state: Type.Optional(Type.String({
|
|
8806
|
+
description: "OAuth state parameter"
|
|
8807
|
+
})),
|
|
8808
|
+
error: Type.Optional(Type.String({
|
|
8809
|
+
description: "Error code from Google"
|
|
8810
|
+
})),
|
|
8811
|
+
error_description: Type.Optional(Type.String({
|
|
8812
|
+
description: "Error description from Google"
|
|
8813
|
+
}))
|
|
8814
|
+
})
|
|
8815
|
+
}).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
8816
|
+
const { query } = await c.data();
|
|
8817
|
+
if (query.error) {
|
|
8818
|
+
const errorMessage = query.error_description || query.error;
|
|
8819
|
+
return c.redirect(buildOAuthErrorUrl(errorMessage));
|
|
8820
|
+
}
|
|
8821
|
+
if (!query.code || !query.state) {
|
|
8822
|
+
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
8823
|
+
}
|
|
8824
|
+
try {
|
|
8825
|
+
const result = await oauthCallbackService({
|
|
8826
|
+
provider: "google",
|
|
8827
|
+
code: query.code,
|
|
8828
|
+
state: query.state
|
|
8829
|
+
});
|
|
8830
|
+
return c.redirect(result.redirectUrl);
|
|
8831
|
+
} catch (err) {
|
|
8832
|
+
const message = err instanceof Error ? err.message : "OAuth callback failed";
|
|
8833
|
+
return c.redirect(buildOAuthErrorUrl(message));
|
|
8834
|
+
}
|
|
8835
|
+
});
|
|
8836
|
+
var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
8837
|
+
body: Type.Object({
|
|
8838
|
+
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
8839
|
+
description: "OAuth provider (google, github, kakao, naver)"
|
|
8840
|
+
}),
|
|
8841
|
+
returnUrl: Type.String({
|
|
8842
|
+
description: "URL to redirect after OAuth success"
|
|
8843
|
+
}),
|
|
8844
|
+
publicKey: Type.String({
|
|
8845
|
+
description: "Client public key (Base64 DER)"
|
|
8846
|
+
}),
|
|
8847
|
+
keyId: Type.String({
|
|
8848
|
+
description: "Key identifier (UUID)"
|
|
8849
|
+
}),
|
|
8850
|
+
fingerprint: Type.String({
|
|
8851
|
+
description: "Key fingerprint (SHA-256 hex)"
|
|
8852
|
+
}),
|
|
8853
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
|
|
8854
|
+
description: "Key algorithm (ES256 or RS256)"
|
|
8855
|
+
})
|
|
8856
|
+
})
|
|
8857
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8858
|
+
const { body } = await c.data();
|
|
8859
|
+
const result = await oauthStartService(body);
|
|
8860
|
+
return result;
|
|
8861
|
+
});
|
|
8862
|
+
var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
8863
|
+
return {
|
|
8864
|
+
providers: getEnabledOAuthProviders()
|
|
8865
|
+
};
|
|
8866
|
+
});
|
|
8867
|
+
var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
8868
|
+
body: Type.Object({
|
|
8869
|
+
returnUrl: Type.Optional(Type.String({
|
|
8870
|
+
description: "URL to redirect after OAuth success"
|
|
8871
|
+
})),
|
|
8872
|
+
state: Type.Optional(Type.String({
|
|
8873
|
+
description: "Encrypted OAuth state (injected by interceptor)"
|
|
8874
|
+
}))
|
|
8875
|
+
})
|
|
8876
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8877
|
+
const { body } = await c.data();
|
|
8878
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8879
|
+
throw new Error("Google OAuth is not configured");
|
|
8880
|
+
}
|
|
8881
|
+
if (!body.state) {
|
|
8882
|
+
throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
|
|
8883
|
+
}
|
|
8884
|
+
return { authUrl: getGoogleAuthUrl(body.state) };
|
|
8885
|
+
});
|
|
8886
|
+
var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
|
|
8887
|
+
body: Type.Object({
|
|
8888
|
+
userId: Type.String({ description: "User ID from OAuth callback" }),
|
|
8889
|
+
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
8890
|
+
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
8891
|
+
})
|
|
8892
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8893
|
+
const { body } = await c.data();
|
|
8894
|
+
return {
|
|
8895
|
+
success: true,
|
|
8896
|
+
userId: body.userId,
|
|
8897
|
+
keyId: body.keyId,
|
|
8898
|
+
returnUrl: body.returnUrl || "/"
|
|
8899
|
+
};
|
|
8900
|
+
});
|
|
8901
|
+
var oauthRouter = defineRouter4({
|
|
8902
|
+
oauthGoogleStart,
|
|
8903
|
+
oauthGoogleCallback,
|
|
8904
|
+
oauthStart,
|
|
8905
|
+
oauthProviders,
|
|
8906
|
+
getGoogleOAuthUrl,
|
|
8907
|
+
oauthFinalize
|
|
8908
|
+
});
|
|
8909
|
+
|
|
8910
|
+
// src/server/routes/admin/index.ts
|
|
8911
|
+
init_esm();
|
|
8912
|
+
import { route as route5 } from "@spfn/core/route";
|
|
8913
|
+
var listRoles = route5.get("/_auth/admin/roles").input({
|
|
8914
|
+
query: Type.Object({
|
|
8915
|
+
includeInactive: Type.Optional(Type.Boolean({
|
|
8916
|
+
description: "Include inactive roles (default: false)"
|
|
8917
|
+
}))
|
|
8918
|
+
})
|
|
8919
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8920
|
+
const { query } = await c.data();
|
|
8921
|
+
const roles2 = await getAllRoles(query.includeInactive ?? false);
|
|
8922
|
+
return { roles: roles2 };
|
|
8923
|
+
});
|
|
8924
|
+
var createAdminRole = route5.post("/_auth/admin/roles").input({
|
|
8925
|
+
body: Type.Object({
|
|
8926
|
+
name: Type.String({ description: "Unique role name (slug)" }),
|
|
8927
|
+
displayName: Type.String({ description: "Human-readable role name" }),
|
|
8928
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
8929
|
+
priority: Type.Optional(Type.Number({ description: "Role priority (default: 10)" })),
|
|
8930
|
+
permissionIds: Type.Optional(Type.Array(
|
|
8931
|
+
Type.Number({ description: "Permission ID" }),
|
|
8932
|
+
{ description: "Permission IDs to assign" }
|
|
8933
|
+
))
|
|
8934
|
+
})
|
|
8935
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8936
|
+
const { body } = await c.data();
|
|
8937
|
+
const role = await createRole({
|
|
8938
|
+
name: body.name,
|
|
8939
|
+
displayName: body.displayName,
|
|
8940
|
+
description: body.description,
|
|
8941
|
+
priority: body.priority,
|
|
8942
|
+
permissionIds: body.permissionIds
|
|
8943
|
+
});
|
|
8944
|
+
return { role };
|
|
8945
|
+
});
|
|
8946
|
+
var updateAdminRole = route5.patch("/_auth/admin/roles/:id").input({
|
|
8947
|
+
params: Type.Object({
|
|
8948
|
+
id: Type.Number({ description: "Role ID" })
|
|
8949
|
+
}),
|
|
8950
|
+
body: Type.Object({
|
|
8951
|
+
displayName: Type.Optional(Type.String({ description: "Human-readable role name" })),
|
|
8952
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
8953
|
+
priority: Type.Optional(Type.Number({ description: "Role priority" })),
|
|
8954
|
+
isActive: Type.Optional(Type.Boolean({ description: "Active status" }))
|
|
8955
|
+
})
|
|
8956
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8957
|
+
const { params, body } = await c.data();
|
|
8958
|
+
const role = await updateRole(params.id, body);
|
|
8959
|
+
return { role };
|
|
8960
|
+
});
|
|
8961
|
+
var deleteAdminRole = route5.delete("/_auth/admin/roles/:id").input({
|
|
8962
|
+
params: Type.Object({
|
|
8963
|
+
id: Type.Number({ description: "Role ID" })
|
|
8964
|
+
})
|
|
8965
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8966
|
+
const { params } = await c.data();
|
|
8967
|
+
await deleteRole(params.id);
|
|
8968
|
+
return c.noContent();
|
|
8969
|
+
});
|
|
8970
|
+
var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
8971
|
+
params: Type.Object({
|
|
8972
|
+
userId: Type.Number({ description: "User ID" })
|
|
8973
|
+
}),
|
|
8974
|
+
body: Type.Object({
|
|
8975
|
+
roleId: Type.Number({ description: "New role ID to assign" })
|
|
8976
|
+
})
|
|
8977
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8978
|
+
const { params, body } = await c.data();
|
|
8979
|
+
await updateUserService(params.userId, { roleId: body.roleId });
|
|
8980
|
+
return { userId: params.userId, roleId: body.roleId };
|
|
8426
8981
|
});
|
|
8427
8982
|
|
|
8428
8983
|
// src/server/routes/index.ts
|
|
8429
|
-
var mainAuthRouter =
|
|
8430
|
-
//
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8984
|
+
var mainAuthRouter = defineRouter5({
|
|
8985
|
+
// Auth routes
|
|
8986
|
+
checkAccountExists,
|
|
8987
|
+
sendVerificationCode,
|
|
8988
|
+
verifyCode,
|
|
8989
|
+
register,
|
|
8990
|
+
login,
|
|
8991
|
+
logout,
|
|
8992
|
+
rotateKey,
|
|
8993
|
+
changePassword,
|
|
8994
|
+
getAuthSession,
|
|
8995
|
+
// OAuth routes
|
|
8996
|
+
oauthGoogleStart,
|
|
8997
|
+
oauthGoogleCallback,
|
|
8998
|
+
oauthStart,
|
|
8999
|
+
oauthProviders,
|
|
9000
|
+
getGoogleOAuthUrl,
|
|
9001
|
+
oauthFinalize,
|
|
9002
|
+
// Invitation routes
|
|
9003
|
+
getInvitation,
|
|
9004
|
+
acceptInvitation: acceptInvitation2,
|
|
9005
|
+
createInvitation: createInvitation2,
|
|
9006
|
+
listInvitations: listInvitations2,
|
|
9007
|
+
cancelInvitation: cancelInvitation2,
|
|
9008
|
+
resendInvitation: resendInvitation2,
|
|
9009
|
+
deleteInvitation: deleteInvitation2,
|
|
9010
|
+
// User routes
|
|
9011
|
+
getUserProfile,
|
|
9012
|
+
updateUserProfile,
|
|
9013
|
+
checkUsername,
|
|
9014
|
+
updateUsername,
|
|
9015
|
+
// Admin routes (superadmin only)
|
|
9016
|
+
listRoles,
|
|
9017
|
+
createAdminRole,
|
|
9018
|
+
updateAdminRole,
|
|
9019
|
+
deleteAdminRole,
|
|
9020
|
+
updateUserRole
|
|
8434
9021
|
});
|
|
8435
9022
|
|
|
8436
9023
|
// src/server.ts
|
|
@@ -8540,11 +9127,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
8540
9127
|
}
|
|
8541
9128
|
|
|
8542
9129
|
// src/server/lib/session.ts
|
|
8543
|
-
import * as
|
|
8544
|
-
import { env as
|
|
9130
|
+
import * as jose2 from "jose";
|
|
9131
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
8545
9132
|
import { env as coreEnv } from "@spfn/core/config";
|
|
8546
9133
|
async function getSessionSecretKey() {
|
|
8547
|
-
const secret =
|
|
9134
|
+
const secret = env8.SPFN_AUTH_SESSION_SECRET;
|
|
8548
9135
|
const encoder = new TextEncoder();
|
|
8549
9136
|
const data = encoder.encode(secret);
|
|
8550
9137
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -8552,24 +9139,24 @@ async function getSessionSecretKey() {
|
|
|
8552
9139
|
}
|
|
8553
9140
|
async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
|
|
8554
9141
|
const secret = await getSessionSecretKey();
|
|
8555
|
-
return await new
|
|
9142
|
+
return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
|
|
8556
9143
|
}
|
|
8557
9144
|
async function unsealSession(jwt4) {
|
|
8558
9145
|
try {
|
|
8559
9146
|
const secret = await getSessionSecretKey();
|
|
8560
|
-
const { payload } = await
|
|
9147
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
|
|
8561
9148
|
issuer: "spfn-auth",
|
|
8562
9149
|
audience: "spfn-client"
|
|
8563
9150
|
});
|
|
8564
9151
|
return payload.data;
|
|
8565
9152
|
} catch (err) {
|
|
8566
|
-
if (err instanceof
|
|
9153
|
+
if (err instanceof jose2.errors.JWTExpired) {
|
|
8567
9154
|
throw new Error("Session expired");
|
|
8568
9155
|
}
|
|
8569
|
-
if (err instanceof
|
|
9156
|
+
if (err instanceof jose2.errors.JWEDecryptionFailed) {
|
|
8570
9157
|
throw new Error("Invalid session");
|
|
8571
9158
|
}
|
|
8572
|
-
if (err instanceof
|
|
9159
|
+
if (err instanceof jose2.errors.JWTClaimValidationFailed) {
|
|
8573
9160
|
throw new Error("Session validation failed");
|
|
8574
9161
|
}
|
|
8575
9162
|
throw new Error("Failed to unseal session");
|
|
@@ -8578,7 +9165,7 @@ async function unsealSession(jwt4) {
|
|
|
8578
9165
|
async function getSessionInfo(jwt4) {
|
|
8579
9166
|
const secret = await getSessionSecretKey();
|
|
8580
9167
|
try {
|
|
8581
|
-
const { payload } = await
|
|
9168
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret);
|
|
8582
9169
|
return {
|
|
8583
9170
|
issuedAt: new Date(payload.iat * 1e3),
|
|
8584
9171
|
expiresAt: new Date(payload.exp * 1e3),
|
|
@@ -8602,14 +9189,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
8602
9189
|
}
|
|
8603
9190
|
|
|
8604
9191
|
// src/server/setup.ts
|
|
8605
|
-
import { env as
|
|
9192
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
8606
9193
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
8607
9194
|
init_repositories();
|
|
8608
9195
|
function parseAdminAccounts() {
|
|
8609
9196
|
const accounts = [];
|
|
8610
|
-
if (
|
|
9197
|
+
if (env9.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
8611
9198
|
try {
|
|
8612
|
-
const accountsJson =
|
|
9199
|
+
const accountsJson = env9.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
8613
9200
|
const parsed = JSON.parse(accountsJson);
|
|
8614
9201
|
if (!Array.isArray(parsed)) {
|
|
8615
9202
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -8636,11 +9223,11 @@ function parseAdminAccounts() {
|
|
|
8636
9223
|
return accounts;
|
|
8637
9224
|
}
|
|
8638
9225
|
}
|
|
8639
|
-
const adminEmails =
|
|
9226
|
+
const adminEmails = env9.SPFN_AUTH_ADMIN_EMAILS;
|
|
8640
9227
|
if (adminEmails) {
|
|
8641
9228
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
8642
|
-
const passwords = (
|
|
8643
|
-
const roles2 = (
|
|
9229
|
+
const passwords = (env9.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
9230
|
+
const roles2 = (env9.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
8644
9231
|
if (passwords.length !== emails.length) {
|
|
8645
9232
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
8646
9233
|
return accounts;
|
|
@@ -8662,8 +9249,8 @@ function parseAdminAccounts() {
|
|
|
8662
9249
|
}
|
|
8663
9250
|
return accounts;
|
|
8664
9251
|
}
|
|
8665
|
-
const adminEmail =
|
|
8666
|
-
const adminPassword =
|
|
9252
|
+
const adminEmail = env9.SPFN_AUTH_ADMIN_EMAIL;
|
|
9253
|
+
const adminPassword = env9.SPFN_AUTH_ADMIN_PASSWORD;
|
|
8667
9254
|
if (adminEmail && adminPassword) {
|
|
8668
9255
|
accounts.push({
|
|
8669
9256
|
email: adminEmail,
|
|
@@ -8741,6 +9328,7 @@ function createAuthLifecycle(options = {}) {
|
|
|
8741
9328
|
};
|
|
8742
9329
|
}
|
|
8743
9330
|
export {
|
|
9331
|
+
AuthProviderSchema,
|
|
8744
9332
|
COOKIE_NAMES,
|
|
8745
9333
|
EmailSchema,
|
|
8746
9334
|
INVITATION_STATUSES,
|
|
@@ -8753,6 +9341,7 @@ export {
|
|
|
8753
9341
|
RolePermissionsRepository,
|
|
8754
9342
|
RolesRepository,
|
|
8755
9343
|
SOCIAL_PROVIDERS,
|
|
9344
|
+
SocialAccountsRepository,
|
|
8756
9345
|
TargetTypeSchema,
|
|
8757
9346
|
USER_STATUSES,
|
|
8758
9347
|
UserPermissionsRepository,
|
|
@@ -8765,19 +9354,25 @@ export {
|
|
|
8765
9354
|
acceptInvitation,
|
|
8766
9355
|
addPermissionToRole,
|
|
8767
9356
|
authLogger,
|
|
9357
|
+
authLoginEvent,
|
|
9358
|
+
authRegisterEvent,
|
|
8768
9359
|
mainAuthRouter as authRouter,
|
|
8769
9360
|
authSchema,
|
|
8770
9361
|
authenticate,
|
|
9362
|
+
buildOAuthErrorUrl,
|
|
8771
9363
|
cancelInvitation,
|
|
8772
9364
|
changePasswordService,
|
|
8773
9365
|
checkAccountExistsService,
|
|
9366
|
+
checkUsernameAvailableService,
|
|
8774
9367
|
configureAuth,
|
|
8775
9368
|
createAuthLifecycle,
|
|
8776
9369
|
createInvitation,
|
|
9370
|
+
createOAuthState,
|
|
8777
9371
|
createRole,
|
|
8778
9372
|
decodeToken,
|
|
8779
9373
|
deleteInvitation,
|
|
8780
9374
|
deleteRole,
|
|
9375
|
+
exchangeCodeForTokens,
|
|
8781
9376
|
expireOldInvitations,
|
|
8782
9377
|
generateClientToken,
|
|
8783
9378
|
generateKeyPair,
|
|
@@ -8788,12 +9383,17 @@ export {
|
|
|
8788
9383
|
getAuth,
|
|
8789
9384
|
getAuthConfig,
|
|
8790
9385
|
getAuthSessionService,
|
|
9386
|
+
getEnabledOAuthProviders,
|
|
9387
|
+
getGoogleAccessToken,
|
|
9388
|
+
getGoogleAuthUrl,
|
|
9389
|
+
getGoogleOAuthConfig,
|
|
9390
|
+
getGoogleUserInfo,
|
|
8791
9391
|
getInvitationByToken,
|
|
8792
|
-
getInvitationTemplate,
|
|
8793
9392
|
getInvitationWithDetails,
|
|
8794
9393
|
getKeyId,
|
|
8795
9394
|
getKeySize,
|
|
8796
|
-
|
|
9395
|
+
getOptionalAuth,
|
|
9396
|
+
getRole,
|
|
8797
9397
|
getRoleByName,
|
|
8798
9398
|
getRolePermissions,
|
|
8799
9399
|
getSessionInfo,
|
|
@@ -8805,8 +9405,7 @@ export {
|
|
|
8805
9405
|
getUserId,
|
|
8806
9406
|
getUserPermissions,
|
|
8807
9407
|
getUserProfileService,
|
|
8808
|
-
|
|
8809
|
-
getWelcomeTemplate,
|
|
9408
|
+
getUserRole,
|
|
8810
9409
|
hasAllPermissions,
|
|
8811
9410
|
hasAnyPermission,
|
|
8812
9411
|
hasAnyRole,
|
|
@@ -8815,17 +9414,20 @@ export {
|
|
|
8815
9414
|
hashPassword,
|
|
8816
9415
|
initializeAuth,
|
|
8817
9416
|
invitationsRepository,
|
|
9417
|
+
isGoogleOAuthEnabled,
|
|
9418
|
+
isOAuthProviderEnabled,
|
|
8818
9419
|
keysRepository,
|
|
8819
9420
|
listInvitations,
|
|
8820
9421
|
loginService,
|
|
8821
9422
|
logoutService,
|
|
9423
|
+
oauthCallbackService,
|
|
9424
|
+
oauthStartService,
|
|
9425
|
+
optionalAuth,
|
|
8822
9426
|
parseDuration,
|
|
8823
9427
|
permissions,
|
|
8824
9428
|
permissionsRepository,
|
|
8825
|
-
|
|
8826
|
-
registerEmailTemplates,
|
|
9429
|
+
refreshAccessToken,
|
|
8827
9430
|
registerPublicKeyService,
|
|
8828
|
-
registerSMSProvider,
|
|
8829
9431
|
registerService,
|
|
8830
9432
|
removePermissionFromRole,
|
|
8831
9433
|
requireAnyPermission,
|
|
@@ -8833,22 +9435,24 @@ export {
|
|
|
8833
9435
|
requireRole,
|
|
8834
9436
|
resendInvitation,
|
|
8835
9437
|
revokeKeyService,
|
|
9438
|
+
roleGuard,
|
|
8836
9439
|
rolePermissions,
|
|
8837
9440
|
rolePermissionsRepository,
|
|
8838
9441
|
roles,
|
|
8839
9442
|
rolesRepository,
|
|
8840
9443
|
rotateKeyService,
|
|
8841
9444
|
sealSession,
|
|
8842
|
-
sendEmail,
|
|
8843
|
-
sendSMS,
|
|
8844
9445
|
sendVerificationCodeService,
|
|
8845
9446
|
setRolePermissions,
|
|
8846
9447
|
shouldRefreshSession,
|
|
8847
9448
|
shouldRotateKey,
|
|
9449
|
+
socialAccountsRepository,
|
|
8848
9450
|
unsealSession,
|
|
8849
9451
|
updateLastLoginService,
|
|
8850
9452
|
updateRole,
|
|
9453
|
+
updateUserProfileService,
|
|
8851
9454
|
updateUserService,
|
|
9455
|
+
updateUsernameService,
|
|
8852
9456
|
userInvitations,
|
|
8853
9457
|
userPermissions,
|
|
8854
9458
|
userPermissionsRepository,
|
|
@@ -8865,6 +9469,7 @@ export {
|
|
|
8865
9469
|
verifyClientToken,
|
|
8866
9470
|
verifyCodeService,
|
|
8867
9471
|
verifyKeyFingerprint,
|
|
9472
|
+
verifyOAuthState,
|
|
8868
9473
|
verifyPassword,
|
|
8869
9474
|
verifyToken
|
|
8870
9475
|
};
|