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

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
@@ -6132,6 +6132,23 @@ var init_user_profiles_repository = __esm({
6132
6132
  const result = await this.db.delete(userProfiles).where(eq8(userProfiles.userId, userId)).returning();
6133
6133
  return result[0] ?? null;
6134
6134
  }
6135
+ /**
6136
+ * 프로필 Upsert (by User ID)
6137
+ *
6138
+ * 프로필이 없으면 생성, 있으면 업데이트
6139
+ * 새로 생성 시 displayName은 필수 (없으면 'User'로 설정)
6140
+ */
6141
+ async upsertByUserId(userId, data) {
6142
+ const existing = await this.findByUserId(userId);
6143
+ if (existing) {
6144
+ return await this.updateByUserId(userId, data);
6145
+ }
6146
+ return await this.create({
6147
+ userId,
6148
+ displayName: data.displayName || "User",
6149
+ ...data
6150
+ });
6151
+ }
6135
6152
  /**
6136
6153
  * User ID로 프로필 데이터 조회 (formatted)
6137
6154
  *
@@ -6151,6 +6168,7 @@ var init_user_profiles_repository = __esm({
6151
6168
  location: userProfiles.location,
6152
6169
  company: userProfiles.company,
6153
6170
  jobTitle: userProfiles.jobTitle,
6171
+ metadata: userProfiles.metadata,
6154
6172
  createdAt: userProfiles.createdAt,
6155
6173
  updatedAt: userProfiles.updatedAt
6156
6174
  }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
@@ -6170,6 +6188,7 @@ var init_user_profiles_repository = __esm({
6170
6188
  location: profile.location,
6171
6189
  company: profile.company,
6172
6190
  jobTitle: profile.jobTitle,
6191
+ metadata: profile.metadata,
6173
6192
  createdAt: profile.createdAt,
6174
6193
  updatedAt: profile.updatedAt
6175
6194
  };
@@ -6856,100 +6875,115 @@ function isValidEmail(email) {
6856
6875
  return emailRegex.test(email);
6857
6876
  }
6858
6877
  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"
6878
+ return {
6879
+ name: "aws-ses",
6880
+ sendEmail: async (params) => {
6881
+ const { to, subject, text: text10, html, purpose } = params;
6882
+ if (!isValidEmail(to)) {
6883
+ return {
6884
+ success: false,
6885
+ error: "Invalid email address format"
6886
+ };
6887
+ }
6888
+ if (!env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID) {
6889
+ authLogger.email.warn("AWS SES credentials not configured", {
6890
+ hint: "Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable"
6891
+ });
6892
+ return {
6893
+ success: false,
6894
+ error: "AWS SES credentials not configured. Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable."
6895
+ };
6896
+ }
6897
+ if (!env4.SPFN_AUTH_AWS_SES_FROM_EMAIL) {
6898
+ authLogger.email.warn("AWS SES sender email not configured", {
6899
+ hint: "Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable"
6900
+ });
6901
+ return {
6902
+ success: false,
6903
+ error: "AWS SES sender email not configured. Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable."
6904
+ };
6905
+ }
6906
+ let SESClient;
6907
+ let SendEmailCommand;
6908
+ try {
6909
+ const ses = await import("@aws-sdk/client-ses");
6910
+ SESClient = ses.SESClient;
6911
+ SendEmailCommand = ses.SendEmailCommand;
6912
+ } catch (error) {
6913
+ authLogger.email.warn("@aws-sdk/client-ses not installed", {
6914
+ error: error instanceof Error ? error.message : String(error),
6915
+ hint: "Run: pnpm add @aws-sdk/client-ses"
6916
+ });
6917
+ return {
6918
+ success: false,
6919
+ error: "@aws-sdk/client-ses not installed. Run: pnpm add @aws-sdk/client-ses"
6920
+ };
6921
+ }
6922
+ try {
6923
+ const config = {
6924
+ region: env4.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
6925
+ };
6926
+ if (env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID && env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY) {
6927
+ config.credentials = {
6928
+ accessKeyId: env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID,
6929
+ secretAccessKey: env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY
6869
6930
  };
6870
6931
  }
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."
6932
+ const client = new SESClient(config);
6933
+ const body = {};
6934
+ if (text10) {
6935
+ body.Text = {
6936
+ Charset: "UTF-8",
6937
+ Data: text10
6875
6938
  };
6876
6939
  }
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."
6940
+ if (html) {
6941
+ body.Html = {
6942
+ Charset: "UTF-8",
6943
+ Data: html
6881
6944
  };
6882
6945
  }
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 = {
6946
+ const command = new SendEmailCommand({
6947
+ Source: env4.SPFN_AUTH_AWS_SES_FROM_EMAIL,
6948
+ Destination: {
6949
+ ToAddresses: [to]
6950
+ },
6951
+ Message: {
6952
+ Subject: {
6903
6953
  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]
6954
+ Data: subject
6911
6955
  },
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
- }
6956
+ Body: body
6957
+ }
6958
+ });
6959
+ const response = await client.send(command);
6960
+ authLogger.email.info("Email sent via AWS SES", {
6961
+ to,
6962
+ messageId: response.MessageId,
6963
+ purpose: purpose || "N/A"
6964
+ });
6965
+ return {
6966
+ success: true,
6967
+ messageId: response.MessageId
6968
+ };
6969
+ } catch (error) {
6970
+ const err = error;
6971
+ authLogger.email.error("Failed to send email via AWS SES", {
6972
+ to,
6973
+ error: err.message
6974
+ });
6975
+ return {
6976
+ success: false,
6977
+ error: err.message || "Failed to send email via AWS SES"
6978
+ };
6941
6979
  }
6942
- };
6943
- } catch (error) {
6944
- return null;
6945
- }
6980
+ }
6981
+ };
6946
6982
  }
6947
6983
  var awsSESProvider = createAWSSESProvider();
6948
6984
 
6949
6985
  // src/server/services/email/index.ts
6950
- if (awsSESProvider) {
6951
- registerEmailProvider(awsSESProvider);
6952
- }
6986
+ registerEmailProvider(awsSESProvider);
6953
6987
 
6954
6988
  // src/server/services/email/templates/verification-code.ts
6955
6989
  function getSubject(purpose) {
@@ -7673,14 +7707,18 @@ async function hasAllPermissions(userId, permissionNames) {
7673
7707
  const perms = await getUserPermissions(userId);
7674
7708
  return permissionNames.every((p) => perms.includes(p));
7675
7709
  }
7676
- async function hasRole(userId, roleName) {
7710
+ async function getUserRole(userId) {
7677
7711
  const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7678
7712
  const user = await usersRepository.findById(userIdNum);
7679
7713
  if (!user || !user.roleId) {
7680
- return false;
7714
+ return null;
7681
7715
  }
7682
7716
  const role = await rolesRepository.findById(user.roleId);
7683
- return role?.name === roleName;
7717
+ return role?.name || null;
7718
+ }
7719
+ async function hasRole(userId, roleName) {
7720
+ const role = await getUserRole(userId);
7721
+ return role === roleName;
7684
7722
  }
7685
7723
  async function hasAnyRole(userId, roleNames) {
7686
7724
  for (const roleName of roleNames) {
@@ -7887,6 +7925,65 @@ async function getUserProfileService(userId) {
7887
7925
  profile
7888
7926
  };
7889
7927
  }
7928
+ function emptyToNull(value) {
7929
+ if (value === "") {
7930
+ return null;
7931
+ }
7932
+ return value;
7933
+ }
7934
+ async function updateUserProfileService(userId, params) {
7935
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7936
+ const updateData = {};
7937
+ if (params.displayName !== void 0) {
7938
+ updateData.displayName = emptyToNull(params.displayName) || "User";
7939
+ }
7940
+ if (params.firstName !== void 0) {
7941
+ updateData.firstName = emptyToNull(params.firstName);
7942
+ }
7943
+ if (params.lastName !== void 0) {
7944
+ updateData.lastName = emptyToNull(params.lastName);
7945
+ }
7946
+ if (params.avatarUrl !== void 0) {
7947
+ updateData.avatarUrl = emptyToNull(params.avatarUrl);
7948
+ }
7949
+ if (params.bio !== void 0) {
7950
+ updateData.bio = emptyToNull(params.bio);
7951
+ }
7952
+ if (params.locale !== void 0) {
7953
+ updateData.locale = emptyToNull(params.locale) || "en";
7954
+ }
7955
+ if (params.timezone !== void 0) {
7956
+ updateData.timezone = emptyToNull(params.timezone) || "UTC";
7957
+ }
7958
+ if (params.dateOfBirth !== void 0) {
7959
+ updateData.dateOfBirth = emptyToNull(params.dateOfBirth);
7960
+ }
7961
+ if (params.gender !== void 0) {
7962
+ updateData.gender = emptyToNull(params.gender);
7963
+ }
7964
+ if (params.website !== void 0) {
7965
+ updateData.website = emptyToNull(params.website);
7966
+ }
7967
+ if (params.location !== void 0) {
7968
+ updateData.location = emptyToNull(params.location);
7969
+ }
7970
+ if (params.company !== void 0) {
7971
+ updateData.company = emptyToNull(params.company);
7972
+ }
7973
+ if (params.jobTitle !== void 0) {
7974
+ updateData.jobTitle = emptyToNull(params.jobTitle);
7975
+ }
7976
+ if (params.metadata !== void 0) {
7977
+ updateData.metadata = params.metadata;
7978
+ }
7979
+ const existing = await userProfilesRepository.findByUserId(userIdNum);
7980
+ if (!existing && !updateData.displayName) {
7981
+ updateData.displayName = "User";
7982
+ }
7983
+ await userProfilesRepository.upsertByUserId(userIdNum, updateData);
7984
+ const profile = await userProfilesRepository.fetchProfileData(userIdNum);
7985
+ return profile;
7986
+ }
7890
7987
 
7891
7988
  // src/server/routes/auth/index.ts
7892
7989
  init_esm();
@@ -7980,9 +8077,7 @@ var login = route.post("/_auth/login").input({
7980
8077
  const { body } = await c.data();
7981
8078
  return await loginService(body);
7982
8079
  });
7983
- var logout = route.post("/_auth/logout").input({
7984
- body: Type.Object({})
7985
- }).handler(async (c) => {
8080
+ var logout = route.post("/_auth/logout").handler(async (c) => {
7986
8081
  const auth = getAuth(c);
7987
8082
  if (!auth) {
7988
8083
  return c.noContent();
@@ -7991,9 +8086,7 @@ var logout = route.post("/_auth/logout").input({
7991
8086
  await logoutService({ userId: Number(userId), keyId });
7992
8087
  return c.noContent();
7993
8088
  });
7994
- var rotateKey = route.post("/_auth/keys/rotate").input({
7995
- body: Type.Object({})
7996
- }).interceptor({
8089
+ var rotateKey = route.post("/_auth/keys/rotate").interceptor({
7997
8090
  body: Type.Object({
7998
8091
  publicKey: Type.String({ description: "New public key" }),
7999
8092
  keyId: Type.String({ description: "New key identifier" }),
@@ -8223,6 +8316,59 @@ var requireRole = defineMiddleware3(
8223
8316
  }
8224
8317
  );
8225
8318
 
8319
+ // src/server/middleware/role-guard.ts
8320
+ import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
8321
+ import { getAuth as getAuth4, getUserRole as getUserRole2, authLogger as authLogger5 } from "@spfn/auth/server";
8322
+ import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
8323
+ import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
8324
+ var roleGuard = defineMiddleware4(
8325
+ "roleGuard",
8326
+ (options) => async (c, next) => {
8327
+ const { allow, deny } = options;
8328
+ if (!allow && !deny) {
8329
+ throw new Error("roleGuard requires at least one of: allow, deny");
8330
+ }
8331
+ const auth = getAuth4(c);
8332
+ if (!auth) {
8333
+ authLogger5.middleware.warn("Role guard failed: not authenticated", {
8334
+ path: c.req.path
8335
+ });
8336
+ throw new ForbiddenError3({ message: "Authentication required" });
8337
+ }
8338
+ const { userId } = auth;
8339
+ const userRole = await getUserRole2(userId);
8340
+ if (deny && deny.length > 0) {
8341
+ if (userRole && deny.includes(userRole)) {
8342
+ authLogger5.middleware.warn("Role guard denied", {
8343
+ userId,
8344
+ userRole,
8345
+ deniedRoles: deny,
8346
+ path: c.req.path
8347
+ });
8348
+ throw new InsufficientRoleError2({ requiredRoles: allow || [] });
8349
+ }
8350
+ }
8351
+ if (allow && allow.length > 0) {
8352
+ if (!userRole || !allow.includes(userRole)) {
8353
+ authLogger5.middleware.warn("Role guard failed: role not allowed", {
8354
+ userId,
8355
+ userRole,
8356
+ allowedRoles: allow,
8357
+ path: c.req.path
8358
+ });
8359
+ throw new InsufficientRoleError2({ requiredRoles: allow });
8360
+ }
8361
+ }
8362
+ authLogger5.middleware.debug("Role guard passed", {
8363
+ userId,
8364
+ userRole,
8365
+ allow,
8366
+ deny
8367
+ });
8368
+ await next();
8369
+ }
8370
+ );
8371
+
8226
8372
  // src/server/routes/invitations/index.ts
8227
8373
  init_types();
8228
8374
  init_esm();
@@ -8416,13 +8562,37 @@ var invitationRouter = defineRouter2({
8416
8562
  });
8417
8563
 
8418
8564
  // src/server/routes/users/index.ts
8565
+ init_esm();
8419
8566
  import { defineRouter as defineRouter3, route as route3 } from "@spfn/core/route";
8420
8567
  var getUserProfile = route3.get("/_auth/users/profile").handler(async (c) => {
8421
8568
  const { userId } = getAuth(c);
8422
8569
  return await getUserProfileService(userId);
8423
8570
  });
8571
+ var updateUserProfile = route3.patch("/_auth/users/profile").input({
8572
+ body: Type.Object({
8573
+ displayName: Type.Optional(Type.String({ description: "Display name shown in UI" })),
8574
+ firstName: Type.Optional(Type.String({ description: "First name" })),
8575
+ lastName: Type.Optional(Type.String({ description: "Last name" })),
8576
+ avatarUrl: Type.Optional(Type.String({ description: "Avatar/profile picture URL" })),
8577
+ bio: Type.Optional(Type.String({ description: "Short bio/description" })),
8578
+ locale: Type.Optional(Type.String({ description: "Locale/language preference (e.g., en, ko)" })),
8579
+ timezone: Type.Optional(Type.String({ description: "Timezone (e.g., Asia/Seoul)" })),
8580
+ dateOfBirth: Type.Optional(Type.String({ description: "Date of birth (YYYY-MM-DD)" })),
8581
+ gender: Type.Optional(Type.String({ description: "Gender" })),
8582
+ website: Type.Optional(Type.String({ description: "Personal or professional website" })),
8583
+ location: Type.Optional(Type.String({ description: "Location (city, country, etc.)" })),
8584
+ company: Type.Optional(Type.String({ description: "Company name" })),
8585
+ jobTitle: Type.Optional(Type.String({ description: "Job title" })),
8586
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Additional metadata" }))
8587
+ })
8588
+ }).handler(async (c) => {
8589
+ const { userId } = getAuth(c);
8590
+ const { body } = await c.data();
8591
+ return await updateUserProfileService(userId, body);
8592
+ });
8424
8593
  var userRouter = defineRouter3({
8425
- getUserProfile
8594
+ getUserProfile,
8595
+ updateUserProfile
8426
8596
  });
8427
8597
 
8428
8598
  // src/server/routes/index.ts
@@ -8805,6 +8975,7 @@ export {
8805
8975
  getUserId,
8806
8976
  getUserPermissions,
8807
8977
  getUserProfileService,
8978
+ getUserRole,
8808
8979
  getVerificationCodeTemplate,
8809
8980
  getWelcomeTemplate,
8810
8981
  hasAllPermissions,
@@ -8833,6 +9004,7 @@ export {
8833
9004
  requireRole,
8834
9005
  resendInvitation,
8835
9006
  revokeKeyService,
9007
+ roleGuard,
8836
9008
  rolePermissions,
8837
9009
  rolePermissionsRepository,
8838
9010
  roles,
@@ -8848,6 +9020,7 @@ export {
8848
9020
  unsealSession,
8849
9021
  updateLastLoginService,
8850
9022
  updateRole,
9023
+ updateUserProfileService,
8851
9024
  updateUserService,
8852
9025
  userInvitations,
8853
9026
  userPermissions,