@spfn/auth 0.2.0-beta.2 → 0.2.0-beta.21
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 +635 -180
- package/dist/{dto-CLYtuAom.d.ts → authenticate-BmzJ6hTF.d.ts} +364 -154
- package/dist/config.d.ts +58 -40
- package/dist/config.js +64 -35
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +256 -103
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +187 -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 +68 -2
- package/dist/nextjs/server.js +125 -4
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +427 -360
- package/dist/server.js +943 -477
- package/dist/server.js.map +1 -1
- package/migrations/0000_premium_famine.sql +292 -0
- package/migrations/meta/0000_snapshot.json +1 -1
- package/migrations/meta/_journal.json +2 -2
- package/package.json +15 -11
- package/migrations/0000_marvelous_justice.sql +0 -197
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,
|
|
@@ -6132,6 +6126,23 @@ var init_user_profiles_repository = __esm({
|
|
|
6132
6126
|
const result = await this.db.delete(userProfiles).where(eq8(userProfiles.userId, userId)).returning();
|
|
6133
6127
|
return result[0] ?? null;
|
|
6134
6128
|
}
|
|
6129
|
+
/**
|
|
6130
|
+
* 프로필 Upsert (by User ID)
|
|
6131
|
+
*
|
|
6132
|
+
* 프로필이 없으면 생성, 있으면 업데이트
|
|
6133
|
+
* 새로 생성 시 displayName은 필수 (없으면 'User'로 설정)
|
|
6134
|
+
*/
|
|
6135
|
+
async upsertByUserId(userId, data) {
|
|
6136
|
+
const existing = await this.findByUserId(userId);
|
|
6137
|
+
if (existing) {
|
|
6138
|
+
return await this.updateByUserId(userId, data);
|
|
6139
|
+
}
|
|
6140
|
+
return await this.create({
|
|
6141
|
+
userId,
|
|
6142
|
+
displayName: data.displayName || "User",
|
|
6143
|
+
...data
|
|
6144
|
+
});
|
|
6145
|
+
}
|
|
6135
6146
|
/**
|
|
6136
6147
|
* User ID로 프로필 데이터 조회 (formatted)
|
|
6137
6148
|
*
|
|
@@ -6151,6 +6162,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6151
6162
|
location: userProfiles.location,
|
|
6152
6163
|
company: userProfiles.company,
|
|
6153
6164
|
jobTitle: userProfiles.jobTitle,
|
|
6165
|
+
metadata: userProfiles.metadata,
|
|
6154
6166
|
createdAt: userProfiles.createdAt,
|
|
6155
6167
|
updatedAt: userProfiles.updatedAt
|
|
6156
6168
|
}).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
@@ -6170,6 +6182,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6170
6182
|
location: profile.location,
|
|
6171
6183
|
company: profile.company,
|
|
6172
6184
|
jobTitle: profile.jobTitle,
|
|
6185
|
+
metadata: profile.metadata,
|
|
6173
6186
|
createdAt: profile.createdAt,
|
|
6174
6187
|
updatedAt: profile.updatedAt
|
|
6175
6188
|
};
|
|
@@ -6394,6 +6407,96 @@ var init_invitations_repository = __esm({
|
|
|
6394
6407
|
}
|
|
6395
6408
|
});
|
|
6396
6409
|
|
|
6410
|
+
// src/server/repositories/social-accounts.repository.ts
|
|
6411
|
+
import { eq as eq10, and as and7 } from "drizzle-orm";
|
|
6412
|
+
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
6413
|
+
var SocialAccountsRepository, socialAccountsRepository;
|
|
6414
|
+
var init_social_accounts_repository = __esm({
|
|
6415
|
+
"src/server/repositories/social-accounts.repository.ts"() {
|
|
6416
|
+
"use strict";
|
|
6417
|
+
init_entities();
|
|
6418
|
+
SocialAccountsRepository = class extends BaseRepository10 {
|
|
6419
|
+
/**
|
|
6420
|
+
* provider와 providerUserId로 소셜 계정 조회
|
|
6421
|
+
* Read replica 사용
|
|
6422
|
+
*/
|
|
6423
|
+
async findByProviderAndProviderId(provider, providerUserId) {
|
|
6424
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6425
|
+
and7(
|
|
6426
|
+
eq10(userSocialAccounts.provider, provider),
|
|
6427
|
+
eq10(userSocialAccounts.providerUserId, providerUserId)
|
|
6428
|
+
)
|
|
6429
|
+
).limit(1);
|
|
6430
|
+
return result[0] ?? null;
|
|
6431
|
+
}
|
|
6432
|
+
/**
|
|
6433
|
+
* userId로 모든 소셜 계정 조회
|
|
6434
|
+
* Read replica 사용
|
|
6435
|
+
*/
|
|
6436
|
+
async findByUserId(userId) {
|
|
6437
|
+
return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
|
|
6438
|
+
}
|
|
6439
|
+
/**
|
|
6440
|
+
* userId와 provider로 소셜 계정 조회
|
|
6441
|
+
* Read replica 사용
|
|
6442
|
+
*/
|
|
6443
|
+
async findByUserIdAndProvider(userId, provider) {
|
|
6444
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
6445
|
+
and7(
|
|
6446
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6447
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6448
|
+
)
|
|
6449
|
+
).limit(1);
|
|
6450
|
+
return result[0] ?? null;
|
|
6451
|
+
}
|
|
6452
|
+
/**
|
|
6453
|
+
* 소셜 계정 생성
|
|
6454
|
+
* Write primary 사용
|
|
6455
|
+
*/
|
|
6456
|
+
async create(data) {
|
|
6457
|
+
return await this._create(userSocialAccounts, {
|
|
6458
|
+
...data,
|
|
6459
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
6460
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6461
|
+
});
|
|
6462
|
+
}
|
|
6463
|
+
/**
|
|
6464
|
+
* 토큰 정보 업데이트
|
|
6465
|
+
* Write primary 사용
|
|
6466
|
+
*/
|
|
6467
|
+
async updateTokens(id11, data) {
|
|
6468
|
+
const result = await this.db.update(userSocialAccounts).set({
|
|
6469
|
+
...data,
|
|
6470
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6471
|
+
}).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6472
|
+
return result[0] ?? null;
|
|
6473
|
+
}
|
|
6474
|
+
/**
|
|
6475
|
+
* 소셜 계정 삭제
|
|
6476
|
+
* Write primary 사용
|
|
6477
|
+
*/
|
|
6478
|
+
async deleteById(id11) {
|
|
6479
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6480
|
+
return result[0] ?? null;
|
|
6481
|
+
}
|
|
6482
|
+
/**
|
|
6483
|
+
* userId와 provider로 소셜 계정 삭제
|
|
6484
|
+
* Write primary 사용
|
|
6485
|
+
*/
|
|
6486
|
+
async deleteByUserIdAndProvider(userId, provider) {
|
|
6487
|
+
const result = await this.db.delete(userSocialAccounts).where(
|
|
6488
|
+
and7(
|
|
6489
|
+
eq10(userSocialAccounts.userId, userId),
|
|
6490
|
+
eq10(userSocialAccounts.provider, provider)
|
|
6491
|
+
)
|
|
6492
|
+
).returning();
|
|
6493
|
+
return result[0] ?? null;
|
|
6494
|
+
}
|
|
6495
|
+
};
|
|
6496
|
+
socialAccountsRepository = new SocialAccountsRepository();
|
|
6497
|
+
}
|
|
6498
|
+
});
|
|
6499
|
+
|
|
6397
6500
|
// src/server/repositories/index.ts
|
|
6398
6501
|
var init_repositories = __esm({
|
|
6399
6502
|
"src/server/repositories/index.ts"() {
|
|
@@ -6407,6 +6510,7 @@ var init_repositories = __esm({
|
|
|
6407
6510
|
init_user_permissions_repository();
|
|
6408
6511
|
init_user_profiles_repository();
|
|
6409
6512
|
init_invitations_repository();
|
|
6513
|
+
init_social_accounts_repository();
|
|
6410
6514
|
}
|
|
6411
6515
|
});
|
|
6412
6516
|
|
|
@@ -6533,7 +6637,7 @@ var init_role_service = __esm({
|
|
|
6533
6637
|
import "@spfn/auth/config";
|
|
6534
6638
|
|
|
6535
6639
|
// src/server/routes/index.ts
|
|
6536
|
-
import { defineRouter as
|
|
6640
|
+
import { defineRouter as defineRouter5 } from "@spfn/core/route";
|
|
6537
6641
|
|
|
6538
6642
|
// src/server/routes/auth/index.ts
|
|
6539
6643
|
init_schema3();
|
|
@@ -6682,9 +6786,10 @@ import {
|
|
|
6682
6786
|
} from "@spfn/auth/errors";
|
|
6683
6787
|
|
|
6684
6788
|
// src/server/services/verification.service.ts
|
|
6685
|
-
import { env as
|
|
6789
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
6686
6790
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
6687
6791
|
import jwt2 from "jsonwebtoken";
|
|
6792
|
+
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
6688
6793
|
|
|
6689
6794
|
// src/server/logger.ts
|
|
6690
6795
|
import { logger as rootLogger } from "@spfn/core/logger";
|
|
@@ -6694,7 +6799,8 @@ var authLogger = {
|
|
|
6694
6799
|
interceptor: {
|
|
6695
6800
|
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
6696
6801
|
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
6697
|
-
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation")
|
|
6802
|
+
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
6803
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
6698
6804
|
},
|
|
6699
6805
|
service: rootLogger.child("@spfn/auth:service"),
|
|
6700
6806
|
setup: rootLogger.child("@spfn/auth:setup"),
|
|
@@ -6704,410 +6810,6 @@ var authLogger = {
|
|
|
6704
6810
|
|
|
6705
6811
|
// src/server/services/verification.service.ts
|
|
6706
6812
|
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
6813
|
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
7112
6814
|
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
7113
6815
|
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
@@ -7151,7 +6853,7 @@ async function markCodeAsUsed(codeId) {
|
|
|
7151
6853
|
await verificationCodesRepository.markAsUsed(codeId);
|
|
7152
6854
|
}
|
|
7153
6855
|
function createVerificationToken(payload) {
|
|
7154
|
-
return jwt2.sign(payload,
|
|
6856
|
+
return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7155
6857
|
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
7156
6858
|
issuer: "spfn-auth",
|
|
7157
6859
|
audience: "spfn-client"
|
|
@@ -7159,7 +6861,7 @@ function createVerificationToken(payload) {
|
|
|
7159
6861
|
}
|
|
7160
6862
|
function validateVerificationToken(token) {
|
|
7161
6863
|
try {
|
|
7162
|
-
const decoded = jwt2.verify(token,
|
|
6864
|
+
const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7163
6865
|
issuer: "spfn-auth",
|
|
7164
6866
|
audience: "spfn-client"
|
|
7165
6867
|
});
|
|
@@ -7173,17 +6875,14 @@ function validateVerificationToken(token) {
|
|
|
7173
6875
|
}
|
|
7174
6876
|
}
|
|
7175
6877
|
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
6878
|
const result = await sendEmail({
|
|
7182
6879
|
to: email,
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
6880
|
+
template: "verification-code",
|
|
6881
|
+
data: {
|
|
6882
|
+
code,
|
|
6883
|
+
purpose,
|
|
6884
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
6885
|
+
}
|
|
7187
6886
|
});
|
|
7188
6887
|
if (!result.success) {
|
|
7189
6888
|
authLogger.email.error("Failed to send verification email", {
|
|
@@ -7194,11 +6893,13 @@ async function sendVerificationEmail(email, code, purpose) {
|
|
|
7194
6893
|
}
|
|
7195
6894
|
}
|
|
7196
6895
|
async function sendVerificationSMS(phone, code, purpose) {
|
|
7197
|
-
const message = `Your verification code is: ${code}`;
|
|
7198
6896
|
const result = await sendSMS({
|
|
7199
|
-
phone,
|
|
7200
|
-
|
|
7201
|
-
|
|
6897
|
+
to: phone,
|
|
6898
|
+
template: "verification-code",
|
|
6899
|
+
data: {
|
|
6900
|
+
code,
|
|
6901
|
+
expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
|
|
6902
|
+
}
|
|
7202
6903
|
});
|
|
7203
6904
|
if (!result.success) {
|
|
7204
6905
|
authLogger.sms.error("Failed to send verification SMS", {
|
|
@@ -7315,6 +7016,33 @@ async function updateUserService(userId, updates) {
|
|
|
7315
7016
|
await usersRepository.updateById(userId, updates);
|
|
7316
7017
|
}
|
|
7317
7018
|
|
|
7019
|
+
// src/server/events/index.ts
|
|
7020
|
+
init_esm();
|
|
7021
|
+
import { defineEvent } from "@spfn/core/event";
|
|
7022
|
+
var AuthProviderSchema = Type.Union([
|
|
7023
|
+
Type.Literal("email"),
|
|
7024
|
+
Type.Literal("phone"),
|
|
7025
|
+
Type.Literal("google")
|
|
7026
|
+
]);
|
|
7027
|
+
var authLoginEvent = defineEvent(
|
|
7028
|
+
"auth.login",
|
|
7029
|
+
Type.Object({
|
|
7030
|
+
userId: Type.String(),
|
|
7031
|
+
provider: AuthProviderSchema,
|
|
7032
|
+
email: Type.Optional(Type.String()),
|
|
7033
|
+
phone: Type.Optional(Type.String())
|
|
7034
|
+
})
|
|
7035
|
+
);
|
|
7036
|
+
var authRegisterEvent = defineEvent(
|
|
7037
|
+
"auth.register",
|
|
7038
|
+
Type.Object({
|
|
7039
|
+
userId: Type.String(),
|
|
7040
|
+
provider: AuthProviderSchema,
|
|
7041
|
+
email: Type.Optional(Type.String()),
|
|
7042
|
+
phone: Type.Optional(Type.String())
|
|
7043
|
+
})
|
|
7044
|
+
);
|
|
7045
|
+
|
|
7318
7046
|
// src/server/services/auth.service.ts
|
|
7319
7047
|
async function checkAccountExistsService(params) {
|
|
7320
7048
|
const { email, phone } = params;
|
|
@@ -7381,11 +7109,18 @@ async function registerService(params) {
|
|
|
7381
7109
|
fingerprint,
|
|
7382
7110
|
algorithm
|
|
7383
7111
|
});
|
|
7384
|
-
|
|
7112
|
+
const result = {
|
|
7385
7113
|
userId: String(newUser.id),
|
|
7386
7114
|
email: newUser.email || void 0,
|
|
7387
7115
|
phone: newUser.phone || void 0
|
|
7388
7116
|
};
|
|
7117
|
+
await authRegisterEvent.emit({
|
|
7118
|
+
userId: result.userId,
|
|
7119
|
+
provider: email ? "email" : "phone",
|
|
7120
|
+
email: result.email,
|
|
7121
|
+
phone: result.phone
|
|
7122
|
+
});
|
|
7123
|
+
return result;
|
|
7389
7124
|
}
|
|
7390
7125
|
async function loginService(params) {
|
|
7391
7126
|
const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
|
|
@@ -7418,12 +7153,19 @@ async function loginService(params) {
|
|
|
7418
7153
|
algorithm
|
|
7419
7154
|
});
|
|
7420
7155
|
await updateLastLoginService(user.id);
|
|
7421
|
-
|
|
7156
|
+
const result = {
|
|
7422
7157
|
userId: String(user.id),
|
|
7423
7158
|
email: user.email || void 0,
|
|
7424
7159
|
phone: user.phone || void 0,
|
|
7425
7160
|
passwordChangeRequired: user.passwordChangeRequired
|
|
7426
7161
|
};
|
|
7162
|
+
await authLoginEvent.emit({
|
|
7163
|
+
userId: result.userId,
|
|
7164
|
+
provider: email ? "email" : "phone",
|
|
7165
|
+
email: result.email,
|
|
7166
|
+
phone: result.phone
|
|
7167
|
+
});
|
|
7168
|
+
return result;
|
|
7427
7169
|
}
|
|
7428
7170
|
async function logoutService(params) {
|
|
7429
7171
|
const { userId, keyId } = params;
|
|
@@ -7461,12 +7203,14 @@ init_repositories();
|
|
|
7461
7203
|
init_rbac();
|
|
7462
7204
|
|
|
7463
7205
|
// src/server/lib/config.ts
|
|
7464
|
-
import { env as
|
|
7206
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
7465
7207
|
var COOKIE_NAMES = {
|
|
7466
7208
|
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
7467
7209
|
SESSION: "spfn_session",
|
|
7468
7210
|
/** Current key ID (for key rotation) */
|
|
7469
|
-
SESSION_KEY_ID: "spfn_session_key_id"
|
|
7211
|
+
SESSION_KEY_ID: "spfn_session_key_id",
|
|
7212
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7213
|
+
OAUTH_PENDING: "spfn_oauth_pending"
|
|
7470
7214
|
};
|
|
7471
7215
|
function parseDuration(duration) {
|
|
7472
7216
|
if (typeof duration === "number") {
|
|
@@ -7511,7 +7255,7 @@ function getSessionTtl(override) {
|
|
|
7511
7255
|
if (globalConfig.sessionTtl !== void 0) {
|
|
7512
7256
|
return parseDuration(globalConfig.sessionTtl);
|
|
7513
7257
|
}
|
|
7514
|
-
const envTtl =
|
|
7258
|
+
const envTtl = env4.SPFN_AUTH_SESSION_TTL;
|
|
7515
7259
|
if (envTtl) {
|
|
7516
7260
|
return parseDuration(envTtl);
|
|
7517
7261
|
}
|
|
@@ -7673,14 +7417,18 @@ async function hasAllPermissions(userId, permissionNames) {
|
|
|
7673
7417
|
const perms = await getUserPermissions(userId);
|
|
7674
7418
|
return permissionNames.every((p) => perms.includes(p));
|
|
7675
7419
|
}
|
|
7676
|
-
async function
|
|
7420
|
+
async function getUserRole(userId) {
|
|
7677
7421
|
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7678
7422
|
const user = await usersRepository.findById(userIdNum);
|
|
7679
7423
|
if (!user || !user.roleId) {
|
|
7680
|
-
return
|
|
7424
|
+
return null;
|
|
7681
7425
|
}
|
|
7682
7426
|
const role = await rolesRepository.findById(user.roleId);
|
|
7683
|
-
return role?.name
|
|
7427
|
+
return role?.name || null;
|
|
7428
|
+
}
|
|
7429
|
+
async function hasRole(userId, roleName) {
|
|
7430
|
+
const role = await getUserRole(userId);
|
|
7431
|
+
return role === roleName;
|
|
7684
7432
|
}
|
|
7685
7433
|
async function hasAnyRole(userId, roleNames) {
|
|
7686
7434
|
for (const roleName of roleNames) {
|
|
@@ -7887,6 +7635,406 @@ async function getUserProfileService(userId) {
|
|
|
7887
7635
|
profile
|
|
7888
7636
|
};
|
|
7889
7637
|
}
|
|
7638
|
+
function emptyToNull(value) {
|
|
7639
|
+
if (value === "") {
|
|
7640
|
+
return null;
|
|
7641
|
+
}
|
|
7642
|
+
return value;
|
|
7643
|
+
}
|
|
7644
|
+
async function updateUserProfileService(userId, params) {
|
|
7645
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7646
|
+
const updateData = {};
|
|
7647
|
+
if (params.displayName !== void 0) {
|
|
7648
|
+
updateData.displayName = emptyToNull(params.displayName) || "User";
|
|
7649
|
+
}
|
|
7650
|
+
if (params.firstName !== void 0) {
|
|
7651
|
+
updateData.firstName = emptyToNull(params.firstName);
|
|
7652
|
+
}
|
|
7653
|
+
if (params.lastName !== void 0) {
|
|
7654
|
+
updateData.lastName = emptyToNull(params.lastName);
|
|
7655
|
+
}
|
|
7656
|
+
if (params.avatarUrl !== void 0) {
|
|
7657
|
+
updateData.avatarUrl = emptyToNull(params.avatarUrl);
|
|
7658
|
+
}
|
|
7659
|
+
if (params.bio !== void 0) {
|
|
7660
|
+
updateData.bio = emptyToNull(params.bio);
|
|
7661
|
+
}
|
|
7662
|
+
if (params.locale !== void 0) {
|
|
7663
|
+
updateData.locale = emptyToNull(params.locale) || "en";
|
|
7664
|
+
}
|
|
7665
|
+
if (params.timezone !== void 0) {
|
|
7666
|
+
updateData.timezone = emptyToNull(params.timezone) || "UTC";
|
|
7667
|
+
}
|
|
7668
|
+
if (params.dateOfBirth !== void 0) {
|
|
7669
|
+
updateData.dateOfBirth = emptyToNull(params.dateOfBirth);
|
|
7670
|
+
}
|
|
7671
|
+
if (params.gender !== void 0) {
|
|
7672
|
+
updateData.gender = emptyToNull(params.gender);
|
|
7673
|
+
}
|
|
7674
|
+
if (params.website !== void 0) {
|
|
7675
|
+
updateData.website = emptyToNull(params.website);
|
|
7676
|
+
}
|
|
7677
|
+
if (params.location !== void 0) {
|
|
7678
|
+
updateData.location = emptyToNull(params.location);
|
|
7679
|
+
}
|
|
7680
|
+
if (params.company !== void 0) {
|
|
7681
|
+
updateData.company = emptyToNull(params.company);
|
|
7682
|
+
}
|
|
7683
|
+
if (params.jobTitle !== void 0) {
|
|
7684
|
+
updateData.jobTitle = emptyToNull(params.jobTitle);
|
|
7685
|
+
}
|
|
7686
|
+
if (params.metadata !== void 0) {
|
|
7687
|
+
updateData.metadata = params.metadata;
|
|
7688
|
+
}
|
|
7689
|
+
const existing = await userProfilesRepository.findByUserId(userIdNum);
|
|
7690
|
+
if (!existing && !updateData.displayName) {
|
|
7691
|
+
updateData.displayName = "User";
|
|
7692
|
+
}
|
|
7693
|
+
await userProfilesRepository.upsertByUserId(userIdNum, updateData);
|
|
7694
|
+
const profile = await userProfilesRepository.fetchProfileData(userIdNum);
|
|
7695
|
+
return profile;
|
|
7696
|
+
}
|
|
7697
|
+
|
|
7698
|
+
// src/server/services/oauth.service.ts
|
|
7699
|
+
init_repositories();
|
|
7700
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
7701
|
+
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
7702
|
+
|
|
7703
|
+
// src/server/lib/oauth/google.ts
|
|
7704
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7705
|
+
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
7706
|
+
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
7707
|
+
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
7708
|
+
function isGoogleOAuthEnabled() {
|
|
7709
|
+
return !!(env5.SPFN_AUTH_GOOGLE_CLIENT_ID && env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
7710
|
+
}
|
|
7711
|
+
function getGoogleOAuthConfig() {
|
|
7712
|
+
const clientId = env5.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
7713
|
+
const clientSecret = env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
7714
|
+
if (!clientId || !clientSecret) {
|
|
7715
|
+
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
7716
|
+
}
|
|
7717
|
+
const baseUrl = env5.NEXT_PUBLIC_SPFN_API_URL || env5.SPFN_API_URL;
|
|
7718
|
+
const redirectUri = env5.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
7719
|
+
return {
|
|
7720
|
+
clientId,
|
|
7721
|
+
clientSecret,
|
|
7722
|
+
redirectUri
|
|
7723
|
+
};
|
|
7724
|
+
}
|
|
7725
|
+
function getDefaultScopes() {
|
|
7726
|
+
const envScopes = env5.SPFN_AUTH_GOOGLE_SCOPES;
|
|
7727
|
+
if (envScopes) {
|
|
7728
|
+
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7729
|
+
}
|
|
7730
|
+
return ["email", "profile"];
|
|
7731
|
+
}
|
|
7732
|
+
function getGoogleAuthUrl(state, scopes) {
|
|
7733
|
+
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
7734
|
+
const config = getGoogleOAuthConfig();
|
|
7735
|
+
const params = new URLSearchParams({
|
|
7736
|
+
client_id: config.clientId,
|
|
7737
|
+
redirect_uri: config.redirectUri,
|
|
7738
|
+
response_type: "code",
|
|
7739
|
+
scope: resolvedScopes.join(" "),
|
|
7740
|
+
state,
|
|
7741
|
+
access_type: "offline",
|
|
7742
|
+
// refresh_token 받기 위해
|
|
7743
|
+
prompt: "consent"
|
|
7744
|
+
// 매번 동의 화면 표시 (refresh_token 보장)
|
|
7745
|
+
});
|
|
7746
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
7747
|
+
}
|
|
7748
|
+
async function exchangeCodeForTokens(code) {
|
|
7749
|
+
const config = getGoogleOAuthConfig();
|
|
7750
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7751
|
+
method: "POST",
|
|
7752
|
+
headers: {
|
|
7753
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7754
|
+
},
|
|
7755
|
+
body: new URLSearchParams({
|
|
7756
|
+
client_id: config.clientId,
|
|
7757
|
+
client_secret: config.clientSecret,
|
|
7758
|
+
redirect_uri: config.redirectUri,
|
|
7759
|
+
grant_type: "authorization_code",
|
|
7760
|
+
code
|
|
7761
|
+
})
|
|
7762
|
+
});
|
|
7763
|
+
if (!response.ok) {
|
|
7764
|
+
const error = await response.text();
|
|
7765
|
+
throw new Error(`Failed to exchange code for tokens: ${error}`);
|
|
7766
|
+
}
|
|
7767
|
+
return response.json();
|
|
7768
|
+
}
|
|
7769
|
+
async function getGoogleUserInfo(accessToken) {
|
|
7770
|
+
const response = await fetch(GOOGLE_USERINFO_URL, {
|
|
7771
|
+
headers: {
|
|
7772
|
+
Authorization: `Bearer ${accessToken}`
|
|
7773
|
+
}
|
|
7774
|
+
});
|
|
7775
|
+
if (!response.ok) {
|
|
7776
|
+
const error = await response.text();
|
|
7777
|
+
throw new Error(`Failed to get user info: ${error}`);
|
|
7778
|
+
}
|
|
7779
|
+
return response.json();
|
|
7780
|
+
}
|
|
7781
|
+
async function refreshAccessToken(refreshToken) {
|
|
7782
|
+
const config = getGoogleOAuthConfig();
|
|
7783
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
7784
|
+
method: "POST",
|
|
7785
|
+
headers: {
|
|
7786
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
7787
|
+
},
|
|
7788
|
+
body: new URLSearchParams({
|
|
7789
|
+
client_id: config.clientId,
|
|
7790
|
+
client_secret: config.clientSecret,
|
|
7791
|
+
refresh_token: refreshToken,
|
|
7792
|
+
grant_type: "refresh_token"
|
|
7793
|
+
})
|
|
7794
|
+
});
|
|
7795
|
+
if (!response.ok) {
|
|
7796
|
+
const error = await response.text();
|
|
7797
|
+
throw new Error(`Failed to refresh access token: ${error}`);
|
|
7798
|
+
}
|
|
7799
|
+
return response.json();
|
|
7800
|
+
}
|
|
7801
|
+
|
|
7802
|
+
// src/server/lib/oauth/state.ts
|
|
7803
|
+
import * as jose from "jose";
|
|
7804
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
7805
|
+
async function getStateKey() {
|
|
7806
|
+
const secret = env6.SPFN_AUTH_SESSION_SECRET;
|
|
7807
|
+
const encoder = new TextEncoder();
|
|
7808
|
+
const data = encoder.encode(`oauth-state:${secret}`);
|
|
7809
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
7810
|
+
return new Uint8Array(hashBuffer);
|
|
7811
|
+
}
|
|
7812
|
+
function generateNonce() {
|
|
7813
|
+
const array = new Uint8Array(16);
|
|
7814
|
+
crypto.getRandomValues(array);
|
|
7815
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
7816
|
+
}
|
|
7817
|
+
async function createOAuthState(params) {
|
|
7818
|
+
const key = await getStateKey();
|
|
7819
|
+
const state = {
|
|
7820
|
+
returnUrl: params.returnUrl,
|
|
7821
|
+
nonce: generateNonce(),
|
|
7822
|
+
provider: params.provider,
|
|
7823
|
+
publicKey: params.publicKey,
|
|
7824
|
+
keyId: params.keyId,
|
|
7825
|
+
fingerprint: params.fingerprint,
|
|
7826
|
+
algorithm: params.algorithm
|
|
7827
|
+
};
|
|
7828
|
+
const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
|
|
7829
|
+
return encodeURIComponent(jwe);
|
|
7830
|
+
}
|
|
7831
|
+
async function verifyOAuthState(encryptedState) {
|
|
7832
|
+
const key = await getStateKey();
|
|
7833
|
+
const jwe = decodeURIComponent(encryptedState);
|
|
7834
|
+
const { payload } = await jose.jwtDecrypt(jwe, key);
|
|
7835
|
+
return payload.state;
|
|
7836
|
+
}
|
|
7837
|
+
|
|
7838
|
+
// src/server/services/oauth.service.ts
|
|
7839
|
+
async function oauthStartService(params) {
|
|
7840
|
+
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm } = params;
|
|
7841
|
+
if (provider === "google") {
|
|
7842
|
+
if (!isGoogleOAuthEnabled()) {
|
|
7843
|
+
throw new ValidationError2({
|
|
7844
|
+
message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
|
|
7845
|
+
});
|
|
7846
|
+
}
|
|
7847
|
+
const state = await createOAuthState({
|
|
7848
|
+
provider: "google",
|
|
7849
|
+
returnUrl,
|
|
7850
|
+
publicKey,
|
|
7851
|
+
keyId,
|
|
7852
|
+
fingerprint,
|
|
7853
|
+
algorithm
|
|
7854
|
+
});
|
|
7855
|
+
const authUrl = getGoogleAuthUrl(state);
|
|
7856
|
+
return { authUrl };
|
|
7857
|
+
}
|
|
7858
|
+
throw new ValidationError2({
|
|
7859
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7860
|
+
});
|
|
7861
|
+
}
|
|
7862
|
+
async function oauthCallbackService(params) {
|
|
7863
|
+
const { provider, code, state } = params;
|
|
7864
|
+
const stateData = await verifyOAuthState(state);
|
|
7865
|
+
if (stateData.provider !== provider) {
|
|
7866
|
+
throw new ValidationError2({
|
|
7867
|
+
message: "OAuth state provider mismatch"
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7870
|
+
if (provider === "google") {
|
|
7871
|
+
return handleGoogleCallback(code, stateData);
|
|
7872
|
+
}
|
|
7873
|
+
throw new ValidationError2({
|
|
7874
|
+
message: `Unsupported OAuth provider: ${provider}`
|
|
7875
|
+
});
|
|
7876
|
+
}
|
|
7877
|
+
async function handleGoogleCallback(code, stateData) {
|
|
7878
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
7879
|
+
const googleUser = await getGoogleUserInfo(tokens.access_token);
|
|
7880
|
+
const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
|
|
7881
|
+
"google",
|
|
7882
|
+
googleUser.id
|
|
7883
|
+
);
|
|
7884
|
+
let userId;
|
|
7885
|
+
let isNewUser = false;
|
|
7886
|
+
if (existingSocialAccount) {
|
|
7887
|
+
userId = existingSocialAccount.userId;
|
|
7888
|
+
await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
|
|
7889
|
+
accessToken: tokens.access_token,
|
|
7890
|
+
refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
|
|
7891
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
7892
|
+
});
|
|
7893
|
+
} else {
|
|
7894
|
+
const result = await createOrLinkUser(googleUser, tokens);
|
|
7895
|
+
userId = result.userId;
|
|
7896
|
+
isNewUser = result.isNewUser;
|
|
7897
|
+
}
|
|
7898
|
+
await registerPublicKeyService({
|
|
7899
|
+
userId,
|
|
7900
|
+
keyId: stateData.keyId,
|
|
7901
|
+
publicKey: stateData.publicKey,
|
|
7902
|
+
fingerprint: stateData.fingerprint,
|
|
7903
|
+
algorithm: stateData.algorithm
|
|
7904
|
+
});
|
|
7905
|
+
await updateLastLoginService(userId);
|
|
7906
|
+
const appUrl = env7.NEXT_PUBLIC_SPFN_APP_URL || env7.SPFN_APP_URL;
|
|
7907
|
+
const callbackPath = env7.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
7908
|
+
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
7909
|
+
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
7910
|
+
userId: String(userId),
|
|
7911
|
+
keyId: stateData.keyId,
|
|
7912
|
+
returnUrl: stateData.returnUrl,
|
|
7913
|
+
isNewUser: String(isNewUser)
|
|
7914
|
+
});
|
|
7915
|
+
const user = await usersRepository.findById(userId);
|
|
7916
|
+
const eventPayload = {
|
|
7917
|
+
userId: String(userId),
|
|
7918
|
+
provider: "google",
|
|
7919
|
+
email: user?.email || void 0,
|
|
7920
|
+
phone: user?.phone || void 0
|
|
7921
|
+
};
|
|
7922
|
+
if (isNewUser) {
|
|
7923
|
+
await authRegisterEvent.emit(eventPayload);
|
|
7924
|
+
} else {
|
|
7925
|
+
await authLoginEvent.emit(eventPayload);
|
|
7926
|
+
}
|
|
7927
|
+
return {
|
|
7928
|
+
redirectUrl,
|
|
7929
|
+
userId: String(userId),
|
|
7930
|
+
keyId: stateData.keyId,
|
|
7931
|
+
isNewUser
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7934
|
+
async function createOrLinkUser(googleUser, tokens) {
|
|
7935
|
+
const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
|
|
7936
|
+
let userId;
|
|
7937
|
+
let isNewUser = false;
|
|
7938
|
+
if (existingUser) {
|
|
7939
|
+
if (!googleUser.verified_email) {
|
|
7940
|
+
throw new ValidationError2({
|
|
7941
|
+
message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
|
|
7942
|
+
});
|
|
7943
|
+
}
|
|
7944
|
+
userId = existingUser.id;
|
|
7945
|
+
if (!existingUser.emailVerifiedAt) {
|
|
7946
|
+
await usersRepository.updateById(existingUser.id, {
|
|
7947
|
+
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
7948
|
+
});
|
|
7949
|
+
}
|
|
7950
|
+
} else {
|
|
7951
|
+
const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
7952
|
+
const userRole = await getRoleByName3("user");
|
|
7953
|
+
if (!userRole) {
|
|
7954
|
+
throw new Error("Default user role not found. Run initializeAuth() first.");
|
|
7955
|
+
}
|
|
7956
|
+
const newUser = await usersRepository.create({
|
|
7957
|
+
email: googleUser.verified_email ? googleUser.email : null,
|
|
7958
|
+
phone: null,
|
|
7959
|
+
passwordHash: null,
|
|
7960
|
+
// OAuth 사용자는 비밀번호 없음
|
|
7961
|
+
passwordChangeRequired: false,
|
|
7962
|
+
roleId: userRole.id,
|
|
7963
|
+
status: "active",
|
|
7964
|
+
emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
|
|
7965
|
+
});
|
|
7966
|
+
userId = newUser.id;
|
|
7967
|
+
isNewUser = true;
|
|
7968
|
+
}
|
|
7969
|
+
await socialAccountsRepository.create({
|
|
7970
|
+
userId,
|
|
7971
|
+
provider: "google",
|
|
7972
|
+
providerUserId: googleUser.id,
|
|
7973
|
+
providerEmail: googleUser.email,
|
|
7974
|
+
accessToken: tokens.access_token,
|
|
7975
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
7976
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
7977
|
+
});
|
|
7978
|
+
return { userId, isNewUser };
|
|
7979
|
+
}
|
|
7980
|
+
function buildRedirectUrl(baseUrl, params) {
|
|
7981
|
+
const url = new URL(baseUrl, "http://placeholder");
|
|
7982
|
+
for (const [key, value] of Object.entries(params)) {
|
|
7983
|
+
url.searchParams.set(key, value);
|
|
7984
|
+
}
|
|
7985
|
+
if (baseUrl.startsWith("http")) {
|
|
7986
|
+
return url.toString();
|
|
7987
|
+
}
|
|
7988
|
+
return `${url.pathname}${url.search}`;
|
|
7989
|
+
}
|
|
7990
|
+
function buildOAuthErrorUrl(error) {
|
|
7991
|
+
const errorUrl = env7.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
7992
|
+
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
7993
|
+
}
|
|
7994
|
+
function isOAuthProviderEnabled(provider) {
|
|
7995
|
+
switch (provider) {
|
|
7996
|
+
case "google":
|
|
7997
|
+
return isGoogleOAuthEnabled();
|
|
7998
|
+
case "github":
|
|
7999
|
+
case "kakao":
|
|
8000
|
+
case "naver":
|
|
8001
|
+
return false;
|
|
8002
|
+
default:
|
|
8003
|
+
return false;
|
|
8004
|
+
}
|
|
8005
|
+
}
|
|
8006
|
+
function getEnabledOAuthProviders() {
|
|
8007
|
+
const providers = [];
|
|
8008
|
+
if (isGoogleOAuthEnabled()) {
|
|
8009
|
+
providers.push("google");
|
|
8010
|
+
}
|
|
8011
|
+
return providers;
|
|
8012
|
+
}
|
|
8013
|
+
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
8014
|
+
async function getGoogleAccessToken(userId) {
|
|
8015
|
+
const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
|
|
8016
|
+
if (!account) {
|
|
8017
|
+
throw new ValidationError2({
|
|
8018
|
+
message: "No Google account linked. User must sign in with Google first."
|
|
8019
|
+
});
|
|
8020
|
+
}
|
|
8021
|
+
const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
|
|
8022
|
+
if (!isExpired && account.accessToken) {
|
|
8023
|
+
return account.accessToken;
|
|
8024
|
+
}
|
|
8025
|
+
if (!account.refreshToken) {
|
|
8026
|
+
throw new ValidationError2({
|
|
8027
|
+
message: "Google refresh token not available. User must re-authenticate with Google."
|
|
8028
|
+
});
|
|
8029
|
+
}
|
|
8030
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
8031
|
+
await socialAccountsRepository.updateTokens(account.id, {
|
|
8032
|
+
accessToken: tokens.access_token,
|
|
8033
|
+
refreshToken: tokens.refresh_token ?? account.refreshToken,
|
|
8034
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
|
|
8035
|
+
});
|
|
8036
|
+
return tokens.access_token;
|
|
8037
|
+
}
|
|
7890
8038
|
|
|
7891
8039
|
// src/server/routes/auth/index.ts
|
|
7892
8040
|
init_esm();
|
|
@@ -7980,9 +8128,7 @@ var login = route.post("/_auth/login").input({
|
|
|
7980
8128
|
const { body } = await c.data();
|
|
7981
8129
|
return await loginService(body);
|
|
7982
8130
|
});
|
|
7983
|
-
var logout = route.post("/_auth/logout").
|
|
7984
|
-
body: Type.Object({})
|
|
7985
|
-
}).handler(async (c) => {
|
|
8131
|
+
var logout = route.post("/_auth/logout").handler(async (c) => {
|
|
7986
8132
|
const auth = getAuth(c);
|
|
7987
8133
|
if (!auth) {
|
|
7988
8134
|
return c.noContent();
|
|
@@ -7991,9 +8137,7 @@ var logout = route.post("/_auth/logout").input({
|
|
|
7991
8137
|
await logoutService({ userId: Number(userId), keyId });
|
|
7992
8138
|
return c.noContent();
|
|
7993
8139
|
});
|
|
7994
|
-
var rotateKey = route.post("/_auth/keys/rotate").
|
|
7995
|
-
body: Type.Object({})
|
|
7996
|
-
}).interceptor({
|
|
8140
|
+
var rotateKey = route.post("/_auth/keys/rotate").interceptor({
|
|
7997
8141
|
body: Type.Object({
|
|
7998
8142
|
publicKey: Type.String({ description: "New public key" }),
|
|
7999
8143
|
keyId: Type.String({ description: "New key identifier" }),
|
|
@@ -8223,6 +8367,59 @@ var requireRole = defineMiddleware3(
|
|
|
8223
8367
|
}
|
|
8224
8368
|
);
|
|
8225
8369
|
|
|
8370
|
+
// src/server/middleware/role-guard.ts
|
|
8371
|
+
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
8372
|
+
import { getAuth as getAuth4, getUserRole as getUserRole2, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
8373
|
+
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
8374
|
+
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
8375
|
+
var roleGuard = defineMiddleware4(
|
|
8376
|
+
"roleGuard",
|
|
8377
|
+
(options) => async (c, next) => {
|
|
8378
|
+
const { allow, deny } = options;
|
|
8379
|
+
if (!allow && !deny) {
|
|
8380
|
+
throw new Error("roleGuard requires at least one of: allow, deny");
|
|
8381
|
+
}
|
|
8382
|
+
const auth = getAuth4(c);
|
|
8383
|
+
if (!auth) {
|
|
8384
|
+
authLogger5.middleware.warn("Role guard failed: not authenticated", {
|
|
8385
|
+
path: c.req.path
|
|
8386
|
+
});
|
|
8387
|
+
throw new ForbiddenError3({ message: "Authentication required" });
|
|
8388
|
+
}
|
|
8389
|
+
const { userId } = auth;
|
|
8390
|
+
const userRole = await getUserRole2(userId);
|
|
8391
|
+
if (deny && deny.length > 0) {
|
|
8392
|
+
if (userRole && deny.includes(userRole)) {
|
|
8393
|
+
authLogger5.middleware.warn("Role guard denied", {
|
|
8394
|
+
userId,
|
|
8395
|
+
userRole,
|
|
8396
|
+
deniedRoles: deny,
|
|
8397
|
+
path: c.req.path
|
|
8398
|
+
});
|
|
8399
|
+
throw new InsufficientRoleError2({ requiredRoles: allow || [] });
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
if (allow && allow.length > 0) {
|
|
8403
|
+
if (!userRole || !allow.includes(userRole)) {
|
|
8404
|
+
authLogger5.middleware.warn("Role guard failed: role not allowed", {
|
|
8405
|
+
userId,
|
|
8406
|
+
userRole,
|
|
8407
|
+
allowedRoles: allow,
|
|
8408
|
+
path: c.req.path
|
|
8409
|
+
});
|
|
8410
|
+
throw new InsufficientRoleError2({ requiredRoles: allow });
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
authLogger5.middleware.debug("Role guard passed", {
|
|
8414
|
+
userId,
|
|
8415
|
+
userRole,
|
|
8416
|
+
allow,
|
|
8417
|
+
deny
|
|
8418
|
+
});
|
|
8419
|
+
await next();
|
|
8420
|
+
}
|
|
8421
|
+
);
|
|
8422
|
+
|
|
8226
8423
|
// src/server/routes/invitations/index.ts
|
|
8227
8424
|
init_types();
|
|
8228
8425
|
init_esm();
|
|
@@ -8416,21 +8613,277 @@ var invitationRouter = defineRouter2({
|
|
|
8416
8613
|
});
|
|
8417
8614
|
|
|
8418
8615
|
// src/server/routes/users/index.ts
|
|
8616
|
+
init_esm();
|
|
8419
8617
|
import { defineRouter as defineRouter3, route as route3 } from "@spfn/core/route";
|
|
8420
8618
|
var getUserProfile = route3.get("/_auth/users/profile").handler(async (c) => {
|
|
8421
8619
|
const { userId } = getAuth(c);
|
|
8422
8620
|
return await getUserProfileService(userId);
|
|
8423
8621
|
});
|
|
8622
|
+
var updateUserProfile = route3.patch("/_auth/users/profile").input({
|
|
8623
|
+
body: Type.Object({
|
|
8624
|
+
displayName: Type.Optional(Type.String({ description: "Display name shown in UI" })),
|
|
8625
|
+
firstName: Type.Optional(Type.String({ description: "First name" })),
|
|
8626
|
+
lastName: Type.Optional(Type.String({ description: "Last name" })),
|
|
8627
|
+
avatarUrl: Type.Optional(Type.String({ description: "Avatar/profile picture URL" })),
|
|
8628
|
+
bio: Type.Optional(Type.String({ description: "Short bio/description" })),
|
|
8629
|
+
locale: Type.Optional(Type.String({ description: "Locale/language preference (e.g., en, ko)" })),
|
|
8630
|
+
timezone: Type.Optional(Type.String({ description: "Timezone (e.g., Asia/Seoul)" })),
|
|
8631
|
+
dateOfBirth: Type.Optional(Type.String({ description: "Date of birth (YYYY-MM-DD)" })),
|
|
8632
|
+
gender: Type.Optional(Type.String({ description: "Gender" })),
|
|
8633
|
+
website: Type.Optional(Type.String({ description: "Personal or professional website" })),
|
|
8634
|
+
location: Type.Optional(Type.String({ description: "Location (city, country, etc.)" })),
|
|
8635
|
+
company: Type.Optional(Type.String({ description: "Company name" })),
|
|
8636
|
+
jobTitle: Type.Optional(Type.String({ description: "Job title" })),
|
|
8637
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Additional metadata" }))
|
|
8638
|
+
})
|
|
8639
|
+
}).handler(async (c) => {
|
|
8640
|
+
const { userId } = getAuth(c);
|
|
8641
|
+
const { body } = await c.data();
|
|
8642
|
+
return await updateUserProfileService(userId, body);
|
|
8643
|
+
});
|
|
8424
8644
|
var userRouter = defineRouter3({
|
|
8425
|
-
getUserProfile
|
|
8645
|
+
getUserProfile,
|
|
8646
|
+
updateUserProfile
|
|
8647
|
+
});
|
|
8648
|
+
|
|
8649
|
+
// src/server/routes/oauth/index.ts
|
|
8650
|
+
init_esm();
|
|
8651
|
+
init_types();
|
|
8652
|
+
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
8653
|
+
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
8654
|
+
var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
8655
|
+
query: Type.Object({
|
|
8656
|
+
state: Type.String({
|
|
8657
|
+
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
8658
|
+
})
|
|
8659
|
+
})
|
|
8660
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8661
|
+
const { query } = await c.data();
|
|
8662
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8663
|
+
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
8664
|
+
}
|
|
8665
|
+
const authUrl = getGoogleAuthUrl(query.state);
|
|
8666
|
+
return c.redirect(authUrl);
|
|
8667
|
+
});
|
|
8668
|
+
var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
8669
|
+
query: Type.Object({
|
|
8670
|
+
code: Type.Optional(Type.String({
|
|
8671
|
+
description: "Authorization code from Google"
|
|
8672
|
+
})),
|
|
8673
|
+
state: Type.Optional(Type.String({
|
|
8674
|
+
description: "OAuth state parameter"
|
|
8675
|
+
})),
|
|
8676
|
+
error: Type.Optional(Type.String({
|
|
8677
|
+
description: "Error code from Google"
|
|
8678
|
+
})),
|
|
8679
|
+
error_description: Type.Optional(Type.String({
|
|
8680
|
+
description: "Error description from Google"
|
|
8681
|
+
}))
|
|
8682
|
+
})
|
|
8683
|
+
}).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
8684
|
+
const { query } = await c.data();
|
|
8685
|
+
if (query.error) {
|
|
8686
|
+
const errorMessage = query.error_description || query.error;
|
|
8687
|
+
return c.redirect(buildOAuthErrorUrl(errorMessage));
|
|
8688
|
+
}
|
|
8689
|
+
if (!query.code || !query.state) {
|
|
8690
|
+
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
8691
|
+
}
|
|
8692
|
+
try {
|
|
8693
|
+
const result = await oauthCallbackService({
|
|
8694
|
+
provider: "google",
|
|
8695
|
+
code: query.code,
|
|
8696
|
+
state: query.state
|
|
8697
|
+
});
|
|
8698
|
+
return c.redirect(result.redirectUrl);
|
|
8699
|
+
} catch (err) {
|
|
8700
|
+
const message = err instanceof Error ? err.message : "OAuth callback failed";
|
|
8701
|
+
return c.redirect(buildOAuthErrorUrl(message));
|
|
8702
|
+
}
|
|
8703
|
+
});
|
|
8704
|
+
var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
8705
|
+
body: Type.Object({
|
|
8706
|
+
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
8707
|
+
description: "OAuth provider (google, github, kakao, naver)"
|
|
8708
|
+
}),
|
|
8709
|
+
returnUrl: Type.String({
|
|
8710
|
+
description: "URL to redirect after OAuth success"
|
|
8711
|
+
}),
|
|
8712
|
+
publicKey: Type.String({
|
|
8713
|
+
description: "Client public key (Base64 DER)"
|
|
8714
|
+
}),
|
|
8715
|
+
keyId: Type.String({
|
|
8716
|
+
description: "Key identifier (UUID)"
|
|
8717
|
+
}),
|
|
8718
|
+
fingerprint: Type.String({
|
|
8719
|
+
description: "Key fingerprint (SHA-256 hex)"
|
|
8720
|
+
}),
|
|
8721
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
|
|
8722
|
+
description: "Key algorithm (ES256 or RS256)"
|
|
8723
|
+
})
|
|
8724
|
+
})
|
|
8725
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8726
|
+
const { body } = await c.data();
|
|
8727
|
+
const result = await oauthStartService(body);
|
|
8728
|
+
return result;
|
|
8729
|
+
});
|
|
8730
|
+
var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
8731
|
+
return {
|
|
8732
|
+
providers: getEnabledOAuthProviders()
|
|
8733
|
+
};
|
|
8734
|
+
});
|
|
8735
|
+
var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
8736
|
+
body: Type.Object({
|
|
8737
|
+
returnUrl: Type.Optional(Type.String({
|
|
8738
|
+
description: "URL to redirect after OAuth success"
|
|
8739
|
+
})),
|
|
8740
|
+
state: Type.Optional(Type.String({
|
|
8741
|
+
description: "Encrypted OAuth state (injected by interceptor)"
|
|
8742
|
+
}))
|
|
8743
|
+
})
|
|
8744
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8745
|
+
const { body } = await c.data();
|
|
8746
|
+
if (!isGoogleOAuthEnabled()) {
|
|
8747
|
+
throw new Error("Google OAuth is not configured");
|
|
8748
|
+
}
|
|
8749
|
+
if (!body.state) {
|
|
8750
|
+
throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
|
|
8751
|
+
}
|
|
8752
|
+
return { authUrl: getGoogleAuthUrl(body.state) };
|
|
8753
|
+
});
|
|
8754
|
+
var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
|
|
8755
|
+
body: Type.Object({
|
|
8756
|
+
userId: Type.String({ description: "User ID from OAuth callback" }),
|
|
8757
|
+
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
8758
|
+
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
8759
|
+
})
|
|
8760
|
+
}).skip(["auth"]).handler(async (c) => {
|
|
8761
|
+
const { body } = await c.data();
|
|
8762
|
+
return {
|
|
8763
|
+
success: true,
|
|
8764
|
+
userId: body.userId,
|
|
8765
|
+
keyId: body.keyId,
|
|
8766
|
+
returnUrl: body.returnUrl || "/"
|
|
8767
|
+
};
|
|
8768
|
+
});
|
|
8769
|
+
var oauthRouter = defineRouter4({
|
|
8770
|
+
oauthGoogleStart,
|
|
8771
|
+
oauthGoogleCallback,
|
|
8772
|
+
oauthStart,
|
|
8773
|
+
oauthProviders,
|
|
8774
|
+
getGoogleOAuthUrl,
|
|
8775
|
+
oauthFinalize
|
|
8776
|
+
});
|
|
8777
|
+
|
|
8778
|
+
// src/server/routes/admin/index.ts
|
|
8779
|
+
init_esm();
|
|
8780
|
+
import { route as route5 } from "@spfn/core/route";
|
|
8781
|
+
var listRoles = route5.get("/_auth/admin/roles").input({
|
|
8782
|
+
query: Type.Object({
|
|
8783
|
+
includeInactive: Type.Optional(Type.Boolean({
|
|
8784
|
+
description: "Include inactive roles (default: false)"
|
|
8785
|
+
}))
|
|
8786
|
+
})
|
|
8787
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8788
|
+
const { query } = await c.data();
|
|
8789
|
+
const roles2 = await getAllRoles(query.includeInactive ?? false);
|
|
8790
|
+
return { roles: roles2 };
|
|
8791
|
+
});
|
|
8792
|
+
var createAdminRole = route5.post("/_auth/admin/roles").input({
|
|
8793
|
+
body: Type.Object({
|
|
8794
|
+
name: Type.String({ description: "Unique role name (slug)" }),
|
|
8795
|
+
displayName: Type.String({ description: "Human-readable role name" }),
|
|
8796
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
8797
|
+
priority: Type.Optional(Type.Number({ description: "Role priority (default: 10)" })),
|
|
8798
|
+
permissionIds: Type.Optional(Type.Array(
|
|
8799
|
+
Type.Number({ description: "Permission ID" }),
|
|
8800
|
+
{ description: "Permission IDs to assign" }
|
|
8801
|
+
))
|
|
8802
|
+
})
|
|
8803
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8804
|
+
const { body } = await c.data();
|
|
8805
|
+
const role = await createRole({
|
|
8806
|
+
name: body.name,
|
|
8807
|
+
displayName: body.displayName,
|
|
8808
|
+
description: body.description,
|
|
8809
|
+
priority: body.priority,
|
|
8810
|
+
permissionIds: body.permissionIds
|
|
8811
|
+
});
|
|
8812
|
+
return { role };
|
|
8813
|
+
});
|
|
8814
|
+
var updateAdminRole = route5.patch("/_auth/admin/roles/:id").input({
|
|
8815
|
+
params: Type.Object({
|
|
8816
|
+
id: Type.Number({ description: "Role ID" })
|
|
8817
|
+
}),
|
|
8818
|
+
body: Type.Object({
|
|
8819
|
+
displayName: Type.Optional(Type.String({ description: "Human-readable role name" })),
|
|
8820
|
+
description: Type.Optional(Type.String({ description: "Role description" })),
|
|
8821
|
+
priority: Type.Optional(Type.Number({ description: "Role priority" })),
|
|
8822
|
+
isActive: Type.Optional(Type.Boolean({ description: "Active status" }))
|
|
8823
|
+
})
|
|
8824
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8825
|
+
const { params, body } = await c.data();
|
|
8826
|
+
const role = await updateRole(params.id, body);
|
|
8827
|
+
return { role };
|
|
8828
|
+
});
|
|
8829
|
+
var deleteAdminRole = route5.delete("/_auth/admin/roles/:id").input({
|
|
8830
|
+
params: Type.Object({
|
|
8831
|
+
id: Type.Number({ description: "Role ID" })
|
|
8832
|
+
})
|
|
8833
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8834
|
+
const { params } = await c.data();
|
|
8835
|
+
await deleteRole(params.id);
|
|
8836
|
+
return c.noContent();
|
|
8837
|
+
});
|
|
8838
|
+
var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
8839
|
+
params: Type.Object({
|
|
8840
|
+
userId: Type.Number({ description: "User ID" })
|
|
8841
|
+
}),
|
|
8842
|
+
body: Type.Object({
|
|
8843
|
+
roleId: Type.Number({ description: "New role ID to assign" })
|
|
8844
|
+
})
|
|
8845
|
+
}).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
|
|
8846
|
+
const { params, body } = await c.data();
|
|
8847
|
+
await updateUserService(params.userId, { roleId: body.roleId });
|
|
8848
|
+
return { userId: params.userId, roleId: body.roleId };
|
|
8426
8849
|
});
|
|
8427
8850
|
|
|
8428
8851
|
// src/server/routes/index.ts
|
|
8429
|
-
var mainAuthRouter =
|
|
8430
|
-
//
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8852
|
+
var mainAuthRouter = defineRouter5({
|
|
8853
|
+
// Auth routes
|
|
8854
|
+
checkAccountExists,
|
|
8855
|
+
sendVerificationCode,
|
|
8856
|
+
verifyCode,
|
|
8857
|
+
register,
|
|
8858
|
+
login,
|
|
8859
|
+
logout,
|
|
8860
|
+
rotateKey,
|
|
8861
|
+
changePassword,
|
|
8862
|
+
getAuthSession,
|
|
8863
|
+
// OAuth routes
|
|
8864
|
+
oauthGoogleStart,
|
|
8865
|
+
oauthGoogleCallback,
|
|
8866
|
+
oauthStart,
|
|
8867
|
+
oauthProviders,
|
|
8868
|
+
getGoogleOAuthUrl,
|
|
8869
|
+
oauthFinalize,
|
|
8870
|
+
// Invitation routes
|
|
8871
|
+
getInvitation,
|
|
8872
|
+
acceptInvitation: acceptInvitation2,
|
|
8873
|
+
createInvitation: createInvitation2,
|
|
8874
|
+
listInvitations: listInvitations2,
|
|
8875
|
+
cancelInvitation: cancelInvitation2,
|
|
8876
|
+
resendInvitation: resendInvitation2,
|
|
8877
|
+
deleteInvitation: deleteInvitation2,
|
|
8878
|
+
// User routes
|
|
8879
|
+
getUserProfile,
|
|
8880
|
+
updateUserProfile,
|
|
8881
|
+
// Admin routes (superadmin only)
|
|
8882
|
+
listRoles,
|
|
8883
|
+
createAdminRole,
|
|
8884
|
+
updateAdminRole,
|
|
8885
|
+
deleteAdminRole,
|
|
8886
|
+
updateUserRole
|
|
8434
8887
|
});
|
|
8435
8888
|
|
|
8436
8889
|
// src/server.ts
|
|
@@ -8540,11 +8993,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
8540
8993
|
}
|
|
8541
8994
|
|
|
8542
8995
|
// src/server/lib/session.ts
|
|
8543
|
-
import * as
|
|
8544
|
-
import { env as
|
|
8996
|
+
import * as jose2 from "jose";
|
|
8997
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
8545
8998
|
import { env as coreEnv } from "@spfn/core/config";
|
|
8546
8999
|
async function getSessionSecretKey() {
|
|
8547
|
-
const secret =
|
|
9000
|
+
const secret = env8.SPFN_AUTH_SESSION_SECRET;
|
|
8548
9001
|
const encoder = new TextEncoder();
|
|
8549
9002
|
const data = encoder.encode(secret);
|
|
8550
9003
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -8552,24 +9005,24 @@ async function getSessionSecretKey() {
|
|
|
8552
9005
|
}
|
|
8553
9006
|
async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
|
|
8554
9007
|
const secret = await getSessionSecretKey();
|
|
8555
|
-
return await new
|
|
9008
|
+
return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
|
|
8556
9009
|
}
|
|
8557
9010
|
async function unsealSession(jwt4) {
|
|
8558
9011
|
try {
|
|
8559
9012
|
const secret = await getSessionSecretKey();
|
|
8560
|
-
const { payload } = await
|
|
9013
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
|
|
8561
9014
|
issuer: "spfn-auth",
|
|
8562
9015
|
audience: "spfn-client"
|
|
8563
9016
|
});
|
|
8564
9017
|
return payload.data;
|
|
8565
9018
|
} catch (err) {
|
|
8566
|
-
if (err instanceof
|
|
9019
|
+
if (err instanceof jose2.errors.JWTExpired) {
|
|
8567
9020
|
throw new Error("Session expired");
|
|
8568
9021
|
}
|
|
8569
|
-
if (err instanceof
|
|
9022
|
+
if (err instanceof jose2.errors.JWEDecryptionFailed) {
|
|
8570
9023
|
throw new Error("Invalid session");
|
|
8571
9024
|
}
|
|
8572
|
-
if (err instanceof
|
|
9025
|
+
if (err instanceof jose2.errors.JWTClaimValidationFailed) {
|
|
8573
9026
|
throw new Error("Session validation failed");
|
|
8574
9027
|
}
|
|
8575
9028
|
throw new Error("Failed to unseal session");
|
|
@@ -8578,7 +9031,7 @@ async function unsealSession(jwt4) {
|
|
|
8578
9031
|
async function getSessionInfo(jwt4) {
|
|
8579
9032
|
const secret = await getSessionSecretKey();
|
|
8580
9033
|
try {
|
|
8581
|
-
const { payload } = await
|
|
9034
|
+
const { payload } = await jose2.jwtDecrypt(jwt4, secret);
|
|
8582
9035
|
return {
|
|
8583
9036
|
issuedAt: new Date(payload.iat * 1e3),
|
|
8584
9037
|
expiresAt: new Date(payload.exp * 1e3),
|
|
@@ -8602,14 +9055,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
8602
9055
|
}
|
|
8603
9056
|
|
|
8604
9057
|
// src/server/setup.ts
|
|
8605
|
-
import { env as
|
|
9058
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
8606
9059
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
8607
9060
|
init_repositories();
|
|
8608
9061
|
function parseAdminAccounts() {
|
|
8609
9062
|
const accounts = [];
|
|
8610
|
-
if (
|
|
9063
|
+
if (env9.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
8611
9064
|
try {
|
|
8612
|
-
const accountsJson =
|
|
9065
|
+
const accountsJson = env9.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
8613
9066
|
const parsed = JSON.parse(accountsJson);
|
|
8614
9067
|
if (!Array.isArray(parsed)) {
|
|
8615
9068
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -8636,11 +9089,11 @@ function parseAdminAccounts() {
|
|
|
8636
9089
|
return accounts;
|
|
8637
9090
|
}
|
|
8638
9091
|
}
|
|
8639
|
-
const adminEmails =
|
|
9092
|
+
const adminEmails = env9.SPFN_AUTH_ADMIN_EMAILS;
|
|
8640
9093
|
if (adminEmails) {
|
|
8641
9094
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
8642
|
-
const passwords = (
|
|
8643
|
-
const roles2 = (
|
|
9095
|
+
const passwords = (env9.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
9096
|
+
const roles2 = (env9.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
8644
9097
|
if (passwords.length !== emails.length) {
|
|
8645
9098
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
8646
9099
|
return accounts;
|
|
@@ -8662,8 +9115,8 @@ function parseAdminAccounts() {
|
|
|
8662
9115
|
}
|
|
8663
9116
|
return accounts;
|
|
8664
9117
|
}
|
|
8665
|
-
const adminEmail =
|
|
8666
|
-
const adminPassword =
|
|
9118
|
+
const adminEmail = env9.SPFN_AUTH_ADMIN_EMAIL;
|
|
9119
|
+
const adminPassword = env9.SPFN_AUTH_ADMIN_PASSWORD;
|
|
8667
9120
|
if (adminEmail && adminPassword) {
|
|
8668
9121
|
accounts.push({
|
|
8669
9122
|
email: adminEmail,
|
|
@@ -8741,6 +9194,7 @@ function createAuthLifecycle(options = {}) {
|
|
|
8741
9194
|
};
|
|
8742
9195
|
}
|
|
8743
9196
|
export {
|
|
9197
|
+
AuthProviderSchema,
|
|
8744
9198
|
COOKIE_NAMES,
|
|
8745
9199
|
EmailSchema,
|
|
8746
9200
|
INVITATION_STATUSES,
|
|
@@ -8753,6 +9207,7 @@ export {
|
|
|
8753
9207
|
RolePermissionsRepository,
|
|
8754
9208
|
RolesRepository,
|
|
8755
9209
|
SOCIAL_PROVIDERS,
|
|
9210
|
+
SocialAccountsRepository,
|
|
8756
9211
|
TargetTypeSchema,
|
|
8757
9212
|
USER_STATUSES,
|
|
8758
9213
|
UserPermissionsRepository,
|
|
@@ -8765,19 +9220,24 @@ export {
|
|
|
8765
9220
|
acceptInvitation,
|
|
8766
9221
|
addPermissionToRole,
|
|
8767
9222
|
authLogger,
|
|
9223
|
+
authLoginEvent,
|
|
9224
|
+
authRegisterEvent,
|
|
8768
9225
|
mainAuthRouter as authRouter,
|
|
8769
9226
|
authSchema,
|
|
8770
9227
|
authenticate,
|
|
9228
|
+
buildOAuthErrorUrl,
|
|
8771
9229
|
cancelInvitation,
|
|
8772
9230
|
changePasswordService,
|
|
8773
9231
|
checkAccountExistsService,
|
|
8774
9232
|
configureAuth,
|
|
8775
9233
|
createAuthLifecycle,
|
|
8776
9234
|
createInvitation,
|
|
9235
|
+
createOAuthState,
|
|
8777
9236
|
createRole,
|
|
8778
9237
|
decodeToken,
|
|
8779
9238
|
deleteInvitation,
|
|
8780
9239
|
deleteRole,
|
|
9240
|
+
exchangeCodeForTokens,
|
|
8781
9241
|
expireOldInvitations,
|
|
8782
9242
|
generateClientToken,
|
|
8783
9243
|
generateKeyPair,
|
|
@@ -8788,12 +9248,15 @@ export {
|
|
|
8788
9248
|
getAuth,
|
|
8789
9249
|
getAuthConfig,
|
|
8790
9250
|
getAuthSessionService,
|
|
9251
|
+
getEnabledOAuthProviders,
|
|
9252
|
+
getGoogleAccessToken,
|
|
9253
|
+
getGoogleAuthUrl,
|
|
9254
|
+
getGoogleOAuthConfig,
|
|
9255
|
+
getGoogleUserInfo,
|
|
8791
9256
|
getInvitationByToken,
|
|
8792
|
-
getInvitationTemplate,
|
|
8793
9257
|
getInvitationWithDetails,
|
|
8794
9258
|
getKeyId,
|
|
8795
9259
|
getKeySize,
|
|
8796
|
-
getPasswordResetTemplate,
|
|
8797
9260
|
getRoleByName,
|
|
8798
9261
|
getRolePermissions,
|
|
8799
9262
|
getSessionInfo,
|
|
@@ -8805,8 +9268,7 @@ export {
|
|
|
8805
9268
|
getUserId,
|
|
8806
9269
|
getUserPermissions,
|
|
8807
9270
|
getUserProfileService,
|
|
8808
|
-
|
|
8809
|
-
getWelcomeTemplate,
|
|
9271
|
+
getUserRole,
|
|
8810
9272
|
hasAllPermissions,
|
|
8811
9273
|
hasAnyPermission,
|
|
8812
9274
|
hasAnyRole,
|
|
@@ -8815,17 +9277,19 @@ export {
|
|
|
8815
9277
|
hashPassword,
|
|
8816
9278
|
initializeAuth,
|
|
8817
9279
|
invitationsRepository,
|
|
9280
|
+
isGoogleOAuthEnabled,
|
|
9281
|
+
isOAuthProviderEnabled,
|
|
8818
9282
|
keysRepository,
|
|
8819
9283
|
listInvitations,
|
|
8820
9284
|
loginService,
|
|
8821
9285
|
logoutService,
|
|
9286
|
+
oauthCallbackService,
|
|
9287
|
+
oauthStartService,
|
|
8822
9288
|
parseDuration,
|
|
8823
9289
|
permissions,
|
|
8824
9290
|
permissionsRepository,
|
|
8825
|
-
|
|
8826
|
-
registerEmailTemplates,
|
|
9291
|
+
refreshAccessToken,
|
|
8827
9292
|
registerPublicKeyService,
|
|
8828
|
-
registerSMSProvider,
|
|
8829
9293
|
registerService,
|
|
8830
9294
|
removePermissionFromRole,
|
|
8831
9295
|
requireAnyPermission,
|
|
@@ -8833,21 +9297,22 @@ export {
|
|
|
8833
9297
|
requireRole,
|
|
8834
9298
|
resendInvitation,
|
|
8835
9299
|
revokeKeyService,
|
|
9300
|
+
roleGuard,
|
|
8836
9301
|
rolePermissions,
|
|
8837
9302
|
rolePermissionsRepository,
|
|
8838
9303
|
roles,
|
|
8839
9304
|
rolesRepository,
|
|
8840
9305
|
rotateKeyService,
|
|
8841
9306
|
sealSession,
|
|
8842
|
-
sendEmail,
|
|
8843
|
-
sendSMS,
|
|
8844
9307
|
sendVerificationCodeService,
|
|
8845
9308
|
setRolePermissions,
|
|
8846
9309
|
shouldRefreshSession,
|
|
8847
9310
|
shouldRotateKey,
|
|
9311
|
+
socialAccountsRepository,
|
|
8848
9312
|
unsealSession,
|
|
8849
9313
|
updateLastLoginService,
|
|
8850
9314
|
updateRole,
|
|
9315
|
+
updateUserProfileService,
|
|
8851
9316
|
updateUserService,
|
|
8852
9317
|
userInvitations,
|
|
8853
9318
|
userPermissions,
|
|
@@ -8865,6 +9330,7 @@ export {
|
|
|
8865
9330
|
verifyClientToken,
|
|
8866
9331
|
verifyCodeService,
|
|
8867
9332
|
verifyKeyFingerprint,
|
|
9333
|
+
verifyOAuthState,
|
|
8868
9334
|
verifyPassword,
|
|
8869
9335
|
verifyToken
|
|
8870
9336
|
};
|