@spfn/auth 0.2.0-beta.3 → 0.2.0-beta.31

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