@spfn/auth 0.2.0-beta.4 → 0.2.0-beta.41
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 +713 -180
- package/dist/{dto-Bb2qFUO6.d.ts → authenticate-2953PCm8.d.ts} +420 -161
- package/dist/config.d.ts +156 -44
- package/dist/config.js +90 -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 +283 -113
- package/dist/index.js +58 -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 +146 -21
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +564 -443
- package/dist/server.js +1030 -492
- 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 +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,
|
|
@@ -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,
|
|
@@ -6087,6 +6119,13 @@ var init_user_profiles_repository = __esm({
|
|
|
6087
6119
|
const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id, id11)).limit(1);
|
|
6088
6120
|
return result[0] ?? null;
|
|
6089
6121
|
}
|
|
6122
|
+
/**
|
|
6123
|
+
* User ID로 locale만 조회 (경량)
|
|
6124
|
+
*/
|
|
6125
|
+
async findLocaleByUserId(userId) {
|
|
6126
|
+
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1);
|
|
6127
|
+
return result[0]?.locale || "en";
|
|
6128
|
+
}
|
|
6090
6129
|
/**
|
|
6091
6130
|
* User ID로 프로필 조회
|
|
6092
6131
|
*/
|
|
@@ -6413,6 +6452,96 @@ var init_invitations_repository = __esm({
|
|
|
6413
6452
|
}
|
|
6414
6453
|
});
|
|
6415
6454
|
|
|
6455
|
+
// src/server/repositories/social-accounts.repository.ts
|
|
6456
|
+
import { eq as eq10, and as and7 } from "drizzle-orm";
|
|
6457
|
+
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
6458
|
+
var SocialAccountsRepository, socialAccountsRepository;
|
|
6459
|
+
var init_social_accounts_repository = __esm({
|
|
6460
|
+
"src/server/repositories/social-accounts.repository.ts"() {
|
|
6461
|
+
"use strict";
|
|
6462
|
+
init_entities();
|
|
6463
|
+
SocialAccountsRepository = class extends BaseRepository10 {
|
|
6464
|
+
/**
|
|
6465
|
+
* provider와 providerUserId로 소셜 계정 조회
|
|
6466
|
+
* Read replica 사용
|
|
6467
|
+
*/
|
|
6468
|
+
async findByProviderAndProviderId(provider, providerUserId) {
|
|
6469
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6470
|
+
and7(
|
|
6471
|
+
eq10(userSocialAccounts.provider, provider),
|
|
6472
|
+
eq10(userSocialAccounts.providerUserId, providerUserId)
|
|
6473
|
+
)
|
|
6474
|
+
).limit(1);
|
|
6475
|
+
return result[0] ?? null;
|
|
6476
|
+
}
|
|
6477
|
+
/**
|
|
6478
|
+
* userId로 모든 소셜 계정 조회
|
|
6479
|
+
* Read replica 사용
|
|
6480
|
+
*/
|
|
6481
|
+
async findByUserId(userId) {
|
|
6482
|
+
return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
|
|
6483
|
+
}
|
|
6484
|
+
/**
|
|
6485
|
+
* userId와 provider로 소셜 계정 조회
|
|
6486
|
+
* Read replica 사용
|
|
6487
|
+
*/
|
|
6488
|
+
async findByUserIdAndProvider(userId, provider) {
|
|
6489
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6490
|
+
and7(
|
|
6491
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6492
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6493
|
+
)
|
|
6494
|
+
).limit(1);
|
|
6495
|
+
return result[0] ?? null;
|
|
6496
|
+
}
|
|
6497
|
+
/**
|
|
6498
|
+
* 소셜 계정 생성
|
|
6499
|
+
* Write primary 사용
|
|
6500
|
+
*/
|
|
6501
|
+
async create(data) {
|
|
6502
|
+
return await this._create(userSocialAccounts, {
|
|
6503
|
+
...data,
|
|
6504
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
6505
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6506
|
+
});
|
|
6507
|
+
}
|
|
6508
|
+
/**
|
|
6509
|
+
* 토큰 정보 업데이트
|
|
6510
|
+
* Write primary 사용
|
|
6511
|
+
*/
|
|
6512
|
+
async updateTokens(id11, data) {
|
|
6513
|
+
const result = await this.db.update(userSocialAccounts).set({
|
|
6514
|
+
...data,
|
|
6515
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6516
|
+
}).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6517
|
+
return result[0] ?? null;
|
|
6518
|
+
}
|
|
6519
|
+
/**
|
|
6520
|
+
* 소셜 계정 삭제
|
|
6521
|
+
* Write primary 사용
|
|
6522
|
+
*/
|
|
6523
|
+
async deleteById(id11) {
|
|
6524
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6525
|
+
return result[0] ?? null;
|
|
6526
|
+
}
|
|
6527
|
+
/**
|
|
6528
|
+
* userId와 provider로 소셜 계정 삭제
|
|
6529
|
+
* Write primary 사용
|
|
6530
|
+
*/
|
|
6531
|
+
async deleteByUserIdAndProvider(userId, provider) {
|
|
6532
|
+
const result = await this.db.delete(userSocialAccounts).where(
|
|
6533
|
+
and7(
|
|
6534
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6535
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6536
|
+
)
|
|
6537
|
+
).returning();
|
|
6538
|
+
return result[0] ?? null;
|
|
6539
|
+
}
|
|
6540
|
+
};
|
|
6541
|
+
socialAccountsRepository = new SocialAccountsRepository();
|
|
6542
|
+
}
|
|
6543
|
+
});
|
|
6544
|
+
|
|
6416
6545
|
// src/server/repositories/index.ts
|
|
6417
6546
|
var init_repositories = __esm({
|
|
6418
6547
|
"src/server/repositories/index.ts"() {
|
|
@@ -6426,6 +6555,7 @@ var init_repositories = __esm({
|
|
|
6426
6555
|
init_user_permissions_repository();
|
|
6427
6556
|
init_user_profiles_repository();
|
|
6428
6557
|
init_invitations_repository();
|
|
6558
|
+
init_social_accounts_repository();
|
|
6429
6559
|
}
|
|
6430
6560
|
});
|
|
6431
6561
|
|
|
@@ -6552,7 +6682,7 @@ var init_role_service = __esm({
|
|
|
6552
6682
|
import "@spfn/auth/config";
|
|
6553
6683
|
|
|
6554
6684
|
// src/server/routes/index.ts
|
|
6555
|
-
import { defineRouter as
|
|
6685
|
+
import { defineRouter as defineRouter5 } from "@spfn/core/route";
|
|
6556
6686
|
|
|
6557
6687
|
// src/server/routes/auth/index.ts
|
|
6558
6688
|
init_schema3();
|
|
@@ -6675,12 +6805,24 @@ function getAuth(c) {
|
|
|
6675
6805
|
}
|
|
6676
6806
|
return c.get("auth");
|
|
6677
6807
|
}
|
|
6808
|
+
function getOptionalAuth(c) {
|
|
6809
|
+
if ("raw" in c && c.raw) {
|
|
6810
|
+
return c.raw.get("auth");
|
|
6811
|
+
}
|
|
6812
|
+
return c.get("auth");
|
|
6813
|
+
}
|
|
6678
6814
|
function getUser(c) {
|
|
6679
6815
|
return getAuth(c).user;
|
|
6680
6816
|
}
|
|
6681
6817
|
function getUserId(c) {
|
|
6682
6818
|
return getAuth(c).userId;
|
|
6683
6819
|
}
|
|
6820
|
+
function getRole(c) {
|
|
6821
|
+
return getAuth(c).role;
|
|
6822
|
+
}
|
|
6823
|
+
function getLocale(c) {
|
|
6824
|
+
return getAuth(c).locale;
|
|
6825
|
+
}
|
|
6684
6826
|
function getKeyId(c) {
|
|
6685
6827
|
return getAuth(c).keyId;
|
|
6686
6828
|
}
|
|
@@ -6690,7 +6832,7 @@ init_types();
|
|
|
6690
6832
|
|
|
6691
6833
|
// src/server/services/auth.service.ts
|
|
6692
6834
|
init_repositories();
|
|
6693
|
-
import { ValidationError } from "@spfn/core/errors";
|
|
6835
|
+
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
6694
6836
|
import {
|
|
6695
6837
|
InvalidCredentialsError,
|
|
6696
6838
|
AccountDisabledError,
|
|
@@ -6701,9 +6843,10 @@ import {
|
|
|
6701
6843
|
} from "@spfn/auth/errors";
|
|
6702
6844
|
|
|
6703
6845
|
// src/server/services/verification.service.ts
|
|
6704
|
-
import { env as
|
|
6846
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
6705
6847
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
6706
6848
|
import jwt2 from "jsonwebtoken";
|
|
6849
|
+
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
6707
6850
|
|
|
6708
6851
|
// src/server/logger.ts
|
|
6709
6852
|
import { logger as rootLogger } from "@spfn/core/logger";
|
|
@@ -6713,7 +6856,8 @@ var authLogger = {
|
|
|
6713
6856
|
interceptor: {
|
|
6714
6857
|
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
6715
6858
|
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
6716
|
-
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation")
|
|
6859
|
+
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
6860
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
6717
6861
|
},
|
|
6718
6862
|
service: rootLogger.child("@spfn/auth:service"),
|
|
6719
6863
|
setup: rootLogger.child("@spfn/auth:setup"),
|
|
@@ -6723,410 +6867,6 @@ var authLogger = {
|
|
|
6723
6867
|
|
|
6724
6868
|
// src/server/services/verification.service.ts
|
|
6725
6869
|
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
6870
|
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
7131
6871
|
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
7132
6872
|
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
@@ -7170,7 +6910,7 @@ async function markCodeAsUsed(codeId) {
|
|
|
7170
6910
|
await verificationCodesRepository.markAsUsed(codeId);
|
|
7171
6911
|
}
|
|
7172
6912
|
function createVerificationToken(payload) {
|
|
7173
|
-
return jwt2.sign(payload,
|
|
6913
|
+
return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7174
6914
|
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
7175
6915
|
issuer: "spfn-auth",
|
|
7176
6916
|
audience: "spfn-client"
|
|
@@ -7178,7 +6918,7 @@ function createVerificationToken(payload) {
|
|
|
7178
6918
|
}
|
|
7179
6919
|
function validateVerificationToken(token) {
|
|
7180
6920
|
try {
|
|
7181
|
-
const decoded = jwt2.verify(token,
|
|
6921
|
+
const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7182
6922
|
issuer: "spfn-auth",
|
|
7183
6923
|
audience: "spfn-client"
|
|
7184
6924
|
});
|
|
@@ -7192,17 +6932,14 @@ function validateVerificationToken(token) {
|
|
|
7192
6932
|
}
|
|
7193
6933
|
}
|
|
7194
6934
|
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
6935
|
const result = await sendEmail({
|
|
7201
6936
|
to: email,
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
6937
|
+
template: "verification-code",
|
|
6938
|
+
data: {
|
|
6939
|
+
code,
|
|
6940
|
+
purpose,
|
|
6941
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
6942
|
+
}
|
|
7206
6943
|
});
|
|
7207
6944
|
if (!result.success) {
|
|
7208
6945
|
authLogger.email.error("Failed to send verification email", {
|
|
@@ -7213,11 +6950,13 @@ async function sendVerificationEmail(email, code, purpose) {
|
|
|
7213
6950
|
}
|
|
7214
6951
|
}
|
|
7215
6952
|
async function sendVerificationSMS(phone, code, purpose) {
|
|
7216
|
-
const message = `Your verification code is: ${code}`;
|
|
7217
6953
|
const result = await sendSMS({
|
|
7218
|
-
phone,
|
|
7219
|
-
|
|
7220
|
-
|
|
6954
|
+
to: phone,
|
|
6955
|
+
template: "verification-code",
|
|
6956
|
+
data: {
|
|
6957
|
+
code,
|
|
6958
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
6959
|
+
}
|
|
7221
6960
|
});
|
|
7222
6961
|
if (!result.success) {
|
|
7223
6962
|
authLogger.sms.error("Failed to send verification SMS", {
|
|
@@ -7318,6 +7057,9 @@ async function revokeKeyService(params) {
|
|
|
7318
7057
|
|
|
7319
7058
|
// src/server/services/user.service.ts
|
|
7320
7059
|
init_repositories();
|
|
7060
|
+
import { ValidationError } from "@spfn/core/errors";
|
|
7061
|
+
import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
7062
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
7321
7063
|
async function getUserByIdService(userId) {
|
|
7322
7064
|
return await usersRepository.findById(userId);
|
|
7323
7065
|
}
|
|
@@ -7333,6 +7075,84 @@ async function updateLastLoginService(userId) {
|
|
|
7333
7075
|
async function updateUserService(userId, updates) {
|
|
7334
7076
|
await usersRepository.updateById(userId, updates);
|
|
7335
7077
|
}
|
|
7078
|
+
function getReservedUsernames() {
|
|
7079
|
+
const raw = env4.SPFN_AUTH_RESERVED_USERNAMES ?? "";
|
|
7080
|
+
if (!raw) {
|
|
7081
|
+
return /* @__PURE__ */ new Set();
|
|
7082
|
+
}
|
|
7083
|
+
return new Set(
|
|
7084
|
+
raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
7085
|
+
);
|
|
7086
|
+
}
|
|
7087
|
+
function isReservedUsername(username) {
|
|
7088
|
+
return getReservedUsernames().has(username.toLowerCase());
|
|
7089
|
+
}
|
|
7090
|
+
function validateUsernameLength(username) {
|
|
7091
|
+
const min = env4.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
|
|
7092
|
+
const max = env4.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
|
|
7093
|
+
if (username.length < min) {
|
|
7094
|
+
throw new ValidationError({
|
|
7095
|
+
message: `Username must be at least ${min} characters`,
|
|
7096
|
+
details: { minLength: min, actual: username.length }
|
|
7097
|
+
});
|
|
7098
|
+
}
|
|
7099
|
+
if (username.length > max) {
|
|
7100
|
+
throw new ValidationError({
|
|
7101
|
+
message: `Username must be at most ${max} characters`,
|
|
7102
|
+
details: { maxLength: max, actual: username.length }
|
|
7103
|
+
});
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
async function checkUsernameAvailableService(username) {
|
|
7107
|
+
validateUsernameLength(username);
|
|
7108
|
+
if (isReservedUsername(username)) {
|
|
7109
|
+
return false;
|
|
7110
|
+
}
|
|
7111
|
+
const existing = await usersRepository.findByUsername(username);
|
|
7112
|
+
return !existing;
|
|
7113
|
+
}
|
|
7114
|
+
async function updateUsernameService(userId, username) {
|
|
7115
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7116
|
+
if (username !== null) {
|
|
7117
|
+
validateUsernameLength(username);
|
|
7118
|
+
if (isReservedUsername(username)) {
|
|
7119
|
+
throw new ReservedUsernameError({ username });
|
|
7120
|
+
}
|
|
7121
|
+
const existing = await usersRepository.findByUsername(username);
|
|
7122
|
+
if (existing && existing.id !== userIdNum) {
|
|
7123
|
+
throw new UsernameAlreadyTakenError({ username });
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
return await usersRepository.updateById(userIdNum, { username });
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
// src/server/events/index.ts
|
|
7130
|
+
init_esm();
|
|
7131
|
+
import { defineEvent } from "@spfn/core/event";
|
|
7132
|
+
var AuthProviderSchema = Type.Union([
|
|
7133
|
+
Type.Literal("email"),
|
|
7134
|
+
Type.Literal("phone"),
|
|
7135
|
+
Type.Literal("google")
|
|
7136
|
+
]);
|
|
7137
|
+
var authLoginEvent = defineEvent(
|
|
7138
|
+
"auth.login",
|
|
7139
|
+
Type.Object({
|
|
7140
|
+
userId: Type.String(),
|
|
7141
|
+
provider: AuthProviderSchema,
|
|
7142
|
+
email: Type.Optional(Type.String()),
|
|
7143
|
+
phone: Type.Optional(Type.String())
|
|
7144
|
+
})
|
|
7145
|
+
);
|
|
7146
|
+
var authRegisterEvent = defineEvent(
|
|
7147
|
+
"auth.register",
|
|
7148
|
+
Type.Object({
|
|
7149
|
+
userId: Type.String(),
|
|
7150
|
+
provider: AuthProviderSchema,
|
|
7151
|
+
email: Type.Optional(Type.String()),
|
|
7152
|
+
phone: Type.Optional(Type.String()),
|
|
7153
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
|
|
7154
|
+
})
|
|
7155
|
+
);
|
|
7336
7156
|
|
|
7337
7157
|
// src/server/services/auth.service.ts
|
|
7338
7158
|
async function checkAccountExistsService(params) {
|
|
@@ -7349,7 +7169,7 @@ async function checkAccountExistsService(params) {
|
|
|
7349
7169
|
identifierType = "phone";
|
|
7350
7170
|
user = await usersRepository.findByPhone(phone);
|
|
7351
7171
|
} else {
|
|
7352
|
-
throw new
|
|
7172
|
+
throw new ValidationError2({ message: "Either email or phone must be provided" });
|
|
7353
7173
|
}
|
|
7354
7174
|
return {
|
|
7355
7175
|
exists: !!user,
|
|
@@ -7358,7 +7178,7 @@ async function checkAccountExistsService(params) {
|
|
|
7358
7178
|
};
|
|
7359
7179
|
}
|
|
7360
7180
|
async function registerService(params) {
|
|
7361
|
-
const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm } = params;
|
|
7181
|
+
const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
7362
7182
|
const tokenPayload = validateVerificationToken(verificationToken);
|
|
7363
7183
|
if (!tokenPayload) {
|
|
7364
7184
|
throw new InvalidVerificationTokenError();
|
|
@@ -7400,17 +7220,25 @@ async function registerService(params) {
|
|
|
7400
7220
|
fingerprint,
|
|
7401
7221
|
algorithm
|
|
7402
7222
|
});
|
|
7403
|
-
|
|
7223
|
+
const result = {
|
|
7404
7224
|
userId: String(newUser.id),
|
|
7405
7225
|
email: newUser.email || void 0,
|
|
7406
7226
|
phone: newUser.phone || void 0
|
|
7407
7227
|
};
|
|
7228
|
+
await authRegisterEvent.emit({
|
|
7229
|
+
userId: result.userId,
|
|
7230
|
+
provider: email ? "email" : "phone",
|
|
7231
|
+
email: result.email,
|
|
7232
|
+
phone: result.phone,
|
|
7233
|
+
metadata
|
|
7234
|
+
});
|
|
7235
|
+
return result;
|
|
7408
7236
|
}
|
|
7409
7237
|
async function loginService(params) {
|
|
7410
7238
|
const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
|
|
7411
7239
|
const user = await usersRepository.findByEmailOrPhone(email, phone);
|
|
7412
7240
|
if (!email && !phone) {
|
|
7413
|
-
throw new
|
|
7241
|
+
throw new ValidationError2({ message: "Either email or phone must be provided" });
|
|
7414
7242
|
}
|
|
7415
7243
|
if (!user || !user.passwordHash) {
|
|
7416
7244
|
throw new InvalidCredentialsError();
|
|
@@ -7437,12 +7265,19 @@ async function loginService(params) {
|
|
|
7437
7265
|
algorithm
|
|
7438
7266
|
});
|
|
7439
7267
|
await updateLastLoginService(user.id);
|
|
7440
|
-
|
|
7268
|
+
const result = {
|
|
7441
7269
|
userId: String(user.id),
|
|
7442
7270
|
email: user.email || void 0,
|
|
7443
7271
|
phone: user.phone || void 0,
|
|
7444
7272
|
passwordChangeRequired: user.passwordChangeRequired
|
|
7445
7273
|
};
|
|
7274
|
+
await authLoginEvent.emit({
|
|
7275
|
+
userId: result.userId,
|
|
7276
|
+
provider: email ? "email" : "phone",
|
|
7277
|
+
email: result.email,
|
|
7278
|
+
phone: result.phone
|
|
7279
|
+
});
|
|
7280
|
+
return result;
|
|
7446
7281
|
}
|
|
7447
7282
|
async function logoutService(params) {
|
|
7448
7283
|
const { userId, keyId } = params;
|
|
@@ -7460,12 +7295,12 @@ async function changePasswordService(params) {
|
|
|
7460
7295
|
} else {
|
|
7461
7296
|
const user = await usersRepository.findById(userId);
|
|
7462
7297
|
if (!user) {
|
|
7463
|
-
throw new
|
|
7298
|
+
throw new ValidationError2({ message: "User not found" });
|
|
7464
7299
|
}
|
|
7465
7300
|
passwordHash = user.passwordHash;
|
|
7466
7301
|
}
|
|
7467
7302
|
if (!passwordHash) {
|
|
7468
|
-
throw new
|
|
7303
|
+
throw new ValidationError2({ message: "No password set for this account" });
|
|
7469
7304
|
}
|
|
7470
7305
|
const isValid = await verifyPassword(currentPassword, passwordHash);
|
|
7471
7306
|
if (!isValid) {
|
|
@@ -7480,12 +7315,14 @@ init_repositories();
|
|
|
7480
7315
|
init_rbac();
|
|
7481
7316
|
|
|
7482
7317
|
// src/server/lib/config.ts
|
|
7483
|
-
import { env as
|
|
7318
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7484
7319
|
var COOKIE_NAMES = {
|
|
7485
7320
|
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
7486
7321
|
SESSION: "spfn_session",
|
|
7487
7322
|
/** Current key ID (for key rotation) */
|
|
7488
|
-
SESSION_KEY_ID: "spfn_session_key_id"
|
|
7323
|
+
SESSION_KEY_ID: "spfn_session_key_id",
|
|
7324
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7325
|
+
OAUTH_PENDING: "spfn_oauth_pending"
|
|
7489
7326
|
};
|
|
7490
7327
|
function parseDuration(duration) {
|
|
7491
7328
|
if (typeof duration === "number") {
|
|
@@ -7530,7 +7367,7 @@ function getSessionTtl(override) {
|
|
|
7530
7367
|
if (globalConfig.sessionTtl !== void 0) {
|
|
7531
7368
|
return parseDuration(globalConfig.sessionTtl);
|
|
7532
7369
|
}
|
|
7533
|
-
const envTtl =
|
|
7370
|
+
const envTtl = env5.SPFN_AUTH_SESSION_TTL;
|
|
7534
7371
|
if (envTtl) {
|
|
7535
7372
|
return parseDuration(envTtl);
|
|
7536
7373
|
}
|
|
@@ -7902,6 +7739,7 @@ async function getUserProfileService(userId) {
|
|
|
7902
7739
|
return {
|
|
7903
7740
|
userId: user.userId,
|
|
7904
7741
|
email: user.email,
|
|
7742
|
+
username: user.username,
|
|
7905
7743
|
emailVerified: user.isEmailVerified,
|
|
7906
7744
|
phoneVerified: user.isPhoneVerified,
|
|
7907
7745
|
lastLoginAt: user.lastLoginAt,
|
|
@@ -7910,6 +7748,12 @@ async function getUserProfileService(userId) {
|
|
|
7910
7748
|
profile
|
|
7911
7749
|
};
|
|
7912
7750
|
}
|
|
7751
|
+
async function updateLocaleService(userId, locale) {
|
|
7752
|
+
const userIdNum = Number(userId);
|
|
7753
|
+
const normalized = locale.trim() || "en";
|
|
7754
|
+
await userProfilesRepository.upsertByUserId(userIdNum, { locale: normalized });
|
|
7755
|
+
return { locale: normalized };
|
|
7756
|
+
}
|
|
7913
7757
|
function emptyToNull(value) {
|
|
7914
7758
|
if (value === "") {
|
|
7915
7759
|
return null;
|
|
@@ -7970,6 +7814,350 @@ async function updateUserProfileService(userId, params) {
|
|
|
7970
7814
|
return profile;
|
|
7971
7815
|
}
|
|
7972
7816
|
|
|
7817
|
+
// src/server/services/oauth.service.ts
|
|
7818
|
+
init_repositories();
|
|
7819
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
7820
|
+
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
7821
|
+
|
|
7822
|
+
// src/server/lib/oauth/google.ts
|
|
7823
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
7824
|
+
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
7825
|
+
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
7826
|
+
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
7827
|
+
function isGoogleOAuthEnabled() {
|
|
7828
|
+
return !!(env6.SPFN_AUTH_GOOGLE_CLIENT_ID && env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
7829
|
+
}
|
|
7830
|
+
function getGoogleOAuthConfig() {
|
|
7831
|
+
const clientId = env6.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
7832
|
+
const clientSecret = env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
7833
|
+
if (!clientId || !clientSecret) {
|
|
7834
|
+
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
7835
|
+
}
|
|
7836
|
+
const baseUrl = env6.NEXT_PUBLIC_SPFN_API_URL || env6.SPFN_API_URL;
|
|
7837
|
+
const redirectUri = env6.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
7838
|
+
return {
|
|
7839
|
+
clientId,
|
|
7840
|
+
clientSecret,
|
|
7841
|
+
redirectUri
|
|
7842
|
+
};
|
|
7843
|
+
}
|
|
7844
|
+
function getDefaultScopes() {
|
|
7845
|
+
const envScopes = env6.SPFN_AUTH_GOOGLE_SCOPES;
|
|
7846
|
+
if (envScopes) {
|
|
7847
|
+
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7848
|
+
}
|
|
7849
|
+
return ["email", "profile"];
|
|
7850
|
+
}
|
|
7851
|
+
function getGoogleAuthUrl(state, scopes) {
|
|
7852
|
+
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
7853
|
+
const config = getGoogleOAuthConfig();
|
|
7854
|
+
const params = new URLSearchParams({
|
|
7855
|
+
client_id: config.clientId,
|
|
7856
|
+
redirect_uri: config.redirectUri,
|
|
7857
|
+
response_type: "code",
|
|
7858
|
+
scope: resolvedScopes.join(" "),
|
|
7859
|
+
state,
|
|
7860
|
+
access_type: "offline",
|
|
7861
|
+
// refresh_token 받기 위해
|
|
7862
|
+
prompt: "consent"
|
|
7863
|
+
// 매번 동의 화면 표시 (refresh_token 보장)
|
|
7864
|
+
});
|
|
7865
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
7866
|
+
}
|
|
7867
|
+
async function exchangeCodeForTokens(code) {
|
|
7868
|
+
const config = getGoogleOAuthConfig();
|
|
7869
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7870
|
+
method: "POST",
|
|
7871
|
+
headers: {
|
|
7872
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7873
|
+
},
|
|
7874
|
+
body: new URLSearchParams({
|
|
7875
|
+
client_id: config.clientId,
|
|
7876
|
+
client_secret: config.clientSecret,
|
|
7877
|
+
redirect_uri: config.redirectUri,
|
|
7878
|
+
grant_type: "authorization_code",
|
|
7879
|
+
code
|
|
7880
|
+
})
|
|
7881
|
+
});
|
|
7882
|
+
if (!response.ok) {
|
|
7883
|
+
const error = await response.text();
|
|
7884
|
+
throw new Error(`Failed to exchange code for tokens: ${error}`);
|
|
7885
|
+
}
|
|
7886
|
+
return response.json();
|
|
7887
|
+
}
|
|
7888
|
+
async function getGoogleUserInfo(accessToken) {
|
|
7889
|
+
const response = await fetch(GOOGLE_USERINFO_URL, {
|
|
7890
|
+
headers: {
|
|
7891
|
+
Authorization: `Bearer ${accessToken}`
|
|
7892
|
+
}
|
|
7893
|
+
});
|
|
7894
|
+
if (!response.ok) {
|
|
7895
|
+
const error = await response.text();
|
|
7896
|
+
throw new Error(`Failed to get user info: ${error}`);
|
|
7897
|
+
}
|
|
7898
|
+
return response.json();
|
|
7899
|
+
}
|
|
7900
|
+
async function refreshAccessToken(refreshToken) {
|
|
7901
|
+
const config = getGoogleOAuthConfig();
|
|
7902
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7903
|
+
method: "POST",
|
|
7904
|
+
headers: {
|
|
7905
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7906
|
+
},
|
|
7907
|
+
body: new URLSearchParams({
|
|
7908
|
+
client_id: config.clientId,
|
|
7909
|
+
client_secret: config.clientSecret,
|
|
7910
|
+
refresh_token: refreshToken,
|
|
7911
|
+
grant_type: "refresh_token"
|
|
7912
|
+
})
|
|
7913
|
+
});
|
|
7914
|
+
if (!response.ok) {
|
|
7915
|
+
const error = await response.text();
|
|
7916
|
+
throw new Error(`Failed to refresh access token: ${error}`);
|
|
7917
|
+
}
|
|
7918
|
+
return response.json();
|
|
7919
|
+
}
|
|
7920
|
+
|
|
7921
|
+
// src/server/lib/oauth/state.ts
|
|
7922
|
+
import * as jose from "jose";
|
|
7923
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
7924
|
+
async function getStateKey() {
|
|
7925
|
+
const secret = env7.SPFN_AUTH_SESSION_SECRET;
|
|
7926
|
+
const encoder = new TextEncoder();
|
|
7927
|
+
const data = encoder.encode(`oauth-state:${secret}`);
|
|
7928
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
7929
|
+
return new Uint8Array(hashBuffer);
|
|
7930
|
+
}
|
|
7931
|
+
function generateNonce() {
|
|
7932
|
+
const array = new Uint8Array(16);
|
|
7933
|
+
crypto.getRandomValues(array);
|
|
7934
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
7935
|
+
}
|
|
7936
|
+
async function createOAuthState(params) {
|
|
7937
|
+
const key = await getStateKey();
|
|
7938
|
+
const state = {
|
|
7939
|
+
returnUrl: params.returnUrl,
|
|
7940
|
+
nonce: generateNonce(),
|
|
7941
|
+
provider: params.provider,
|
|
7942
|
+
publicKey: params.publicKey,
|
|
7943
|
+
keyId: params.keyId,
|
|
7944
|
+
fingerprint: params.fingerprint,
|
|
7945
|
+
algorithm: params.algorithm,
|
|
7946
|
+
metadata: params.metadata
|
|
7947
|
+
};
|
|
7948
|
+
const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
|
|
7949
|
+
return encodeURIComponent(jwe);
|
|
7950
|
+
}
|
|
7951
|
+
async function verifyOAuthState(encryptedState) {
|
|
7952
|
+
const key = await getStateKey();
|
|
7953
|
+
const jwe = decodeURIComponent(encryptedState);
|
|
7954
|
+
const { payload } = await jose.jwtDecrypt(jwe, key);
|
|
7955
|
+
return payload.state;
|
|
7956
|
+
}
|
|
7957
|
+
|
|
7958
|
+
// src/server/services/oauth.service.ts
|
|
7959
|
+
async function oauthStartService(params) {
|
|
7960
|
+
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
7961
|
+
if (provider === "google") {
|
|
7962
|
+
if (!isGoogleOAuthEnabled()) {
|
|
7963
|
+
throw new ValidationError3({
|
|
7964
|
+
message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
|
|
7965
|
+
});
|
|
7966
|
+
}
|
|
7967
|
+
const state = await createOAuthState({
|
|
7968
|
+
provider: "google",
|
|
7969
|
+
returnUrl,
|
|
7970
|
+
publicKey,
|
|
7971
|
+
keyId,
|
|
7972
|
+
fingerprint,
|
|
7973
|
+
algorithm,
|
|
7974
|
+
metadata
|
|
7975
|
+
});
|
|
7976
|
+
const authUrl = getGoogleAuthUrl(state);
|
|
7977
|
+
return { authUrl };
|
|
7978
|
+
}
|
|
7979
|
+
throw new ValidationError3({
|
|
7980
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7981
|
+
});
|
|
7982
|
+
}
|
|
7983
|
+
async function oauthCallbackService(params) {
|
|
7984
|
+
const { provider, code, state } = params;
|
|
7985
|
+
const stateData = await verifyOAuthState(state);
|
|
7986
|
+
if (stateData.provider !== provider) {
|
|
7987
|
+
throw new ValidationError3({
|
|
7988
|
+
message: "OAuth state provider mismatch"
|
|
7989
|
+
});
|
|
7990
|
+
}
|
|
7991
|
+
if (provider === "google") {
|
|
7992
|
+
return handleGoogleCallback(code, stateData);
|
|
7993
|
+
}
|
|
7994
|
+
throw new ValidationError3({
|
|
7995
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7996
|
+
});
|
|
7997
|
+
}
|
|
7998
|
+
async function handleGoogleCallback(code, stateData) {
|
|
7999
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
8000
|
+
const googleUser = await getGoogleUserInfo(tokens.access_token);
|
|
8001
|
+
const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
|
|
8002
|
+
"google",
|
|
8003
|
+
googleUser.id
|
|
8004
|
+
);
|
|
8005
|
+
let userId;
|
|
8006
|
+
let isNewUser = false;
|
|
8007
|
+
if (existingSocialAccount) {
|
|
8008
|
+
userId = existingSocialAccount.userId;
|
|
8009
|
+
await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
|
|
8010
|
+
accessToken: tokens.access_token,
|
|
8011
|
+
refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
|
|
8012
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8013
|
+
});
|
|
8014
|
+
} else {
|
|
8015
|
+
const result = await createOrLinkUser(googleUser, tokens);
|
|
8016
|
+
userId = result.userId;
|
|
8017
|
+
isNewUser = result.isNewUser;
|
|
8018
|
+
}
|
|
8019
|
+
await registerPublicKeyService({
|
|
8020
|
+
userId,
|
|
8021
|
+
keyId: stateData.keyId,
|
|
8022
|
+
publicKey: stateData.publicKey,
|
|
8023
|
+
fingerprint: stateData.fingerprint,
|
|
8024
|
+
algorithm: stateData.algorithm
|
|
8025
|
+
});
|
|
8026
|
+
await updateLastLoginService(userId);
|
|
8027
|
+
const appUrl = env8.NEXT_PUBLIC_SPFN_APP_URL || env8.SPFN_APP_URL;
|
|
8028
|
+
const callbackPath = env8.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
8029
|
+
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
8030
|
+
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
8031
|
+
userId: String(userId),
|
|
8032
|
+
keyId: stateData.keyId,
|
|
8033
|
+
returnUrl: stateData.returnUrl,
|
|
8034
|
+
isNewUser: String(isNewUser)
|
|
8035
|
+
});
|
|
8036
|
+
const user = await usersRepository.findById(userId);
|
|
8037
|
+
const eventPayload = {
|
|
8038
|
+
userId: String(userId),
|
|
8039
|
+
provider: "google",
|
|
8040
|
+
email: user?.email || void 0,
|
|
8041
|
+
phone: user?.phone || void 0,
|
|
8042
|
+
metadata: stateData.metadata
|
|
8043
|
+
};
|
|
8044
|
+
if (isNewUser) {
|
|
8045
|
+
await authRegisterEvent.emit(eventPayload);
|
|
8046
|
+
} else {
|
|
8047
|
+
await authLoginEvent.emit(eventPayload);
|
|
8048
|
+
}
|
|
8049
|
+
return {
|
|
8050
|
+
redirectUrl,
|
|
8051
|
+
userId: String(userId),
|
|
8052
|
+
keyId: stateData.keyId,
|
|
8053
|
+
isNewUser
|
|
8054
|
+
};
|
|
8055
|
+
}
|
|
8056
|
+
async function createOrLinkUser(googleUser, tokens) {
|
|
8057
|
+
const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
|
|
8058
|
+
let userId;
|
|
8059
|
+
let isNewUser = false;
|
|
8060
|
+
if (existingUser) {
|
|
8061
|
+
if (!googleUser.verified_email) {
|
|
8062
|
+
throw new ValidationError3({
|
|
8063
|
+
message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
|
|
8064
|
+
});
|
|
8065
|
+
}
|
|
8066
|
+
userId = existingUser.id;
|
|
8067
|
+
if (!existingUser.emailVerifiedAt) {
|
|
8068
|
+
await usersRepository.updateById(existingUser.id, {
|
|
8069
|
+
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
8070
|
+
});
|
|
8071
|
+
}
|
|
8072
|
+
} else {
|
|
8073
|
+
const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
8074
|
+
const userRole = await getRoleByName3("user");
|
|
8075
|
+
if (!userRole) {
|
|
8076
|
+
throw new Error("Default user role not found. Run initializeAuth() first.");
|
|
8077
|
+
}
|
|
8078
|
+
const newUser = await usersRepository.create({
|
|
8079
|
+
email: googleUser.verified_email ? googleUser.email : null,
|
|
8080
|
+
phone: null,
|
|
8081
|
+
passwordHash: null,
|
|
8082
|
+
// OAuth 사용자는 비밀번호 없음
|
|
8083
|
+
passwordChangeRequired: false,
|
|
8084
|
+
roleId: userRole.id,
|
|
8085
|
+
status: "active",
|
|
8086
|
+
emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
|
|
8087
|
+
});
|
|
8088
|
+
userId = newUser.id;
|
|
8089
|
+
isNewUser = true;
|
|
8090
|
+
}
|
|
8091
|
+
await socialAccountsRepository.create({
|
|
8092
|
+
userId,
|
|
8093
|
+
provider: "google",
|
|
8094
|
+
providerUserId: googleUser.id,
|
|
8095
|
+
providerEmail: googleUser.email,
|
|
8096
|
+
accessToken: tokens.access_token,
|
|
8097
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
8098
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8099
|
+
});
|
|
8100
|
+
return { userId, isNewUser };
|
|
8101
|
+
}
|
|
8102
|
+
function buildRedirectUrl(baseUrl, params) {
|
|
8103
|
+
const url = new URL(baseUrl, "http://placeholder");
|
|
8104
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8105
|
+
url.searchParams.set(key, value);
|
|
8106
|
+
}
|
|
8107
|
+
if (baseUrl.startsWith("http")) {
|
|
8108
|
+
return url.toString();
|
|
8109
|
+
}
|
|
8110
|
+
return `${url.pathname}${url.search}`;
|
|
8111
|
+
}
|
|
8112
|
+
function buildOAuthErrorUrl(error) {
|
|
8113
|
+
const errorUrl = env8.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
8114
|
+
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
8115
|
+
}
|
|
8116
|
+
function isOAuthProviderEnabled(provider) {
|
|
8117
|
+
switch (provider) {
|
|
8118
|
+
case "google":
|
|
8119
|
+
return isGoogleOAuthEnabled();
|
|
8120
|
+
case "github":
|
|
8121
|
+
case "kakao":
|
|
8122
|
+
case "naver":
|
|
8123
|
+
return false;
|
|
8124
|
+
default:
|
|
8125
|
+
return false;
|
|
8126
|
+
}
|
|
8127
|
+
}
|
|
8128
|
+
function getEnabledOAuthProviders() {
|
|
8129
|
+
const providers = [];
|
|
8130
|
+
if (isGoogleOAuthEnabled()) {
|
|
8131
|
+
providers.push("google");
|
|
8132
|
+
}
|
|
8133
|
+
return providers;
|
|
8134
|
+
}
|
|
8135
|
+
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
8136
|
+
async function getGoogleAccessToken(userId) {
|
|
8137
|
+
const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
|
|
8138
|
+
if (!account) {
|
|
8139
|
+
throw new ValidationError3({
|
|
8140
|
+
message: "No Google account linked. User must sign in with Google first."
|
|
8141
|
+
});
|
|
8142
|
+
}
|
|
8143
|
+
const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
|
|
8144
|
+
if (!isExpired && account.accessToken) {
|
|
8145
|
+
return account.accessToken;
|
|
8146
|
+
}
|
|
8147
|
+
if (!account.refreshToken) {
|
|
8148
|
+
throw new ValidationError3({
|
|
8149
|
+
message: "Google refresh token not available. User must re-authenticate with Google."
|
|
8150
|
+
});
|
|
8151
|
+
}
|
|
8152
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
8153
|
+
await socialAccountsRepository.updateTokens(account.id, {
|
|
8154
|
+
accessToken: tokens.access_token,
|
|
8155
|
+
refreshToken: tokens.refresh_token ?? account.refreshToken,
|
|
8156
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8157
|
+
});
|
|
8158
|
+
return tokens.access_token;
|
|
8159
|
+
}
|
|
8160
|
+
|
|
7973
8161
|
// src/server/routes/auth/index.ts
|
|
7974
8162
|
init_esm();
|
|
7975
8163
|
import { Transactional } from "@spfn/core/db";
|
|
@@ -8020,7 +8208,10 @@ var register = route.post("/_auth/register").input({
|
|
|
8020
8208
|
verificationToken: Type.String({
|
|
8021
8209
|
description: "Verification token obtained from /verify-code endpoint"
|
|
8022
8210
|
}),
|
|
8023
|
-
password: PasswordSchema
|
|
8211
|
+
password: PasswordSchema,
|
|
8212
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
8213
|
+
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
8214
|
+
}))
|
|
8024
8215
|
}, {
|
|
8025
8216
|
minProperties: 3,
|
|
8026
8217
|
// email/phone + verificationToken + password
|
|
@@ -8062,9 +8253,7 @@ var login = route.post("/_auth/login").input({
|
|
|
8062
8253
|
const { body } = await c.data();
|
|
8063
8254
|
return await loginService(body);
|
|
8064
8255
|
});
|
|
8065
|
-
var logout = route.post("/_auth/logout").
|
|
8066
|
-
body: Type.Object({})
|
|
8067
|
-
}).handler(async (c) => {
|
|
8256
|
+
var logout = route.post("/_auth/logout").handler(async (c) => {
|
|
8068
8257
|
const auth = getAuth(c);
|
|
8069
8258
|
if (!auth) {
|
|
8070
8259
|
return c.noContent();
|
|
@@ -8073,9 +8262,7 @@ var logout = route.post("/_auth/logout").input({
|
|
|
8073
8262
|
await logoutService({ userId: Number(userId), keyId });
|
|
8074
8263
|
return c.noContent();
|
|
8075
8264
|
});
|
|
8076
|
-
var rotateKey = route.post("/_auth/keys/rotate").
|
|
8077
|
-
body: Type.Object({})
|
|
8078
|
-
}).interceptor({
|
|
8265
|
+
var rotateKey = route.post("/_auth/keys/rotate").interceptor({
|
|
8079
8266
|
body: Type.Object({
|
|
8080
8267
|
publicKey: Type.String({ description: "New public key" }),
|
|
8081
8268
|
keyId: Type.String({ description: "New key identifier" }),
|
|
@@ -8135,7 +8322,7 @@ import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
|
|
|
8135
8322
|
// src/server/middleware/authenticate.ts
|
|
8136
8323
|
import { defineMiddleware } from "@spfn/core/route";
|
|
8137
8324
|
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";
|
|
8325
|
+
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2, userProfilesRepository as userProfilesRepository2 } from "@spfn/auth/server";
|
|
8139
8326
|
import {
|
|
8140
8327
|
InvalidTokenError,
|
|
8141
8328
|
TokenExpiredError,
|
|
@@ -8182,10 +8369,14 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8182
8369
|
}
|
|
8183
8370
|
throw new UnauthorizedError({ message: "Authentication failed" });
|
|
8184
8371
|
}
|
|
8185
|
-
const
|
|
8186
|
-
|
|
8372
|
+
const [result, locale] = await Promise.all([
|
|
8373
|
+
usersRepository2.findByIdWithRole(keyRecord.userId),
|
|
8374
|
+
userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
|
|
8375
|
+
]);
|
|
8376
|
+
if (!result) {
|
|
8187
8377
|
throw new UnauthorizedError({ message: "User not found" });
|
|
8188
8378
|
}
|
|
8379
|
+
const { user, role } = result;
|
|
8189
8380
|
if (user.status !== "active") {
|
|
8190
8381
|
throw new AccountDisabledError2({ status: user.status });
|
|
8191
8382
|
}
|
|
@@ -8193,7 +8384,9 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8193
8384
|
c.set("auth", {
|
|
8194
8385
|
user,
|
|
8195
8386
|
userId: String(user.id),
|
|
8196
|
-
keyId
|
|
8387
|
+
keyId,
|
|
8388
|
+
role: role?.name ?? null,
|
|
8389
|
+
locale
|
|
8197
8390
|
});
|
|
8198
8391
|
const method = c.req.method;
|
|
8199
8392
|
const path = c.req.path;
|
|
@@ -8208,6 +8401,55 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
8208
8401
|
});
|
|
8209
8402
|
await next();
|
|
8210
8403
|
});
|
|
8404
|
+
var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
8405
|
+
const authHeader = c.req.header("Authorization");
|
|
8406
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
8407
|
+
await next();
|
|
8408
|
+
return;
|
|
8409
|
+
}
|
|
8410
|
+
const token = authHeader.substring(7);
|
|
8411
|
+
try {
|
|
8412
|
+
const decoded = decodeToken2(token);
|
|
8413
|
+
if (!decoded || !decoded.keyId) {
|
|
8414
|
+
await next();
|
|
8415
|
+
return;
|
|
8416
|
+
}
|
|
8417
|
+
const keyId = decoded.keyId;
|
|
8418
|
+
const keyRecord = await keysRepository2.findActiveByKeyId(keyId);
|
|
8419
|
+
if (!keyRecord) {
|
|
8420
|
+
await next();
|
|
8421
|
+
return;
|
|
8422
|
+
}
|
|
8423
|
+
if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
|
|
8424
|
+
await next();
|
|
8425
|
+
return;
|
|
8426
|
+
}
|
|
8427
|
+
verifyClientToken2(
|
|
8428
|
+
token,
|
|
8429
|
+
keyRecord.publicKey,
|
|
8430
|
+
keyRecord.algorithm
|
|
8431
|
+
);
|
|
8432
|
+
const [result, locale] = await Promise.all([
|
|
8433
|
+
usersRepository2.findByIdWithRole(keyRecord.userId),
|
|
8434
|
+
userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
|
|
8435
|
+
]);
|
|
8436
|
+
if (!result || result.user.status !== "active") {
|
|
8437
|
+
await next();
|
|
8438
|
+
return;
|
|
8439
|
+
}
|
|
8440
|
+
const { user, role } = result;
|
|
8441
|
+
keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
8442
|
+
c.set("auth", {
|
|
8443
|
+
user,
|
|
8444
|
+
userId: String(user.id),
|
|
8445
|
+
keyId,
|
|
8446
|
+
role: role?.name ?? null,
|
|
8447
|
+
locale
|
|
8448
|
+
});
|
|
8449
|
+
} catch {
|
|
8450
|
+
}
|
|
8451
|
+
await next();
|
|
8452
|
+
}, { skips: ["auth"] });
|
|
8211
8453
|
|
|
8212
8454
|
// src/server/middleware/require-permission.ts
|
|
8213
8455
|
import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
|
|
@@ -8273,7 +8515,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
8273
8515
|
|
|
8274
8516
|
// src/server/middleware/require-role.ts
|
|
8275
8517
|
import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
|
|
8276
|
-
import { getAuth as getAuth3,
|
|
8518
|
+
import { getAuth as getAuth3, authLogger as authLogger4 } from "@spfn/auth/server";
|
|
8277
8519
|
import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
|
|
8278
8520
|
import { InsufficientRoleError } from "@spfn/auth/errors";
|
|
8279
8521
|
var requireRole = defineMiddleware3(
|
|
@@ -8287,11 +8529,11 @@ var requireRole = defineMiddleware3(
|
|
|
8287
8529
|
});
|
|
8288
8530
|
throw new ForbiddenError2({ message: "Authentication required" });
|
|
8289
8531
|
}
|
|
8290
|
-
const { userId } = auth;
|
|
8291
|
-
|
|
8292
|
-
if (!allowed) {
|
|
8532
|
+
const { userId, role: userRole } = auth;
|
|
8533
|
+
if (!userRole || !roleNames.includes(userRole)) {
|
|
8293
8534
|
authLogger4.middleware.warn("Role check failed", {
|
|
8294
8535
|
userId,
|
|
8536
|
+
userRole,
|
|
8295
8537
|
requiredRoles: roleNames,
|
|
8296
8538
|
path: c.req.path
|
|
8297
8539
|
});
|
|
@@ -8299,6 +8541,7 @@ var requireRole = defineMiddleware3(
|
|
|
8299
8541
|
}
|
|
8300
8542
|
authLogger4.middleware.debug("Role check passed", {
|
|
8301
8543
|
userId,
|
|
8544
|
+
userRole,
|
|
8302
8545
|
roles: roleNames
|
|
8303
8546
|
});
|
|
8304
8547
|
await next();
|
|
@@ -8307,7 +8550,7 @@ var requireRole = defineMiddleware3(
|
|
|
8307
8550
|
|
|
8308
8551
|
// src/server/middleware/role-guard.ts
|
|
8309
8552
|
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
8310
|
-
import { getAuth as getAuth4,
|
|
8553
|
+
import { getAuth as getAuth4, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
8311
8554
|
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
8312
8555
|
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
8313
8556
|
var roleGuard = defineMiddleware4(
|
|
@@ -8324,8 +8567,7 @@ var roleGuard = defineMiddleware4(
|
|
|
8324
8567
|
});
|
|
8325
8568
|
throw new ForbiddenError3({ message: "Authentication required" });
|
|
8326
8569
|
}
|
|
8327
|
-
const { userId } = auth;
|
|
8328
|
-
const userRole = await getUserRole2(userId);
|
|
8570
|
+
const { userId, role: userRole } = auth;
|
|
8329
8571
|
if (deny && deny.length > 0) {
|
|
8330
8572
|
if (userRole && deny.includes(userRole)) {
|
|
8331
8573
|
authLogger5.middleware.warn("Role guard denied", {
|
|
@@ -8579,17 +8821,296 @@ var updateUserProfile = route3.patch("/_auth/users/profile").input({
|
|
|
8579
8821
|
const { body } = await c.data();
|
|
8580
8822
|
return await updateUserProfileService(userId, body);
|
|
8581
8823
|
});
|
|
8824
|
+
var checkUsername = route3.get("/_auth/users/username/check").input({
|
|
8825
|
+
query: Type.Object({
|
|
8826
|
+
username: Type.String({ minLength: 1 })
|
|
8827
|
+
})
|
|
8828
|
+
}).handler(async (c) => {
|
|
8829
|
+
const { query } = await c.data();
|
|
8830
|
+
return { available: await checkUsernameAvailableService(query.username) };
|
|
8831
|
+
});
|
|
8832
|
+
var updateUsername = route3.patch("/_auth/users/username").input({
|
|
8833
|
+
body: Type.Object({
|
|
8834
|
+
username: Type.Union([
|
|
8835
|
+
Type.String({ minLength: 1 }),
|
|
8836
|
+
Type.Null()
|
|
8837
|
+
], { description: "New username or null to clear" })
|
|
8838
|
+
})
|
|
8839
|
+
}).handler(async (c) => {
|
|
8840
|
+
const { userId } = getAuth(c);
|
|
8841
|
+
const { body } = await c.data();
|
|
8842
|
+
return await updateUsernameService(userId, body.username);
|
|
8843
|
+
});
|
|
8844
|
+
var updateLocale = route3.patch("/_auth/users/locale").input({
|
|
8845
|
+
body: Type.Object({
|
|
8846
|
+
locale: Type.String({ minLength: 1, description: "Locale code (e.g., en, ko, ja)" })
|
|
8847
|
+
})
|
|
8848
|
+
}).handler(async (c) => {
|
|
8849
|
+
const { userId } = getAuth(c);
|
|
8850
|
+
const { body } = await c.data();
|
|
8851
|
+
return await updateLocaleService(userId, body.locale);
|
|
8852
|
+
});
|
|
8582
8853
|
var userRouter = defineRouter3({
|
|
8583
8854
|
getUserProfile,
|
|
8584
|
-
updateUserProfile
|
|
8855
|
+
updateUserProfile,
|
|
8856
|
+
checkUsername,
|
|
8857
|
+
updateUsername,
|
|
8858
|
+
updateLocale
|
|
8859
|
+
});
|
|
8860
|
+
|
|
8861
|
+
// src/server/routes/oauth/index.ts
|
|
8862
|
+
init_esm();
|
|
8863
|
+
init_types();
|
|
8864
|
+
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
8865
|
+
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
8866
|
+
var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
8867
|
+
query: Type.Object({
|
|
8868
|
+
state: Type.String({
|
|
8869
|
+
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
8870
|
+
})
|
|
8871
|
+
})
|
|
8872
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8873
|
+
const { query } = await c.data();
|
|
8874
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8875
|
+
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
8876
|
+
}
|
|
8877
|
+
const authUrl = getGoogleAuthUrl(query.state);
|
|
8878
|
+
return c.redirect(authUrl);
|
|
8879
|
+
});
|
|
8880
|
+
var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
8881
|
+
query: Type.Object({
|
|
8882
|
+
code: Type.Optional(Type.String({
|
|
8883
|
+
description: "Authorization code from Google"
|
|
8884
|
+
})),
|
|
8885
|
+
state: Type.Optional(Type.String({
|
|
8886
|
+
description: "OAuth state parameter"
|
|
8887
|
+
})),
|
|
8888
|
+
error: Type.Optional(Type.String({
|
|
8889
|
+
description: "Error code from Google"
|
|
8890
|
+
})),
|
|
8891
|
+
error_description: Type.Optional(Type.String({
|
|
8892
|
+
description: "Error description from Google"
|
|
8893
|
+
}))
|
|
8894
|
+
})
|
|
8895
|
+
}).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
8896
|
+
const { query } = await c.data();
|
|
8897
|
+
if (query.error) {
|
|
8898
|
+
const errorMessage = query.error_description || query.error;
|
|
8899
|
+
return c.redirect(buildOAuthErrorUrl(errorMessage));
|
|
8900
|
+
}
|
|
8901
|
+
if (!query.code || !query.state) {
|
|
8902
|
+
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
8903
|
+
}
|
|
8904
|
+
try {
|
|
8905
|
+
const result = await oauthCallbackService({
|
|
8906
|
+
provider: "google",
|
|
8907
|
+
code: query.code,
|
|
8908
|
+
state: query.state
|
|
8909
|
+
});
|
|
8910
|
+
return c.redirect(result.redirectUrl);
|
|
8911
|
+
} catch (err) {
|
|
8912
|
+
const message = err instanceof Error ? err.message : "OAuth callback failed";
|
|
8913
|
+
return c.redirect(buildOAuthErrorUrl(message));
|
|
8914
|
+
}
|
|
8915
|
+
});
|
|
8916
|
+
var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
8917
|
+
body: Type.Object({
|
|
8918
|
+
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
8919
|
+
description: "OAuth provider (google, github, kakao, naver)"
|
|
8920
|
+
}),
|
|
8921
|
+
returnUrl: Type.String({
|
|
8922
|
+
description: "URL to redirect after OAuth success"
|
|
8923
|
+
}),
|
|
8924
|
+
publicKey: Type.String({
|
|
8925
|
+
description: "Client public key (Base64 DER)"
|
|
8926
|
+
}),
|
|
8927
|
+
keyId: Type.String({
|
|
8928
|
+
description: "Key identifier (UUID)"
|
|
8929
|
+
}),
|
|
8930
|
+
fingerprint: Type.String({
|
|
8931
|
+
description: "Key fingerprint (SHA-256 hex)"
|
|
8932
|
+
}),
|
|
8933
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
|
|
8934
|
+
description: "Key algorithm (ES256 or RS256)"
|
|
8935
|
+
}),
|
|
8936
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
8937
|
+
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
8938
|
+
}))
|
|
8939
|
+
})
|
|
8940
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8941
|
+
const { body } = await c.data();
|
|
8942
|
+
const result = await oauthStartService(body);
|
|
8943
|
+
return result;
|
|
8944
|
+
});
|
|
8945
|
+
var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
8946
|
+
return {
|
|
8947
|
+
providers: getEnabledOAuthProviders()
|
|
8948
|
+
};
|
|
8949
|
+
});
|
|
8950
|
+
var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
8951
|
+
body: Type.Object({
|
|
8952
|
+
returnUrl: Type.Optional(Type.String({
|
|
8953
|
+
description: "URL to redirect after OAuth success"
|
|
8954
|
+
})),
|
|
8955
|
+
state: Type.Optional(Type.String({
|
|
8956
|
+
description: "Encrypted OAuth state (injected by interceptor)"
|
|
8957
|
+
}))
|
|
8958
|
+
})
|
|
8959
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8960
|
+
const { body } = await c.data();
|
|
8961
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8962
|
+
throw new Error("Google OAuth is not configured");
|
|
8963
|
+
}
|
|
8964
|
+
if (!body.state) {
|
|
8965
|
+
throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
|
|
8966
|
+
}
|
|
8967
|
+
return { authUrl: getGoogleAuthUrl(body.state) };
|
|
8968
|
+
});
|
|
8969
|
+
var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
|
|
8970
|
+
body: Type.Object({
|
|
8971
|
+
userId: Type.String({ description: "User ID from OAuth callback" }),
|
|
8972
|
+
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
8973
|
+
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
8974
|
+
})
|
|
8975
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8976
|
+
const { body } = await c.data();
|
|
8977
|
+
return {
|
|
8978
|
+
success: true,
|
|
8979
|
+
userId: body.userId,
|
|
8980
|
+
keyId: body.keyId,
|
|
8981
|
+
returnUrl: body.returnUrl || "/"
|
|
8982
|
+
};
|
|
8983
|
+
});
|
|
8984
|
+
var oauthRouter = defineRouter4({
|
|
8985
|
+
oauthGoogleStart,
|
|
8986
|
+
oauthGoogleCallback,
|
|
8987
|
+
oauthStart,
|
|
8988
|
+
oauthProviders,
|
|
8989
|
+
getGoogleOAuthUrl,
|
|
8990
|
+
oauthFinalize
|
|
8991
|
+
});
|
|
8992
|
+
|
|
8993
|
+
// src/server/routes/admin/index.ts
|
|
8994
|
+
init_esm();
|
|
8995
|
+
import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
|
|
8996
|
+
import { route as route5 } from "@spfn/core/route";
|
|
8997
|
+
var listRoles = route5.get("/_auth/admin/roles").input({
|
|
8998
|
+
query: Type.Object({
|
|
8999
|
+
includeInactive: Type.Optional(Type.Boolean({
|
|
9000
|
+
description: "Include inactive roles (default: false)"
|
|
9001
|
+
}))
|
|
9002
|
+
})
|
|
9003
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9004
|
+
const { query } = await c.data();
|
|
9005
|
+
const roles2 = await getAllRoles(query.includeInactive ?? false);
|
|
9006
|
+
return { roles: roles2 };
|
|
9007
|
+
});
|
|
9008
|
+
var createAdminRole = route5.post("/_auth/admin/roles").input({
|
|
9009
|
+
body: Type.Object({
|
|
9010
|
+
name: Type.String({ description: "Unique role name (slug)" }),
|
|
9011
|
+
displayName: Type.String({ description: "Human-readable role name" }),
|
|
9012
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
9013
|
+
priority: Type.Optional(Type.Number({ description: "Role priority (default: 10)" })),
|
|
9014
|
+
permissionIds: Type.Optional(Type.Array(
|
|
9015
|
+
Type.Number({ description: "Permission ID" }),
|
|
9016
|
+
{ description: "Permission IDs to assign" }
|
|
9017
|
+
))
|
|
9018
|
+
})
|
|
9019
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9020
|
+
const { body } = await c.data();
|
|
9021
|
+
const role = await createRole({
|
|
9022
|
+
name: body.name,
|
|
9023
|
+
displayName: body.displayName,
|
|
9024
|
+
description: body.description,
|
|
9025
|
+
priority: body.priority,
|
|
9026
|
+
permissionIds: body.permissionIds
|
|
9027
|
+
});
|
|
9028
|
+
return { role };
|
|
9029
|
+
});
|
|
9030
|
+
var updateAdminRole = route5.patch("/_auth/admin/roles/:id").input({
|
|
9031
|
+
params: Type.Object({
|
|
9032
|
+
id: Type.Number({ description: "Role ID" })
|
|
9033
|
+
}),
|
|
9034
|
+
body: Type.Object({
|
|
9035
|
+
displayName: Type.Optional(Type.String({ description: "Human-readable role name" })),
|
|
9036
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
9037
|
+
priority: Type.Optional(Type.Number({ description: "Role priority" })),
|
|
9038
|
+
isActive: Type.Optional(Type.Boolean({ description: "Active status" }))
|
|
9039
|
+
})
|
|
9040
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9041
|
+
const { params, body } = await c.data();
|
|
9042
|
+
const role = await updateRole(params.id, body);
|
|
9043
|
+
return { role };
|
|
9044
|
+
});
|
|
9045
|
+
var deleteAdminRole = route5.delete("/_auth/admin/roles/:id").input({
|
|
9046
|
+
params: Type.Object({
|
|
9047
|
+
id: Type.Number({ description: "Role ID" })
|
|
9048
|
+
})
|
|
9049
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9050
|
+
const { params } = await c.data();
|
|
9051
|
+
await deleteRole(params.id);
|
|
9052
|
+
return c.noContent();
|
|
9053
|
+
});
|
|
9054
|
+
var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
9055
|
+
params: Type.Object({
|
|
9056
|
+
userId: Type.Number({ description: "User ID" })
|
|
9057
|
+
}),
|
|
9058
|
+
body: Type.Object({
|
|
9059
|
+
roleId: Type.Number({ description: "New role ID to assign" })
|
|
9060
|
+
})
|
|
9061
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
9062
|
+
const { params, body } = await c.data();
|
|
9063
|
+
const auth = getAuth(c);
|
|
9064
|
+
if (params.userId === Number(auth.userId)) {
|
|
9065
|
+
throw new ForbiddenError4({ message: "Cannot change your own role" });
|
|
9066
|
+
}
|
|
9067
|
+
const targetRole = await getUserRole(params.userId);
|
|
9068
|
+
if (targetRole === "superadmin") {
|
|
9069
|
+
throw new ForbiddenError4({ message: "Cannot modify superadmin role" });
|
|
9070
|
+
}
|
|
9071
|
+
await updateUserService(params.userId, { roleId: body.roleId });
|
|
9072
|
+
return { userId: params.userId, roleId: body.roleId };
|
|
8585
9073
|
});
|
|
8586
9074
|
|
|
8587
9075
|
// src/server/routes/index.ts
|
|
8588
|
-
var mainAuthRouter =
|
|
8589
|
-
//
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
9076
|
+
var mainAuthRouter = defineRouter5({
|
|
9077
|
+
// Auth routes
|
|
9078
|
+
checkAccountExists,
|
|
9079
|
+
sendVerificationCode,
|
|
9080
|
+
verifyCode,
|
|
9081
|
+
register,
|
|
9082
|
+
login,
|
|
9083
|
+
logout,
|
|
9084
|
+
rotateKey,
|
|
9085
|
+
changePassword,
|
|
9086
|
+
getAuthSession,
|
|
9087
|
+
// OAuth routes
|
|
9088
|
+
oauthGoogleStart,
|
|
9089
|
+
oauthGoogleCallback,
|
|
9090
|
+
oauthStart,
|
|
9091
|
+
oauthProviders,
|
|
9092
|
+
getGoogleOAuthUrl,
|
|
9093
|
+
oauthFinalize,
|
|
9094
|
+
// Invitation routes
|
|
9095
|
+
getInvitation,
|
|
9096
|
+
acceptInvitation: acceptInvitation2,
|
|
9097
|
+
createInvitation: createInvitation2,
|
|
9098
|
+
listInvitations: listInvitations2,
|
|
9099
|
+
cancelInvitation: cancelInvitation2,
|
|
9100
|
+
resendInvitation: resendInvitation2,
|
|
9101
|
+
deleteInvitation: deleteInvitation2,
|
|
9102
|
+
// User routes
|
|
9103
|
+
getUserProfile,
|
|
9104
|
+
updateUserProfile,
|
|
9105
|
+
checkUsername,
|
|
9106
|
+
updateUsername,
|
|
9107
|
+
updateLocale,
|
|
9108
|
+
// Admin routes (superadmin only)
|
|
9109
|
+
listRoles,
|
|
9110
|
+
createAdminRole,
|
|
9111
|
+
updateAdminRole,
|
|
9112
|
+
deleteAdminRole,
|
|
9113
|
+
updateUserRole
|
|
8593
9114
|
});
|
|
8594
9115
|
|
|
8595
9116
|
// src/server.ts
|
|
@@ -8699,11 +9220,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
8699
9220
|
}
|
|
8700
9221
|
|
|
8701
9222
|
// src/server/lib/session.ts
|
|
8702
|
-
import * as
|
|
8703
|
-
import { env as
|
|
9223
|
+
import * as jose2 from "jose";
|
|
9224
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
8704
9225
|
import { env as coreEnv } from "@spfn/core/config";
|
|
8705
9226
|
async function getSessionSecretKey() {
|
|
8706
|
-
const secret =
|
|
9227
|
+
const secret = env9.SPFN_AUTH_SESSION_SECRET;
|
|
8707
9228
|
const encoder = new TextEncoder();
|
|
8708
9229
|
const data = encoder.encode(secret);
|
|
8709
9230
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -8711,24 +9232,24 @@ async function getSessionSecretKey() {
|
|
|
8711
9232
|
}
|
|
8712
9233
|
async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
|
|
8713
9234
|
const secret = await getSessionSecretKey();
|
|
8714
|
-
return await new
|
|
9235
|
+
return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
|
|
8715
9236
|
}
|
|
8716
9237
|
async function unsealSession(jwt4) {
|
|
8717
9238
|
try {
|
|
8718
9239
|
const secret = await getSessionSecretKey();
|
|
8719
|
-
const { payload } = await
|
|
9240
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
|
|
8720
9241
|
issuer: "spfn-auth",
|
|
8721
9242
|
audience: "spfn-client"
|
|
8722
9243
|
});
|
|
8723
9244
|
return payload.data;
|
|
8724
9245
|
} catch (err) {
|
|
8725
|
-
if (err instanceof
|
|
9246
|
+
if (err instanceof jose2.errors.JWTExpired) {
|
|
8726
9247
|
throw new Error("Session expired");
|
|
8727
9248
|
}
|
|
8728
|
-
if (err instanceof
|
|
9249
|
+
if (err instanceof jose2.errors.JWEDecryptionFailed) {
|
|
8729
9250
|
throw new Error("Invalid session");
|
|
8730
9251
|
}
|
|
8731
|
-
if (err instanceof
|
|
9252
|
+
if (err instanceof jose2.errors.JWTClaimValidationFailed) {
|
|
8732
9253
|
throw new Error("Session validation failed");
|
|
8733
9254
|
}
|
|
8734
9255
|
throw new Error("Failed to unseal session");
|
|
@@ -8737,7 +9258,7 @@ async function unsealSession(jwt4) {
|
|
|
8737
9258
|
async function getSessionInfo(jwt4) {
|
|
8738
9259
|
const secret = await getSessionSecretKey();
|
|
8739
9260
|
try {
|
|
8740
|
-
const { payload } = await
|
|
9261
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret);
|
|
8741
9262
|
return {
|
|
8742
9263
|
issuedAt: new Date(payload.iat * 1e3),
|
|
8743
9264
|
expiresAt: new Date(payload.exp * 1e3),
|
|
@@ -8761,14 +9282,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
8761
9282
|
}
|
|
8762
9283
|
|
|
8763
9284
|
// src/server/setup.ts
|
|
8764
|
-
import { env as
|
|
9285
|
+
import { env as env10 } from "@spfn/auth/config";
|
|
8765
9286
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
8766
9287
|
init_repositories();
|
|
8767
9288
|
function parseAdminAccounts() {
|
|
8768
9289
|
const accounts = [];
|
|
8769
|
-
if (
|
|
9290
|
+
if (env10.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
8770
9291
|
try {
|
|
8771
|
-
const accountsJson =
|
|
9292
|
+
const accountsJson = env10.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
8772
9293
|
const parsed = JSON.parse(accountsJson);
|
|
8773
9294
|
if (!Array.isArray(parsed)) {
|
|
8774
9295
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -8795,11 +9316,11 @@ function parseAdminAccounts() {
|
|
|
8795
9316
|
return accounts;
|
|
8796
9317
|
}
|
|
8797
9318
|
}
|
|
8798
|
-
const adminEmails =
|
|
9319
|
+
const adminEmails = env10.SPFN_AUTH_ADMIN_EMAILS;
|
|
8799
9320
|
if (adminEmails) {
|
|
8800
9321
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
8801
|
-
const passwords = (
|
|
8802
|
-
const roles2 = (
|
|
9322
|
+
const passwords = (env10.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
9323
|
+
const roles2 = (env10.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
8803
9324
|
if (passwords.length !== emails.length) {
|
|
8804
9325
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
8805
9326
|
return accounts;
|
|
@@ -8821,8 +9342,8 @@ function parseAdminAccounts() {
|
|
|
8821
9342
|
}
|
|
8822
9343
|
return accounts;
|
|
8823
9344
|
}
|
|
8824
|
-
const adminEmail =
|
|
8825
|
-
const adminPassword =
|
|
9345
|
+
const adminEmail = env10.SPFN_AUTH_ADMIN_EMAIL;
|
|
9346
|
+
const adminPassword = env10.SPFN_AUTH_ADMIN_PASSWORD;
|
|
8826
9347
|
if (adminEmail && adminPassword) {
|
|
8827
9348
|
accounts.push({
|
|
8828
9349
|
email: adminEmail,
|
|
@@ -8900,6 +9421,7 @@ function createAuthLifecycle(options = {}) {
|
|
|
8900
9421
|
};
|
|
8901
9422
|
}
|
|
8902
9423
|
export {
|
|
9424
|
+
AuthProviderSchema,
|
|
8903
9425
|
COOKIE_NAMES,
|
|
8904
9426
|
EmailSchema,
|
|
8905
9427
|
INVITATION_STATUSES,
|
|
@@ -8912,6 +9434,7 @@ export {
|
|
|
8912
9434
|
RolePermissionsRepository,
|
|
8913
9435
|
RolesRepository,
|
|
8914
9436
|
SOCIAL_PROVIDERS,
|
|
9437
|
+
SocialAccountsRepository,
|
|
8915
9438
|
TargetTypeSchema,
|
|
8916
9439
|
USER_STATUSES,
|
|
8917
9440
|
UserPermissionsRepository,
|
|
@@ -8924,19 +9447,25 @@ export {
|
|
|
8924
9447
|
acceptInvitation,
|
|
8925
9448
|
addPermissionToRole,
|
|
8926
9449
|
authLogger,
|
|
9450
|
+
authLoginEvent,
|
|
9451
|
+
authRegisterEvent,
|
|
8927
9452
|
mainAuthRouter as authRouter,
|
|
8928
9453
|
authSchema,
|
|
8929
9454
|
authenticate,
|
|
9455
|
+
buildOAuthErrorUrl,
|
|
8930
9456
|
cancelInvitation,
|
|
8931
9457
|
changePasswordService,
|
|
8932
9458
|
checkAccountExistsService,
|
|
9459
|
+
checkUsernameAvailableService,
|
|
8933
9460
|
configureAuth,
|
|
8934
9461
|
createAuthLifecycle,
|
|
8935
9462
|
createInvitation,
|
|
9463
|
+
createOAuthState,
|
|
8936
9464
|
createRole,
|
|
8937
9465
|
decodeToken,
|
|
8938
9466
|
deleteInvitation,
|
|
8939
9467
|
deleteRole,
|
|
9468
|
+
exchangeCodeForTokens,
|
|
8940
9469
|
expireOldInvitations,
|
|
8941
9470
|
generateClientToken,
|
|
8942
9471
|
generateKeyPair,
|
|
@@ -8947,12 +9476,18 @@ export {
|
|
|
8947
9476
|
getAuth,
|
|
8948
9477
|
getAuthConfig,
|
|
8949
9478
|
getAuthSessionService,
|
|
9479
|
+
getEnabledOAuthProviders,
|
|
9480
|
+
getGoogleAccessToken,
|
|
9481
|
+
getGoogleAuthUrl,
|
|
9482
|
+
getGoogleOAuthConfig,
|
|
9483
|
+
getGoogleUserInfo,
|
|
8950
9484
|
getInvitationByToken,
|
|
8951
|
-
getInvitationTemplate,
|
|
8952
9485
|
getInvitationWithDetails,
|
|
8953
9486
|
getKeyId,
|
|
8954
9487
|
getKeySize,
|
|
8955
|
-
|
|
9488
|
+
getLocale,
|
|
9489
|
+
getOptionalAuth,
|
|
9490
|
+
getRole,
|
|
8956
9491
|
getRoleByName,
|
|
8957
9492
|
getRolePermissions,
|
|
8958
9493
|
getSessionInfo,
|
|
@@ -8965,8 +9500,6 @@ export {
|
|
|
8965
9500
|
getUserPermissions,
|
|
8966
9501
|
getUserProfileService,
|
|
8967
9502
|
getUserRole,
|
|
8968
|
-
getVerificationCodeTemplate,
|
|
8969
|
-
getWelcomeTemplate,
|
|
8970
9503
|
hasAllPermissions,
|
|
8971
9504
|
hasAnyPermission,
|
|
8972
9505
|
hasAnyRole,
|
|
@@ -8975,17 +9508,20 @@ export {
|
|
|
8975
9508
|
hashPassword,
|
|
8976
9509
|
initializeAuth,
|
|
8977
9510
|
invitationsRepository,
|
|
9511
|
+
isGoogleOAuthEnabled,
|
|
9512
|
+
isOAuthProviderEnabled,
|
|
8978
9513
|
keysRepository,
|
|
8979
9514
|
listInvitations,
|
|
8980
9515
|
loginService,
|
|
8981
9516
|
logoutService,
|
|
9517
|
+
oauthCallbackService,
|
|
9518
|
+
oauthStartService,
|
|
9519
|
+
optionalAuth,
|
|
8982
9520
|
parseDuration,
|
|
8983
9521
|
permissions,
|
|
8984
9522
|
permissionsRepository,
|
|
8985
|
-
|
|
8986
|
-
registerEmailTemplates,
|
|
9523
|
+
refreshAccessToken,
|
|
8987
9524
|
registerPublicKeyService,
|
|
8988
|
-
registerSMSProvider,
|
|
8989
9525
|
registerService,
|
|
8990
9526
|
removePermissionFromRole,
|
|
8991
9527
|
requireAnyPermission,
|
|
@@ -9000,17 +9536,18 @@ export {
|
|
|
9000
9536
|
rolesRepository,
|
|
9001
9537
|
rotateKeyService,
|
|
9002
9538
|
sealSession,
|
|
9003
|
-
sendEmail,
|
|
9004
|
-
sendSMS,
|
|
9005
9539
|
sendVerificationCodeService,
|
|
9006
9540
|
setRolePermissions,
|
|
9007
9541
|
shouldRefreshSession,
|
|
9008
9542
|
shouldRotateKey,
|
|
9543
|
+
socialAccountsRepository,
|
|
9009
9544
|
unsealSession,
|
|
9010
9545
|
updateLastLoginService,
|
|
9546
|
+
updateLocaleService,
|
|
9011
9547
|
updateRole,
|
|
9012
9548
|
updateUserProfileService,
|
|
9013
9549
|
updateUserService,
|
|
9550
|
+
updateUsernameService,
|
|
9014
9551
|
userInvitations,
|
|
9015
9552
|
userPermissions,
|
|
9016
9553
|
userPermissionsRepository,
|
|
@@ -9027,6 +9564,7 @@ export {
|
|
|
9027
9564
|
verifyClientToken,
|
|
9028
9565
|
verifyCodeService,
|
|
9029
9566
|
verifyKeyFingerprint,
|
|
9567
|
+
verifyOAuthState,
|
|
9030
9568
|
verifyPassword,
|
|
9031
9569
|
verifyToken
|
|
9032
9570
|
};
|