@spfn/auth 0.2.0-beta.74 → 0.2.0-beta.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -15
- package/dist/{authenticate-DXVtYh8X.d.ts → authenticate-ofdEmk6x.d.ts} +43 -13
- package/dist/config.d.ts +150 -0
- package/dist/config.js +71 -4
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +75 -3
- package/dist/errors.js +50 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.js +53 -5
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +13 -11
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.d.ts +2 -2
- package/dist/nextjs/server.js +3 -3
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +1040 -42
- package/dist/server.js +1894 -397
- package/dist/server.js.map +1 -1
- package/dist/{session-s_hiXmXC.d.ts → session-CGxgH3C9.d.ts} +1 -1
- package/dist/types-1BMx0OX1.d.ts +84 -0
- package/migrations/0006_easy_hardball.sql +24 -0
- package/migrations/0007_glossy_major_mapleleaf.sql +1 -0
- package/migrations/meta/0006_snapshot.json +1921 -0
- package/migrations/meta/0007_snapshot.json +1916 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +29 -27
- package/dist/types-BtksCI9X.d.ts +0 -45
package/dist/server.js
CHANGED
|
@@ -897,8 +897,8 @@ var init_array2 = __esm({
|
|
|
897
897
|
});
|
|
898
898
|
|
|
899
899
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/argument/argument.mjs
|
|
900
|
-
function Argument(
|
|
901
|
-
return CreateType({ [Kind]: "Argument", index:
|
|
900
|
+
function Argument(index12) {
|
|
901
|
+
return CreateType({ [Kind]: "Argument", index: index12 });
|
|
902
902
|
}
|
|
903
903
|
var init_argument = __esm({
|
|
904
904
|
"../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/argument/argument.mjs"() {
|
|
@@ -1136,28 +1136,28 @@ var init_union2 = __esm({
|
|
|
1136
1136
|
function Unescape(pattern) {
|
|
1137
1137
|
return pattern.replace(/\\\$/g, "$").replace(/\\\*/g, "*").replace(/\\\^/g, "^").replace(/\\\|/g, "|").replace(/\\\(/g, "(").replace(/\\\)/g, ")");
|
|
1138
1138
|
}
|
|
1139
|
-
function IsNonEscaped(pattern,
|
|
1140
|
-
return pattern[
|
|
1139
|
+
function IsNonEscaped(pattern, index12, char) {
|
|
1140
|
+
return pattern[index12] === char && pattern.charCodeAt(index12 - 1) !== 92;
|
|
1141
1141
|
}
|
|
1142
|
-
function IsOpenParen(pattern,
|
|
1143
|
-
return IsNonEscaped(pattern,
|
|
1142
|
+
function IsOpenParen(pattern, index12) {
|
|
1143
|
+
return IsNonEscaped(pattern, index12, "(");
|
|
1144
1144
|
}
|
|
1145
|
-
function IsCloseParen(pattern,
|
|
1146
|
-
return IsNonEscaped(pattern,
|
|
1145
|
+
function IsCloseParen(pattern, index12) {
|
|
1146
|
+
return IsNonEscaped(pattern, index12, ")");
|
|
1147
1147
|
}
|
|
1148
|
-
function IsSeparator(pattern,
|
|
1149
|
-
return IsNonEscaped(pattern,
|
|
1148
|
+
function IsSeparator(pattern, index12) {
|
|
1149
|
+
return IsNonEscaped(pattern, index12, "|");
|
|
1150
1150
|
}
|
|
1151
1151
|
function IsGroup(pattern) {
|
|
1152
1152
|
if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1)))
|
|
1153
1153
|
return false;
|
|
1154
1154
|
let count = 0;
|
|
1155
|
-
for (let
|
|
1156
|
-
if (IsOpenParen(pattern,
|
|
1155
|
+
for (let index12 = 0; index12 < pattern.length; index12++) {
|
|
1156
|
+
if (IsOpenParen(pattern, index12))
|
|
1157
1157
|
count += 1;
|
|
1158
|
-
if (IsCloseParen(pattern,
|
|
1158
|
+
if (IsCloseParen(pattern, index12))
|
|
1159
1159
|
count -= 1;
|
|
1160
|
-
if (count === 0 &&
|
|
1160
|
+
if (count === 0 && index12 !== pattern.length - 1)
|
|
1161
1161
|
return false;
|
|
1162
1162
|
}
|
|
1163
1163
|
return true;
|
|
@@ -1167,19 +1167,19 @@ function InGroup(pattern) {
|
|
|
1167
1167
|
}
|
|
1168
1168
|
function IsPrecedenceOr(pattern) {
|
|
1169
1169
|
let count = 0;
|
|
1170
|
-
for (let
|
|
1171
|
-
if (IsOpenParen(pattern,
|
|
1170
|
+
for (let index12 = 0; index12 < pattern.length; index12++) {
|
|
1171
|
+
if (IsOpenParen(pattern, index12))
|
|
1172
1172
|
count += 1;
|
|
1173
|
-
if (IsCloseParen(pattern,
|
|
1173
|
+
if (IsCloseParen(pattern, index12))
|
|
1174
1174
|
count -= 1;
|
|
1175
|
-
if (IsSeparator(pattern,
|
|
1175
|
+
if (IsSeparator(pattern, index12) && count === 0)
|
|
1176
1176
|
return true;
|
|
1177
1177
|
}
|
|
1178
1178
|
return false;
|
|
1179
1179
|
}
|
|
1180
1180
|
function IsPrecedenceAnd(pattern) {
|
|
1181
|
-
for (let
|
|
1182
|
-
if (IsOpenParen(pattern,
|
|
1181
|
+
for (let index12 = 0; index12 < pattern.length; index12++) {
|
|
1182
|
+
if (IsOpenParen(pattern, index12))
|
|
1183
1183
|
return true;
|
|
1184
1184
|
}
|
|
1185
1185
|
return false;
|
|
@@ -1187,16 +1187,16 @@ function IsPrecedenceAnd(pattern) {
|
|
|
1187
1187
|
function Or(pattern) {
|
|
1188
1188
|
let [count, start] = [0, 0];
|
|
1189
1189
|
const expressions = [];
|
|
1190
|
-
for (let
|
|
1191
|
-
if (IsOpenParen(pattern,
|
|
1190
|
+
for (let index12 = 0; index12 < pattern.length; index12++) {
|
|
1191
|
+
if (IsOpenParen(pattern, index12))
|
|
1192
1192
|
count += 1;
|
|
1193
|
-
if (IsCloseParen(pattern,
|
|
1193
|
+
if (IsCloseParen(pattern, index12))
|
|
1194
1194
|
count -= 1;
|
|
1195
|
-
if (IsSeparator(pattern,
|
|
1196
|
-
const range2 = pattern.slice(start,
|
|
1195
|
+
if (IsSeparator(pattern, index12) && count === 0) {
|
|
1196
|
+
const range2 = pattern.slice(start, index12);
|
|
1197
1197
|
if (range2.length > 0)
|
|
1198
1198
|
expressions.push(TemplateLiteralParse(range2));
|
|
1199
|
-
start =
|
|
1199
|
+
start = index12 + 1;
|
|
1200
1200
|
}
|
|
1201
1201
|
}
|
|
1202
1202
|
const range = pattern.slice(start);
|
|
@@ -1209,40 +1209,40 @@ function Or(pattern) {
|
|
|
1209
1209
|
return { type: "or", expr: expressions };
|
|
1210
1210
|
}
|
|
1211
1211
|
function And(pattern) {
|
|
1212
|
-
function Group(value,
|
|
1213
|
-
if (!IsOpenParen(value,
|
|
1212
|
+
function Group(value, index12) {
|
|
1213
|
+
if (!IsOpenParen(value, index12))
|
|
1214
1214
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`);
|
|
1215
1215
|
let count = 0;
|
|
1216
|
-
for (let scan =
|
|
1216
|
+
for (let scan = index12; scan < value.length; scan++) {
|
|
1217
1217
|
if (IsOpenParen(value, scan))
|
|
1218
1218
|
count += 1;
|
|
1219
1219
|
if (IsCloseParen(value, scan))
|
|
1220
1220
|
count -= 1;
|
|
1221
1221
|
if (count === 0)
|
|
1222
|
-
return [
|
|
1222
|
+
return [index12, scan];
|
|
1223
1223
|
}
|
|
1224
1224
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Unclosed group parens in expression`);
|
|
1225
1225
|
}
|
|
1226
|
-
function Range(pattern2,
|
|
1227
|
-
for (let scan =
|
|
1226
|
+
function Range(pattern2, index12) {
|
|
1227
|
+
for (let scan = index12; scan < pattern2.length; scan++) {
|
|
1228
1228
|
if (IsOpenParen(pattern2, scan))
|
|
1229
|
-
return [
|
|
1229
|
+
return [index12, scan];
|
|
1230
1230
|
}
|
|
1231
|
-
return [
|
|
1231
|
+
return [index12, pattern2.length];
|
|
1232
1232
|
}
|
|
1233
1233
|
const expressions = [];
|
|
1234
|
-
for (let
|
|
1235
|
-
if (IsOpenParen(pattern,
|
|
1236
|
-
const [start, end] = Group(pattern,
|
|
1234
|
+
for (let index12 = 0; index12 < pattern.length; index12++) {
|
|
1235
|
+
if (IsOpenParen(pattern, index12)) {
|
|
1236
|
+
const [start, end] = Group(pattern, index12);
|
|
1237
1237
|
const range = pattern.slice(start, end + 1);
|
|
1238
1238
|
expressions.push(TemplateLiteralParse(range));
|
|
1239
|
-
|
|
1239
|
+
index12 = end;
|
|
1240
1240
|
} else {
|
|
1241
|
-
const [start, end] = Range(pattern,
|
|
1241
|
+
const [start, end] = Range(pattern, index12);
|
|
1242
1242
|
const range = pattern.slice(start, end);
|
|
1243
1243
|
if (range.length > 0)
|
|
1244
1244
|
expressions.push(TemplateLiteralParse(range));
|
|
1245
|
-
|
|
1245
|
+
index12 = end - 1;
|
|
1246
1246
|
}
|
|
1247
1247
|
}
|
|
1248
1248
|
return expressions.length === 0 ? { type: "const", const: "" } : expressions.length === 1 ? expressions[0] : { type: "and", expr: expressions };
|
|
@@ -2577,13 +2577,13 @@ function FromBoolean(left, right) {
|
|
|
2577
2577
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsBoolean(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
2578
2578
|
}
|
|
2579
2579
|
function FromConstructor(left, right) {
|
|
2580
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema,
|
|
2580
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index12) => IntoBooleanResult(Visit3(right.parameters[index12], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
2581
2581
|
}
|
|
2582
2582
|
function FromDate(left, right) {
|
|
2583
2583
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsDate(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
2584
2584
|
}
|
|
2585
2585
|
function FromFunction(left, right) {
|
|
2586
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema,
|
|
2586
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index12) => IntoBooleanResult(Visit3(right.parameters[index12], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
2587
2587
|
}
|
|
2588
2588
|
function FromIntegerRight(left, right) {
|
|
2589
2589
|
return type_exports.IsLiteral(left) && value_exports.IsNumber(left.const) ? ExtendsResult.True : type_exports.IsNumber(left) || type_exports.IsInteger(left) ? ExtendsResult.True : ExtendsResult.False;
|
|
@@ -2743,7 +2743,7 @@ function FromTupleRight(left, right) {
|
|
|
2743
2743
|
return type_exports.IsNever(left) ? ExtendsResult.True : type_exports.IsUnknown(left) ? ExtendsResult.False : type_exports.IsAny(left) ? ExtendsResult.Union : ExtendsResult.False;
|
|
2744
2744
|
}
|
|
2745
2745
|
function FromTuple3(left, right) {
|
|
2746
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema,
|
|
2746
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema, index12) => Visit3(schema, right.items[index12]) === ExtendsResult.True) ? ExtendsResult.True : ExtendsResult.False;
|
|
2747
2747
|
}
|
|
2748
2748
|
function FromUint8Array(left, right) {
|
|
2749
2749
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsUint8Array(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
@@ -4486,23 +4486,27 @@ var init_schema3 = __esm({
|
|
|
4486
4486
|
Type.Literal("login"),
|
|
4487
4487
|
Type.Literal("password_reset"),
|
|
4488
4488
|
Type.Literal("email_change"),
|
|
4489
|
-
Type.Literal("phone_change")
|
|
4489
|
+
Type.Literal("phone_change"),
|
|
4490
|
+
Type.Literal("account_deletion")
|
|
4490
4491
|
], {
|
|
4491
4492
|
description: "Purpose of verification"
|
|
4492
4493
|
});
|
|
4493
|
-
VERIFICATION_PURPOSES = ["registration", "login", "password_reset", "email_change", "phone_change"];
|
|
4494
|
+
VERIFICATION_PURPOSES = ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"];
|
|
4494
4495
|
}
|
|
4495
4496
|
});
|
|
4496
4497
|
|
|
4497
4498
|
// src/server/types.ts
|
|
4498
|
-
var KEY_ALGORITHM, INVITATION_STATUSES, USER_STATUSES, SOCIAL_PROVIDERS;
|
|
4499
|
+
var KEY_ALGORITHM, INVITATION_STATUSES, USER_STATUSES, SOCIAL_PROVIDERS, ACCOUNT_DELETION_REQUEST_STATUSES, ACCOUNT_DELETION_REQUESTED_BY, PURGE_STRATEGIES;
|
|
4499
4500
|
var init_types = __esm({
|
|
4500
4501
|
"src/server/types.ts"() {
|
|
4501
4502
|
"use strict";
|
|
4502
4503
|
KEY_ALGORITHM = ["ES256", "RS256"];
|
|
4503
4504
|
INVITATION_STATUSES = ["pending", "accepted", "expired", "cancelled"];
|
|
4504
|
-
USER_STATUSES = ["active", "inactive", "suspended"];
|
|
4505
|
+
USER_STATUSES = ["active", "inactive", "suspended", "pending_deletion", "deleted"];
|
|
4505
4506
|
SOCIAL_PROVIDERS = ["google", "apple", "github", "kakao", "naver", "superself"];
|
|
4507
|
+
ACCOUNT_DELETION_REQUEST_STATUSES = ["pending", "cancelled", "completed"];
|
|
4508
|
+
ACCOUNT_DELETION_REQUESTED_BY = ["self", "admin"];
|
|
4509
|
+
PURGE_STRATEGIES = ["anonymize", "hard-delete"];
|
|
4506
4510
|
}
|
|
4507
4511
|
});
|
|
4508
4512
|
|
|
@@ -4565,9 +4569,8 @@ var init_roles = __esm({
|
|
|
4565
4569
|
});
|
|
4566
4570
|
|
|
4567
4571
|
// src/server/entities/users.ts
|
|
4568
|
-
import { text as text2,
|
|
4569
|
-
import { id as id2, timestamps as timestamps2, enumText, utcTimestamp, foreignKey } from "@spfn/core/db";
|
|
4570
|
-
import { sql } from "drizzle-orm";
|
|
4572
|
+
import { text as text2, boolean as boolean2, index as index2, uuid } from "drizzle-orm/pg-core";
|
|
4573
|
+
import { id as id2, timestamps as timestamps2, enumText, utcTimestamp, foreignKey, softDelete } from "@spfn/core/db";
|
|
4571
4574
|
var users;
|
|
4572
4575
|
var init_users = __esm({
|
|
4573
4576
|
"src/server/entities/users.ts"() {
|
|
@@ -4609,6 +4612,8 @@ var init_users = __esm({
|
|
|
4609
4612
|
// - active: Normal operation (default)
|
|
4610
4613
|
// - inactive: Deactivated (user request, dormant)
|
|
4611
4614
|
// - suspended: Locked (security incident, ToS violation)
|
|
4615
|
+
// - pending_deletion: Deletion requested, within the grace period (recoverable)
|
|
4616
|
+
// - deleted: Grace period elapsed and the account was purged (anonymize mode)
|
|
4612
4617
|
status: enumText("status", USER_STATUSES).default("active").notNull(),
|
|
4613
4618
|
// Verification timestamps
|
|
4614
4619
|
// null = unverified, timestamp = verified at this time
|
|
@@ -4620,15 +4625,13 @@ var init_users = __esm({
|
|
|
4620
4625
|
// Last successful login timestamp
|
|
4621
4626
|
// Used for: security auditing, dormant account detection
|
|
4622
4627
|
lastLoginAt: utcTimestamp("last_login_at"),
|
|
4623
|
-
...timestamps2()
|
|
4628
|
+
...timestamps2(),
|
|
4629
|
+
// Soft delete (deletedAt / deletedBy) — set when the account deletion purge job
|
|
4630
|
+
// anonymizes the row (status -> 'deleted'). Hard-delete mode removes the row
|
|
4631
|
+
// instead, so these stay null for that strategy.
|
|
4632
|
+
...softDelete()
|
|
4624
4633
|
},
|
|
4625
4634
|
(table) => [
|
|
4626
|
-
// Database constraints
|
|
4627
|
-
// Ensure at least one identifier exists (email OR phone)
|
|
4628
|
-
check(
|
|
4629
|
-
"email_or_phone_check",
|
|
4630
|
-
sql`${table.email} IS NOT NULL OR ${table.phone} IS NOT NULL`
|
|
4631
|
-
),
|
|
4632
4635
|
// Indexes for query optimization
|
|
4633
4636
|
index2("users_public_id_idx").on(table.publicId),
|
|
4634
4637
|
index2("users_email_idx").on(table.email),
|
|
@@ -4858,9 +4861,9 @@ var init_user_social_accounts = __esm({
|
|
|
4858
4861
|
});
|
|
4859
4862
|
|
|
4860
4863
|
// src/server/entities/verification-codes.ts
|
|
4861
|
-
import { text as text6, index as index6, integer as integer2, check
|
|
4864
|
+
import { text as text6, index as index6, integer as integer2, check } from "drizzle-orm/pg-core";
|
|
4862
4865
|
import { id as id6, timestamps as timestamps5, enumText as enumText4, utcTimestamp as utcTimestamp4 } from "@spfn/core/db";
|
|
4863
|
-
import { sql
|
|
4866
|
+
import { sql } from "drizzle-orm";
|
|
4864
4867
|
var verificationCodes;
|
|
4865
4868
|
var init_verification_codes = __esm({
|
|
4866
4869
|
"src/server/entities/verification-codes.ts"() {
|
|
@@ -4900,7 +4903,7 @@ var init_verification_codes = __esm({
|
|
|
4900
4903
|
(table) => [
|
|
4901
4904
|
// Database constraints
|
|
4902
4905
|
// Limit verification attempts to prevent brute force attacks
|
|
4903
|
-
|
|
4906
|
+
check("attempts_limit_check", sql`${table.attempts} >= 0 AND ${table.attempts} <= 10`),
|
|
4904
4907
|
// Index for quick lookup by target and purpose
|
|
4905
4908
|
index6("target_purpose_idx").on(table.target, table.purpose, table.expiresAt)
|
|
4906
4909
|
]
|
|
@@ -4980,6 +4983,59 @@ var init_user_invitations = __esm({
|
|
|
4980
4983
|
}
|
|
4981
4984
|
});
|
|
4982
4985
|
|
|
4986
|
+
// src/server/entities/account-deletion-requests.ts
|
|
4987
|
+
import { text as text8, index as index8, uniqueIndex as uniqueIndex2 } from "drizzle-orm/pg-core";
|
|
4988
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
4989
|
+
import { id as id8, timestamps as timestamps7, enumText as enumText6, utcTimestamp as utcTimestamp6, optionalForeignKey } from "@spfn/core/db";
|
|
4990
|
+
var accountDeletionRequests;
|
|
4991
|
+
var init_account_deletion_requests = __esm({
|
|
4992
|
+
"src/server/entities/account-deletion-requests.ts"() {
|
|
4993
|
+
"use strict";
|
|
4994
|
+
init_types();
|
|
4995
|
+
init_users();
|
|
4996
|
+
init_schema4();
|
|
4997
|
+
accountDeletionRequests = authSchema.table(
|
|
4998
|
+
"account_deletion_requests",
|
|
4999
|
+
{
|
|
5000
|
+
id: id8(),
|
|
5001
|
+
// Foreign key to users table. `set null` (optionalForeignKey default) so this
|
|
5002
|
+
// row survives a hard-delete purge of the user it refers to.
|
|
5003
|
+
userId: optionalForeignKey("user", () => users.id),
|
|
5004
|
+
// Snapshot of the user's public UUID at request time — stays readable even
|
|
5005
|
+
// after userId is nulled out or the account is anonymized.
|
|
5006
|
+
userPublicId: text8("user_public_id").notNull(),
|
|
5007
|
+
// When the deletion was requested
|
|
5008
|
+
requestedAt: utcTimestamp6("requested_at").notNull().defaultNow(),
|
|
5009
|
+
// When the purge job is allowed to run (requestedAt + grace period; equals
|
|
5010
|
+
// requestedAt itself for immediate/zero-grace deletions)
|
|
5011
|
+
purgeScheduledAt: utcTimestamp6("purge_scheduled_at").notNull(),
|
|
5012
|
+
// Request lifecycle status
|
|
5013
|
+
// - pending: awaiting purgeScheduledAt (or immediate purge)
|
|
5014
|
+
// - cancelled: recovered before purge
|
|
5015
|
+
// - completed: purge ran
|
|
5016
|
+
status: enumText6("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
|
|
5017
|
+
// Who initiated the request
|
|
5018
|
+
requestedBy: enumText6("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
|
|
5019
|
+
// Optional free-text reason (self-service UI, admin note, DSR reference, ...)
|
|
5020
|
+
reason: text8("reason"),
|
|
5021
|
+
cancelledAt: utcTimestamp6("cancelled_at"),
|
|
5022
|
+
completedAt: utcTimestamp6("completed_at"),
|
|
5023
|
+
// Purge strategy actually executed (set on completion; null while pending)
|
|
5024
|
+
purgeStrategy: enumText6("purge_strategy", PURGE_STRATEGIES),
|
|
5025
|
+
...timestamps7()
|
|
5026
|
+
},
|
|
5027
|
+
(table) => [
|
|
5028
|
+
index8("account_deletion_requests_user_id_idx").on(table.userId),
|
|
5029
|
+
index8("account_deletion_requests_status_idx").on(table.status),
|
|
5030
|
+
index8("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
|
|
5031
|
+
index8("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
|
|
5032
|
+
// Partial unique index: at most one pending request per user at a time.
|
|
5033
|
+
uniqueIndex2("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
|
|
5034
|
+
]
|
|
5035
|
+
);
|
|
5036
|
+
}
|
|
5037
|
+
});
|
|
5038
|
+
|
|
4983
5039
|
// src/server/rbac/types.ts
|
|
4984
5040
|
var PERMISSION_CATEGORIES;
|
|
4985
5041
|
var init_types2 = __esm({
|
|
@@ -5126,8 +5182,8 @@ var init_rbac = __esm({
|
|
|
5126
5182
|
});
|
|
5127
5183
|
|
|
5128
5184
|
// src/server/entities/permissions.ts
|
|
5129
|
-
import { text as
|
|
5130
|
-
import { id as
|
|
5185
|
+
import { text as text9, boolean as boolean4, index as index9 } from "drizzle-orm/pg-core";
|
|
5186
|
+
import { id as id9, timestamps as timestamps8, enumText as enumText7, typedJsonb as typedJsonb3 } from "@spfn/core/db";
|
|
5131
5187
|
var permissions;
|
|
5132
5188
|
var init_permissions = __esm({
|
|
5133
5189
|
"src/server/entities/permissions.ts"() {
|
|
@@ -5138,7 +5194,7 @@ var init_permissions = __esm({
|
|
|
5138
5194
|
"permissions",
|
|
5139
5195
|
{
|
|
5140
5196
|
// Primary key
|
|
5141
|
-
id:
|
|
5197
|
+
id: id9(),
|
|
5142
5198
|
// Permission identifier
|
|
5143
5199
|
// Format: resource:action or namespace:resource:action
|
|
5144
5200
|
// Examples:
|
|
@@ -5146,20 +5202,20 @@ var init_permissions = __esm({
|
|
|
5146
5202
|
// - Namespaced: 'auth:user:delete', 'cms:post:publish'
|
|
5147
5203
|
// Must be unique across all permissions
|
|
5148
5204
|
// Used in: permission checks, role assignments, API guards
|
|
5149
|
-
name:
|
|
5205
|
+
name: text9("name").notNull().unique(),
|
|
5150
5206
|
// Display name for UI
|
|
5151
5207
|
// Human-readable name shown in admin panels
|
|
5152
5208
|
// Example: "Delete Users", "Publish Posts"
|
|
5153
|
-
displayName:
|
|
5209
|
+
displayName: text9("display_name").notNull(),
|
|
5154
5210
|
// Permission description
|
|
5155
5211
|
// Detailed explanation of what this permission allows
|
|
5156
5212
|
// Example: "Allows deletion of user accounts from the system"
|
|
5157
|
-
description:
|
|
5213
|
+
description: text9("description"),
|
|
5158
5214
|
// Category for grouping
|
|
5159
5215
|
// Used for: organizing permissions in UI, filtering
|
|
5160
5216
|
// Built-in categories: auth, user, rbac, system
|
|
5161
5217
|
// Custom categories: any app-specific category
|
|
5162
|
-
category:
|
|
5218
|
+
category: enumText7("category", PERMISSION_CATEGORIES),
|
|
5163
5219
|
// Built-in permission flag
|
|
5164
5220
|
// true: Core package permissions (auth:*, user:*, rbac:*)
|
|
5165
5221
|
// - Cannot be deleted or modified
|
|
@@ -5190,22 +5246,22 @@ var init_permissions = __esm({
|
|
|
5190
5246
|
// - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
|
|
5191
5247
|
// Example: { icon: 'trash', color: 'red', requiresMfa: true }
|
|
5192
5248
|
metadata: typedJsonb3("metadata"),
|
|
5193
|
-
...
|
|
5249
|
+
...timestamps8()
|
|
5194
5250
|
},
|
|
5195
5251
|
(table) => [
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5252
|
+
index9("permissions_name_idx").on(table.name),
|
|
5253
|
+
index9("permissions_category_idx").on(table.category),
|
|
5254
|
+
index9("permissions_is_system_idx").on(table.isSystem),
|
|
5255
|
+
index9("permissions_is_active_idx").on(table.isActive),
|
|
5256
|
+
index9("permissions_is_builtin_idx").on(table.isBuiltin)
|
|
5201
5257
|
]
|
|
5202
5258
|
);
|
|
5203
5259
|
}
|
|
5204
5260
|
});
|
|
5205
5261
|
|
|
5206
5262
|
// src/server/entities/role-permissions.ts
|
|
5207
|
-
import { index as
|
|
5208
|
-
import { id as
|
|
5263
|
+
import { index as index10, unique } from "drizzle-orm/pg-core";
|
|
5264
|
+
import { id as id10, timestamps as timestamps9, foreignKey as foreignKey6 } from "@spfn/core/db";
|
|
5209
5265
|
var rolePermissions;
|
|
5210
5266
|
var init_role_permissions = __esm({
|
|
5211
5267
|
"src/server/entities/role-permissions.ts"() {
|
|
@@ -5217,7 +5273,7 @@ var init_role_permissions = __esm({
|
|
|
5217
5273
|
"role_permissions",
|
|
5218
5274
|
{
|
|
5219
5275
|
// Primary key
|
|
5220
|
-
id:
|
|
5276
|
+
id: id10(),
|
|
5221
5277
|
// Role reference
|
|
5222
5278
|
// Foreign key to roles table
|
|
5223
5279
|
// Cascade delete: when role is deleted, all role-permission mappings are removed
|
|
@@ -5230,12 +5286,12 @@ var init_role_permissions = __esm({
|
|
|
5230
5286
|
// Used for: granting permissions to roles
|
|
5231
5287
|
// Example: user:delete permission → [Admin, Superadmin]
|
|
5232
5288
|
permissionId: foreignKey6("permission", () => permissions.id, { onDelete: "cascade" }),
|
|
5233
|
-
...
|
|
5289
|
+
...timestamps9()
|
|
5234
5290
|
},
|
|
5235
5291
|
(table) => [
|
|
5236
5292
|
// Indexes for query performance
|
|
5237
|
-
|
|
5238
|
-
|
|
5293
|
+
index10("role_permissions_role_id_idx").on(table.roleId),
|
|
5294
|
+
index10("role_permissions_permission_id_idx").on(table.permissionId),
|
|
5239
5295
|
// Unique constraint: one role-permission pair only
|
|
5240
5296
|
unique("role_permissions_unique").on(table.roleId, table.permissionId)
|
|
5241
5297
|
]
|
|
@@ -5244,8 +5300,8 @@ var init_role_permissions = __esm({
|
|
|
5244
5300
|
});
|
|
5245
5301
|
|
|
5246
5302
|
// src/server/entities/user-permissions.ts
|
|
5247
|
-
import { boolean as boolean5, text as
|
|
5248
|
-
import { id as
|
|
5303
|
+
import { boolean as boolean5, text as text10, index as index11, unique as unique2 } from "drizzle-orm/pg-core";
|
|
5304
|
+
import { id as id11, timestamps as timestamps10, utcTimestamp as utcTimestamp7, foreignKey as foreignKey7 } from "@spfn/core/db";
|
|
5249
5305
|
var userPermissions;
|
|
5250
5306
|
var init_user_permissions = __esm({
|
|
5251
5307
|
"src/server/entities/user-permissions.ts"() {
|
|
@@ -5257,7 +5313,7 @@ var init_user_permissions = __esm({
|
|
|
5257
5313
|
"user_permissions",
|
|
5258
5314
|
{
|
|
5259
5315
|
// Primary key
|
|
5260
|
-
id:
|
|
5316
|
+
id: id11(),
|
|
5261
5317
|
// User reference
|
|
5262
5318
|
// Foreign key to users table
|
|
5263
5319
|
// Cascade delete: when user is deleted, all overrides are removed
|
|
@@ -5280,19 +5336,19 @@ var init_user_permissions = __esm({
|
|
|
5280
5336
|
// Reason for grant/revocation
|
|
5281
5337
|
// Used for: audit trail, compliance documentation
|
|
5282
5338
|
// Example: "Temporary access for project X", "Security incident - restricted"
|
|
5283
|
-
reason:
|
|
5339
|
+
reason: text10("reason"),
|
|
5284
5340
|
// Expiration timestamp (optional)
|
|
5285
5341
|
// null: Permanent override (remains until manually removed)
|
|
5286
5342
|
// timestamp: Permission expires at this time (auto-revoked by background job)
|
|
5287
5343
|
// Use case: Time-limited elevated access, temporary restrictions
|
|
5288
|
-
expiresAt:
|
|
5289
|
-
...
|
|
5344
|
+
expiresAt: utcTimestamp7("expires_at"),
|
|
5345
|
+
...timestamps10()
|
|
5290
5346
|
},
|
|
5291
5347
|
(table) => [
|
|
5292
5348
|
// Indexes for query performance
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5349
|
+
index11("user_permissions_user_id_idx").on(table.userId),
|
|
5350
|
+
index11("user_permissions_permission_id_idx").on(table.permissionId),
|
|
5351
|
+
index11("user_permissions_expires_at_idx").on(table.expiresAt),
|
|
5296
5352
|
// Unique constraint: one user-permission pair only
|
|
5297
5353
|
unique2("user_permissions_unique").on(table.userId, table.permissionId)
|
|
5298
5354
|
]
|
|
@@ -5301,7 +5357,7 @@ var init_user_permissions = __esm({
|
|
|
5301
5357
|
});
|
|
5302
5358
|
|
|
5303
5359
|
// src/server/entities/auth-metadata.ts
|
|
5304
|
-
import { text as
|
|
5360
|
+
import { text as text11, timestamp } from "drizzle-orm/pg-core";
|
|
5305
5361
|
var authMetadata;
|
|
5306
5362
|
var init_auth_metadata = __esm({
|
|
5307
5363
|
"src/server/entities/auth-metadata.ts"() {
|
|
@@ -5311,9 +5367,9 @@ var init_auth_metadata = __esm({
|
|
|
5311
5367
|
"auth_metadata",
|
|
5312
5368
|
{
|
|
5313
5369
|
// Metadata key (primary key)
|
|
5314
|
-
key:
|
|
5370
|
+
key: text11("key").primaryKey(),
|
|
5315
5371
|
// Metadata value
|
|
5316
|
-
value:
|
|
5372
|
+
value: text11("value").notNull(),
|
|
5317
5373
|
// Last updated timestamp
|
|
5318
5374
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
5319
5375
|
}
|
|
@@ -5332,6 +5388,7 @@ var init_entities = __esm({
|
|
|
5332
5388
|
init_user_social_accounts();
|
|
5333
5389
|
init_verification_codes();
|
|
5334
5390
|
init_user_invitations();
|
|
5391
|
+
init_account_deletion_requests();
|
|
5335
5392
|
init_roles();
|
|
5336
5393
|
init_permissions();
|
|
5337
5394
|
init_role_permissions();
|
|
@@ -5354,8 +5411,19 @@ var init_users_repository = __esm({
|
|
|
5354
5411
|
* ID로 사용자 조회
|
|
5355
5412
|
* Read replica 사용
|
|
5356
5413
|
*/
|
|
5357
|
-
async findById(
|
|
5358
|
-
const result = await this.readDb.select().from(users).where(eq(users.id,
|
|
5414
|
+
async findById(id12) {
|
|
5415
|
+
const result = await this.readDb.select().from(users).where(eq(users.id, id12)).limit(1);
|
|
5416
|
+
return result[0] ?? null;
|
|
5417
|
+
}
|
|
5418
|
+
/**
|
|
5419
|
+
* ID로 사용자 조회 — Write primary에서 직접 읽는다.
|
|
5420
|
+
*
|
|
5421
|
+
* 복제 지연 창에서 status 전이(예: 삭제 요청으로 pending_deletion 전환)를 놓치면
|
|
5422
|
+
* 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
|
|
5423
|
+
* 계속 사용할 것.
|
|
5424
|
+
*/
|
|
5425
|
+
async findByIdOnPrimary(id12) {
|
|
5426
|
+
const result = await this.db.select().from(users).where(eq(users.id, id12)).limit(1);
|
|
5359
5427
|
return result[0] ?? null;
|
|
5360
5428
|
}
|
|
5361
5429
|
/**
|
|
@@ -5408,13 +5476,13 @@ var init_users_repository = __esm({
|
|
|
5408
5476
|
*
|
|
5409
5477
|
* roleId가 null인 유저는 role: null 반환
|
|
5410
5478
|
*/
|
|
5411
|
-
async findByIdWithRole(
|
|
5479
|
+
async findByIdWithRole(id12) {
|
|
5412
5480
|
const result = await this.readDb.select({
|
|
5413
5481
|
user: users,
|
|
5414
5482
|
roleName: roles.name,
|
|
5415
5483
|
roleDisplayName: roles.displayName,
|
|
5416
5484
|
rolePriority: roles.priority
|
|
5417
|
-
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id,
|
|
5485
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id12)).limit(1);
|
|
5418
5486
|
const row = result[0];
|
|
5419
5487
|
if (!row) {
|
|
5420
5488
|
return null;
|
|
@@ -5439,15 +5507,33 @@ var init_users_repository = __esm({
|
|
|
5439
5507
|
* 사용자 정보 업데이트
|
|
5440
5508
|
* Write primary 사용
|
|
5441
5509
|
*/
|
|
5442
|
-
async updateById(
|
|
5443
|
-
const result = await this.db.update(users).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq(users.id,
|
|
5510
|
+
async updateById(id12, data) {
|
|
5511
|
+
const result = await this.db.update(users).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq(users.id, id12)).returning();
|
|
5512
|
+
return result[0] ?? null;
|
|
5513
|
+
}
|
|
5514
|
+
/**
|
|
5515
|
+
* 계정 삭제 취소(복구) — `WHERE status = 'pending_deletion'` 조건부 UPDATE.
|
|
5516
|
+
*
|
|
5517
|
+
* cancelAccountDeletionService가 account_deletion_requests claim(조건부
|
|
5518
|
+
* markCancelled)에 성공한 **이후에만** 호출한다. 그 claim이 이미 purge와의
|
|
5519
|
+
* 경합을 해결하므로 이 조건은 방어적 이중 안전장치 — 0 row 매치(= 이미
|
|
5520
|
+
* status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
|
|
5521
|
+
* Write primary 사용
|
|
5522
|
+
*/
|
|
5523
|
+
async reactivateFromPendingDeletion(id12) {
|
|
5524
|
+
const result = await this.db.update(users).set({ status: "active", updatedAt: /* @__PURE__ */ new Date() }).where(
|
|
5525
|
+
and(
|
|
5526
|
+
eq(users.id, id12),
|
|
5527
|
+
eq(users.status, "pending_deletion")
|
|
5528
|
+
)
|
|
5529
|
+
).returning();
|
|
5444
5530
|
return result[0] ?? null;
|
|
5445
5531
|
}
|
|
5446
5532
|
/**
|
|
5447
5533
|
* 비밀번호 업데이트
|
|
5448
5534
|
* Write primary 사용
|
|
5449
5535
|
*/
|
|
5450
|
-
async updatePassword(
|
|
5536
|
+
async updatePassword(id12, passwordHash, clearPasswordChangeRequired = true) {
|
|
5451
5537
|
const updateData = {
|
|
5452
5538
|
passwordHash,
|
|
5453
5539
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -5455,26 +5541,26 @@ var init_users_repository = __esm({
|
|
|
5455
5541
|
if (clearPasswordChangeRequired) {
|
|
5456
5542
|
updateData.passwordChangeRequired = false;
|
|
5457
5543
|
}
|
|
5458
|
-
const result = await this.db.update(users).set(updateData).where(eq(users.id,
|
|
5544
|
+
const result = await this.db.update(users).set(updateData).where(eq(users.id, id12)).returning();
|
|
5459
5545
|
return result[0] ?? null;
|
|
5460
5546
|
}
|
|
5461
5547
|
/**
|
|
5462
5548
|
* 마지막 로그인 시간 업데이트
|
|
5463
5549
|
* Write primary 사용
|
|
5464
5550
|
*/
|
|
5465
|
-
async updateLastLogin(
|
|
5551
|
+
async updateLastLogin(id12) {
|
|
5466
5552
|
const result = await this.db.update(users).set({
|
|
5467
5553
|
lastLoginAt: /* @__PURE__ */ new Date(),
|
|
5468
5554
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5469
|
-
}).where(eq(users.id,
|
|
5555
|
+
}).where(eq(users.id, id12)).returning();
|
|
5470
5556
|
return result[0] ?? null;
|
|
5471
5557
|
}
|
|
5472
5558
|
/**
|
|
5473
5559
|
* 사용자 삭제
|
|
5474
5560
|
* Write primary 사용
|
|
5475
5561
|
*/
|
|
5476
|
-
async deleteById(
|
|
5477
|
-
const result = await this.db.delete(users).where(eq(users.id,
|
|
5562
|
+
async deleteById(id12) {
|
|
5563
|
+
const result = await this.db.delete(users).where(eq(users.id, id12)).returning();
|
|
5478
5564
|
return result[0] ?? null;
|
|
5479
5565
|
}
|
|
5480
5566
|
/**
|
|
@@ -5690,6 +5776,17 @@ var init_keys_repository = __esm({
|
|
|
5690
5776
|
).returning();
|
|
5691
5777
|
return result[0] ?? null;
|
|
5692
5778
|
}
|
|
5779
|
+
/**
|
|
5780
|
+
* 사용자의 모든 공개키 삭제 (계정 익명화 파기용)
|
|
5781
|
+
*
|
|
5782
|
+
* hard-delete는 FK cascade로 자동 처리되지만, anonymize 모드는 users row를
|
|
5783
|
+
* 남기므로 자식 row를 직접 지워야 한다.
|
|
5784
|
+
* Write primary 사용
|
|
5785
|
+
*/
|
|
5786
|
+
async deleteAllByUserId(userId) {
|
|
5787
|
+
const result = await this.db.delete(userPublicKeys).where(eq2(userPublicKeys.userId, userId)).returning();
|
|
5788
|
+
return result.length;
|
|
5789
|
+
}
|
|
5693
5790
|
/**
|
|
5694
5791
|
* 마지막 사용 시간 업데이트
|
|
5695
5792
|
* Write primary 사용
|
|
@@ -5727,12 +5824,12 @@ var init_keys_repository = __esm({
|
|
|
5727
5824
|
* throttle lives in the WHERE clause — atomic, no read-then-write race. No
|
|
5728
5825
|
* RETURNING (callers fire-and-forget and discard the row).
|
|
5729
5826
|
*/
|
|
5730
|
-
async updateLastUsedById(
|
|
5827
|
+
async updateLastUsedById(id12) {
|
|
5731
5828
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
5732
5829
|
await this.db.update(userPublicKeys).set({
|
|
5733
5830
|
lastUsedAt: /* @__PURE__ */ new Date()
|
|
5734
5831
|
}).where(and2(
|
|
5735
|
-
eq2(userPublicKeys.id,
|
|
5832
|
+
eq2(userPublicKeys.id, id12),
|
|
5736
5833
|
or(
|
|
5737
5834
|
isNull(userPublicKeys.lastUsedAt),
|
|
5738
5835
|
lt(userPublicKeys.lastUsedAt, staleBefore)
|
|
@@ -5774,8 +5871,8 @@ var init_verification_codes_repository = __esm({
|
|
|
5774
5871
|
* ID로 인증 코드 조회
|
|
5775
5872
|
* Read replica 사용
|
|
5776
5873
|
*/
|
|
5777
|
-
async findById(
|
|
5778
|
-
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id,
|
|
5874
|
+
async findById(id12) {
|
|
5875
|
+
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id12)).limit(1);
|
|
5779
5876
|
return result[0] ?? null;
|
|
5780
5877
|
}
|
|
5781
5878
|
/**
|
|
@@ -5793,24 +5890,24 @@ var init_verification_codes_repository = __esm({
|
|
|
5793
5890
|
* 인증 코드 사용 처리
|
|
5794
5891
|
* Write primary 사용
|
|
5795
5892
|
*/
|
|
5796
|
-
async markAsUsed(
|
|
5893
|
+
async markAsUsed(id12) {
|
|
5797
5894
|
const result = await this.db.update(verificationCodes).set({
|
|
5798
5895
|
usedAt: /* @__PURE__ */ new Date(),
|
|
5799
5896
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5800
|
-
}).where(eq3(verificationCodes.id,
|
|
5897
|
+
}).where(eq3(verificationCodes.id, id12)).returning();
|
|
5801
5898
|
return result[0] ?? null;
|
|
5802
5899
|
}
|
|
5803
5900
|
/**
|
|
5804
5901
|
* 시도 횟수 증가
|
|
5805
5902
|
* Write primary 사용
|
|
5806
5903
|
*/
|
|
5807
|
-
async incrementAttempts(
|
|
5808
|
-
const code = await this.findById(
|
|
5904
|
+
async incrementAttempts(id12) {
|
|
5905
|
+
const code = await this.findById(id12);
|
|
5809
5906
|
if (!code) return null;
|
|
5810
5907
|
const result = await this.db.update(verificationCodes).set({
|
|
5811
5908
|
attempts: code.attempts + 1,
|
|
5812
5909
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5813
|
-
}).where(eq3(verificationCodes.id,
|
|
5910
|
+
}).where(eq3(verificationCodes.id, id12)).returning();
|
|
5814
5911
|
return result[0] ?? null;
|
|
5815
5912
|
}
|
|
5816
5913
|
/**
|
|
@@ -5822,6 +5919,17 @@ var init_verification_codes_repository = __esm({
|
|
|
5822
5919
|
const result = await this.db.delete(verificationCodes).where(lt2(verificationCodes.expiresAt, now)).returning();
|
|
5823
5920
|
return result.length;
|
|
5824
5921
|
}
|
|
5922
|
+
/**
|
|
5923
|
+
* Target(email/phone)의 모든 인증 코드 삭제 (계정 파기용)
|
|
5924
|
+
*
|
|
5925
|
+
* verificationCodes는 userId FK가 없고 target 텍스트로만 연결되므로, 파기 시
|
|
5926
|
+
* 원래 email/phone을 이 메서드로 직접 정리해야 한다.
|
|
5927
|
+
* Write primary 사용
|
|
5928
|
+
*/
|
|
5929
|
+
async deleteByTarget(target) {
|
|
5930
|
+
const result = await this.db.delete(verificationCodes).where(eq3(verificationCodes.target, target)).returning();
|
|
5931
|
+
return result.length;
|
|
5932
|
+
}
|
|
5825
5933
|
/**
|
|
5826
5934
|
* Target의 모든 이전 코드 무효화 (새 코드 발급 시)
|
|
5827
5935
|
* Write primary 사용
|
|
@@ -5856,8 +5964,8 @@ var init_roles_repository = __esm({
|
|
|
5856
5964
|
/**
|
|
5857
5965
|
* ID로 역할 조회
|
|
5858
5966
|
*/
|
|
5859
|
-
async findById(
|
|
5860
|
-
const result = await this.readDb.select().from(roles).where(eq4(roles.id,
|
|
5967
|
+
async findById(id12) {
|
|
5968
|
+
const result = await this.readDb.select().from(roles).where(eq4(roles.id, id12)).limit(1);
|
|
5861
5969
|
return result[0] ?? null;
|
|
5862
5970
|
}
|
|
5863
5971
|
/**
|
|
@@ -5892,15 +6000,15 @@ var init_roles_repository = __esm({
|
|
|
5892
6000
|
/**
|
|
5893
6001
|
* 역할 업데이트
|
|
5894
6002
|
*/
|
|
5895
|
-
async updateById(
|
|
5896
|
-
const result = await this.db.update(roles).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq4(roles.id,
|
|
6003
|
+
async updateById(id12, data) {
|
|
6004
|
+
const result = await this.db.update(roles).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq4(roles.id, id12)).returning();
|
|
5897
6005
|
return result[0] ?? null;
|
|
5898
6006
|
}
|
|
5899
6007
|
/**
|
|
5900
6008
|
* 역할 삭제
|
|
5901
6009
|
*/
|
|
5902
|
-
async deleteById(
|
|
5903
|
-
const result = await this.db.delete(roles).where(eq4(roles.id,
|
|
6010
|
+
async deleteById(id12) {
|
|
6011
|
+
const result = await this.db.delete(roles).where(eq4(roles.id, id12)).returning();
|
|
5904
6012
|
return result[0] ?? null;
|
|
5905
6013
|
}
|
|
5906
6014
|
};
|
|
@@ -5920,8 +6028,8 @@ var init_permissions_repository = __esm({
|
|
|
5920
6028
|
/**
|
|
5921
6029
|
* ID로 권한 조회
|
|
5922
6030
|
*/
|
|
5923
|
-
async findById(
|
|
5924
|
-
const result = await this.readDb.select().from(permissions).where(eq5(permissions.id,
|
|
6031
|
+
async findById(id12) {
|
|
6032
|
+
const result = await this.readDb.select().from(permissions).where(eq5(permissions.id, id12)).limit(1);
|
|
5925
6033
|
return result[0] ?? null;
|
|
5926
6034
|
}
|
|
5927
6035
|
/**
|
|
@@ -5982,15 +6090,15 @@ var init_permissions_repository = __esm({
|
|
|
5982
6090
|
/**
|
|
5983
6091
|
* 권한 업데이트
|
|
5984
6092
|
*/
|
|
5985
|
-
async updateById(
|
|
5986
|
-
const result = await this.db.update(permissions).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq5(permissions.id,
|
|
6093
|
+
async updateById(id12, data) {
|
|
6094
|
+
const result = await this.db.update(permissions).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq5(permissions.id, id12)).returning();
|
|
5987
6095
|
return result[0] ?? null;
|
|
5988
6096
|
}
|
|
5989
6097
|
/**
|
|
5990
6098
|
* 권한 삭제
|
|
5991
6099
|
*/
|
|
5992
|
-
async deleteById(
|
|
5993
|
-
const result = await this.db.delete(permissions).where(eq5(permissions.id,
|
|
6100
|
+
async deleteById(id12) {
|
|
6101
|
+
const result = await this.db.delete(permissions).where(eq5(permissions.id, id12)).returning();
|
|
5994
6102
|
return result[0] ?? null;
|
|
5995
6103
|
}
|
|
5996
6104
|
};
|
|
@@ -6134,8 +6242,8 @@ var init_user_permissions_repository = __esm({
|
|
|
6134
6242
|
/**
|
|
6135
6243
|
* 사용자 권한 오버라이드 업데이트
|
|
6136
6244
|
*/
|
|
6137
|
-
async updateById(
|
|
6138
|
-
const result = await this.db.update(userPermissions).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq7(userPermissions.id,
|
|
6245
|
+
async updateById(id12, data) {
|
|
6246
|
+
const result = await this.db.update(userPermissions).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq7(userPermissions.id, id12)).returning();
|
|
6139
6247
|
return result[0] ?? null;
|
|
6140
6248
|
}
|
|
6141
6249
|
/**
|
|
@@ -6187,8 +6295,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6187
6295
|
/**
|
|
6188
6296
|
* ID로 프로필 조회
|
|
6189
6297
|
*/
|
|
6190
|
-
async findById(
|
|
6191
|
-
const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id,
|
|
6298
|
+
async findById(id12) {
|
|
6299
|
+
const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id, id12)).limit(1);
|
|
6192
6300
|
return result[0] ?? null;
|
|
6193
6301
|
}
|
|
6194
6302
|
/**
|
|
@@ -6218,8 +6326,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6218
6326
|
/**
|
|
6219
6327
|
* 프로필 업데이트 (by ID)
|
|
6220
6328
|
*/
|
|
6221
|
-
async updateById(
|
|
6222
|
-
const result = await this.db.update(userProfiles).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq8(userProfiles.id,
|
|
6329
|
+
async updateById(id12, data) {
|
|
6330
|
+
const result = await this.db.update(userProfiles).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq8(userProfiles.id, id12)).returning();
|
|
6223
6331
|
return result[0] ?? null;
|
|
6224
6332
|
}
|
|
6225
6333
|
/**
|
|
@@ -6232,8 +6340,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6232
6340
|
/**
|
|
6233
6341
|
* 프로필 삭제 (by ID)
|
|
6234
6342
|
*/
|
|
6235
|
-
async deleteById(
|
|
6236
|
-
const result = await this.db.delete(userProfiles).where(eq8(userProfiles.id,
|
|
6343
|
+
async deleteById(id12) {
|
|
6344
|
+
const result = await this.db.delete(userProfiles).where(eq8(userProfiles.id, id12)).returning();
|
|
6237
6345
|
return result[0] ?? null;
|
|
6238
6346
|
}
|
|
6239
6347
|
/**
|
|
@@ -6323,8 +6431,8 @@ var init_invitations_repository = __esm({
|
|
|
6323
6431
|
/**
|
|
6324
6432
|
* ID로 초대 조회
|
|
6325
6433
|
*/
|
|
6326
|
-
async findById(
|
|
6327
|
-
const result = await this.readDb.select().from(userInvitations).where(eq9(userInvitations.id,
|
|
6434
|
+
async findById(id12) {
|
|
6435
|
+
const result = await this.readDb.select().from(userInvitations).where(eq9(userInvitations.id, id12)).limit(1);
|
|
6328
6436
|
return result[0] ?? null;
|
|
6329
6437
|
}
|
|
6330
6438
|
/**
|
|
@@ -6371,7 +6479,7 @@ var init_invitations_repository = __esm({
|
|
|
6371
6479
|
/**
|
|
6372
6480
|
* 초대 상태 업데이트
|
|
6373
6481
|
*/
|
|
6374
|
-
async updateStatus(
|
|
6482
|
+
async updateStatus(id12, status, timestamp2) {
|
|
6375
6483
|
const updates = {
|
|
6376
6484
|
status,
|
|
6377
6485
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -6383,14 +6491,14 @@ var init_invitations_repository = __esm({
|
|
|
6383
6491
|
updates.cancelledAt = timestamp2;
|
|
6384
6492
|
}
|
|
6385
6493
|
}
|
|
6386
|
-
const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id,
|
|
6494
|
+
const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id, id12)).returning();
|
|
6387
6495
|
return result[0] ?? null;
|
|
6388
6496
|
}
|
|
6389
6497
|
/**
|
|
6390
6498
|
* 초대 삭제
|
|
6391
6499
|
*/
|
|
6392
|
-
async deleteById(
|
|
6393
|
-
const result = await this.db.delete(userInvitations).where(eq9(userInvitations.id,
|
|
6500
|
+
async deleteById(id12) {
|
|
6501
|
+
const result = await this.db.delete(userInvitations).where(eq9(userInvitations.id, id12)).returning();
|
|
6394
6502
|
return result[0] ?? null;
|
|
6395
6503
|
}
|
|
6396
6504
|
/**
|
|
@@ -6488,35 +6596,35 @@ var init_invitations_repository = __esm({
|
|
|
6488
6596
|
/**
|
|
6489
6597
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
6490
6598
|
*/
|
|
6491
|
-
async updateById(
|
|
6599
|
+
async updateById(id12, data) {
|
|
6492
6600
|
const result = await this.db.update(userInvitations).set({
|
|
6493
6601
|
...data,
|
|
6494
6602
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6495
|
-
}).where(eq9(userInvitations.id,
|
|
6603
|
+
}).where(eq9(userInvitations.id, id12)).returning();
|
|
6496
6604
|
return result[0] ?? null;
|
|
6497
6605
|
}
|
|
6498
6606
|
/**
|
|
6499
6607
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
6500
6608
|
*/
|
|
6501
|
-
async resend(
|
|
6609
|
+
async resend(id12, newExpiresAt) {
|
|
6502
6610
|
const result = await this.db.update(userInvitations).set({
|
|
6503
6611
|
status: "pending",
|
|
6504
6612
|
expiresAt: newExpiresAt,
|
|
6505
6613
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6506
|
-
}).where(eq9(userInvitations.id,
|
|
6614
|
+
}).where(eq9(userInvitations.id, id12)).returning();
|
|
6507
6615
|
return result[0] ?? null;
|
|
6508
6616
|
}
|
|
6509
6617
|
/**
|
|
6510
6618
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
6511
6619
|
*/
|
|
6512
|
-
async cancel(
|
|
6620
|
+
async cancel(id12, cancelledBy, reason, currentMetadata) {
|
|
6513
6621
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
6514
6622
|
const result = await this.db.update(userInvitations).set({
|
|
6515
6623
|
status: "cancelled",
|
|
6516
6624
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
6517
6625
|
metadata: newMetadata,
|
|
6518
6626
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6519
|
-
}).where(eq9(userInvitations.id,
|
|
6627
|
+
}).where(eq9(userInvitations.id, id12)).returning();
|
|
6520
6628
|
return result[0] ?? null;
|
|
6521
6629
|
}
|
|
6522
6630
|
};
|
|
@@ -6524,47 +6632,556 @@ var init_invitations_repository = __esm({
|
|
|
6524
6632
|
}
|
|
6525
6633
|
});
|
|
6526
6634
|
|
|
6635
|
+
// src/config/schema.ts
|
|
6636
|
+
import {
|
|
6637
|
+
defineEnvSchema,
|
|
6638
|
+
envString,
|
|
6639
|
+
envNumber,
|
|
6640
|
+
envBoolean,
|
|
6641
|
+
createSecureSecretParser,
|
|
6642
|
+
createPasswordParser as createPasswordParser2
|
|
6643
|
+
} from "@spfn/core/env";
|
|
6644
|
+
var authEnvSchema;
|
|
6645
|
+
var init_schema5 = __esm({
|
|
6646
|
+
"src/config/schema.ts"() {
|
|
6647
|
+
"use strict";
|
|
6648
|
+
authEnvSchema = defineEnvSchema({
|
|
6649
|
+
// ============================================================================
|
|
6650
|
+
// Session Configuration
|
|
6651
|
+
// ============================================================================
|
|
6652
|
+
SPFN_AUTH_SESSION_SECRET: {
|
|
6653
|
+
...envString({
|
|
6654
|
+
description: "Session encryption secret (minimum 32 characters for AES-256)",
|
|
6655
|
+
required: true,
|
|
6656
|
+
fallbackKeys: ["SESSION_SECRET"],
|
|
6657
|
+
validator: createSecureSecretParser({
|
|
6658
|
+
minLength: 32,
|
|
6659
|
+
minUniqueChars: 16,
|
|
6660
|
+
minEntropy: 3.5
|
|
6661
|
+
}),
|
|
6662
|
+
sensitive: true,
|
|
6663
|
+
nextjs: true,
|
|
6664
|
+
// Required for Next.js RSC session validation
|
|
6665
|
+
examples: [
|
|
6666
|
+
"my-super-secret-session-key-at-least-32-chars-long",
|
|
6667
|
+
"use-a-cryptographically-secure-random-string-here"
|
|
6668
|
+
]
|
|
6669
|
+
})
|
|
6670
|
+
},
|
|
6671
|
+
SPFN_AUTH_SESSION_TTL: {
|
|
6672
|
+
...envString({
|
|
6673
|
+
description: "Session TTL (time to live) - supports duration strings like '7d', '12h', '45m'",
|
|
6674
|
+
default: "7d",
|
|
6675
|
+
required: false,
|
|
6676
|
+
nextjs: true,
|
|
6677
|
+
// May be needed for session validation in Next.js RSC
|
|
6678
|
+
examples: ["7d", "30d", "12h", "45m", "3600"]
|
|
6679
|
+
})
|
|
6680
|
+
},
|
|
6681
|
+
// ============================================================================
|
|
6682
|
+
// JWT Configuration
|
|
6683
|
+
// ============================================================================
|
|
6684
|
+
SPFN_AUTH_JWT_SECRET: {
|
|
6685
|
+
...envString({
|
|
6686
|
+
description: "JWT signing secret for server-signed tokens (legacy mode)",
|
|
6687
|
+
default: "dev-secret-key-change-in-production",
|
|
6688
|
+
required: false,
|
|
6689
|
+
examples: [
|
|
6690
|
+
"your-jwt-secret-key-here",
|
|
6691
|
+
"use-different-from-session-secret"
|
|
6692
|
+
]
|
|
6693
|
+
})
|
|
6694
|
+
},
|
|
6695
|
+
SPFN_AUTH_JWT_EXPIRES_IN: {
|
|
6696
|
+
...envString({
|
|
6697
|
+
description: "JWT token expiration time (e.g., '7d', '24h', '1h')",
|
|
6698
|
+
default: "7d",
|
|
6699
|
+
required: false,
|
|
6700
|
+
examples: ["7d", "24h", "1h", "30m"]
|
|
6701
|
+
})
|
|
6702
|
+
},
|
|
6703
|
+
// ============================================================================
|
|
6704
|
+
// Security Configuration
|
|
6705
|
+
// ============================================================================
|
|
6706
|
+
SPFN_AUTH_COOKIE_SECURE: {
|
|
6707
|
+
...envBoolean({
|
|
6708
|
+
description: 'Override cookie Secure flag. Defaults to NODE_ENV === "production". Set to false for HTTP-only environments (e.g. bastion over plain HTTP).',
|
|
6709
|
+
required: false,
|
|
6710
|
+
nextjs: true,
|
|
6711
|
+
examples: [true, false]
|
|
6712
|
+
})
|
|
6713
|
+
},
|
|
6714
|
+
SPFN_AUTH_BCRYPT_SALT_ROUNDS: {
|
|
6715
|
+
...envNumber({
|
|
6716
|
+
description: "Bcrypt salt rounds (cost factor, higher = more secure but slower)",
|
|
6717
|
+
default: 12,
|
|
6718
|
+
required: false,
|
|
6719
|
+
examples: [10, 12, 14]
|
|
6720
|
+
}),
|
|
6721
|
+
key: "SPFN_AUTH_BCRYPT_SALT_ROUNDS"
|
|
6722
|
+
},
|
|
6723
|
+
SPFN_AUTH_VERIFICATION_TOKEN_SECRET: {
|
|
6724
|
+
...envString({
|
|
6725
|
+
description: "Verification token secret for email verification, password reset, etc.",
|
|
6726
|
+
required: true,
|
|
6727
|
+
examples: [
|
|
6728
|
+
"your-verification-token-secret",
|
|
6729
|
+
"can-be-different-from-jwt-secret"
|
|
6730
|
+
]
|
|
6731
|
+
})
|
|
6732
|
+
},
|
|
6733
|
+
SPFN_AUTH_TOKEN_ENCRYPTION_KEYS: {
|
|
6734
|
+
...envString({
|
|
6735
|
+
description: "Backend-only OAuth token encryption keyring. Comma-separated <keyId>:<base64-encoded 32-byte key> entries; the first key encrypts new values and remaining keys decrypt during rotation.",
|
|
6736
|
+
required: false,
|
|
6737
|
+
sensitive: true,
|
|
6738
|
+
examples: [
|
|
6739
|
+
"v2:<base64-encoded-32-byte-key>,v1:<previous-base64-encoded-32-byte-key>"
|
|
6740
|
+
]
|
|
6741
|
+
})
|
|
6742
|
+
},
|
|
6743
|
+
// ============================================================================
|
|
6744
|
+
// Admin Account Configuration
|
|
6745
|
+
// ============================================================================
|
|
6746
|
+
SPFN_AUTH_ADMIN_ACCOUNTS: {
|
|
6747
|
+
...envString({
|
|
6748
|
+
description: "JSON array of admin accounts (recommended for multiple admins)",
|
|
6749
|
+
required: false,
|
|
6750
|
+
examples: [
|
|
6751
|
+
'[{"email":"admin@example.com","password":"secure-pass","role":"admin"}]',
|
|
6752
|
+
'[{"email":"super@example.com","password":"pass1","role":"superadmin"},{"email":"admin@example.com","password":"pass2","role":"admin"}]'
|
|
6753
|
+
]
|
|
6754
|
+
})
|
|
6755
|
+
},
|
|
6756
|
+
SPFN_AUTH_ADMIN_EMAILS: {
|
|
6757
|
+
...envString({
|
|
6758
|
+
description: "Comma-separated list of admin emails (legacy CSV format)",
|
|
6759
|
+
required: false,
|
|
6760
|
+
examples: [
|
|
6761
|
+
"admin@example.com,user@example.com",
|
|
6762
|
+
"super@example.com,admin@example.com,user@example.com"
|
|
6763
|
+
]
|
|
6764
|
+
})
|
|
6765
|
+
},
|
|
6766
|
+
SPFN_AUTH_ADMIN_PASSWORDS: {
|
|
6767
|
+
...envString({
|
|
6768
|
+
description: "Comma-separated list of admin passwords (legacy CSV format)",
|
|
6769
|
+
required: false,
|
|
6770
|
+
examples: [
|
|
6771
|
+
"admin-pass,user-pass",
|
|
6772
|
+
"super-pass,admin-pass,user-pass"
|
|
6773
|
+
]
|
|
6774
|
+
})
|
|
6775
|
+
},
|
|
6776
|
+
SPFN_AUTH_ADMIN_ROLES: {
|
|
6777
|
+
...envString({
|
|
6778
|
+
description: "Comma-separated list of admin roles (legacy CSV format)",
|
|
6779
|
+
required: false,
|
|
6780
|
+
examples: [
|
|
6781
|
+
"admin,user",
|
|
6782
|
+
"superadmin,admin,user"
|
|
6783
|
+
]
|
|
6784
|
+
})
|
|
6785
|
+
},
|
|
6786
|
+
SPFN_AUTH_ADMIN_EMAIL: {
|
|
6787
|
+
...envString({
|
|
6788
|
+
description: "Single admin email (simplest format)",
|
|
6789
|
+
required: false,
|
|
6790
|
+
examples: ["admin@example.com"]
|
|
6791
|
+
})
|
|
6792
|
+
},
|
|
6793
|
+
SPFN_AUTH_ADMIN_PASSWORD: {
|
|
6794
|
+
...envString({
|
|
6795
|
+
description: "Single admin password (simplest format)",
|
|
6796
|
+
required: false,
|
|
6797
|
+
validator: createPasswordParser2({
|
|
6798
|
+
minLength: 8,
|
|
6799
|
+
requireUppercase: true,
|
|
6800
|
+
requireLowercase: true,
|
|
6801
|
+
requireNumber: true,
|
|
6802
|
+
requireSpecial: true
|
|
6803
|
+
}),
|
|
6804
|
+
sensitive: true,
|
|
6805
|
+
examples: ["SecureAdmin123!"]
|
|
6806
|
+
})
|
|
6807
|
+
},
|
|
6808
|
+
// ============================================================================
|
|
6809
|
+
// Username Configuration
|
|
6810
|
+
// ============================================================================
|
|
6811
|
+
SPFN_AUTH_RESERVED_USERNAMES: {
|
|
6812
|
+
...envString({
|
|
6813
|
+
description: "Comma-separated list of reserved usernames that cannot be registered",
|
|
6814
|
+
required: false,
|
|
6815
|
+
default: "admin,root,system,support,help,moderator,superadmin",
|
|
6816
|
+
examples: [
|
|
6817
|
+
"admin,root,system,support,help",
|
|
6818
|
+
"admin,root,system,support,help,moderator,superadmin,operator"
|
|
6819
|
+
]
|
|
6820
|
+
})
|
|
6821
|
+
},
|
|
6822
|
+
SPFN_AUTH_USERNAME_MIN_LENGTH: {
|
|
6823
|
+
...envNumber({
|
|
6824
|
+
description: "Minimum username length",
|
|
6825
|
+
default: 3,
|
|
6826
|
+
required: false,
|
|
6827
|
+
examples: [2, 3, 4]
|
|
6828
|
+
})
|
|
6829
|
+
},
|
|
6830
|
+
SPFN_AUTH_USERNAME_MAX_LENGTH: {
|
|
6831
|
+
...envNumber({
|
|
6832
|
+
description: "Maximum username length",
|
|
6833
|
+
default: 30,
|
|
6834
|
+
required: false,
|
|
6835
|
+
examples: [20, 30, 50]
|
|
6836
|
+
})
|
|
6837
|
+
},
|
|
6838
|
+
// ============================================================================
|
|
6839
|
+
// API Configuration
|
|
6840
|
+
// ============================================================================
|
|
6841
|
+
SPFN_API_URL: {
|
|
6842
|
+
...envString({
|
|
6843
|
+
description: "Internal API URL for server-to-server communication",
|
|
6844
|
+
default: "http://localhost:8790",
|
|
6845
|
+
required: false,
|
|
6846
|
+
examples: [
|
|
6847
|
+
"https://api.example.com",
|
|
6848
|
+
"http://localhost:8790"
|
|
6849
|
+
]
|
|
6850
|
+
})
|
|
6851
|
+
},
|
|
6852
|
+
NEXT_PUBLIC_SPFN_API_URL: {
|
|
6853
|
+
...envString({
|
|
6854
|
+
description: "Public-facing API URL used for browser-facing redirects. Falls back to SPFN_API_URL if not set.",
|
|
6855
|
+
required: false,
|
|
6856
|
+
examples: [
|
|
6857
|
+
"https://api.example.com",
|
|
6858
|
+
"http://localhost:8790"
|
|
6859
|
+
]
|
|
6860
|
+
})
|
|
6861
|
+
},
|
|
6862
|
+
SPFN_APP_URL: {
|
|
6863
|
+
...envString({
|
|
6864
|
+
description: "Next.js application URL (internal). Used for server-to-server communication.",
|
|
6865
|
+
default: "http://localhost:3000",
|
|
6866
|
+
required: false,
|
|
6867
|
+
examples: [
|
|
6868
|
+
"https://app.example.com",
|
|
6869
|
+
"http://localhost:3000"
|
|
6870
|
+
]
|
|
6871
|
+
})
|
|
6872
|
+
},
|
|
6873
|
+
NEXT_PUBLIC_SPFN_APP_URL: {
|
|
6874
|
+
...envString({
|
|
6875
|
+
description: "Public-facing Next.js app URL for browser redirects (e.g. OAuth redirect). Falls back to SPFN_APP_URL if not set.",
|
|
6876
|
+
required: false,
|
|
6877
|
+
examples: [
|
|
6878
|
+
"https://app.example.com",
|
|
6879
|
+
"http://localhost:3000"
|
|
6880
|
+
]
|
|
6881
|
+
})
|
|
6882
|
+
},
|
|
6883
|
+
// ============================================================================
|
|
6884
|
+
// OAuth Configuration - Google
|
|
6885
|
+
// ============================================================================
|
|
6886
|
+
SPFN_AUTH_GOOGLE_CLIENT_ID: {
|
|
6887
|
+
...envString({
|
|
6888
|
+
description: "Google OAuth 2.0 Client ID. When set, Google OAuth routes are automatically enabled.",
|
|
6889
|
+
required: false,
|
|
6890
|
+
examples: ["123456789-abc123.apps.googleusercontent.com"]
|
|
6891
|
+
})
|
|
6892
|
+
},
|
|
6893
|
+
SPFN_AUTH_GOOGLE_CLIENT_SECRET: {
|
|
6894
|
+
...envString({
|
|
6895
|
+
description: "Google OAuth 2.0 Client Secret",
|
|
6896
|
+
required: false,
|
|
6897
|
+
sensitive: true,
|
|
6898
|
+
examples: ["GOCSPX-abcdefghijklmnop"]
|
|
6899
|
+
})
|
|
6900
|
+
},
|
|
6901
|
+
SPFN_AUTH_GOOGLE_SCOPES: {
|
|
6902
|
+
...envString({
|
|
6903
|
+
description: 'Comma-separated Google OAuth scopes. Defaults to "email,profile" if not set.',
|
|
6904
|
+
required: false,
|
|
6905
|
+
examples: [
|
|
6906
|
+
"email,profile",
|
|
6907
|
+
"email,profile,https://www.googleapis.com/auth/gmail.readonly",
|
|
6908
|
+
"email,profile,https://www.googleapis.com/auth/calendar.readonly"
|
|
6909
|
+
]
|
|
6910
|
+
})
|
|
6911
|
+
},
|
|
6912
|
+
SPFN_AUTH_GOOGLE_REDIRECT_URI: {
|
|
6913
|
+
...envString({
|
|
6914
|
+
description: "Google OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/google/callback \u2014 the callback must return to the web app origin that set the oauth_csrf cookie (the app rewrites /_auth/:path* to the API). Set this explicitly only when the callback should hit a different host (e.g. the API host for the direct oauthStart flow).",
|
|
6915
|
+
required: false,
|
|
6916
|
+
examples: [
|
|
6917
|
+
"https://app.example.com/_auth/oauth/google/callback",
|
|
6918
|
+
"http://localhost:3000/_auth/oauth/google/callback"
|
|
6919
|
+
]
|
|
6920
|
+
})
|
|
6921
|
+
},
|
|
6922
|
+
// ============================================================================
|
|
6923
|
+
// OAuth Configuration - Kakao
|
|
6924
|
+
// ============================================================================
|
|
6925
|
+
SPFN_AUTH_KAKAO_CLIENT_ID: {
|
|
6926
|
+
...envString({
|
|
6927
|
+
description: "Kakao Login REST API key. Used as the OAuth client_id.",
|
|
6928
|
+
required: false,
|
|
6929
|
+
examples: ["your-kakao-rest-api-key"]
|
|
6930
|
+
})
|
|
6931
|
+
},
|
|
6932
|
+
SPFN_AUTH_KAKAO_CLIENT_SECRET: {
|
|
6933
|
+
...envString({
|
|
6934
|
+
description: "Kakao Login client secret. Required when the Kakao client-secret feature is enabled.",
|
|
6935
|
+
required: false,
|
|
6936
|
+
sensitive: true,
|
|
6937
|
+
examples: ["your-kakao-client-secret"]
|
|
6938
|
+
})
|
|
6939
|
+
},
|
|
6940
|
+
SPFN_AUTH_KAKAO_SCOPES: {
|
|
6941
|
+
...envString({
|
|
6942
|
+
description: "Comma-separated Kakao consent scopes. Defaults to account_email.",
|
|
6943
|
+
required: false,
|
|
6944
|
+
examples: ["account_email"]
|
|
6945
|
+
})
|
|
6946
|
+
},
|
|
6947
|
+
SPFN_AUTH_KAKAO_REDIRECT_URI: {
|
|
6948
|
+
...envString({
|
|
6949
|
+
description: "Kakao OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/kakao/callback.",
|
|
6950
|
+
required: false,
|
|
6951
|
+
examples: ["https://app.example.com/_auth/oauth/kakao/callback"]
|
|
6952
|
+
})
|
|
6953
|
+
},
|
|
6954
|
+
// ============================================================================
|
|
6955
|
+
// OAuth Configuration - Naver
|
|
6956
|
+
// ============================================================================
|
|
6957
|
+
SPFN_AUTH_NAVER_CLIENT_ID: {
|
|
6958
|
+
...envString({
|
|
6959
|
+
description: "Naver Login OAuth client ID.",
|
|
6960
|
+
required: false,
|
|
6961
|
+
examples: ["your-naver-client-id"]
|
|
6962
|
+
})
|
|
6963
|
+
},
|
|
6964
|
+
SPFN_AUTH_NAVER_CLIENT_SECRET: {
|
|
6965
|
+
...envString({
|
|
6966
|
+
description: "Naver Login OAuth client secret.",
|
|
6967
|
+
required: false,
|
|
6968
|
+
sensitive: true,
|
|
6969
|
+
examples: ["your-naver-client-secret"]
|
|
6970
|
+
})
|
|
6971
|
+
},
|
|
6972
|
+
SPFN_AUTH_NAVER_REDIRECT_URI: {
|
|
6973
|
+
...envString({
|
|
6974
|
+
description: "Naver OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/naver/callback.",
|
|
6975
|
+
required: false,
|
|
6976
|
+
examples: ["https://app.example.com/_auth/oauth/naver/callback"]
|
|
6977
|
+
})
|
|
6978
|
+
},
|
|
6979
|
+
// ============================================================================
|
|
6980
|
+
// Native Social Login (mobile/web id_token verification)
|
|
6981
|
+
//
|
|
6982
|
+
// 네이티브 SDK가 받은 id_token을 서버가 JWKS로 검증하는 경로 전용 설정.
|
|
6983
|
+
// authorization code 교환을 하지 않으므로 client secret이 필요 없다.
|
|
6984
|
+
// audience(aud)로 허용할 client id 목록만 지정한다.
|
|
6985
|
+
// ============================================================================
|
|
6986
|
+
SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS: {
|
|
6987
|
+
...envString({
|
|
6988
|
+
description: "Comma-separated Google client IDs accepted as id_token audience for native sign-in (iOS, Android, web). When set, Google native sign-in is enabled. SPFN_AUTH_GOOGLE_CLIENT_ID is also accepted automatically.",
|
|
6989
|
+
required: false,
|
|
6990
|
+
examples: [
|
|
6991
|
+
"123-ios.apps.googleusercontent.com,123-android.apps.googleusercontent.com"
|
|
6992
|
+
]
|
|
6993
|
+
})
|
|
6994
|
+
},
|
|
6995
|
+
SPFN_AUTH_APPLE_CLIENT_IDS: {
|
|
6996
|
+
...envString({
|
|
6997
|
+
description: "Comma-separated Apple client IDs accepted as id_token audience for native sign-in (iOS bundle ID, web/Android Services ID). When set, Apple native sign-in is enabled.",
|
|
6998
|
+
required: false,
|
|
6999
|
+
examples: [
|
|
7000
|
+
"com.example.app,com.example.app.service"
|
|
7001
|
+
]
|
|
7002
|
+
})
|
|
7003
|
+
},
|
|
7004
|
+
SPFN_AUTH_OAUTH_SUCCESS_URL: {
|
|
7005
|
+
...envString({
|
|
7006
|
+
description: "OAuth callback page URL. This page should use OAuthCallback component to finalize session.",
|
|
7007
|
+
required: false,
|
|
7008
|
+
default: "/auth/callback",
|
|
7009
|
+
examples: [
|
|
7010
|
+
"/auth/callback",
|
|
7011
|
+
"https://app.example.com/auth/callback"
|
|
7012
|
+
]
|
|
7013
|
+
})
|
|
7014
|
+
},
|
|
7015
|
+
SPFN_AUTH_OAUTH_ERROR_URL: {
|
|
7016
|
+
...envString({
|
|
7017
|
+
description: "URL to redirect after OAuth error. Use {error} placeholder for error message.",
|
|
7018
|
+
required: false,
|
|
7019
|
+
default: "/auth/error?error={error}",
|
|
7020
|
+
examples: [
|
|
7021
|
+
"https://app.example.com/auth/error?error={error}",
|
|
7022
|
+
"http://localhost:3000/auth/error?error={error}"
|
|
7023
|
+
]
|
|
7024
|
+
})
|
|
7025
|
+
}
|
|
7026
|
+
});
|
|
7027
|
+
}
|
|
7028
|
+
});
|
|
7029
|
+
|
|
7030
|
+
// src/config/index.ts
|
|
7031
|
+
import { createEnvRegistry } from "@spfn/core/env";
|
|
7032
|
+
var registry, env3;
|
|
7033
|
+
var init_config = __esm({
|
|
7034
|
+
"src/config/index.ts"() {
|
|
7035
|
+
"use strict";
|
|
7036
|
+
init_schema5();
|
|
7037
|
+
init_schema5();
|
|
7038
|
+
registry = createEnvRegistry(authEnvSchema);
|
|
7039
|
+
env3 = registry.validate();
|
|
7040
|
+
}
|
|
7041
|
+
});
|
|
7042
|
+
|
|
6527
7043
|
// src/server/lib/oauth/token-cipher.ts
|
|
6528
7044
|
import crypto3 from "crypto";
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
7045
|
+
function getTokenKeys() {
|
|
7046
|
+
const raw = env3.SPFN_AUTH_TOKEN_ENCRYPTION_KEYS;
|
|
7047
|
+
if (!raw) {
|
|
7048
|
+
return [];
|
|
7049
|
+
}
|
|
7050
|
+
const keys = [];
|
|
7051
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7052
|
+
for (const entry of raw.split(",")) {
|
|
7053
|
+
const trimmed = entry.trim();
|
|
7054
|
+
const separator = trimmed.indexOf(":");
|
|
7055
|
+
if (separator <= 0 || separator === trimmed.length - 1) {
|
|
7056
|
+
throw new Error("Invalid SPFN_AUTH_TOKEN_ENCRYPTION_KEYS entry; expected <keyId>:<base64-key>");
|
|
7057
|
+
}
|
|
7058
|
+
const keyId = trimmed.slice(0, separator);
|
|
7059
|
+
const encoded = trimmed.slice(separator + 1);
|
|
7060
|
+
if (!KEY_ID_PATTERN.test(keyId)) {
|
|
7061
|
+
throw new Error("Invalid token encryption key ID; use 1-64 letters, digits, dot, underscore, or hyphen");
|
|
7062
|
+
}
|
|
7063
|
+
if (seen.has(keyId)) {
|
|
7064
|
+
throw new Error("Duplicate token encryption key ID");
|
|
7065
|
+
}
|
|
7066
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) || encoded.length % 4 !== 0) {
|
|
7067
|
+
throw new Error("Invalid token encryption key encoding; expected canonical base64");
|
|
7068
|
+
}
|
|
7069
|
+
const key = Buffer.from(encoded, "base64");
|
|
7070
|
+
if (key.length !== KEY_BYTES || key.toString("base64") !== encoded) {
|
|
7071
|
+
throw new Error("Invalid token encryption key length; expected exactly 32 bytes");
|
|
7072
|
+
}
|
|
7073
|
+
seen.add(keyId);
|
|
7074
|
+
keys.push({ keyId, key });
|
|
7075
|
+
}
|
|
7076
|
+
return keys;
|
|
6532
7077
|
}
|
|
6533
|
-
function
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
}
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
7078
|
+
function requireTokenKeys() {
|
|
7079
|
+
const keys = getTokenKeys();
|
|
7080
|
+
if (keys.length === 0) {
|
|
7081
|
+
throw new Error(
|
|
7082
|
+
"OAuth token encryption is not configured. Set SPFN_AUTH_TOKEN_ENCRYPTION_KEYS in .env.server."
|
|
7083
|
+
);
|
|
7084
|
+
}
|
|
7085
|
+
return keys;
|
|
7086
|
+
}
|
|
7087
|
+
function getAad(context) {
|
|
7088
|
+
return Buffer.from([
|
|
7089
|
+
"spfn-auth-social-token:v2",
|
|
7090
|
+
context.provider,
|
|
7091
|
+
context.providerUserId,
|
|
7092
|
+
context.tokenType
|
|
7093
|
+
].join("\0"), "utf8");
|
|
7094
|
+
}
|
|
7095
|
+
function unpackCiphertext(payload, encoding) {
|
|
7096
|
+
const packed = Buffer.from(payload, encoding);
|
|
7097
|
+
if (packed.length <= IV_BYTES + TAG_BYTES) {
|
|
7098
|
+
throw new Error("Malformed encrypted token payload");
|
|
7099
|
+
}
|
|
7100
|
+
return {
|
|
7101
|
+
iv: packed.subarray(0, IV_BYTES),
|
|
7102
|
+
tag: packed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES),
|
|
7103
|
+
ciphertext: packed.subarray(IV_BYTES + TAG_BYTES)
|
|
7104
|
+
};
|
|
7105
|
+
}
|
|
7106
|
+
function decryptAesGcm(payload, encoding, key, aad) {
|
|
7107
|
+
const { iv, tag, ciphertext } = unpackCiphertext(payload, encoding);
|
|
7108
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", key, iv);
|
|
7109
|
+
if (aad) {
|
|
7110
|
+
decipher.setAAD(aad);
|
|
7111
|
+
}
|
|
6558
7112
|
decipher.setAuthTag(tag);
|
|
6559
7113
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
6560
7114
|
}
|
|
6561
|
-
|
|
7115
|
+
function getLegacyV1Key() {
|
|
7116
|
+
return crypto3.createHash("sha256").update(`social-token:${env3.SPFN_AUTH_SESSION_SECRET}`).digest();
|
|
7117
|
+
}
|
|
7118
|
+
function configureOAuthTokenCipher(cipher) {
|
|
7119
|
+
configuredCipher = cipher;
|
|
7120
|
+
}
|
|
7121
|
+
function encryptToken(value, context) {
|
|
7122
|
+
return configuredCipher.encrypt(value, context);
|
|
7123
|
+
}
|
|
7124
|
+
function decryptToken(value, context) {
|
|
7125
|
+
return configuredCipher.decrypt(value, context);
|
|
7126
|
+
}
|
|
7127
|
+
function isEncrypted(value) {
|
|
7128
|
+
return value.startsWith(ENCRYPTED_PREFIX);
|
|
7129
|
+
}
|
|
7130
|
+
var V1_PREFIX, V2_PREFIX, ENCRYPTED_PREFIX, IV_BYTES, TAG_BYTES, KEY_BYTES, KEY_ID_PATTERN, EnvironmentKeyringTokenCipher, configuredCipher;
|
|
6562
7131
|
var init_token_cipher = __esm({
|
|
6563
7132
|
"src/server/lib/oauth/token-cipher.ts"() {
|
|
6564
7133
|
"use strict";
|
|
6565
|
-
|
|
7134
|
+
init_config();
|
|
7135
|
+
V1_PREFIX = "enc:v1:";
|
|
7136
|
+
V2_PREFIX = "enc:v2:";
|
|
7137
|
+
ENCRYPTED_PREFIX = "enc:";
|
|
6566
7138
|
IV_BYTES = 12;
|
|
6567
7139
|
TAG_BYTES = 16;
|
|
7140
|
+
KEY_BYTES = 32;
|
|
7141
|
+
KEY_ID_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
7142
|
+
EnvironmentKeyringTokenCipher = class {
|
|
7143
|
+
async encrypt(value, context) {
|
|
7144
|
+
const activeKey = requireTokenKeys()[0];
|
|
7145
|
+
const iv = crypto3.randomBytes(IV_BYTES);
|
|
7146
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", activeKey.key, iv);
|
|
7147
|
+
cipher.setAAD(getAad(context));
|
|
7148
|
+
const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
7149
|
+
const tag = cipher.getAuthTag();
|
|
7150
|
+
const payload = Buffer.concat([iv, tag, ciphertext]).toString("base64url");
|
|
7151
|
+
return `${V2_PREFIX}${activeKey.keyId}:${payload}`;
|
|
7152
|
+
}
|
|
7153
|
+
async decrypt(value, context) {
|
|
7154
|
+
if (value.startsWith(V2_PREFIX)) {
|
|
7155
|
+
const remainder = value.slice(V2_PREFIX.length);
|
|
7156
|
+
const separator = remainder.indexOf(":");
|
|
7157
|
+
if (separator <= 0 || separator === remainder.length - 1) {
|
|
7158
|
+
throw new Error("Malformed v2 encrypted token");
|
|
7159
|
+
}
|
|
7160
|
+
const keyId = remainder.slice(0, separator);
|
|
7161
|
+
const payload = remainder.slice(separator + 1);
|
|
7162
|
+
const keys = requireTokenKeys();
|
|
7163
|
+
const selected = keys.find((key) => key.keyId === keyId);
|
|
7164
|
+
if (!selected) {
|
|
7165
|
+
throw new Error("Encrypted token references an unavailable key ID");
|
|
7166
|
+
}
|
|
7167
|
+
return {
|
|
7168
|
+
value: decryptAesGcm(payload, "base64url", selected.key, getAad(context)),
|
|
7169
|
+
needsRotation: selected.keyId !== keys[0].keyId
|
|
7170
|
+
};
|
|
7171
|
+
}
|
|
7172
|
+
if (value.startsWith(V1_PREFIX)) {
|
|
7173
|
+
return {
|
|
7174
|
+
value: decryptAesGcm(value.slice(V1_PREFIX.length), "base64", getLegacyV1Key()),
|
|
7175
|
+
needsRotation: true
|
|
7176
|
+
};
|
|
7177
|
+
}
|
|
7178
|
+
if (value.startsWith(ENCRYPTED_PREFIX)) {
|
|
7179
|
+
throw new Error("Unsupported encrypted token version");
|
|
7180
|
+
}
|
|
7181
|
+
return { value, needsRotation: true };
|
|
7182
|
+
}
|
|
7183
|
+
};
|
|
7184
|
+
configuredCipher = new EnvironmentKeyringTokenCipher();
|
|
6568
7185
|
}
|
|
6569
7186
|
});
|
|
6570
7187
|
|
|
@@ -6589,23 +7206,34 @@ var init_social_accounts_repository = __esm({
|
|
|
6589
7206
|
if (!account) {
|
|
6590
7207
|
return account;
|
|
6591
7208
|
}
|
|
7209
|
+
const context = (tokenType) => ({
|
|
7210
|
+
provider: account.provider,
|
|
7211
|
+
providerUserId: account.providerUserId,
|
|
7212
|
+
tokenType
|
|
7213
|
+
});
|
|
7214
|
+
const access = account.accessToken ? await decryptToken(account.accessToken, context("access")) : null;
|
|
7215
|
+
const refresh = account.refreshToken ? await decryptToken(account.refreshToken, context("refresh")) : null;
|
|
6592
7216
|
const heal = {};
|
|
6593
|
-
if (
|
|
6594
|
-
heal.accessToken = encryptToken(account.accessToken);
|
|
6595
|
-
}
|
|
6596
|
-
if (account.refreshToken && !isEncrypted(account.refreshToken)) {
|
|
6597
|
-
heal.refreshToken = encryptToken(account.refreshToken);
|
|
6598
|
-
}
|
|
6599
|
-
if (heal.accessToken || heal.refreshToken) {
|
|
7217
|
+
if (access?.needsRotation || refresh?.needsRotation) {
|
|
6600
7218
|
try {
|
|
6601
|
-
|
|
7219
|
+
if (access?.needsRotation) {
|
|
7220
|
+
heal.accessToken = await encryptToken(access.value, context("access"));
|
|
7221
|
+
}
|
|
7222
|
+
if (refresh?.needsRotation) {
|
|
7223
|
+
heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
|
|
7224
|
+
}
|
|
7225
|
+
await this.db.update(userSocialAccounts).set(heal).where(and7(
|
|
7226
|
+
eq10(userSocialAccounts.id, account.id),
|
|
7227
|
+
access?.needsRotation && account.accessToken !== null ? eq10(userSocialAccounts.accessToken, account.accessToken) : void 0,
|
|
7228
|
+
refresh?.needsRotation && account.refreshToken !== null ? eq10(userSocialAccounts.refreshToken, account.refreshToken) : void 0
|
|
7229
|
+
));
|
|
6602
7230
|
} catch {
|
|
6603
7231
|
}
|
|
6604
7232
|
}
|
|
6605
7233
|
return {
|
|
6606
7234
|
...account,
|
|
6607
|
-
accessToken:
|
|
6608
|
-
refreshToken:
|
|
7235
|
+
accessToken: access?.value ?? account.accessToken,
|
|
7236
|
+
refreshToken: refresh?.value ?? account.refreshToken
|
|
6609
7237
|
};
|
|
6610
7238
|
}
|
|
6611
7239
|
/**
|
|
@@ -6647,10 +7275,15 @@ var init_social_accounts_repository = __esm({
|
|
|
6647
7275
|
* Write primary 사용
|
|
6648
7276
|
*/
|
|
6649
7277
|
async create(data) {
|
|
7278
|
+
const context = (tokenType) => ({
|
|
7279
|
+
provider: data.provider,
|
|
7280
|
+
providerUserId: data.providerUserId,
|
|
7281
|
+
tokenType
|
|
7282
|
+
});
|
|
6650
7283
|
const created = await this._create(userSocialAccounts, {
|
|
6651
7284
|
...data,
|
|
6652
|
-
accessToken: data.accessToken ? encryptToken(data.accessToken) : data.accessToken,
|
|
6653
|
-
refreshToken: data.refreshToken ? encryptToken(data.refreshToken) : data.refreshToken,
|
|
7285
|
+
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
7286
|
+
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken,
|
|
6654
7287
|
createdAt: /* @__PURE__ */ new Date(),
|
|
6655
7288
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6656
7289
|
});
|
|
@@ -6660,21 +7293,34 @@ var init_social_accounts_repository = __esm({
|
|
|
6660
7293
|
* 토큰 정보 업데이트
|
|
6661
7294
|
* Write primary 사용
|
|
6662
7295
|
*/
|
|
6663
|
-
async updateTokens(
|
|
7296
|
+
async updateTokens(id12, data) {
|
|
7297
|
+
const accounts = await this.db.select({
|
|
7298
|
+
provider: userSocialAccounts.provider,
|
|
7299
|
+
providerUserId: userSocialAccounts.providerUserId
|
|
7300
|
+
}).from(userSocialAccounts).where(eq10(userSocialAccounts.id, id12)).limit(1);
|
|
7301
|
+
const account = accounts[0];
|
|
7302
|
+
if (!account) {
|
|
7303
|
+
return null;
|
|
7304
|
+
}
|
|
7305
|
+
const context = (tokenType) => ({
|
|
7306
|
+
provider: account.provider,
|
|
7307
|
+
providerUserId: account.providerUserId,
|
|
7308
|
+
tokenType
|
|
7309
|
+
});
|
|
6664
7310
|
const result = await this.db.update(userSocialAccounts).set({
|
|
6665
7311
|
...data,
|
|
6666
|
-
accessToken: data.accessToken ? encryptToken(data.accessToken) : data.accessToken,
|
|
6667
|
-
refreshToken: data.refreshToken ? encryptToken(data.refreshToken) : data.refreshToken,
|
|
7312
|
+
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
7313
|
+
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken,
|
|
6668
7314
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6669
|
-
}).where(eq10(userSocialAccounts.id,
|
|
7315
|
+
}).where(eq10(userSocialAccounts.id, id12)).returning();
|
|
6670
7316
|
return this.decryptAccount(result[0] ?? null);
|
|
6671
7317
|
}
|
|
6672
7318
|
/**
|
|
6673
7319
|
* 소셜 계정 삭제
|
|
6674
7320
|
* Write primary 사용
|
|
6675
7321
|
*/
|
|
6676
|
-
async deleteById(
|
|
6677
|
-
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id,
|
|
7322
|
+
async deleteById(id12) {
|
|
7323
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id12)).returning();
|
|
6678
7324
|
return result[0] ?? null;
|
|
6679
7325
|
}
|
|
6680
7326
|
/**
|
|
@@ -6690,6 +7336,17 @@ var init_social_accounts_repository = __esm({
|
|
|
6690
7336
|
).returning();
|
|
6691
7337
|
return result[0] ?? null;
|
|
6692
7338
|
}
|
|
7339
|
+
/**
|
|
7340
|
+
* 사용자의 모든 소셜 계정 삭제 (계정 익명화 파기용)
|
|
7341
|
+
*
|
|
7342
|
+
* provider unique index(provider, providerUserId)를 해제해 같은 소셜 계정으로
|
|
7343
|
+
* 재가입할 수 있게 한다.
|
|
7344
|
+
* Write primary 사용
|
|
7345
|
+
*/
|
|
7346
|
+
async deleteAllByUserId(userId) {
|
|
7347
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId)).returning();
|
|
7348
|
+
return result.length;
|
|
7349
|
+
}
|
|
6693
7350
|
};
|
|
6694
7351
|
socialAccountsRepository = new SocialAccountsRepository();
|
|
6695
7352
|
}
|
|
@@ -6732,6 +7389,125 @@ var init_auth_metadata_repository = __esm({
|
|
|
6732
7389
|
}
|
|
6733
7390
|
});
|
|
6734
7391
|
|
|
7392
|
+
// src/server/repositories/account-deletion-requests.repository.ts
|
|
7393
|
+
import { eq as eq12, and as and8, lte } from "drizzle-orm";
|
|
7394
|
+
import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
|
|
7395
|
+
var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
|
|
7396
|
+
var init_account_deletion_requests_repository = __esm({
|
|
7397
|
+
"src/server/repositories/account-deletion-requests.repository.ts"() {
|
|
7398
|
+
"use strict";
|
|
7399
|
+
init_account_deletion_requests();
|
|
7400
|
+
AccountDeletionRequestsRepository = class extends BaseRepository12 {
|
|
7401
|
+
/**
|
|
7402
|
+
* ID로 요청 조회
|
|
7403
|
+
* Read replica 사용
|
|
7404
|
+
*/
|
|
7405
|
+
async findById(id12) {
|
|
7406
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(eq12(accountDeletionRequests.id, id12)).limit(1);
|
|
7407
|
+
return result[0] ?? null;
|
|
7408
|
+
}
|
|
7409
|
+
/**
|
|
7410
|
+
* User ID로 pending 요청 조회 (유저당 최대 1건, partial unique index로 보장)
|
|
7411
|
+
* Read replica 사용
|
|
7412
|
+
*/
|
|
7413
|
+
async findPendingByUserId(userId) {
|
|
7414
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
7415
|
+
and8(
|
|
7416
|
+
eq12(accountDeletionRequests.userId, userId),
|
|
7417
|
+
eq12(accountDeletionRequests.status, "pending")
|
|
7418
|
+
)
|
|
7419
|
+
).limit(1);
|
|
7420
|
+
return result[0] ?? null;
|
|
7421
|
+
}
|
|
7422
|
+
/**
|
|
7423
|
+
* User ID로 pending 요청 조회 — Write primary에서 직접 읽는다.
|
|
7424
|
+
*
|
|
7425
|
+
* 로그인/OAuth/authenticate 게이트가 pending_deletion 유저의 purgeScheduledAt을
|
|
7426
|
+
* 표시하는 데 쓴다. 이 경로들은 복제 지연이 있으면 방금 커밋된 상태 전이를 놓칠 수
|
|
7427
|
+
* 있어(예: 삭제 요청 직후 OAuth 로그인 시도), replica가 아닌 primary에서 읽는다.
|
|
7428
|
+
*/
|
|
7429
|
+
async findPendingByUserIdOnPrimary(userId) {
|
|
7430
|
+
const result = await this.db.select().from(accountDeletionRequests).where(
|
|
7431
|
+
and8(
|
|
7432
|
+
eq12(accountDeletionRequests.userId, userId),
|
|
7433
|
+
eq12(accountDeletionRequests.status, "pending")
|
|
7434
|
+
)
|
|
7435
|
+
).limit(1);
|
|
7436
|
+
return result[0] ?? null;
|
|
7437
|
+
}
|
|
7438
|
+
/**
|
|
7439
|
+
* 파기 스윕 대상 조회 — status='pending' AND purgeScheduledAt <= now
|
|
7440
|
+
* Read replica 사용
|
|
7441
|
+
*/
|
|
7442
|
+
async findDueForPurge(now) {
|
|
7443
|
+
return this.readDb.select().from(accountDeletionRequests).where(
|
|
7444
|
+
and8(
|
|
7445
|
+
eq12(accountDeletionRequests.status, "pending"),
|
|
7446
|
+
lte(accountDeletionRequests.purgeScheduledAt, now)
|
|
7447
|
+
)
|
|
7448
|
+
);
|
|
7449
|
+
}
|
|
7450
|
+
/**
|
|
7451
|
+
* 요청 생성
|
|
7452
|
+
* Write primary 사용
|
|
7453
|
+
*/
|
|
7454
|
+
async create(data) {
|
|
7455
|
+
return await this._create(accountDeletionRequests, {
|
|
7456
|
+
...data,
|
|
7457
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
7458
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
7459
|
+
});
|
|
7460
|
+
}
|
|
7461
|
+
/**
|
|
7462
|
+
* 요청 취소 (복구)
|
|
7463
|
+
*
|
|
7464
|
+
* `WHERE status = 'pending'` 조건부 UPDATE — 이미 completed(파기 완료)된 row를
|
|
7465
|
+
* 뒤늦게 도착한 cancel이 덮어쓰지 않도록 한다. 0 row 매치(= 이미 completed거나
|
|
7466
|
+
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
7467
|
+
* Write primary 사용
|
|
7468
|
+
*/
|
|
7469
|
+
async markCancelled(id12) {
|
|
7470
|
+
const result = await this.db.update(accountDeletionRequests).set({
|
|
7471
|
+
status: "cancelled",
|
|
7472
|
+
cancelledAt: /* @__PURE__ */ new Date(),
|
|
7473
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
7474
|
+
}).where(
|
|
7475
|
+
and8(
|
|
7476
|
+
eq12(accountDeletionRequests.id, id12),
|
|
7477
|
+
eq12(accountDeletionRequests.status, "pending")
|
|
7478
|
+
)
|
|
7479
|
+
).returning();
|
|
7480
|
+
return result[0] ?? null;
|
|
7481
|
+
}
|
|
7482
|
+
/**
|
|
7483
|
+
* 요청 완료 처리 (파기 실행 직전에 호출하는 "claim" — 파기 실행 후가 아니다)
|
|
7484
|
+
*
|
|
7485
|
+
* `WHERE status = 'pending'` 조건부 UPDATE. Postgres UPDATE는 매치되는 row에 락을
|
|
7486
|
+
* 걸고 최신 커밋 상태로 WHERE를 재평가하므로, 이 호출 하나로 (1) 동시에 취소된
|
|
7487
|
+
* row를 안전하게 걸러내고 (2) row-level 락으로 동시 파기 시도를 직렬화한다.
|
|
7488
|
+
* 0 row 매치(= 이미 취소되었거나 다른 파기가 먼저 처리함) 시 null을 반환 — 호출자는
|
|
7489
|
+
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
7490
|
+
* Write primary 사용
|
|
7491
|
+
*/
|
|
7492
|
+
async markCompleted(id12, purgeStrategy) {
|
|
7493
|
+
const result = await this.db.update(accountDeletionRequests).set({
|
|
7494
|
+
status: "completed",
|
|
7495
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
7496
|
+
purgeStrategy,
|
|
7497
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
7498
|
+
}).where(
|
|
7499
|
+
and8(
|
|
7500
|
+
eq12(accountDeletionRequests.id, id12),
|
|
7501
|
+
eq12(accountDeletionRequests.status, "pending")
|
|
7502
|
+
)
|
|
7503
|
+
).returning();
|
|
7504
|
+
return result[0] ?? null;
|
|
7505
|
+
}
|
|
7506
|
+
};
|
|
7507
|
+
accountDeletionRequestsRepository = new AccountDeletionRequestsRepository();
|
|
7508
|
+
}
|
|
7509
|
+
});
|
|
7510
|
+
|
|
6735
7511
|
// src/server/repositories/index.ts
|
|
6736
7512
|
var init_repositories = __esm({
|
|
6737
7513
|
"src/server/repositories/index.ts"() {
|
|
@@ -6747,6 +7523,7 @@ var init_repositories = __esm({
|
|
|
6747
7523
|
init_invitations_repository();
|
|
6748
7524
|
init_social_accounts_repository();
|
|
6749
7525
|
init_auth_metadata_repository();
|
|
7526
|
+
init_account_deletion_requests_repository();
|
|
6750
7527
|
}
|
|
6751
7528
|
});
|
|
6752
7529
|
|
|
@@ -6838,7 +7615,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
6838
7615
|
}
|
|
6839
7616
|
async function setRolePermissions(roleId, permissionIds) {
|
|
6840
7617
|
const roleIdNum = Number(roleId);
|
|
6841
|
-
const permissionIdNums = permissionIds.map((
|
|
7618
|
+
const permissionIdNums = permissionIds.map((id12) => Number(id12));
|
|
6842
7619
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
6843
7620
|
}
|
|
6844
7621
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -6858,7 +7635,7 @@ async function getRolePermissions(roleId) {
|
|
|
6858
7635
|
}
|
|
6859
7636
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
6860
7637
|
const perms = await Promise.all(
|
|
6861
|
-
permissionIds.map((
|
|
7638
|
+
permissionIds.map((id12) => permissionsRepository.findById(id12))
|
|
6862
7639
|
);
|
|
6863
7640
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
6864
7641
|
}
|
|
@@ -6873,7 +7650,7 @@ var init_role_service = __esm({
|
|
|
6873
7650
|
import "@spfn/auth/config";
|
|
6874
7651
|
|
|
6875
7652
|
// src/server/routes/index.ts
|
|
6876
|
-
import { defineRouter as
|
|
7653
|
+
import { defineRouter as defineRouter6 } from "@spfn/core/route";
|
|
6877
7654
|
|
|
6878
7655
|
// src/server/routes/auth/index.ts
|
|
6879
7656
|
init_schema3();
|
|
@@ -6897,6 +7674,13 @@ async function verifyPassword(password, hash2) {
|
|
|
6897
7674
|
}
|
|
6898
7675
|
return bcrypt.verify(password, hash2);
|
|
6899
7676
|
}
|
|
7677
|
+
var dummyHashPromise = null;
|
|
7678
|
+
function getDummyPasswordHash() {
|
|
7679
|
+
if (!dummyHashPromise) {
|
|
7680
|
+
dummyHashPromise = hashPassword("spfn-nonexistent-account-timing-equalizer");
|
|
7681
|
+
}
|
|
7682
|
+
return dummyHashPromise;
|
|
7683
|
+
}
|
|
6900
7684
|
var passwordValidator = createPasswordParser({
|
|
6901
7685
|
minLength: 8,
|
|
6902
7686
|
requireUppercase: true,
|
|
@@ -7049,19 +7833,103 @@ init_types();
|
|
|
7049
7833
|
|
|
7050
7834
|
// src/server/services/auth.service.ts
|
|
7051
7835
|
init_repositories();
|
|
7052
|
-
import { ValidationError as
|
|
7836
|
+
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
7053
7837
|
import {
|
|
7054
|
-
InvalidCredentialsError,
|
|
7838
|
+
InvalidCredentialsError as InvalidCredentialsError2,
|
|
7055
7839
|
AccountDisabledError,
|
|
7840
|
+
AccountPendingDeletionError,
|
|
7056
7841
|
AccountAlreadyExistsError,
|
|
7057
|
-
InvalidVerificationTokenError,
|
|
7058
|
-
VerificationTokenPurposeMismatchError,
|
|
7059
|
-
VerificationTokenTargetMismatchError
|
|
7842
|
+
InvalidVerificationTokenError as InvalidVerificationTokenError2,
|
|
7843
|
+
VerificationTokenPurposeMismatchError as VerificationTokenPurposeMismatchError2,
|
|
7844
|
+
VerificationTokenTargetMismatchError as VerificationTokenTargetMismatchError2
|
|
7060
7845
|
} from "@spfn/auth/errors";
|
|
7061
7846
|
|
|
7847
|
+
// src/server/lib/config.ts
|
|
7848
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
7849
|
+
function getCookieSuffix() {
|
|
7850
|
+
const port = process.env.PORT;
|
|
7851
|
+
return port ? `_${port}` : "";
|
|
7852
|
+
}
|
|
7853
|
+
var COOKIE_NAMES = {
|
|
7854
|
+
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
7855
|
+
get SESSION() {
|
|
7856
|
+
return `spfn_session${getCookieSuffix()}`;
|
|
7857
|
+
},
|
|
7858
|
+
/** Current key ID (for key rotation) */
|
|
7859
|
+
get SESSION_KEY_ID() {
|
|
7860
|
+
return `spfn_session_key_id${getCookieSuffix()}`;
|
|
7861
|
+
},
|
|
7862
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7863
|
+
get OAUTH_PENDING() {
|
|
7864
|
+
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
7865
|
+
},
|
|
7866
|
+
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
7867
|
+
get OAUTH_CSRF() {
|
|
7868
|
+
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
7869
|
+
}
|
|
7870
|
+
};
|
|
7871
|
+
function matchOAuthCsrfCookies(cookies) {
|
|
7872
|
+
return Object.entries(cookies).filter(([name]) => /^spfn_oauth_csrf(_\d+)?$/.test(name)).map(([name, value]) => ({ name, value }));
|
|
7873
|
+
}
|
|
7874
|
+
function parseDuration(duration) {
|
|
7875
|
+
if (typeof duration === "number") {
|
|
7876
|
+
return duration;
|
|
7877
|
+
}
|
|
7878
|
+
const match = duration.match(/^(\d+)([dhms]?)$/);
|
|
7879
|
+
if (!match) {
|
|
7880
|
+
throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);
|
|
7881
|
+
}
|
|
7882
|
+
const value = parseInt(match[1], 10);
|
|
7883
|
+
const unit = match[2] || "s";
|
|
7884
|
+
switch (unit) {
|
|
7885
|
+
case "d":
|
|
7886
|
+
return value * 24 * 60 * 60;
|
|
7887
|
+
case "h":
|
|
7888
|
+
return value * 60 * 60;
|
|
7889
|
+
case "m":
|
|
7890
|
+
return value * 60;
|
|
7891
|
+
case "s":
|
|
7892
|
+
return value;
|
|
7893
|
+
default:
|
|
7894
|
+
throw new Error(`Unknown duration unit: ${unit}`);
|
|
7895
|
+
}
|
|
7896
|
+
}
|
|
7897
|
+
var globalConfig = {
|
|
7898
|
+
sessionTtl: "7d"
|
|
7899
|
+
// Default: 7 days
|
|
7900
|
+
};
|
|
7901
|
+
function configureAuth(config2) {
|
|
7902
|
+
globalConfig = {
|
|
7903
|
+
...globalConfig,
|
|
7904
|
+
...config2
|
|
7905
|
+
};
|
|
7906
|
+
}
|
|
7907
|
+
function getAuthConfig() {
|
|
7908
|
+
return { ...globalConfig };
|
|
7909
|
+
}
|
|
7910
|
+
async function runBeforeRegister(context) {
|
|
7911
|
+
const { beforeRegister } = globalConfig;
|
|
7912
|
+
if (beforeRegister) {
|
|
7913
|
+
await beforeRegister(context);
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
function getSessionTtl(override) {
|
|
7917
|
+
if (override !== void 0) {
|
|
7918
|
+
return parseDuration(override);
|
|
7919
|
+
}
|
|
7920
|
+
if (globalConfig.sessionTtl !== void 0) {
|
|
7921
|
+
return parseDuration(globalConfig.sessionTtl);
|
|
7922
|
+
}
|
|
7923
|
+
const envTtl = env4.SPFN_AUTH_SESSION_TTL;
|
|
7924
|
+
if (envTtl) {
|
|
7925
|
+
return parseDuration(envTtl);
|
|
7926
|
+
}
|
|
7927
|
+
return 7 * 24 * 60 * 60;
|
|
7928
|
+
}
|
|
7929
|
+
|
|
7062
7930
|
// src/server/services/verification.service.ts
|
|
7063
7931
|
import crypto4 from "crypto";
|
|
7064
|
-
import { env as
|
|
7932
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7065
7933
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
7066
7934
|
import jwt2 from "jsonwebtoken";
|
|
7067
7935
|
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
@@ -7130,7 +7998,7 @@ async function markCodeAsUsed(codeId) {
|
|
|
7130
7998
|
await verificationCodesRepository.markAsUsed(codeId);
|
|
7131
7999
|
}
|
|
7132
8000
|
function createVerificationToken(payload) {
|
|
7133
|
-
return jwt2.sign(payload,
|
|
8001
|
+
return jwt2.sign(payload, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7134
8002
|
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
7135
8003
|
issuer: "spfn-auth",
|
|
7136
8004
|
audience: "spfn-client"
|
|
@@ -7138,7 +8006,7 @@ function createVerificationToken(payload) {
|
|
|
7138
8006
|
}
|
|
7139
8007
|
function validateVerificationToken(token) {
|
|
7140
8008
|
try {
|
|
7141
|
-
const decoded = jwt2.verify(token,
|
|
8009
|
+
const decoded = jwt2.verify(token, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7142
8010
|
issuer: "spfn-auth",
|
|
7143
8011
|
audience: "spfn-client"
|
|
7144
8012
|
});
|
|
@@ -7314,7 +8182,7 @@ async function revokeKeyService(params) {
|
|
|
7314
8182
|
init_repositories();
|
|
7315
8183
|
import { ValidationError } from "@spfn/core/errors";
|
|
7316
8184
|
import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
7317
|
-
import { env as
|
|
8185
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
7318
8186
|
async function getUserByIdService(userId) {
|
|
7319
8187
|
return await usersRepository.findById(userId);
|
|
7320
8188
|
}
|
|
@@ -7331,7 +8199,7 @@ async function updateUserService(userId, updates) {
|
|
|
7331
8199
|
await usersRepository.updateById(userId, updates);
|
|
7332
8200
|
}
|
|
7333
8201
|
function getReservedUsernames() {
|
|
7334
|
-
const raw =
|
|
8202
|
+
const raw = env6.SPFN_AUTH_RESERVED_USERNAMES ?? "";
|
|
7335
8203
|
if (!raw) {
|
|
7336
8204
|
return /* @__PURE__ */ new Set();
|
|
7337
8205
|
}
|
|
@@ -7343,8 +8211,8 @@ function isReservedUsername(username) {
|
|
|
7343
8211
|
return getReservedUsernames().has(username.toLowerCase());
|
|
7344
8212
|
}
|
|
7345
8213
|
function validateUsernameLength(username) {
|
|
7346
|
-
const min =
|
|
7347
|
-
const max =
|
|
8214
|
+
const min = env6.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
|
|
8215
|
+
const max = env6.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
|
|
7348
8216
|
if (username.length < min) {
|
|
7349
8217
|
throw new ValidationError({
|
|
7350
8218
|
message: `Username must be at least ${min} characters`,
|
|
@@ -7381,6 +8249,48 @@ async function updateUsernameService(userId, username) {
|
|
|
7381
8249
|
return await usersRepository.updateById(userIdNum, { username });
|
|
7382
8250
|
}
|
|
7383
8251
|
|
|
8252
|
+
// src/server/services/account-deletion.service.ts
|
|
8253
|
+
init_repositories();
|
|
8254
|
+
import { ValidationError as ValidationError2, NotFoundError as NotFoundError2 } from "@spfn/core/errors";
|
|
8255
|
+
import { runInTransaction, onAfterCommit } from "@spfn/core/db";
|
|
8256
|
+
import { sendEmail as sendEmail2 } from "@spfn/notification/server";
|
|
8257
|
+
import {
|
|
8258
|
+
InvalidCredentialsError,
|
|
8259
|
+
InvalidVerificationTokenError,
|
|
8260
|
+
VerificationTokenPurposeMismatchError,
|
|
8261
|
+
VerificationTokenTargetMismatchError,
|
|
8262
|
+
DeletionAlreadyRequestedError,
|
|
8263
|
+
DeletionNotRequestedError,
|
|
8264
|
+
ImmediateDeletionNotAllowedError
|
|
8265
|
+
} from "@spfn/auth/errors";
|
|
8266
|
+
|
|
8267
|
+
// src/server/lib/deletion-config.ts
|
|
8268
|
+
var DEFAULT_DELETION_GRACE_PERIOD_DAYS = 30;
|
|
8269
|
+
var DEFAULT_DELETION_PURGE_STRATEGY = "anonymize";
|
|
8270
|
+
var DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE = false;
|
|
8271
|
+
var DEFAULT_DELETION_PURGE_CRON = "0 4 * * *";
|
|
8272
|
+
var DEFAULT_DELETION_SEND_NOTIFICATIONS = true;
|
|
8273
|
+
var config = {
|
|
8274
|
+
gracePeriodDays: DEFAULT_DELETION_GRACE_PERIOD_DAYS,
|
|
8275
|
+
purgeStrategy: DEFAULT_DELETION_PURGE_STRATEGY,
|
|
8276
|
+
allowSelfImmediate: DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE,
|
|
8277
|
+
purgeCron: DEFAULT_DELETION_PURGE_CRON,
|
|
8278
|
+
sendNotifications: DEFAULT_DELETION_SEND_NOTIFICATIONS
|
|
8279
|
+
};
|
|
8280
|
+
function configureDeletion(options) {
|
|
8281
|
+
config = {
|
|
8282
|
+
gracePeriodDays: options?.gracePeriodDays ?? DEFAULT_DELETION_GRACE_PERIOD_DAYS,
|
|
8283
|
+
purgeStrategy: options?.purgeStrategy ?? DEFAULT_DELETION_PURGE_STRATEGY,
|
|
8284
|
+
allowSelfImmediate: options?.allowSelfImmediate ?? DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE,
|
|
8285
|
+
purgeCron: options?.purgeCron ?? DEFAULT_DELETION_PURGE_CRON,
|
|
8286
|
+
sendNotifications: options?.sendNotifications ?? DEFAULT_DELETION_SEND_NOTIFICATIONS,
|
|
8287
|
+
onBeforePurge: options?.onBeforePurge
|
|
8288
|
+
};
|
|
8289
|
+
}
|
|
8290
|
+
function getDeletionConfig() {
|
|
8291
|
+
return config;
|
|
8292
|
+
}
|
|
8293
|
+
|
|
7384
8294
|
// src/server/events/index.ts
|
|
7385
8295
|
init_esm();
|
|
7386
8296
|
init_types();
|
|
@@ -7433,37 +8343,335 @@ var invitationAcceptedEvent = defineEvent(
|
|
|
7433
8343
|
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
|
|
7434
8344
|
})
|
|
7435
8345
|
);
|
|
8346
|
+
var authDeletionRequestedEvent = defineEvent(
|
|
8347
|
+
"auth.deletion.requested",
|
|
8348
|
+
Type.Object({
|
|
8349
|
+
userId: Type.String(),
|
|
8350
|
+
userPublicId: Type.String(),
|
|
8351
|
+
purgeScheduledAt: Type.String(),
|
|
8352
|
+
requestedBy: Type.Union([Type.Literal("self"), Type.Literal("admin")])
|
|
8353
|
+
})
|
|
8354
|
+
);
|
|
8355
|
+
var authDeletionCancelledEvent = defineEvent(
|
|
8356
|
+
"auth.deletion.cancelled",
|
|
8357
|
+
Type.Object({
|
|
8358
|
+
userId: Type.String(),
|
|
8359
|
+
userPublicId: Type.String()
|
|
8360
|
+
})
|
|
8361
|
+
);
|
|
8362
|
+
var authDeletionCompletedEvent = defineEvent(
|
|
8363
|
+
"auth.deletion.completed",
|
|
8364
|
+
Type.Object({
|
|
8365
|
+
userPublicId: Type.String(),
|
|
8366
|
+
purgeStrategy: Type.Union([Type.Literal("anonymize"), Type.Literal("hard-delete")])
|
|
8367
|
+
})
|
|
8368
|
+
);
|
|
7436
8369
|
|
|
7437
|
-
// src/server/services/
|
|
7438
|
-
var
|
|
7439
|
-
function
|
|
7440
|
-
|
|
7441
|
-
|
|
8370
|
+
// src/server/services/account-deletion.service.ts
|
|
8371
|
+
var POSTGRES_UNIQUE_VIOLATION = "23505";
|
|
8372
|
+
function isUniqueViolation(error) {
|
|
8373
|
+
return typeof error === "object" && error !== null && error.code === POSTGRES_UNIQUE_VIOLATION;
|
|
8374
|
+
}
|
|
8375
|
+
async function getPendingDeletionInfo(userId) {
|
|
8376
|
+
const request = await accountDeletionRequestsRepository.findPendingByUserIdOnPrimary(userId);
|
|
8377
|
+
return request ? { purgeScheduledAt: request.purgeScheduledAt } : null;
|
|
8378
|
+
}
|
|
8379
|
+
async function verifyReauthCredential(user, params) {
|
|
8380
|
+
if (user.passwordHash) {
|
|
8381
|
+
if (!params.password) {
|
|
8382
|
+
throw new ValidationError2({ message: "Password is required to confirm this action" });
|
|
8383
|
+
}
|
|
8384
|
+
const valid = await verifyPassword(params.password, user.passwordHash);
|
|
8385
|
+
if (!valid) {
|
|
8386
|
+
throw new InvalidCredentialsError({ message: "Incorrect password" });
|
|
8387
|
+
}
|
|
8388
|
+
return;
|
|
8389
|
+
}
|
|
8390
|
+
if (!params.verificationToken) {
|
|
8391
|
+
throw new ValidationError2({ message: "A verification code is required to confirm this action" });
|
|
8392
|
+
}
|
|
8393
|
+
const payload = validateVerificationToken(params.verificationToken);
|
|
8394
|
+
if (!payload) {
|
|
8395
|
+
throw new InvalidVerificationTokenError();
|
|
8396
|
+
}
|
|
8397
|
+
if (payload.purpose !== "account_deletion") {
|
|
8398
|
+
throw new VerificationTokenPurposeMismatchError({ expected: "account_deletion", actual: payload.purpose });
|
|
8399
|
+
}
|
|
8400
|
+
const target = user.email ?? user.phone;
|
|
8401
|
+
if (!target || payload.target !== target) {
|
|
8402
|
+
throw new VerificationTokenTargetMismatchError();
|
|
8403
|
+
}
|
|
8404
|
+
}
|
|
8405
|
+
async function sendDeletionEmail(to, subject, text12) {
|
|
8406
|
+
const result = await sendEmail2({ to, subject, text: text12 });
|
|
8407
|
+
if (!result.success) {
|
|
8408
|
+
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
async function notifyDeletionRequested(user, purgeScheduledAt) {
|
|
8412
|
+
if (!getDeletionConfig().sendNotifications || !user.email) {
|
|
8413
|
+
return;
|
|
8414
|
+
}
|
|
8415
|
+
await sendDeletionEmail(
|
|
8416
|
+
user.email,
|
|
8417
|
+
"Account deletion requested",
|
|
8418
|
+
`We received a request to delete your account. It is scheduled for permanent deletion on ${purgeScheduledAt.toISOString()}. If this wasn't you, sign in before that date to cancel it.`
|
|
8419
|
+
);
|
|
8420
|
+
}
|
|
8421
|
+
async function notifyDeletionCancelled(user) {
|
|
8422
|
+
if (!getDeletionConfig().sendNotifications || !user.email) {
|
|
8423
|
+
return;
|
|
8424
|
+
}
|
|
8425
|
+
await sendDeletionEmail(
|
|
8426
|
+
user.email,
|
|
8427
|
+
"Account deletion cancelled",
|
|
8428
|
+
"Your account deletion request was cancelled and your account is active again."
|
|
8429
|
+
);
|
|
8430
|
+
}
|
|
8431
|
+
async function notifyPurgeFinal(email) {
|
|
8432
|
+
if (!getDeletionConfig().sendNotifications) {
|
|
8433
|
+
return;
|
|
8434
|
+
}
|
|
8435
|
+
await sendDeletionEmail(
|
|
8436
|
+
email,
|
|
8437
|
+
"Your account has been deleted",
|
|
8438
|
+
"Your account and associated data have now been permanently deleted, as requested."
|
|
8439
|
+
);
|
|
8440
|
+
}
|
|
8441
|
+
function addDays(date, days) {
|
|
8442
|
+
const result = new Date(date);
|
|
8443
|
+
result.setDate(result.getDate() + days);
|
|
8444
|
+
return result;
|
|
8445
|
+
}
|
|
8446
|
+
async function requestAccountDeletionService(userId, params) {
|
|
8447
|
+
const { requestedBy, password, verificationToken, reason, immediate } = params;
|
|
8448
|
+
const user = await usersRepository.findById(userId);
|
|
8449
|
+
if (!user) {
|
|
8450
|
+
throw new NotFoundError2({ message: "User not found", resource: "User" });
|
|
8451
|
+
}
|
|
8452
|
+
if (user.status === "pending_deletion" || user.status === "deleted") {
|
|
8453
|
+
throw new DeletionAlreadyRequestedError();
|
|
8454
|
+
}
|
|
8455
|
+
if (requestedBy === "self") {
|
|
8456
|
+
await verifyReauthCredential(user, { password, verificationToken });
|
|
8457
|
+
}
|
|
8458
|
+
const config2 = getDeletionConfig();
|
|
8459
|
+
const wantsImmediate = immediate === true;
|
|
8460
|
+
if (wantsImmediate && requestedBy === "self" && !config2.allowSelfImmediate) {
|
|
8461
|
+
throw new ImmediateDeletionNotAllowedError();
|
|
8462
|
+
}
|
|
8463
|
+
const gracePeriodDays = wantsImmediate ? 0 : config2.gracePeriodDays;
|
|
8464
|
+
const requestedAt = /* @__PURE__ */ new Date();
|
|
8465
|
+
const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
|
|
8466
|
+
await usersRepository.updateById(user.id, { status: "pending_deletion" });
|
|
8467
|
+
let request;
|
|
8468
|
+
try {
|
|
8469
|
+
request = await accountDeletionRequestsRepository.create({
|
|
8470
|
+
userId: user.id,
|
|
8471
|
+
userPublicId: user.publicId,
|
|
8472
|
+
requestedAt,
|
|
8473
|
+
purgeScheduledAt,
|
|
8474
|
+
status: "pending",
|
|
8475
|
+
requestedBy,
|
|
8476
|
+
reason: reason ?? null
|
|
8477
|
+
});
|
|
8478
|
+
} catch (error) {
|
|
8479
|
+
if (isUniqueViolation(error)) {
|
|
8480
|
+
throw new DeletionAlreadyRequestedError();
|
|
8481
|
+
}
|
|
8482
|
+
throw error;
|
|
8483
|
+
}
|
|
8484
|
+
await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
|
|
8485
|
+
onAfterCommit(() => authDeletionRequestedEvent.emit({
|
|
8486
|
+
userId: String(user.id),
|
|
8487
|
+
userPublicId: user.publicId,
|
|
8488
|
+
purgeScheduledAt: purgeScheduledAt.toISOString(),
|
|
8489
|
+
requestedBy
|
|
8490
|
+
}));
|
|
8491
|
+
onAfterCommit(() => notifyDeletionRequested(user, purgeScheduledAt));
|
|
8492
|
+
if (gracePeriodDays === 0) {
|
|
8493
|
+
await purgePendingRequest(request);
|
|
8494
|
+
}
|
|
8495
|
+
return { requestId: request.id, purgeScheduledAt };
|
|
8496
|
+
}
|
|
8497
|
+
async function cancelAccountDeletionService(params) {
|
|
8498
|
+
const { email, phone, password, verificationToken } = params;
|
|
8499
|
+
if (!email && !phone) {
|
|
8500
|
+
throw new ValidationError2({ message: "Either email or phone must be provided" });
|
|
8501
|
+
}
|
|
8502
|
+
const user = await usersRepository.findByEmailOrPhone(email, phone);
|
|
8503
|
+
if (!user) {
|
|
8504
|
+
if (password) {
|
|
8505
|
+
await verifyPassword(password, await getDummyPasswordHash());
|
|
8506
|
+
}
|
|
8507
|
+
throw new InvalidCredentialsError();
|
|
8508
|
+
}
|
|
8509
|
+
if (user.status !== "pending_deletion") {
|
|
8510
|
+
await verifyReauthCredential(user, { password, verificationToken });
|
|
8511
|
+
throw new DeletionNotRequestedError();
|
|
8512
|
+
}
|
|
8513
|
+
await verifyReauthCredential(user, { password, verificationToken });
|
|
8514
|
+
const pendingRequest = await accountDeletionRequestsRepository.findPendingByUserId(user.id);
|
|
8515
|
+
if (!pendingRequest) {
|
|
8516
|
+
throw new DeletionNotRequestedError();
|
|
8517
|
+
}
|
|
8518
|
+
const claimed = await accountDeletionRequestsRepository.markCancelled(pendingRequest.id);
|
|
8519
|
+
if (!claimed) {
|
|
8520
|
+
throw new DeletionNotRequestedError();
|
|
8521
|
+
}
|
|
8522
|
+
await usersRepository.reactivateFromPendingDeletion(user.id);
|
|
8523
|
+
onAfterCommit(() => authDeletionCancelledEvent.emit({
|
|
8524
|
+
userId: String(user.id),
|
|
8525
|
+
userPublicId: user.publicId
|
|
8526
|
+
}));
|
|
8527
|
+
onAfterCommit(() => notifyDeletionCancelled(user));
|
|
8528
|
+
return { userId: String(user.id) };
|
|
8529
|
+
}
|
|
8530
|
+
async function anonymizeUser(user) {
|
|
8531
|
+
await usersRepository.updateById(user.id, {
|
|
8532
|
+
email: `deleted-${user.publicId}@deleted.invalid`,
|
|
8533
|
+
phone: null,
|
|
8534
|
+
username: null,
|
|
8535
|
+
passwordHash: null,
|
|
8536
|
+
status: "deleted",
|
|
8537
|
+
deletedAt: /* @__PURE__ */ new Date(),
|
|
8538
|
+
deletedBy: "system:auth.deletion.purge"
|
|
8539
|
+
});
|
|
8540
|
+
await socialAccountsRepository.deleteAllByUserId(user.id);
|
|
8541
|
+
await keysRepository.deleteAllByUserId(user.id);
|
|
8542
|
+
await userProfilesRepository.updateByUserId(user.id, {
|
|
8543
|
+
displayName: null,
|
|
8544
|
+
firstName: null,
|
|
8545
|
+
lastName: null,
|
|
8546
|
+
avatarUrl: null,
|
|
8547
|
+
bio: null,
|
|
8548
|
+
dateOfBirth: null,
|
|
8549
|
+
gender: null,
|
|
8550
|
+
website: null,
|
|
8551
|
+
location: null,
|
|
8552
|
+
company: null,
|
|
8553
|
+
jobTitle: null,
|
|
8554
|
+
metadata: null
|
|
8555
|
+
});
|
|
8556
|
+
}
|
|
8557
|
+
async function purgePendingRequest(request) {
|
|
8558
|
+
if (request.userId === null) {
|
|
8559
|
+
await accountDeletionRequestsRepository.markCompleted(request.id, "hard-delete");
|
|
8560
|
+
return { outcome: "not-found" };
|
|
7442
8561
|
}
|
|
7443
|
-
|
|
8562
|
+
const userId = request.userId;
|
|
8563
|
+
const precheckUser = await usersRepository.findById(userId);
|
|
8564
|
+
if (!precheckUser || precheckUser.status !== "pending_deletion") {
|
|
8565
|
+
return { outcome: "skipped" };
|
|
8566
|
+
}
|
|
8567
|
+
const config2 = getDeletionConfig();
|
|
8568
|
+
if (config2.onBeforePurge) {
|
|
8569
|
+
try {
|
|
8570
|
+
await config2.onBeforePurge({
|
|
8571
|
+
id: precheckUser.id,
|
|
8572
|
+
publicId: precheckUser.publicId,
|
|
8573
|
+
email: precheckUser.email,
|
|
8574
|
+
phone: precheckUser.phone
|
|
8575
|
+
});
|
|
8576
|
+
} catch (error) {
|
|
8577
|
+
authLogger.service.warn("[account-deletion] onBeforePurge threw \u2014 skipping this sweep, will retry", {
|
|
8578
|
+
userId: precheckUser.id,
|
|
8579
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8580
|
+
});
|
|
8581
|
+
return { outcome: "skipped" };
|
|
8582
|
+
}
|
|
8583
|
+
}
|
|
8584
|
+
const purgeStrategy = config2.purgeStrategy;
|
|
8585
|
+
let purgedUser = null;
|
|
8586
|
+
await runInTransaction(async () => {
|
|
8587
|
+
const user = await usersRepository.findById(userId);
|
|
8588
|
+
if (!user || user.status !== "pending_deletion") {
|
|
8589
|
+
return;
|
|
8590
|
+
}
|
|
8591
|
+
const claimed = await accountDeletionRequestsRepository.markCompleted(request.id, purgeStrategy);
|
|
8592
|
+
if (!claimed) {
|
|
8593
|
+
return;
|
|
8594
|
+
}
|
|
8595
|
+
if (purgeStrategy === "hard-delete") {
|
|
8596
|
+
await usersRepository.deleteById(user.id);
|
|
8597
|
+
} else {
|
|
8598
|
+
await anonymizeUser(user);
|
|
8599
|
+
}
|
|
8600
|
+
if (user.email) {
|
|
8601
|
+
await verificationCodesRepository.deleteByTarget(user.email);
|
|
8602
|
+
}
|
|
8603
|
+
if (user.phone) {
|
|
8604
|
+
await verificationCodesRepository.deleteByTarget(user.phone);
|
|
8605
|
+
}
|
|
8606
|
+
purgedUser = user;
|
|
8607
|
+
});
|
|
8608
|
+
if (!purgedUser) {
|
|
8609
|
+
return { outcome: "skipped" };
|
|
8610
|
+
}
|
|
8611
|
+
const { email, publicId } = purgedUser;
|
|
8612
|
+
if (email) {
|
|
8613
|
+
onAfterCommit(() => notifyPurgeFinal(email));
|
|
8614
|
+
}
|
|
8615
|
+
onAfterCommit(() => authDeletionCompletedEvent.emit({
|
|
8616
|
+
userPublicId: publicId,
|
|
8617
|
+
purgeStrategy
|
|
8618
|
+
}));
|
|
8619
|
+
return { outcome: "purged" };
|
|
8620
|
+
}
|
|
8621
|
+
async function purgeUserService(userId) {
|
|
8622
|
+
const request = await accountDeletionRequestsRepository.findPendingByUserId(userId);
|
|
8623
|
+
if (!request) {
|
|
8624
|
+
throw new DeletionNotRequestedError();
|
|
8625
|
+
}
|
|
8626
|
+
return purgePendingRequest(request);
|
|
8627
|
+
}
|
|
8628
|
+
async function sweepDuePurges(now = /* @__PURE__ */ new Date()) {
|
|
8629
|
+
const dueRequests = await accountDeletionRequestsRepository.findDueForPurge(now);
|
|
8630
|
+
let purged = 0;
|
|
8631
|
+
let skipped = 0;
|
|
8632
|
+
for (const request of dueRequests) {
|
|
8633
|
+
try {
|
|
8634
|
+
const result = await purgePendingRequest(request);
|
|
8635
|
+
if (result.outcome === "purged") {
|
|
8636
|
+
purged++;
|
|
8637
|
+
} else {
|
|
8638
|
+
skipped++;
|
|
8639
|
+
}
|
|
8640
|
+
} catch (error) {
|
|
8641
|
+
skipped++;
|
|
8642
|
+
authLogger.service.error("[account-deletion] purge failed, will retry next sweep", {
|
|
8643
|
+
requestId: request.id,
|
|
8644
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8645
|
+
});
|
|
8646
|
+
}
|
|
8647
|
+
}
|
|
8648
|
+
return { processed: dueRequests.length, purged, skipped };
|
|
7444
8649
|
}
|
|
8650
|
+
|
|
8651
|
+
// src/server/services/auth.service.ts
|
|
7445
8652
|
async function registerService(params) {
|
|
7446
8653
|
const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
7447
8654
|
const tokenPayload = validateVerificationToken(verificationToken);
|
|
7448
8655
|
if (!tokenPayload) {
|
|
7449
|
-
throw new
|
|
8656
|
+
throw new InvalidVerificationTokenError2();
|
|
7450
8657
|
}
|
|
7451
8658
|
if (tokenPayload.purpose !== "registration") {
|
|
7452
|
-
throw new
|
|
8659
|
+
throw new VerificationTokenPurposeMismatchError2({ expected: "registration", actual: tokenPayload.purpose });
|
|
7453
8660
|
}
|
|
7454
8661
|
const providedTarget = email || phone;
|
|
7455
8662
|
if (tokenPayload.target !== providedTarget) {
|
|
7456
|
-
throw new
|
|
8663
|
+
throw new VerificationTokenTargetMismatchError2();
|
|
7457
8664
|
}
|
|
7458
8665
|
const providedTargetType = email ? "email" : "phone";
|
|
7459
8666
|
if (tokenPayload.targetType !== providedTargetType) {
|
|
7460
|
-
throw new
|
|
8667
|
+
throw new VerificationTokenTargetMismatchError2();
|
|
7461
8668
|
}
|
|
7462
8669
|
const existingUser = await usersRepository.findByEmailOrPhone(email, phone);
|
|
7463
8670
|
if (existingUser) {
|
|
7464
8671
|
const identifierType = email ? "email" : "phone";
|
|
7465
8672
|
throw new AccountAlreadyExistsError({ identifier: email || phone, identifierType });
|
|
7466
8673
|
}
|
|
8674
|
+
await runBeforeRegister({ channel: "credentials", email, phone, metadata });
|
|
7467
8675
|
const passwordHash = await hashPassword(password);
|
|
7468
8676
|
const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
7469
8677
|
const userRole = await getRoleByName3("user");
|
|
@@ -7504,17 +8712,21 @@ async function loginService(params) {
|
|
|
7504
8712
|
const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
|
|
7505
8713
|
const user = await usersRepository.findByEmailOrPhone(email, phone);
|
|
7506
8714
|
if (!email && !phone) {
|
|
7507
|
-
throw new
|
|
8715
|
+
throw new ValidationError3({ message: "Either email or phone must be provided" });
|
|
7508
8716
|
}
|
|
7509
8717
|
if (!user || !user.passwordHash) {
|
|
7510
|
-
await verifyPassword(password, await
|
|
7511
|
-
throw new
|
|
8718
|
+
await verifyPassword(password, await getDummyPasswordHash());
|
|
8719
|
+
throw new InvalidCredentialsError2();
|
|
7512
8720
|
}
|
|
7513
8721
|
const isValid = await verifyPassword(password, user.passwordHash);
|
|
7514
8722
|
if (!isValid) {
|
|
7515
|
-
throw new
|
|
8723
|
+
throw new InvalidCredentialsError2();
|
|
7516
8724
|
}
|
|
7517
8725
|
if (user.status !== "active") {
|
|
8726
|
+
if (user.status === "pending_deletion") {
|
|
8727
|
+
const pending = await getPendingDeletionInfo(user.id);
|
|
8728
|
+
throw new AccountPendingDeletionError({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
|
|
8729
|
+
}
|
|
7518
8730
|
throw new AccountDisabledError({ status: user.status });
|
|
7519
8731
|
}
|
|
7520
8732
|
if (oldKeyId) {
|
|
@@ -7563,17 +8775,17 @@ async function changePasswordService(params) {
|
|
|
7563
8775
|
} else {
|
|
7564
8776
|
const user = await usersRepository.findById(userId);
|
|
7565
8777
|
if (!user) {
|
|
7566
|
-
throw new
|
|
8778
|
+
throw new ValidationError3({ message: "User not found" });
|
|
7567
8779
|
}
|
|
7568
8780
|
passwordHash = user.passwordHash;
|
|
7569
8781
|
}
|
|
7570
8782
|
if (passwordHash) {
|
|
7571
8783
|
if (!currentPassword) {
|
|
7572
|
-
throw new
|
|
8784
|
+
throw new ValidationError3({ message: "Current password is required" });
|
|
7573
8785
|
}
|
|
7574
8786
|
const isValid = await verifyPassword(currentPassword, passwordHash);
|
|
7575
8787
|
if (!isValid) {
|
|
7576
|
-
throw new
|
|
8788
|
+
throw new InvalidCredentialsError2({ message: "Current password is incorrect" });
|
|
7577
8789
|
}
|
|
7578
8790
|
}
|
|
7579
8791
|
const newPasswordHash = await hashPassword(newPassword);
|
|
@@ -7585,82 +8797,6 @@ async function changePasswordService(params) {
|
|
|
7585
8797
|
init_repositories();
|
|
7586
8798
|
init_rbac();
|
|
7587
8799
|
import { createHash } from "crypto";
|
|
7588
|
-
|
|
7589
|
-
// src/server/lib/config.ts
|
|
7590
|
-
import { env as env6 } from "@spfn/auth/config";
|
|
7591
|
-
function getCookieSuffix() {
|
|
7592
|
-
const port = process.env.PORT;
|
|
7593
|
-
return port ? `_${port}` : "";
|
|
7594
|
-
}
|
|
7595
|
-
var COOKIE_NAMES = {
|
|
7596
|
-
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
7597
|
-
get SESSION() {
|
|
7598
|
-
return `spfn_session${getCookieSuffix()}`;
|
|
7599
|
-
},
|
|
7600
|
-
/** Current key ID (for key rotation) */
|
|
7601
|
-
get SESSION_KEY_ID() {
|
|
7602
|
-
return `spfn_session_key_id${getCookieSuffix()}`;
|
|
7603
|
-
},
|
|
7604
|
-
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7605
|
-
get OAUTH_PENDING() {
|
|
7606
|
-
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
7607
|
-
},
|
|
7608
|
-
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
7609
|
-
get OAUTH_CSRF() {
|
|
7610
|
-
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
7611
|
-
}
|
|
7612
|
-
};
|
|
7613
|
-
function parseDuration(duration) {
|
|
7614
|
-
if (typeof duration === "number") {
|
|
7615
|
-
return duration;
|
|
7616
|
-
}
|
|
7617
|
-
const match = duration.match(/^(\d+)([dhms]?)$/);
|
|
7618
|
-
if (!match) {
|
|
7619
|
-
throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);
|
|
7620
|
-
}
|
|
7621
|
-
const value = parseInt(match[1], 10);
|
|
7622
|
-
const unit = match[2] || "s";
|
|
7623
|
-
switch (unit) {
|
|
7624
|
-
case "d":
|
|
7625
|
-
return value * 24 * 60 * 60;
|
|
7626
|
-
case "h":
|
|
7627
|
-
return value * 60 * 60;
|
|
7628
|
-
case "m":
|
|
7629
|
-
return value * 60;
|
|
7630
|
-
case "s":
|
|
7631
|
-
return value;
|
|
7632
|
-
default:
|
|
7633
|
-
throw new Error(`Unknown duration unit: ${unit}`);
|
|
7634
|
-
}
|
|
7635
|
-
}
|
|
7636
|
-
var globalConfig = {
|
|
7637
|
-
sessionTtl: "7d"
|
|
7638
|
-
// Default: 7 days
|
|
7639
|
-
};
|
|
7640
|
-
function configureAuth(config) {
|
|
7641
|
-
globalConfig = {
|
|
7642
|
-
...globalConfig,
|
|
7643
|
-
...config
|
|
7644
|
-
};
|
|
7645
|
-
}
|
|
7646
|
-
function getAuthConfig() {
|
|
7647
|
-
return { ...globalConfig };
|
|
7648
|
-
}
|
|
7649
|
-
function getSessionTtl(override) {
|
|
7650
|
-
if (override !== void 0) {
|
|
7651
|
-
return parseDuration(override);
|
|
7652
|
-
}
|
|
7653
|
-
if (globalConfig.sessionTtl !== void 0) {
|
|
7654
|
-
return parseDuration(globalConfig.sessionTtl);
|
|
7655
|
-
}
|
|
7656
|
-
const envTtl = env6.SPFN_AUTH_SESSION_TTL;
|
|
7657
|
-
if (envTtl) {
|
|
7658
|
-
return parseDuration(envTtl);
|
|
7659
|
-
}
|
|
7660
|
-
return 7 * 24 * 60 * 60;
|
|
7661
|
-
}
|
|
7662
|
-
|
|
7663
|
-
// src/server/services/rbac.service.ts
|
|
7664
8800
|
var RBAC_HASH_KEY = "rbac_config_hash";
|
|
7665
8801
|
function computeConfigHash(allRoles, allPermissions, allMappings) {
|
|
7666
8802
|
const payload = JSON.stringify({
|
|
@@ -7729,51 +8865,51 @@ async function initializeAuth(options = {}) {
|
|
|
7729
8865
|
authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
|
|
7730
8866
|
}
|
|
7731
8867
|
async function syncRoles(configs, existingByName) {
|
|
7732
|
-
for (const
|
|
7733
|
-
const existing = existingByName.get(
|
|
8868
|
+
for (const config2 of configs) {
|
|
8869
|
+
const existing = existingByName.get(config2.name);
|
|
7734
8870
|
if (!existing) {
|
|
7735
8871
|
await rolesRepository.create({
|
|
7736
|
-
name:
|
|
7737
|
-
displayName:
|
|
7738
|
-
description:
|
|
7739
|
-
priority:
|
|
7740
|
-
isSystem:
|
|
7741
|
-
isBuiltin:
|
|
8872
|
+
name: config2.name,
|
|
8873
|
+
displayName: config2.displayName,
|
|
8874
|
+
description: config2.description || null,
|
|
8875
|
+
priority: config2.priority ?? 10,
|
|
8876
|
+
isSystem: config2.isSystem ?? false,
|
|
8877
|
+
isBuiltin: config2.isBuiltin ?? false,
|
|
7742
8878
|
isActive: true
|
|
7743
8879
|
});
|
|
7744
|
-
authLogger.service.info(` \u2705 Created role: ${
|
|
8880
|
+
authLogger.service.info(` \u2705 Created role: ${config2.name}`);
|
|
7745
8881
|
} else {
|
|
7746
8882
|
const updateData = {
|
|
7747
|
-
displayName:
|
|
7748
|
-
description:
|
|
8883
|
+
displayName: config2.displayName,
|
|
8884
|
+
description: config2.description || null
|
|
7749
8885
|
};
|
|
7750
8886
|
if (!existing.isBuiltin) {
|
|
7751
|
-
updateData.priority =
|
|
8887
|
+
updateData.priority = config2.priority ?? existing.priority;
|
|
7752
8888
|
}
|
|
7753
8889
|
await rolesRepository.updateById(existing.id, updateData);
|
|
7754
8890
|
}
|
|
7755
8891
|
}
|
|
7756
8892
|
}
|
|
7757
8893
|
async function syncPermissions(configs, existingByName) {
|
|
7758
|
-
for (const
|
|
7759
|
-
const existing = existingByName.get(
|
|
8894
|
+
for (const config2 of configs) {
|
|
8895
|
+
const existing = existingByName.get(config2.name);
|
|
7760
8896
|
if (!existing) {
|
|
7761
8897
|
await permissionsRepository.create({
|
|
7762
|
-
name:
|
|
7763
|
-
displayName:
|
|
7764
|
-
description:
|
|
7765
|
-
category:
|
|
7766
|
-
isSystem:
|
|
7767
|
-
isBuiltin:
|
|
8898
|
+
name: config2.name,
|
|
8899
|
+
displayName: config2.displayName,
|
|
8900
|
+
description: config2.description || null,
|
|
8901
|
+
category: config2.category || null,
|
|
8902
|
+
isSystem: config2.isSystem ?? false,
|
|
8903
|
+
isBuiltin: config2.isBuiltin ?? false,
|
|
7768
8904
|
isActive: true,
|
|
7769
8905
|
metadata: null
|
|
7770
8906
|
});
|
|
7771
|
-
authLogger.service.info(` \u2705 Created permission: ${
|
|
8907
|
+
authLogger.service.info(` \u2705 Created permission: ${config2.name}`);
|
|
7772
8908
|
} else {
|
|
7773
8909
|
await permissionsRepository.updateById(existing.id, {
|
|
7774
|
-
displayName:
|
|
7775
|
-
description:
|
|
7776
|
-
category:
|
|
8910
|
+
displayName: config2.displayName,
|
|
8911
|
+
description: config2.description || null,
|
|
8912
|
+
category: config2.category || null
|
|
7777
8913
|
});
|
|
7778
8914
|
}
|
|
7779
8915
|
}
|
|
@@ -7811,7 +8947,7 @@ async function getUserPermissions(userId) {
|
|
|
7811
8947
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
7812
8948
|
if (permIds.length > 0) {
|
|
7813
8949
|
const rolePerms = await Promise.all(
|
|
7814
|
-
permIds.map((
|
|
8950
|
+
permIds.map((id12) => permissionsRepository.findById(id12))
|
|
7815
8951
|
);
|
|
7816
8952
|
for (const perm of rolePerms) {
|
|
7817
8953
|
if (perm && perm.isActive) {
|
|
@@ -7887,7 +9023,7 @@ init_role_service();
|
|
|
7887
9023
|
// src/server/services/invitation.service.ts
|
|
7888
9024
|
init_repositories();
|
|
7889
9025
|
import crypto5 from "crypto";
|
|
7890
|
-
import { BadRequestError, NotFoundError as
|
|
9026
|
+
import { BadRequestError, NotFoundError as NotFoundError3, ConflictError } from "@spfn/core/errors";
|
|
7891
9027
|
function generateInvitationToken() {
|
|
7892
9028
|
return crypto5.randomUUID();
|
|
7893
9029
|
}
|
|
@@ -7912,11 +9048,11 @@ async function createInvitation(params) {
|
|
|
7912
9048
|
}
|
|
7913
9049
|
const role = await rolesRepository.findById(roleId);
|
|
7914
9050
|
if (!role) {
|
|
7915
|
-
throw new
|
|
9051
|
+
throw new NotFoundError3({ message: `Role with id ${roleId} not found`, resource: "Role" });
|
|
7916
9052
|
}
|
|
7917
9053
|
const inviter = await usersRepository.findById(invitedBy);
|
|
7918
9054
|
if (!inviter) {
|
|
7919
|
-
throw new
|
|
9055
|
+
throw new NotFoundError3({ message: `User with id ${invitedBy} not found`, resource: "User" });
|
|
7920
9056
|
}
|
|
7921
9057
|
const token = generateInvitationToken();
|
|
7922
9058
|
const expiresAt = expiresAtParam ?? calculateExpiresAt(expiresInDays);
|
|
@@ -7975,8 +9111,13 @@ async function acceptInvitation(params) {
|
|
|
7975
9111
|
const invitation = validation.invitation;
|
|
7976
9112
|
const role = await rolesRepository.findById(invitation.roleId);
|
|
7977
9113
|
if (!role) {
|
|
7978
|
-
throw new
|
|
9114
|
+
throw new NotFoundError3({ message: "Role not found", resource: "Role" });
|
|
7979
9115
|
}
|
|
9116
|
+
await runBeforeRegister({
|
|
9117
|
+
channel: "invitation",
|
|
9118
|
+
email: invitation.email,
|
|
9119
|
+
metadata: invitation.metadata ?? void 0
|
|
9120
|
+
});
|
|
7980
9121
|
const passwordHash = await hashPassword(password);
|
|
7981
9122
|
const newUser = await usersRepository.create({
|
|
7982
9123
|
email: invitation.email,
|
|
@@ -8019,20 +9160,20 @@ async function acceptInvitation(params) {
|
|
|
8019
9160
|
async function listInvitations(params) {
|
|
8020
9161
|
return await invitationsRepository.list(params);
|
|
8021
9162
|
}
|
|
8022
|
-
async function cancelInvitation(
|
|
8023
|
-
const invitation = await invitationsRepository.findById(
|
|
9163
|
+
async function cancelInvitation(id12, cancelledBy, reason) {
|
|
9164
|
+
const invitation = await invitationsRepository.findById(id12);
|
|
8024
9165
|
if (!invitation) {
|
|
8025
|
-
throw new
|
|
9166
|
+
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
8026
9167
|
}
|
|
8027
9168
|
if (invitation.status !== "pending") {
|
|
8028
9169
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
8029
9170
|
}
|
|
8030
|
-
await invitationsRepository.cancel(
|
|
9171
|
+
await invitationsRepository.cancel(id12, cancelledBy, reason, invitation.metadata);
|
|
8031
9172
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
8032
9173
|
}
|
|
8033
|
-
async function deleteInvitation(
|
|
8034
|
-
await invitationsRepository.deleteById(
|
|
8035
|
-
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${
|
|
9174
|
+
async function deleteInvitation(id12) {
|
|
9175
|
+
await invitationsRepository.deleteById(id12);
|
|
9176
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id12}`);
|
|
8036
9177
|
}
|
|
8037
9178
|
async function expireOldInvitations() {
|
|
8038
9179
|
const count = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -8041,16 +9182,16 @@ async function expireOldInvitations() {
|
|
|
8041
9182
|
}
|
|
8042
9183
|
return count;
|
|
8043
9184
|
}
|
|
8044
|
-
async function resendInvitation(
|
|
8045
|
-
const invitation = await invitationsRepository.findById(
|
|
9185
|
+
async function resendInvitation(id12, expiresInDays = 7) {
|
|
9186
|
+
const invitation = await invitationsRepository.findById(id12);
|
|
8046
9187
|
if (!invitation) {
|
|
8047
|
-
throw new
|
|
9188
|
+
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
8048
9189
|
}
|
|
8049
9190
|
if (!["pending", "expired"].includes(invitation.status)) {
|
|
8050
9191
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
8051
9192
|
}
|
|
8052
9193
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
8053
|
-
const updated = await invitationsRepository.resend(
|
|
9194
|
+
const updated = await invitationsRepository.resend(id12, newExpiresAt);
|
|
8054
9195
|
if (!updated) {
|
|
8055
9196
|
throw new Error("Failed to update invitation");
|
|
8056
9197
|
}
|
|
@@ -8089,13 +9230,13 @@ async function getAuthSessionService(userId) {
|
|
|
8089
9230
|
// src/server/lib/one-time-token.ts
|
|
8090
9231
|
import { SSETokenManager } from "@spfn/core/event/sse";
|
|
8091
9232
|
var manager = null;
|
|
8092
|
-
function initOneTimeTokenManager(
|
|
9233
|
+
function initOneTimeTokenManager(config2) {
|
|
8093
9234
|
if (manager) {
|
|
8094
9235
|
manager.destroy();
|
|
8095
9236
|
}
|
|
8096
9237
|
manager = new SSETokenManager({
|
|
8097
|
-
ttl:
|
|
8098
|
-
store:
|
|
9238
|
+
ttl: config2?.ttl,
|
|
9239
|
+
store: config2?.store
|
|
8099
9240
|
});
|
|
8100
9241
|
}
|
|
8101
9242
|
function getOneTimeTokenManager() {
|
|
@@ -8205,7 +9346,8 @@ async function updateUserProfileService(userId, params) {
|
|
|
8205
9346
|
// src/server/services/oauth.service.ts
|
|
8206
9347
|
init_repositories();
|
|
8207
9348
|
import { env as env11 } from "@spfn/auth/config";
|
|
8208
|
-
import { ValidationError as
|
|
9349
|
+
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
9350
|
+
import { AccountDisabledError as AccountDisabledError2, AccountPendingDeletionError as AccountPendingDeletionError2 } from "@spfn/auth/errors";
|
|
8209
9351
|
|
|
8210
9352
|
// src/server/lib/oauth/google.ts
|
|
8211
9353
|
import { env as env7 } from "@spfn/auth/config";
|
|
@@ -8221,7 +9363,7 @@ function getGoogleOAuthConfig() {
|
|
|
8221
9363
|
if (!clientId || !clientSecret) {
|
|
8222
9364
|
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
8223
9365
|
}
|
|
8224
|
-
const baseUrl = env7.
|
|
9366
|
+
const baseUrl = env7.NEXT_PUBLIC_SPFN_APP_URL || env7.SPFN_APP_URL;
|
|
8225
9367
|
const redirectUri = env7.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
8226
9368
|
return {
|
|
8227
9369
|
clientId,
|
|
@@ -8238,10 +9380,10 @@ function getDefaultScopes() {
|
|
|
8238
9380
|
}
|
|
8239
9381
|
function getGoogleAuthUrl(state, scopes) {
|
|
8240
9382
|
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
8241
|
-
const
|
|
9383
|
+
const config2 = getGoogleOAuthConfig();
|
|
8242
9384
|
const params = new URLSearchParams({
|
|
8243
|
-
client_id:
|
|
8244
|
-
redirect_uri:
|
|
9385
|
+
client_id: config2.clientId,
|
|
9386
|
+
redirect_uri: config2.redirectUri,
|
|
8245
9387
|
response_type: "code",
|
|
8246
9388
|
scope: resolvedScopes.join(" "),
|
|
8247
9389
|
state,
|
|
@@ -8253,16 +9395,16 @@ function getGoogleAuthUrl(state, scopes) {
|
|
|
8253
9395
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
8254
9396
|
}
|
|
8255
9397
|
async function exchangeCodeForTokens(code) {
|
|
8256
|
-
const
|
|
9398
|
+
const config2 = getGoogleOAuthConfig();
|
|
8257
9399
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
8258
9400
|
method: "POST",
|
|
8259
9401
|
headers: {
|
|
8260
9402
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
8261
9403
|
},
|
|
8262
9404
|
body: new URLSearchParams({
|
|
8263
|
-
client_id:
|
|
8264
|
-
client_secret:
|
|
8265
|
-
redirect_uri:
|
|
9405
|
+
client_id: config2.clientId,
|
|
9406
|
+
client_secret: config2.clientSecret,
|
|
9407
|
+
redirect_uri: config2.redirectUri,
|
|
8266
9408
|
grant_type: "authorization_code",
|
|
8267
9409
|
code
|
|
8268
9410
|
})
|
|
@@ -8286,15 +9428,15 @@ async function getGoogleUserInfo(accessToken) {
|
|
|
8286
9428
|
return response.json();
|
|
8287
9429
|
}
|
|
8288
9430
|
async function refreshAccessToken(refreshToken) {
|
|
8289
|
-
const
|
|
9431
|
+
const config2 = getGoogleOAuthConfig();
|
|
8290
9432
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
8291
9433
|
method: "POST",
|
|
8292
9434
|
headers: {
|
|
8293
9435
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
8294
9436
|
},
|
|
8295
9437
|
body: new URLSearchParams({
|
|
8296
|
-
client_id:
|
|
8297
|
-
client_secret:
|
|
9438
|
+
client_id: config2.clientId,
|
|
9439
|
+
client_secret: config2.clientSecret,
|
|
8298
9440
|
refresh_token: refreshToken,
|
|
8299
9441
|
grant_type: "refresh_token"
|
|
8300
9442
|
})
|
|
@@ -8347,15 +9489,15 @@ async function verifyOAuthState(encryptedState) {
|
|
|
8347
9489
|
}
|
|
8348
9490
|
|
|
8349
9491
|
// src/server/lib/oauth/provider.ts
|
|
8350
|
-
var
|
|
9492
|
+
var registry2 = /* @__PURE__ */ new Map();
|
|
8351
9493
|
function registerOAuthProvider(provider) {
|
|
8352
|
-
|
|
9494
|
+
registry2.set(provider.id, provider);
|
|
8353
9495
|
}
|
|
8354
|
-
function getOAuthProvider(
|
|
8355
|
-
return
|
|
9496
|
+
function getOAuthProvider(id12) {
|
|
9497
|
+
return registry2.get(id12);
|
|
8356
9498
|
}
|
|
8357
9499
|
function getRegisteredProviders() {
|
|
8358
|
-
return [...
|
|
9500
|
+
return [...registry2.values()];
|
|
8359
9501
|
}
|
|
8360
9502
|
|
|
8361
9503
|
// src/server/lib/oauth/jwks-verify.ts
|
|
@@ -8399,9 +9541,12 @@ async function verifySignature(params) {
|
|
|
8399
9541
|
}
|
|
8400
9542
|
}
|
|
8401
9543
|
|
|
9544
|
+
// src/server/lib/oauth/index.ts
|
|
9545
|
+
init_token_cipher();
|
|
9546
|
+
|
|
8402
9547
|
// src/server/lib/oauth/google-provider.ts
|
|
8403
9548
|
import { env as env9 } from "@spfn/auth/config";
|
|
8404
|
-
import { ValidationError as
|
|
9549
|
+
import { ValidationError as ValidationError4 } from "@spfn/core/errors";
|
|
8405
9550
|
var GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
|
|
8406
9551
|
var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
8407
9552
|
function getGoogleNativeAudiences() {
|
|
@@ -8444,7 +9589,7 @@ var googleProvider = {
|
|
|
8444
9589
|
async verifyNativeIdToken(idToken, options) {
|
|
8445
9590
|
const audiences = getGoogleNativeAudiences();
|
|
8446
9591
|
if (audiences.length === 0) {
|
|
8447
|
-
throw new
|
|
9592
|
+
throw new ValidationError4({
|
|
8448
9593
|
message: "Google native sign-in is not configured. Set SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS."
|
|
8449
9594
|
});
|
|
8450
9595
|
}
|
|
@@ -8470,7 +9615,7 @@ registerOAuthProvider(googleProvider);
|
|
|
8470
9615
|
// src/server/lib/oauth/apple-provider.ts
|
|
8471
9616
|
import { createHash as createHash2 } from "crypto";
|
|
8472
9617
|
import { env as env10 } from "@spfn/auth/config";
|
|
8473
|
-
import { ValidationError as
|
|
9618
|
+
import { ValidationError as ValidationError5 } from "@spfn/core/errors";
|
|
8474
9619
|
var APPLE_JWKS_URI = "https://appleid.apple.com/auth/keys";
|
|
8475
9620
|
var APPLE_ISSUER = "https://appleid.apple.com";
|
|
8476
9621
|
function getAppleClientIds() {
|
|
@@ -8480,7 +9625,7 @@ function hashNonce(rawNonce) {
|
|
|
8480
9625
|
return createHash2("sha256").update(rawNonce).digest("hex");
|
|
8481
9626
|
}
|
|
8482
9627
|
function unsupportedWebFlow() {
|
|
8483
|
-
throw new
|
|
9628
|
+
throw new ValidationError5({
|
|
8484
9629
|
message: "Apple provider supports native id_token sign-in only. Use POST /_auth/oauth/apple/native."
|
|
8485
9630
|
});
|
|
8486
9631
|
}
|
|
@@ -8501,7 +9646,7 @@ var appleProvider = {
|
|
|
8501
9646
|
async verifyNativeIdToken(idToken, options) {
|
|
8502
9647
|
const audiences = getAppleClientIds();
|
|
8503
9648
|
if (audiences.length === 0) {
|
|
8504
|
-
throw new
|
|
9649
|
+
throw new ValidationError5({
|
|
8505
9650
|
message: "Apple native sign-in is not configured. Set SPFN_AUTH_APPLE_CLIENT_IDS."
|
|
8506
9651
|
});
|
|
8507
9652
|
}
|
|
@@ -8522,16 +9667,226 @@ var appleProvider = {
|
|
|
8522
9667
|
};
|
|
8523
9668
|
registerOAuthProvider(appleProvider);
|
|
8524
9669
|
|
|
9670
|
+
// src/server/lib/oauth/kakao-provider.ts
|
|
9671
|
+
init_config();
|
|
9672
|
+
import { ValidationError as ValidationError6 } from "@spfn/core/errors";
|
|
9673
|
+
var KAKAO_AUTH_URL = "https://kauth.kakao.com/oauth/authorize";
|
|
9674
|
+
var KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token";
|
|
9675
|
+
var KAKAO_USERINFO_URL = "https://kapi.kakao.com/v2/user/me";
|
|
9676
|
+
function getKakaoConfig() {
|
|
9677
|
+
const clientId = env3.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
9678
|
+
const clientSecret = env3.SPFN_AUTH_KAKAO_CLIENT_SECRET;
|
|
9679
|
+
if (!clientId) {
|
|
9680
|
+
throw new ValidationError6({
|
|
9681
|
+
message: "Kakao OAuth is not configured. Set SPFN_AUTH_KAKAO_CLIENT_ID."
|
|
9682
|
+
});
|
|
9683
|
+
}
|
|
9684
|
+
const baseUrl = env3.NEXT_PUBLIC_SPFN_APP_URL || env3.SPFN_APP_URL;
|
|
9685
|
+
return {
|
|
9686
|
+
clientId,
|
|
9687
|
+
clientSecret,
|
|
9688
|
+
redirectUri: env3.SPFN_AUTH_KAKAO_REDIRECT_URI || `${baseUrl}/_auth/oauth/kakao/callback`
|
|
9689
|
+
};
|
|
9690
|
+
}
|
|
9691
|
+
function getKakaoScopes() {
|
|
9692
|
+
const configured = env3.SPFN_AUTH_KAKAO_SCOPES;
|
|
9693
|
+
return configured ? configured.split(",").map((scope) => scope.trim()).filter(Boolean) : ["account_email"];
|
|
9694
|
+
}
|
|
9695
|
+
async function requestKakaoTokens(params) {
|
|
9696
|
+
const response = await fetch(KAKAO_TOKEN_URL, {
|
|
9697
|
+
method: "POST",
|
|
9698
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" },
|
|
9699
|
+
body: params
|
|
9700
|
+
});
|
|
9701
|
+
if (!response.ok) {
|
|
9702
|
+
throw new Error(`Kakao token request failed with status ${response.status}`);
|
|
9703
|
+
}
|
|
9704
|
+
const body = await response.json();
|
|
9705
|
+
const expiresIn = Number(body.expires_in);
|
|
9706
|
+
if (typeof body.access_token !== "string" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
9707
|
+
throw new Error("Kakao token response is invalid");
|
|
9708
|
+
}
|
|
9709
|
+
return {
|
|
9710
|
+
accessToken: body.access_token,
|
|
9711
|
+
refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
|
|
9712
|
+
expiresIn
|
|
9713
|
+
};
|
|
9714
|
+
}
|
|
9715
|
+
var kakaoProvider = {
|
|
9716
|
+
id: "kakao",
|
|
9717
|
+
isEnabled() {
|
|
9718
|
+
return !!env3.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
9719
|
+
},
|
|
9720
|
+
getAuthUrl(state, scopes) {
|
|
9721
|
+
const config2 = getKakaoConfig();
|
|
9722
|
+
const params = new URLSearchParams({
|
|
9723
|
+
response_type: "code",
|
|
9724
|
+
client_id: config2.clientId,
|
|
9725
|
+
redirect_uri: config2.redirectUri,
|
|
9726
|
+
state,
|
|
9727
|
+
scope: (scopes ?? getKakaoScopes()).join(",")
|
|
9728
|
+
});
|
|
9729
|
+
return `${KAKAO_AUTH_URL}?${params.toString()}`;
|
|
9730
|
+
},
|
|
9731
|
+
async exchangeCodeForTokens(code) {
|
|
9732
|
+
const config2 = getKakaoConfig();
|
|
9733
|
+
const params = new URLSearchParams({
|
|
9734
|
+
grant_type: "authorization_code",
|
|
9735
|
+
client_id: config2.clientId,
|
|
9736
|
+
redirect_uri: config2.redirectUri,
|
|
9737
|
+
code
|
|
9738
|
+
});
|
|
9739
|
+
if (config2.clientSecret) {
|
|
9740
|
+
params.set("client_secret", config2.clientSecret);
|
|
9741
|
+
}
|
|
9742
|
+
return requestKakaoTokens(params);
|
|
9743
|
+
},
|
|
9744
|
+
async getUserInfo(accessToken) {
|
|
9745
|
+
const response = await fetch(KAKAO_USERINFO_URL, {
|
|
9746
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
9747
|
+
});
|
|
9748
|
+
if (!response.ok) {
|
|
9749
|
+
throw new Error(`Kakao user-info request failed with status ${response.status}`);
|
|
9750
|
+
}
|
|
9751
|
+
const body = await response.json();
|
|
9752
|
+
if (typeof body.id !== "number" && typeof body.id !== "string") {
|
|
9753
|
+
throw new Error("Kakao user-info response is missing the provider user ID");
|
|
9754
|
+
}
|
|
9755
|
+
const account = body.kakao_account;
|
|
9756
|
+
const email = typeof account?.email === "string" ? account.email : null;
|
|
9757
|
+
return {
|
|
9758
|
+
providerUserId: String(body.id),
|
|
9759
|
+
email,
|
|
9760
|
+
emailVerified: email !== null && account?.is_email_valid === true && account.is_email_verified === true,
|
|
9761
|
+
name: typeof account?.profile?.nickname === "string" ? account.profile.nickname : void 0,
|
|
9762
|
+
avatar: typeof account?.profile?.profile_image_url === "string" ? account.profile.profile_image_url : void 0
|
|
9763
|
+
};
|
|
9764
|
+
},
|
|
9765
|
+
async refreshTokens(refreshToken) {
|
|
9766
|
+
const config2 = getKakaoConfig();
|
|
9767
|
+
const params = new URLSearchParams({
|
|
9768
|
+
grant_type: "refresh_token",
|
|
9769
|
+
client_id: config2.clientId,
|
|
9770
|
+
refresh_token: refreshToken
|
|
9771
|
+
});
|
|
9772
|
+
if (config2.clientSecret) {
|
|
9773
|
+
params.set("client_secret", config2.clientSecret);
|
|
9774
|
+
}
|
|
9775
|
+
return requestKakaoTokens(params);
|
|
9776
|
+
}
|
|
9777
|
+
};
|
|
9778
|
+
registerOAuthProvider(kakaoProvider);
|
|
9779
|
+
|
|
9780
|
+
// src/server/lib/oauth/naver-provider.ts
|
|
9781
|
+
init_config();
|
|
9782
|
+
import { ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
9783
|
+
var NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
|
|
9784
|
+
var NAVER_TOKEN_URL = "https://nid.naver.com/oauth2.0/token";
|
|
9785
|
+
var NAVER_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
|
|
9786
|
+
function getNaverConfig() {
|
|
9787
|
+
const clientId = env3.SPFN_AUTH_NAVER_CLIENT_ID;
|
|
9788
|
+
const clientSecret = env3.SPFN_AUTH_NAVER_CLIENT_SECRET;
|
|
9789
|
+
if (!clientId || !clientSecret) {
|
|
9790
|
+
throw new ValidationError7({
|
|
9791
|
+
message: "Naver OAuth is not configured. Set SPFN_AUTH_NAVER_CLIENT_ID and SPFN_AUTH_NAVER_CLIENT_SECRET."
|
|
9792
|
+
});
|
|
9793
|
+
}
|
|
9794
|
+
const baseUrl = env3.NEXT_PUBLIC_SPFN_APP_URL || env3.SPFN_APP_URL;
|
|
9795
|
+
return {
|
|
9796
|
+
clientId,
|
|
9797
|
+
clientSecret,
|
|
9798
|
+
redirectUri: env3.SPFN_AUTH_NAVER_REDIRECT_URI || `${baseUrl}/_auth/oauth/naver/callback`
|
|
9799
|
+
};
|
|
9800
|
+
}
|
|
9801
|
+
async function requestNaverTokens(params) {
|
|
9802
|
+
const response = await fetch(NAVER_TOKEN_URL, {
|
|
9803
|
+
method: "POST",
|
|
9804
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" },
|
|
9805
|
+
body: params
|
|
9806
|
+
});
|
|
9807
|
+
if (!response.ok) {
|
|
9808
|
+
throw new Error(`Naver token request failed with status ${response.status}`);
|
|
9809
|
+
}
|
|
9810
|
+
const body = await response.json();
|
|
9811
|
+
const expiresIn = Number(body.expires_in);
|
|
9812
|
+
if (typeof body.access_token !== "string" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
9813
|
+
throw new Error("Naver token response is invalid");
|
|
9814
|
+
}
|
|
9815
|
+
return {
|
|
9816
|
+
accessToken: body.access_token,
|
|
9817
|
+
refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
|
|
9818
|
+
expiresIn
|
|
9819
|
+
};
|
|
9820
|
+
}
|
|
9821
|
+
var naverProvider = {
|
|
9822
|
+
id: "naver",
|
|
9823
|
+
isEnabled() {
|
|
9824
|
+
return !!(env3.SPFN_AUTH_NAVER_CLIENT_ID && env3.SPFN_AUTH_NAVER_CLIENT_SECRET);
|
|
9825
|
+
},
|
|
9826
|
+
getAuthUrl(state) {
|
|
9827
|
+
const config2 = getNaverConfig();
|
|
9828
|
+
const params = new URLSearchParams({
|
|
9829
|
+
response_type: "code",
|
|
9830
|
+
client_id: config2.clientId,
|
|
9831
|
+
redirect_uri: config2.redirectUri,
|
|
9832
|
+
state
|
|
9833
|
+
});
|
|
9834
|
+
return `${NAVER_AUTH_URL}?${params.toString()}`;
|
|
9835
|
+
},
|
|
9836
|
+
async exchangeCodeForTokens(code, options) {
|
|
9837
|
+
const config2 = getNaverConfig();
|
|
9838
|
+
return requestNaverTokens(new URLSearchParams({
|
|
9839
|
+
grant_type: "authorization_code",
|
|
9840
|
+
client_id: config2.clientId,
|
|
9841
|
+
client_secret: config2.clientSecret,
|
|
9842
|
+
redirect_uri: config2.redirectUri,
|
|
9843
|
+
code,
|
|
9844
|
+
state: options.state
|
|
9845
|
+
}));
|
|
9846
|
+
},
|
|
9847
|
+
async getUserInfo(accessToken) {
|
|
9848
|
+
const response = await fetch(NAVER_USERINFO_URL, {
|
|
9849
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
9850
|
+
});
|
|
9851
|
+
if (!response.ok) {
|
|
9852
|
+
throw new Error(`Naver user-info request failed with status ${response.status}`);
|
|
9853
|
+
}
|
|
9854
|
+
const body = await response.json();
|
|
9855
|
+
const profile = body.response;
|
|
9856
|
+
if (body.resultcode !== "00" || typeof profile?.id !== "string") {
|
|
9857
|
+
throw new Error("Naver user-info response is invalid");
|
|
9858
|
+
}
|
|
9859
|
+
return {
|
|
9860
|
+
providerUserId: profile.id,
|
|
9861
|
+
email: typeof profile.email === "string" ? profile.email : null,
|
|
9862
|
+
// Naver returns an email address but no independently verified-email claim.
|
|
9863
|
+
emailVerified: false,
|
|
9864
|
+
name: typeof profile.name === "string" ? profile.name : typeof profile.nickname === "string" ? profile.nickname : void 0,
|
|
9865
|
+
avatar: typeof profile.profile_image === "string" ? profile.profile_image : void 0
|
|
9866
|
+
};
|
|
9867
|
+
},
|
|
9868
|
+
async refreshTokens(refreshToken) {
|
|
9869
|
+
const config2 = getNaverConfig();
|
|
9870
|
+
return requestNaverTokens(new URLSearchParams({
|
|
9871
|
+
grant_type: "refresh_token",
|
|
9872
|
+
client_id: config2.clientId,
|
|
9873
|
+
client_secret: config2.clientSecret,
|
|
9874
|
+
refresh_token: refreshToken
|
|
9875
|
+
}));
|
|
9876
|
+
}
|
|
9877
|
+
};
|
|
9878
|
+
registerOAuthProvider(naverProvider);
|
|
9879
|
+
|
|
8525
9880
|
// src/server/services/oauth.service.ts
|
|
8526
9881
|
function requireEnabledProvider(provider) {
|
|
8527
9882
|
const oauthProvider = getOAuthProvider(provider);
|
|
8528
9883
|
if (!oauthProvider) {
|
|
8529
|
-
throw new
|
|
9884
|
+
throw new ValidationError8({
|
|
8530
9885
|
message: `Unsupported OAuth provider: ${provider}. No provider is registered for this id.`
|
|
8531
9886
|
});
|
|
8532
9887
|
}
|
|
8533
9888
|
if (!oauthProvider.isEnabled()) {
|
|
8534
|
-
throw new
|
|
9889
|
+
throw new ValidationError8({
|
|
8535
9890
|
message: `OAuth provider '${provider}' is registered but not configured. Check its required environment variables.`
|
|
8536
9891
|
});
|
|
8537
9892
|
}
|
|
@@ -8539,7 +9894,7 @@ function requireEnabledProvider(provider) {
|
|
|
8539
9894
|
}
|
|
8540
9895
|
function tokenExpiryDate(expiresIn) {
|
|
8541
9896
|
if (!Number.isFinite(expiresIn)) {
|
|
8542
|
-
throw new
|
|
9897
|
+
throw new ValidationError8({
|
|
8543
9898
|
message: `Invalid token expiry returned from OAuth provider: ${expiresIn}`
|
|
8544
9899
|
});
|
|
8545
9900
|
}
|
|
@@ -8563,18 +9918,19 @@ async function oauthStartService(params) {
|
|
|
8563
9918
|
async function oauthCallbackService(params) {
|
|
8564
9919
|
const { provider, code, state, expectedNonce } = params;
|
|
8565
9920
|
const stateData = await verifyOAuthState(state);
|
|
8566
|
-
|
|
8567
|
-
|
|
9921
|
+
const nonceCandidates = typeof expectedNonce === "string" ? [expectedNonce] : expectedNonce ?? [];
|
|
9922
|
+
if (!stateData.nonce || !nonceCandidates.includes(stateData.nonce)) {
|
|
9923
|
+
throw new ValidationError8({
|
|
8568
9924
|
message: "OAuth state validation failed"
|
|
8569
9925
|
});
|
|
8570
9926
|
}
|
|
8571
9927
|
if (stateData.provider !== provider) {
|
|
8572
|
-
throw new
|
|
9928
|
+
throw new ValidationError8({
|
|
8573
9929
|
message: "OAuth state provider mismatch"
|
|
8574
9930
|
});
|
|
8575
9931
|
}
|
|
8576
9932
|
const oauthProvider = requireEnabledProvider(provider);
|
|
8577
|
-
const tokens = await oauthProvider.exchangeCodeForTokens(code);
|
|
9933
|
+
const tokens = await oauthProvider.exchangeCodeForTokens(code, { state });
|
|
8578
9934
|
const identity = await oauthProvider.getUserInfo(tokens.accessToken);
|
|
8579
9935
|
const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
|
|
8580
9936
|
provider,
|
|
@@ -8590,10 +9946,11 @@ async function oauthCallbackService(params) {
|
|
|
8590
9946
|
tokenExpiresAt: tokenExpiryDate(tokens.expiresIn)
|
|
8591
9947
|
});
|
|
8592
9948
|
} else {
|
|
8593
|
-
const result = await createOrLinkUser(provider, identity, tokens);
|
|
9949
|
+
const result = await createOrLinkUser(provider, identity, tokens, stateData.metadata);
|
|
8594
9950
|
userId = result.userId;
|
|
8595
9951
|
isNewUser = result.isNewUser;
|
|
8596
9952
|
}
|
|
9953
|
+
await assertActiveForOAuthSession(userId);
|
|
8597
9954
|
await registerPublicKeyService({
|
|
8598
9955
|
userId,
|
|
8599
9956
|
keyId: stateData.keyId,
|
|
@@ -8631,13 +9988,27 @@ async function oauthCallbackService(params) {
|
|
|
8631
9988
|
isNewUser
|
|
8632
9989
|
};
|
|
8633
9990
|
}
|
|
8634
|
-
async function
|
|
9991
|
+
async function assertActiveForOAuthSession(userId) {
|
|
9992
|
+
const user = await usersRepository.findByIdOnPrimary(userId);
|
|
9993
|
+
if (!user) {
|
|
9994
|
+
throw new ValidationError8({ message: "User not found" });
|
|
9995
|
+
}
|
|
9996
|
+
if (user.status === "active") {
|
|
9997
|
+
return;
|
|
9998
|
+
}
|
|
9999
|
+
if (user.status === "pending_deletion") {
|
|
10000
|
+
const pending = await getPendingDeletionInfo(user.id);
|
|
10001
|
+
throw new AccountPendingDeletionError2({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
|
|
10002
|
+
}
|
|
10003
|
+
throw new AccountDisabledError2({ status: user.status });
|
|
10004
|
+
}
|
|
10005
|
+
async function createOrLinkUser(provider, identity, tokens, metadata) {
|
|
8635
10006
|
const existingUser = identity.email ? await usersRepository.findByEmail(identity.email) : null;
|
|
8636
10007
|
let userId;
|
|
8637
10008
|
let isNewUser = false;
|
|
8638
10009
|
if (existingUser) {
|
|
8639
10010
|
if (!identity.emailVerified) {
|
|
8640
|
-
throw new
|
|
10011
|
+
throw new ValidationError8({
|
|
8641
10012
|
message: "Cannot link to existing account with unverified email. Please verify your email with the provider first."
|
|
8642
10013
|
});
|
|
8643
10014
|
}
|
|
@@ -8648,6 +10019,13 @@ async function createOrLinkUser(provider, identity, tokens) {
|
|
|
8648
10019
|
});
|
|
8649
10020
|
}
|
|
8650
10021
|
} else {
|
|
10022
|
+
await runBeforeRegister({
|
|
10023
|
+
channel: "oauth",
|
|
10024
|
+
provider,
|
|
10025
|
+
email: identity.email ?? void 0,
|
|
10026
|
+
emailVerified: identity.emailVerified,
|
|
10027
|
+
metadata
|
|
10028
|
+
});
|
|
8651
10029
|
const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
8652
10030
|
const userRole = await getRoleByName3("user");
|
|
8653
10031
|
if (!userRole) {
|
|
@@ -8701,7 +10079,7 @@ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
|
8701
10079
|
async function getGoogleAccessToken(userId) {
|
|
8702
10080
|
const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
|
|
8703
10081
|
if (!account) {
|
|
8704
|
-
throw new
|
|
10082
|
+
throw new ValidationError8({
|
|
8705
10083
|
message: "No Google account linked. User must sign in with Google first."
|
|
8706
10084
|
});
|
|
8707
10085
|
}
|
|
@@ -8710,7 +10088,7 @@ async function getGoogleAccessToken(userId) {
|
|
|
8710
10088
|
return account.accessToken;
|
|
8711
10089
|
}
|
|
8712
10090
|
if (!account.refreshToken) {
|
|
8713
|
-
throw new
|
|
10091
|
+
throw new ValidationError8({
|
|
8714
10092
|
message: "Google refresh token not available. User must re-authenticate with Google."
|
|
8715
10093
|
});
|
|
8716
10094
|
}
|
|
@@ -8725,12 +10103,12 @@ async function getGoogleAccessToken(userId) {
|
|
|
8725
10103
|
|
|
8726
10104
|
// src/server/services/oauth-native.service.ts
|
|
8727
10105
|
init_repositories();
|
|
8728
|
-
import { ValidationError as
|
|
8729
|
-
import { runInTransaction, onAfterCommit } from "@spfn/core/db";
|
|
10106
|
+
import { ValidationError as ValidationError9 } from "@spfn/core/errors";
|
|
10107
|
+
import { runInTransaction as runInTransaction2, onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
|
|
8730
10108
|
async function oauthNativeService(params) {
|
|
8731
10109
|
const oauthProvider = getOAuthProvider(params.provider);
|
|
8732
10110
|
if (!oauthProvider?.verifyNativeIdToken) {
|
|
8733
|
-
throw new
|
|
10111
|
+
throw new ValidationError9({
|
|
8734
10112
|
message: `Provider '${params.provider}' does not support native id_token sign-in.`
|
|
8735
10113
|
});
|
|
8736
10114
|
}
|
|
@@ -8741,7 +10119,7 @@ async function oauthNativeService(params) {
|
|
|
8741
10119
|
return persistNativeLogin(identity, params);
|
|
8742
10120
|
}
|
|
8743
10121
|
async function persistNativeLogin(identity, params) {
|
|
8744
|
-
return
|
|
10122
|
+
return runInTransaction2(async () => {
|
|
8745
10123
|
const existing = await socialAccountsRepository.findByProviderAndProviderId(
|
|
8746
10124
|
params.provider,
|
|
8747
10125
|
identity.providerUserId
|
|
@@ -8751,10 +10129,11 @@ async function persistNativeLogin(identity, params) {
|
|
|
8751
10129
|
if (existing) {
|
|
8752
10130
|
userId = existing.userId;
|
|
8753
10131
|
} else {
|
|
8754
|
-
const result = await createOrLinkUser(params.provider, identity);
|
|
10132
|
+
const result = await createOrLinkUser(params.provider, identity, void 0, params.metadata);
|
|
8755
10133
|
userId = result.userId;
|
|
8756
10134
|
isNewUser = result.isNewUser;
|
|
8757
10135
|
}
|
|
10136
|
+
await assertActiveForOAuthSession(userId);
|
|
8758
10137
|
await registerPublicKeyService({
|
|
8759
10138
|
userId,
|
|
8760
10139
|
keyId: params.keyId,
|
|
@@ -8769,7 +10148,7 @@ async function persistNativeLogin(identity, params) {
|
|
|
8769
10148
|
email: identity.email || void 0,
|
|
8770
10149
|
metadata: params.metadata
|
|
8771
10150
|
};
|
|
8772
|
-
|
|
10151
|
+
onAfterCommit2(() => (isNewUser ? authRegisterEvent : authLoginEvent).emit(eventPayload));
|
|
8773
10152
|
return { userId: String(userId), keyId: params.keyId, isNewUser };
|
|
8774
10153
|
}, { context: "auth:oauth-native" });
|
|
8775
10154
|
}
|
|
@@ -8983,12 +10362,13 @@ import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
|
|
|
8983
10362
|
// src/server/middleware/authenticate.ts
|
|
8984
10363
|
import { defineMiddleware } from "@spfn/core/route";
|
|
8985
10364
|
import { UnauthorizedError } from "@spfn/core/errors";
|
|
8986
|
-
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2, userProfilesRepository as userProfilesRepository2 } from "@spfn/auth/server";
|
|
10365
|
+
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2, userProfilesRepository as userProfilesRepository2, getPendingDeletionInfo as getPendingDeletionInfo2 } from "@spfn/auth/server";
|
|
8987
10366
|
import {
|
|
8988
10367
|
InvalidTokenError,
|
|
8989
10368
|
TokenExpiredError,
|
|
8990
10369
|
KeyExpiredError,
|
|
8991
|
-
AccountDisabledError as
|
|
10370
|
+
AccountDisabledError as AccountDisabledError3,
|
|
10371
|
+
AccountPendingDeletionError as AccountPendingDeletionError3
|
|
8992
10372
|
} from "@spfn/auth/errors";
|
|
8993
10373
|
var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
8994
10374
|
const authHeader = c.req.header("Authorization");
|
|
@@ -9039,7 +10419,11 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
9039
10419
|
}
|
|
9040
10420
|
const { user, role } = result;
|
|
9041
10421
|
if (user.status !== "active") {
|
|
9042
|
-
|
|
10422
|
+
if (user.status === "pending_deletion") {
|
|
10423
|
+
const pending = await getPendingDeletionInfo2(user.id);
|
|
10424
|
+
throw new AccountPendingDeletionError3({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
|
|
10425
|
+
}
|
|
10426
|
+
throw new AccountDisabledError3({ status: user.status });
|
|
9043
10427
|
}
|
|
9044
10428
|
keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
9045
10429
|
c.set("auth", {
|
|
@@ -9761,7 +11145,7 @@ var deleteCookie = (c, name, opt) => {
|
|
|
9761
11145
|
// src/server/routes/oauth/index.ts
|
|
9762
11146
|
init_types();
|
|
9763
11147
|
import { Transactional as Transactional3 } from "@spfn/core/db";
|
|
9764
|
-
import { ValidationError as
|
|
11148
|
+
import { ValidationError as ValidationError10 } from "@spfn/core/errors";
|
|
9765
11149
|
import { rateLimitPolicy as rateLimitPolicy4 } from "@spfn/core/middleware";
|
|
9766
11150
|
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
9767
11151
|
var providerParams = Type.Object({
|
|
@@ -9807,14 +11191,16 @@ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
|
9807
11191
|
if (!query.code || !query.state) {
|
|
9808
11192
|
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9809
11193
|
}
|
|
9810
|
-
const
|
|
9811
|
-
|
|
11194
|
+
const csrfCookies = matchOAuthCsrfCookies(getCookie(c.raw));
|
|
11195
|
+
for (const cookie of csrfCookies) {
|
|
11196
|
+
deleteCookie(c.raw, cookie.name, { path: "/" });
|
|
11197
|
+
}
|
|
9812
11198
|
try {
|
|
9813
11199
|
const result = await oauthCallbackService({
|
|
9814
11200
|
provider: "google",
|
|
9815
11201
|
code: query.code,
|
|
9816
11202
|
state: query.state,
|
|
9817
|
-
expectedNonce
|
|
11203
|
+
expectedNonce: csrfCookies.map((cookie) => cookie.value)
|
|
9818
11204
|
});
|
|
9819
11205
|
return c.redirect(result.redirectUrl);
|
|
9820
11206
|
} catch (err) {
|
|
@@ -9869,6 +11255,9 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
|
9869
11255
|
returnUrl: Type.Optional(Type.String({
|
|
9870
11256
|
description: "URL to redirect after OAuth success"
|
|
9871
11257
|
})),
|
|
11258
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
11259
|
+
description: "Custom signup metadata sealed into OAuth state and passed to beforeRegister"
|
|
11260
|
+
})),
|
|
9872
11261
|
state: Type.Optional(Type.String({
|
|
9873
11262
|
description: "Encrypted OAuth state (injected by interceptor)"
|
|
9874
11263
|
}))
|
|
@@ -9876,10 +11265,10 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
|
9876
11265
|
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9877
11266
|
const { body } = await c.data();
|
|
9878
11267
|
if (!isGoogleOAuthEnabled()) {
|
|
9879
|
-
throw new
|
|
11268
|
+
throw new ValidationError10({ message: "Google OAuth is not configured" });
|
|
9880
11269
|
}
|
|
9881
11270
|
if (!body.state) {
|
|
9882
|
-
throw new
|
|
11271
|
+
throw new ValidationError10({
|
|
9883
11272
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
9884
11273
|
});
|
|
9885
11274
|
}
|
|
@@ -9940,14 +11329,16 @@ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input(
|
|
|
9940
11329
|
if (!query.code || !query.state) {
|
|
9941
11330
|
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9942
11331
|
}
|
|
9943
|
-
const
|
|
9944
|
-
|
|
11332
|
+
const csrfCookies = matchOAuthCsrfCookies(getCookie(c.raw));
|
|
11333
|
+
for (const cookie of csrfCookies) {
|
|
11334
|
+
deleteCookie(c.raw, cookie.name, { path: "/" });
|
|
11335
|
+
}
|
|
9945
11336
|
try {
|
|
9946
11337
|
const result = await oauthCallbackService({
|
|
9947
11338
|
provider: params.provider,
|
|
9948
11339
|
code: query.code,
|
|
9949
11340
|
state: query.state,
|
|
9950
|
-
expectedNonce
|
|
11341
|
+
expectedNonce: csrfCookies.map((cookie) => cookie.value)
|
|
9951
11342
|
});
|
|
9952
11343
|
return c.redirect(result.redirectUrl);
|
|
9953
11344
|
} catch (err) {
|
|
@@ -9961,6 +11352,9 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
|
|
|
9961
11352
|
returnUrl: Type.Optional(Type.String({
|
|
9962
11353
|
description: "URL to redirect after OAuth success"
|
|
9963
11354
|
})),
|
|
11355
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
11356
|
+
description: "Custom signup metadata sealed into OAuth state and passed to beforeRegister"
|
|
11357
|
+
})),
|
|
9964
11358
|
state: Type.Optional(Type.String({
|
|
9965
11359
|
description: "Encrypted OAuth state (injected by interceptor)"
|
|
9966
11360
|
}))
|
|
@@ -9969,7 +11363,7 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
|
|
|
9969
11363
|
const { params, body } = await c.data();
|
|
9970
11364
|
const provider = requireEnabledProvider(params.provider);
|
|
9971
11365
|
if (!body.state) {
|
|
9972
|
-
throw new
|
|
11366
|
+
throw new ValidationError10({
|
|
9973
11367
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
9974
11368
|
});
|
|
9975
11369
|
}
|
|
@@ -10095,8 +11489,56 @@ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
|
10095
11489
|
return { userId: params.userId, roleId: body.roleId };
|
|
10096
11490
|
});
|
|
10097
11491
|
|
|
11492
|
+
// src/server/routes/deletion/index.ts
|
|
11493
|
+
init_esm();
|
|
11494
|
+
init_schema3();
|
|
11495
|
+
import { Transactional as Transactional4 } from "@spfn/core/db";
|
|
11496
|
+
import { rateLimitPolicy as rateLimitPolicy5 } from "@spfn/core/middleware";
|
|
11497
|
+
import { defineRouter as defineRouter5, route as route6 } from "@spfn/core/route";
|
|
11498
|
+
var requestAccountDeletion = route6.post("/_auth/deletion/request").input({
|
|
11499
|
+
body: Type.Object({
|
|
11500
|
+
password: Type.Optional(Type.String({ minLength: 1, description: "Current password, if the account has one" })),
|
|
11501
|
+
verificationToken: Type.Optional(Type.String({ description: "Verification token (purpose: account_deletion), for passwordless/OAuth-only accounts" })),
|
|
11502
|
+
reason: Type.Optional(Type.String({ maxLength: 500, description: "Optional free-text reason" })),
|
|
11503
|
+
immediate: Type.Optional(Type.Boolean({ description: "Skip the grace period \u2014 requires deletion.allowSelfImmediate on the server" }))
|
|
11504
|
+
})
|
|
11505
|
+
}).use([rateLimitPolicy5("auth-deletion-request", { limit: 5, windowMs: 6e4 }), Transactional4()]).handler(async (c) => {
|
|
11506
|
+
const { body } = await c.data();
|
|
11507
|
+
const { userId } = getAuth(c);
|
|
11508
|
+
const result = await requestAccountDeletionService(Number(userId), {
|
|
11509
|
+
requestedBy: "self",
|
|
11510
|
+
password: body.password,
|
|
11511
|
+
verificationToken: body.verificationToken,
|
|
11512
|
+
reason: body.reason,
|
|
11513
|
+
immediate: body.immediate
|
|
11514
|
+
});
|
|
11515
|
+
return {
|
|
11516
|
+
purgeScheduledAt: result.purgeScheduledAt.toISOString()
|
|
11517
|
+
};
|
|
11518
|
+
});
|
|
11519
|
+
var cancelAccountDeletion = route6.post("/_auth/deletion/cancel").input({
|
|
11520
|
+
body: Type.Object({
|
|
11521
|
+
email: Type.Optional(EmailSchema),
|
|
11522
|
+
phone: Type.Optional(PhoneSchema),
|
|
11523
|
+
password: Type.Optional(Type.String({ minLength: 1 })),
|
|
11524
|
+
verificationToken: Type.Optional(Type.String({ description: "Verification token (purpose: account_deletion), for passwordless/OAuth-only accounts" }))
|
|
11525
|
+
}, {
|
|
11526
|
+
minProperties: 2,
|
|
11527
|
+
// email/phone + password|verificationToken
|
|
11528
|
+
description: "Email or phone must be provided with password or verificationToken"
|
|
11529
|
+
})
|
|
11530
|
+
}).use([rateLimitPolicy5("auth-deletion-cancel", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 50 }) }), Transactional4()]).skip(["auth"]).handler(async (c) => {
|
|
11531
|
+
const { body } = await c.data();
|
|
11532
|
+
await cancelAccountDeletionService(body);
|
|
11533
|
+
return c.noContent();
|
|
11534
|
+
});
|
|
11535
|
+
var deletionRouter = defineRouter5({
|
|
11536
|
+
requestAccountDeletion,
|
|
11537
|
+
cancelAccountDeletion
|
|
11538
|
+
});
|
|
11539
|
+
|
|
10098
11540
|
// src/server/routes/index.ts
|
|
10099
|
-
var mainAuthRouter =
|
|
11541
|
+
var mainAuthRouter = defineRouter6({
|
|
10100
11542
|
// Auth routes
|
|
10101
11543
|
sendVerificationCode,
|
|
10102
11544
|
verifyCode,
|
|
@@ -10108,6 +11550,9 @@ var mainAuthRouter = defineRouter5({
|
|
|
10108
11550
|
getAuthSession,
|
|
10109
11551
|
// One-Time Token routes
|
|
10110
11552
|
issueOneTimeToken,
|
|
11553
|
+
// Account deletion routes
|
|
11554
|
+
requestAccountDeletion,
|
|
11555
|
+
cancelAccountDeletion,
|
|
10111
11556
|
// OAuth routes
|
|
10112
11557
|
oauthGoogleStart,
|
|
10113
11558
|
oauthGoogleCallback,
|
|
@@ -10458,6 +11903,7 @@ async function ensureAdminExists() {
|
|
|
10458
11903
|
|
|
10459
11904
|
// src/server/lifecycle.ts
|
|
10460
11905
|
function createAuthLifecycle(options = {}) {
|
|
11906
|
+
configureDeletion(options.deletion);
|
|
10461
11907
|
return {
|
|
10462
11908
|
/**
|
|
10463
11909
|
* Initialize auth system after database is ready
|
|
@@ -10474,15 +11920,42 @@ function createAuthLifecycle(options = {}) {
|
|
|
10474
11920
|
}
|
|
10475
11921
|
};
|
|
10476
11922
|
}
|
|
11923
|
+
|
|
11924
|
+
// src/server/jobs/deletion-purge.ts
|
|
11925
|
+
import { job, defineJobRouter } from "@spfn/core/job";
|
|
11926
|
+
function createAuthDeletionPurgeJob(cronExpression = DEFAULT_DELETION_PURGE_CRON) {
|
|
11927
|
+
return job("auth.deletion.purge").cron(cronExpression).options({ retryLimit: 1 }).handler(async () => {
|
|
11928
|
+
const result = await sweepDuePurges();
|
|
11929
|
+
if (result.processed > 0) {
|
|
11930
|
+
authLogger.service.info("[auth.deletion.purge] sweep complete", result);
|
|
11931
|
+
}
|
|
11932
|
+
});
|
|
11933
|
+
}
|
|
11934
|
+
function createAuthDeletionJobRouter(options) {
|
|
11935
|
+
return defineJobRouter({
|
|
11936
|
+
deletionPurge: createAuthDeletionPurgeJob(options?.purgeCron)
|
|
11937
|
+
});
|
|
11938
|
+
}
|
|
11939
|
+
var authJobRouter = createAuthDeletionJobRouter();
|
|
10477
11940
|
export {
|
|
11941
|
+
ACCOUNT_DELETION_REQUESTED_BY,
|
|
11942
|
+
ACCOUNT_DELETION_REQUEST_STATUSES,
|
|
11943
|
+
AccountDeletionRequestsRepository,
|
|
10478
11944
|
AuthMetadataRepository,
|
|
10479
11945
|
AuthProviderSchema,
|
|
10480
11946
|
COOKIE_NAMES,
|
|
11947
|
+
DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE,
|
|
11948
|
+
DEFAULT_DELETION_GRACE_PERIOD_DAYS,
|
|
11949
|
+
DEFAULT_DELETION_PURGE_CRON,
|
|
11950
|
+
DEFAULT_DELETION_PURGE_STRATEGY,
|
|
11951
|
+
DEFAULT_DELETION_SEND_NOTIFICATIONS,
|
|
10481
11952
|
EmailSchema,
|
|
11953
|
+
EnvironmentKeyringTokenCipher,
|
|
10482
11954
|
INVITATION_STATUSES,
|
|
10483
11955
|
InvitationsRepository,
|
|
10484
11956
|
KEY_ALGORITHM,
|
|
10485
11957
|
KeysRepository,
|
|
11958
|
+
PURGE_STRATEGIES,
|
|
10486
11959
|
PasswordSchema,
|
|
10487
11960
|
PermissionsRepository,
|
|
10488
11961
|
PhoneSchema,
|
|
@@ -10500,9 +11973,15 @@ export {
|
|
|
10500
11973
|
VerificationCodesRepository,
|
|
10501
11974
|
VerificationPurposeSchema,
|
|
10502
11975
|
acceptInvitation,
|
|
11976
|
+
accountDeletionRequests,
|
|
11977
|
+
accountDeletionRequestsRepository,
|
|
10503
11978
|
addPermissionToRole,
|
|
10504
11979
|
appleProvider,
|
|
10505
11980
|
assertCanAssignRole,
|
|
11981
|
+
authDeletionCancelledEvent,
|
|
11982
|
+
authDeletionCompletedEvent,
|
|
11983
|
+
authDeletionRequestedEvent,
|
|
11984
|
+
authJobRouter,
|
|
10506
11985
|
authLogger,
|
|
10507
11986
|
authLoginEvent,
|
|
10508
11987
|
authMetadata,
|
|
@@ -10512,17 +11991,24 @@ export {
|
|
|
10512
11991
|
authSchema,
|
|
10513
11992
|
authenticate,
|
|
10514
11993
|
buildOAuthErrorUrl,
|
|
11994
|
+
cancelAccountDeletionService,
|
|
10515
11995
|
cancelInvitation,
|
|
10516
11996
|
changePasswordService,
|
|
10517
11997
|
checkUsernameAvailableService,
|
|
10518
11998
|
configureAuth,
|
|
11999
|
+
configureDeletion,
|
|
12000
|
+
configureOAuthTokenCipher,
|
|
12001
|
+
createAuthDeletionJobRouter,
|
|
12002
|
+
createAuthDeletionPurgeJob,
|
|
10519
12003
|
createAuthLifecycle,
|
|
10520
12004
|
createInvitation,
|
|
10521
12005
|
createOAuthState,
|
|
10522
12006
|
createRole,
|
|
10523
12007
|
decodeToken,
|
|
12008
|
+
decryptToken,
|
|
10524
12009
|
deleteInvitation,
|
|
10525
12010
|
deleteRole,
|
|
12011
|
+
encryptToken,
|
|
10526
12012
|
exchangeCodeForTokens,
|
|
10527
12013
|
expireOldInvitations,
|
|
10528
12014
|
generateClientToken,
|
|
@@ -10535,6 +12021,8 @@ export {
|
|
|
10535
12021
|
getAuth,
|
|
10536
12022
|
getAuthConfig,
|
|
10537
12023
|
getAuthSessionService,
|
|
12024
|
+
getDeletionConfig,
|
|
12025
|
+
getDummyPasswordHash,
|
|
10538
12026
|
getEnabledOAuthProviders,
|
|
10539
12027
|
getGoogleAccessToken,
|
|
10540
12028
|
getGoogleAuthUrl,
|
|
@@ -10548,6 +12036,7 @@ export {
|
|
|
10548
12036
|
getOAuthProvider,
|
|
10549
12037
|
getOneTimeTokenManager,
|
|
10550
12038
|
getOptionalAuth,
|
|
12039
|
+
getPendingDeletionInfo,
|
|
10551
12040
|
getRegisteredProviders,
|
|
10552
12041
|
getRole,
|
|
10553
12042
|
getRoleByName,
|
|
@@ -10574,13 +12063,17 @@ export {
|
|
|
10574
12063
|
invitationAcceptedEvent,
|
|
10575
12064
|
invitationCreatedEvent,
|
|
10576
12065
|
invitationsRepository,
|
|
12066
|
+
isEncrypted,
|
|
10577
12067
|
isGoogleOAuthEnabled,
|
|
10578
12068
|
isOAuthProviderEnabled,
|
|
10579
12069
|
issueOneTimeTokenService,
|
|
12070
|
+
kakaoProvider,
|
|
10580
12071
|
keysRepository,
|
|
10581
12072
|
listInvitations,
|
|
10582
12073
|
loginService,
|
|
10583
12074
|
logoutService,
|
|
12075
|
+
matchOAuthCsrfCookies,
|
|
12076
|
+
naverProvider,
|
|
10584
12077
|
oauthCallbackService,
|
|
10585
12078
|
oauthNativeService,
|
|
10586
12079
|
oauthStartService,
|
|
@@ -10589,11 +12082,13 @@ export {
|
|
|
10589
12082
|
parseDuration,
|
|
10590
12083
|
permissions,
|
|
10591
12084
|
permissionsRepository,
|
|
12085
|
+
purgeUserService,
|
|
10592
12086
|
refreshAccessToken,
|
|
10593
12087
|
registerOAuthProvider,
|
|
10594
12088
|
registerPublicKeyService,
|
|
10595
12089
|
registerService,
|
|
10596
12090
|
removePermissionFromRole,
|
|
12091
|
+
requestAccountDeletionService,
|
|
10597
12092
|
requireAnyPermission,
|
|
10598
12093
|
requireEnabledProvider,
|
|
10599
12094
|
requirePermissions,
|
|
@@ -10606,12 +12101,14 @@ export {
|
|
|
10606
12101
|
roles,
|
|
10607
12102
|
rolesRepository,
|
|
10608
12103
|
rotateKeyService,
|
|
12104
|
+
runBeforeRegister,
|
|
10609
12105
|
sealSession,
|
|
10610
12106
|
sendVerificationCodeService,
|
|
10611
12107
|
setRolePermissions,
|
|
10612
12108
|
shouldRefreshSession,
|
|
10613
12109
|
shouldRotateKey,
|
|
10614
12110
|
socialAccountsRepository,
|
|
12111
|
+
sweepDuePurges,
|
|
10615
12112
|
unsealSession,
|
|
10616
12113
|
updateLastLoginService,
|
|
10617
12114
|
updateLocaleService,
|