@spfn/auth 0.2.0-beta.1 → 0.2.0-beta.11

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
  };
@@ -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
  };
@@ -6682,9 +6695,10 @@ import {
6682
6695
  } from "@spfn/auth/errors";
6683
6696
 
6684
6697
  // src/server/services/verification.service.ts
6685
- import { env as env5 } from "@spfn/auth/config";
6698
+ import { env as env3 } from "@spfn/auth/config";
6686
6699
  import { InvalidVerificationCodeError } from "@spfn/auth/errors";
6687
6700
  import jwt2 from "jsonwebtoken";
6701
+ import { sendEmail, sendSMS } from "@spfn/notification/server";
6688
6702
 
6689
6703
  // src/server/logger.ts
6690
6704
  import { logger as rootLogger } from "@spfn/core/logger";
@@ -6704,410 +6718,6 @@ var authLogger = {
6704
6718
 
6705
6719
  // src/server/services/verification.service.ts
6706
6720
  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
6721
  var VERIFICATION_TOKEN_EXPIRY = "15m";
7112
6722
  var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
7113
6723
  var MAX_VERIFICATION_ATTEMPTS = 5;
@@ -7151,7 +6761,7 @@ async function markCodeAsUsed(codeId) {
7151
6761
  await verificationCodesRepository.markAsUsed(codeId);
7152
6762
  }
7153
6763
  function createVerificationToken(payload) {
7154
- return jwt2.sign(payload, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6764
+ return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7155
6765
  expiresIn: VERIFICATION_TOKEN_EXPIRY,
7156
6766
  issuer: "spfn-auth",
7157
6767
  audience: "spfn-client"
@@ -7159,7 +6769,7 @@ function createVerificationToken(payload) {
7159
6769
  }
7160
6770
  function validateVerificationToken(token) {
7161
6771
  try {
7162
- const decoded = jwt2.verify(token, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6772
+ const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7163
6773
  issuer: "spfn-auth",
7164
6774
  audience: "spfn-client"
7165
6775
  });
@@ -7173,17 +6783,14 @@ function validateVerificationToken(token) {
7173
6783
  }
7174
6784
  }
7175
6785
  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
6786
  const result = await sendEmail({
7182
6787
  to: email,
7183
- subject,
7184
- text: text10,
7185
- html,
7186
- purpose
6788
+ template: "verification-code",
6789
+ data: {
6790
+ code,
6791
+ purpose,
6792
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
6793
+ }
7187
6794
  });
7188
6795
  if (!result.success) {
7189
6796
  authLogger.email.error("Failed to send verification email", {
@@ -7194,11 +6801,13 @@ async function sendVerificationEmail(email, code, purpose) {
7194
6801
  }
7195
6802
  }
7196
6803
  async function sendVerificationSMS(phone, code, purpose) {
7197
- const message = `Your verification code is: ${code}`;
7198
6804
  const result = await sendSMS({
7199
- phone,
7200
- message,
7201
- purpose
6805
+ to: phone,
6806
+ template: "verification-code",
6807
+ data: {
6808
+ code,
6809
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
6810
+ }
7202
6811
  });
7203
6812
  if (!result.success) {
7204
6813
  authLogger.sms.error("Failed to send verification SMS", {
@@ -7461,7 +7070,7 @@ init_repositories();
7461
7070
  init_rbac();
7462
7071
 
7463
7072
  // src/server/lib/config.ts
7464
- import { env as env6 } from "@spfn/auth/config";
7073
+ import { env as env4 } from "@spfn/auth/config";
7465
7074
  var COOKIE_NAMES = {
7466
7075
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
7467
7076
  SESSION: "spfn_session",
@@ -7511,7 +7120,7 @@ function getSessionTtl(override) {
7511
7120
  if (globalConfig.sessionTtl !== void 0) {
7512
7121
  return parseDuration(globalConfig.sessionTtl);
7513
7122
  }
7514
- const envTtl = env6.SPFN_AUTH_SESSION_TTL;
7123
+ const envTtl = env4.SPFN_AUTH_SESSION_TTL;
7515
7124
  if (envTtl) {
7516
7125
  return parseDuration(envTtl);
7517
7126
  }
@@ -7673,14 +7282,18 @@ async function hasAllPermissions(userId, permissionNames) {
7673
7282
  const perms = await getUserPermissions(userId);
7674
7283
  return permissionNames.every((p) => perms.includes(p));
7675
7284
  }
7676
- async function hasRole(userId, roleName) {
7285
+ async function getUserRole(userId) {
7677
7286
  const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7678
7287
  const user = await usersRepository.findById(userIdNum);
7679
7288
  if (!user || !user.roleId) {
7680
- return false;
7289
+ return null;
7681
7290
  }
7682
7291
  const role = await rolesRepository.findById(user.roleId);
7683
- return role?.name === roleName;
7292
+ return role?.name || null;
7293
+ }
7294
+ async function hasRole(userId, roleName) {
7295
+ const role = await getUserRole(userId);
7296
+ return role === roleName;
7684
7297
  }
7685
7298
  async function hasAnyRole(userId, roleNames) {
7686
7299
  for (const roleName of roleNames) {
@@ -7887,6 +7500,65 @@ async function getUserProfileService(userId) {
7887
7500
  profile
7888
7501
  };
7889
7502
  }
7503
+ function emptyToNull(value) {
7504
+ if (value === "") {
7505
+ return null;
7506
+ }
7507
+ return value;
7508
+ }
7509
+ async function updateUserProfileService(userId, params) {
7510
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7511
+ const updateData = {};
7512
+ if (params.displayName !== void 0) {
7513
+ updateData.displayName = emptyToNull(params.displayName) || "User";
7514
+ }
7515
+ if (params.firstName !== void 0) {
7516
+ updateData.firstName = emptyToNull(params.firstName);
7517
+ }
7518
+ if (params.lastName !== void 0) {
7519
+ updateData.lastName = emptyToNull(params.lastName);
7520
+ }
7521
+ if (params.avatarUrl !== void 0) {
7522
+ updateData.avatarUrl = emptyToNull(params.avatarUrl);
7523
+ }
7524
+ if (params.bio !== void 0) {
7525
+ updateData.bio = emptyToNull(params.bio);
7526
+ }
7527
+ if (params.locale !== void 0) {
7528
+ updateData.locale = emptyToNull(params.locale) || "en";
7529
+ }
7530
+ if (params.timezone !== void 0) {
7531
+ updateData.timezone = emptyToNull(params.timezone) || "UTC";
7532
+ }
7533
+ if (params.dateOfBirth !== void 0) {
7534
+ updateData.dateOfBirth = emptyToNull(params.dateOfBirth);
7535
+ }
7536
+ if (params.gender !== void 0) {
7537
+ updateData.gender = emptyToNull(params.gender);
7538
+ }
7539
+ if (params.website !== void 0) {
7540
+ updateData.website = emptyToNull(params.website);
7541
+ }
7542
+ if (params.location !== void 0) {
7543
+ updateData.location = emptyToNull(params.location);
7544
+ }
7545
+ if (params.company !== void 0) {
7546
+ updateData.company = emptyToNull(params.company);
7547
+ }
7548
+ if (params.jobTitle !== void 0) {
7549
+ updateData.jobTitle = emptyToNull(params.jobTitle);
7550
+ }
7551
+ if (params.metadata !== void 0) {
7552
+ updateData.metadata = params.metadata;
7553
+ }
7554
+ const existing = await userProfilesRepository.findByUserId(userIdNum);
7555
+ if (!existing && !updateData.displayName) {
7556
+ updateData.displayName = "User";
7557
+ }
7558
+ await userProfilesRepository.upsertByUserId(userIdNum, updateData);
7559
+ const profile = await userProfilesRepository.fetchProfileData(userIdNum);
7560
+ return profile;
7561
+ }
7890
7562
 
7891
7563
  // src/server/routes/auth/index.ts
7892
7564
  init_esm();
@@ -7980,9 +7652,7 @@ var login = route.post("/_auth/login").input({
7980
7652
  const { body } = await c.data();
7981
7653
  return await loginService(body);
7982
7654
  });
7983
- var logout = route.post("/_auth/logout").input({
7984
- body: Type.Object({})
7985
- }).handler(async (c) => {
7655
+ var logout = route.post("/_auth/logout").handler(async (c) => {
7986
7656
  const auth = getAuth(c);
7987
7657
  if (!auth) {
7988
7658
  return c.noContent();
@@ -7991,9 +7661,7 @@ var logout = route.post("/_auth/logout").input({
7991
7661
  await logoutService({ userId: Number(userId), keyId });
7992
7662
  return c.noContent();
7993
7663
  });
7994
- var rotateKey = route.post("/_auth/keys/rotate").input({
7995
- body: Type.Object({})
7996
- }).interceptor({
7664
+ var rotateKey = route.post("/_auth/keys/rotate").interceptor({
7997
7665
  body: Type.Object({
7998
7666
  publicKey: Type.String({ description: "New public key" }),
7999
7667
  keyId: Type.String({ description: "New key identifier" }),
@@ -8223,6 +7891,59 @@ var requireRole = defineMiddleware3(
8223
7891
  }
8224
7892
  );
8225
7893
 
7894
+ // src/server/middleware/role-guard.ts
7895
+ import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
7896
+ import { getAuth as getAuth4, getUserRole as getUserRole2, authLogger as authLogger5 } from "@spfn/auth/server";
7897
+ import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
7898
+ import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
7899
+ var roleGuard = defineMiddleware4(
7900
+ "roleGuard",
7901
+ (options) => async (c, next) => {
7902
+ const { allow, deny } = options;
7903
+ if (!allow && !deny) {
7904
+ throw new Error("roleGuard requires at least one of: allow, deny");
7905
+ }
7906
+ const auth = getAuth4(c);
7907
+ if (!auth) {
7908
+ authLogger5.middleware.warn("Role guard failed: not authenticated", {
7909
+ path: c.req.path
7910
+ });
7911
+ throw new ForbiddenError3({ message: "Authentication required" });
7912
+ }
7913
+ const { userId } = auth;
7914
+ const userRole = await getUserRole2(userId);
7915
+ if (deny && deny.length > 0) {
7916
+ if (userRole && deny.includes(userRole)) {
7917
+ authLogger5.middleware.warn("Role guard denied", {
7918
+ userId,
7919
+ userRole,
7920
+ deniedRoles: deny,
7921
+ path: c.req.path
7922
+ });
7923
+ throw new InsufficientRoleError2({ requiredRoles: allow || [] });
7924
+ }
7925
+ }
7926
+ if (allow && allow.length > 0) {
7927
+ if (!userRole || !allow.includes(userRole)) {
7928
+ authLogger5.middleware.warn("Role guard failed: role not allowed", {
7929
+ userId,
7930
+ userRole,
7931
+ allowedRoles: allow,
7932
+ path: c.req.path
7933
+ });
7934
+ throw new InsufficientRoleError2({ requiredRoles: allow });
7935
+ }
7936
+ }
7937
+ authLogger5.middleware.debug("Role guard passed", {
7938
+ userId,
7939
+ userRole,
7940
+ allow,
7941
+ deny
7942
+ });
7943
+ await next();
7944
+ }
7945
+ );
7946
+
8226
7947
  // src/server/routes/invitations/index.ts
8227
7948
  init_types();
8228
7949
  init_esm();
@@ -8416,21 +8137,62 @@ var invitationRouter = defineRouter2({
8416
8137
  });
8417
8138
 
8418
8139
  // src/server/routes/users/index.ts
8140
+ init_esm();
8419
8141
  import { defineRouter as defineRouter3, route as route3 } from "@spfn/core/route";
8420
8142
  var getUserProfile = route3.get("/_auth/users/profile").handler(async (c) => {
8421
8143
  const { userId } = getAuth(c);
8422
8144
  return await getUserProfileService(userId);
8423
8145
  });
8146
+ var updateUserProfile = route3.patch("/_auth/users/profile").input({
8147
+ body: Type.Object({
8148
+ displayName: Type.Optional(Type.String({ description: "Display name shown in UI" })),
8149
+ firstName: Type.Optional(Type.String({ description: "First name" })),
8150
+ lastName: Type.Optional(Type.String({ description: "Last name" })),
8151
+ avatarUrl: Type.Optional(Type.String({ description: "Avatar/profile picture URL" })),
8152
+ bio: Type.Optional(Type.String({ description: "Short bio/description" })),
8153
+ locale: Type.Optional(Type.String({ description: "Locale/language preference (e.g., en, ko)" })),
8154
+ timezone: Type.Optional(Type.String({ description: "Timezone (e.g., Asia/Seoul)" })),
8155
+ dateOfBirth: Type.Optional(Type.String({ description: "Date of birth (YYYY-MM-DD)" })),
8156
+ gender: Type.Optional(Type.String({ description: "Gender" })),
8157
+ website: Type.Optional(Type.String({ description: "Personal or professional website" })),
8158
+ location: Type.Optional(Type.String({ description: "Location (city, country, etc.)" })),
8159
+ company: Type.Optional(Type.String({ description: "Company name" })),
8160
+ jobTitle: Type.Optional(Type.String({ description: "Job title" })),
8161
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Additional metadata" }))
8162
+ })
8163
+ }).handler(async (c) => {
8164
+ const { userId } = getAuth(c);
8165
+ const { body } = await c.data();
8166
+ return await updateUserProfileService(userId, body);
8167
+ });
8424
8168
  var userRouter = defineRouter3({
8425
- getUserProfile
8169
+ getUserProfile,
8170
+ updateUserProfile
8426
8171
  });
8427
8172
 
8428
8173
  // src/server/routes/index.ts
8429
8174
  var mainAuthRouter = defineRouter4({
8430
- // Flatten all routes at root level
8431
- ...authRouter.routes,
8432
- ...invitationRouter.routes,
8433
- ...userRouter.routes
8175
+ // Auth routes
8176
+ checkAccountExists,
8177
+ sendVerificationCode,
8178
+ verifyCode,
8179
+ register,
8180
+ login,
8181
+ logout,
8182
+ rotateKey,
8183
+ changePassword,
8184
+ getAuthSession,
8185
+ // Invitation routes
8186
+ getInvitation,
8187
+ acceptInvitation: acceptInvitation2,
8188
+ createInvitation: createInvitation2,
8189
+ listInvitations: listInvitations2,
8190
+ cancelInvitation: cancelInvitation2,
8191
+ resendInvitation: resendInvitation2,
8192
+ deleteInvitation: deleteInvitation2,
8193
+ // User routes
8194
+ getUserProfile,
8195
+ updateUserProfile
8434
8196
  });
8435
8197
 
8436
8198
  // src/server.ts
@@ -8541,10 +8303,10 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
8541
8303
 
8542
8304
  // src/server/lib/session.ts
8543
8305
  import * as jose from "jose";
8544
- import { env as env7 } from "@spfn/auth/config";
8306
+ import { env as env5 } from "@spfn/auth/config";
8545
8307
  import { env as coreEnv } from "@spfn/core/config";
8546
8308
  async function getSessionSecretKey() {
8547
- const secret = env7.SPFN_AUTH_SESSION_SECRET;
8309
+ const secret = env5.SPFN_AUTH_SESSION_SECRET;
8548
8310
  const encoder = new TextEncoder();
8549
8311
  const data = encoder.encode(secret);
8550
8312
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -8602,14 +8364,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
8602
8364
  }
8603
8365
 
8604
8366
  // src/server/setup.ts
8605
- import { env as env8 } from "@spfn/auth/config";
8367
+ import { env as env6 } from "@spfn/auth/config";
8606
8368
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
8607
8369
  init_repositories();
8608
8370
  function parseAdminAccounts() {
8609
8371
  const accounts = [];
8610
- if (env8.SPFN_AUTH_ADMIN_ACCOUNTS) {
8372
+ if (env6.SPFN_AUTH_ADMIN_ACCOUNTS) {
8611
8373
  try {
8612
- const accountsJson = env8.SPFN_AUTH_ADMIN_ACCOUNTS;
8374
+ const accountsJson = env6.SPFN_AUTH_ADMIN_ACCOUNTS;
8613
8375
  const parsed = JSON.parse(accountsJson);
8614
8376
  if (!Array.isArray(parsed)) {
8615
8377
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -8636,11 +8398,11 @@ function parseAdminAccounts() {
8636
8398
  return accounts;
8637
8399
  }
8638
8400
  }
8639
- const adminEmails = env8.SPFN_AUTH_ADMIN_EMAILS;
8401
+ const adminEmails = env6.SPFN_AUTH_ADMIN_EMAILS;
8640
8402
  if (adminEmails) {
8641
8403
  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());
8404
+ const passwords = (env6.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8405
+ const roles2 = (env6.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
8644
8406
  if (passwords.length !== emails.length) {
8645
8407
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
8646
8408
  return accounts;
@@ -8662,8 +8424,8 @@ function parseAdminAccounts() {
8662
8424
  }
8663
8425
  return accounts;
8664
8426
  }
8665
- const adminEmail = env8.SPFN_AUTH_ADMIN_EMAIL;
8666
- const adminPassword = env8.SPFN_AUTH_ADMIN_PASSWORD;
8427
+ const adminEmail = env6.SPFN_AUTH_ADMIN_EMAIL;
8428
+ const adminPassword = env6.SPFN_AUTH_ADMIN_PASSWORD;
8667
8429
  if (adminEmail && adminPassword) {
8668
8430
  accounts.push({
8669
8431
  email: adminEmail,
@@ -8789,11 +8551,9 @@ export {
8789
8551
  getAuthConfig,
8790
8552
  getAuthSessionService,
8791
8553
  getInvitationByToken,
8792
- getInvitationTemplate,
8793
8554
  getInvitationWithDetails,
8794
8555
  getKeyId,
8795
8556
  getKeySize,
8796
- getPasswordResetTemplate,
8797
8557
  getRoleByName,
8798
8558
  getRolePermissions,
8799
8559
  getSessionInfo,
@@ -8805,8 +8565,7 @@ export {
8805
8565
  getUserId,
8806
8566
  getUserPermissions,
8807
8567
  getUserProfileService,
8808
- getVerificationCodeTemplate,
8809
- getWelcomeTemplate,
8568
+ getUserRole,
8810
8569
  hasAllPermissions,
8811
8570
  hasAnyPermission,
8812
8571
  hasAnyRole,
@@ -8822,10 +8581,7 @@ export {
8822
8581
  parseDuration,
8823
8582
  permissions,
8824
8583
  permissionsRepository,
8825
- registerEmailProvider,
8826
- registerEmailTemplates,
8827
8584
  registerPublicKeyService,
8828
- registerSMSProvider,
8829
8585
  registerService,
8830
8586
  removePermissionFromRole,
8831
8587
  requireAnyPermission,
@@ -8833,14 +8589,13 @@ export {
8833
8589
  requireRole,
8834
8590
  resendInvitation,
8835
8591
  revokeKeyService,
8592
+ roleGuard,
8836
8593
  rolePermissions,
8837
8594
  rolePermissionsRepository,
8838
8595
  roles,
8839
8596
  rolesRepository,
8840
8597
  rotateKeyService,
8841
8598
  sealSession,
8842
- sendEmail,
8843
- sendSMS,
8844
8599
  sendVerificationCodeService,
8845
8600
  setRolePermissions,
8846
8601
  shouldRefreshSession,
@@ -8848,6 +8603,7 @@ export {
8848
8603
  unsealSession,
8849
8604
  updateLastLoginService,
8850
8605
  updateRole,
8606
+ updateUserProfileService,
8851
8607
  updateUserService,
8852
8608
  userInvitations,
8853
8609
  userPermissions,