@spfn/auth 0.3.0-beta.4 → 0.3.0-beta.6
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 +101 -0
- package/dist/{authenticate-Ctul07Sc.d.ts → authenticate-98lBIMxP.d.ts} +115 -1
- package/dist/config.d.ts +60 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +39 -2
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +29 -2
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +58 -1
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js +4 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +442 -7
- package/dist/server.js +933 -383
- package/dist/server.js.map +1 -1
- package/migrations/20260810112144_colorful_tomorrow_man/migration.sql +18 -0
- package/migrations/20260810112144_colorful_tomorrow_man/snapshot.json +3576 -0
- package/package.json +2 -2
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(index13) {
|
|
901
|
+
return CreateType({ [Kind]: "Argument", index: index13 });
|
|
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, index13, char) {
|
|
1140
|
+
return pattern[index13] === char && pattern.charCodeAt(index13 - 1) !== 92;
|
|
1141
1141
|
}
|
|
1142
|
-
function IsOpenParen(pattern,
|
|
1143
|
-
return IsNonEscaped(pattern,
|
|
1142
|
+
function IsOpenParen(pattern, index13) {
|
|
1143
|
+
return IsNonEscaped(pattern, index13, "(");
|
|
1144
1144
|
}
|
|
1145
|
-
function IsCloseParen(pattern,
|
|
1146
|
-
return IsNonEscaped(pattern,
|
|
1145
|
+
function IsCloseParen(pattern, index13) {
|
|
1146
|
+
return IsNonEscaped(pattern, index13, ")");
|
|
1147
1147
|
}
|
|
1148
|
-
function IsSeparator(pattern,
|
|
1149
|
-
return IsNonEscaped(pattern,
|
|
1148
|
+
function IsSeparator(pattern, index13) {
|
|
1149
|
+
return IsNonEscaped(pattern, index13, "|");
|
|
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 index13 = 0; index13 < pattern.length; index13++) {
|
|
1156
|
+
if (IsOpenParen(pattern, index13))
|
|
1157
1157
|
count += 1;
|
|
1158
|
-
if (IsCloseParen(pattern,
|
|
1158
|
+
if (IsCloseParen(pattern, index13))
|
|
1159
1159
|
count -= 1;
|
|
1160
|
-
if (count === 0 &&
|
|
1160
|
+
if (count === 0 && index13 !== 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 index13 = 0; index13 < pattern.length; index13++) {
|
|
1171
|
+
if (IsOpenParen(pattern, index13))
|
|
1172
1172
|
count += 1;
|
|
1173
|
-
if (IsCloseParen(pattern,
|
|
1173
|
+
if (IsCloseParen(pattern, index13))
|
|
1174
1174
|
count -= 1;
|
|
1175
|
-
if (IsSeparator(pattern,
|
|
1175
|
+
if (IsSeparator(pattern, index13) && 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 index13 = 0; index13 < pattern.length; index13++) {
|
|
1182
|
+
if (IsOpenParen(pattern, index13))
|
|
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 index13 = 0; index13 < pattern.length; index13++) {
|
|
1191
|
+
if (IsOpenParen(pattern, index13))
|
|
1192
1192
|
count += 1;
|
|
1193
|
-
if (IsCloseParen(pattern,
|
|
1193
|
+
if (IsCloseParen(pattern, index13))
|
|
1194
1194
|
count -= 1;
|
|
1195
|
-
if (IsSeparator(pattern,
|
|
1196
|
-
const range2 = pattern.slice(start,
|
|
1195
|
+
if (IsSeparator(pattern, index13) && count === 0) {
|
|
1196
|
+
const range2 = pattern.slice(start, index13);
|
|
1197
1197
|
if (range2.length > 0)
|
|
1198
1198
|
expressions.push(TemplateLiteralParse(range2));
|
|
1199
|
-
start =
|
|
1199
|
+
start = index13 + 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, index13) {
|
|
1213
|
+
if (!IsOpenParen(value, index13))
|
|
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 = index13; 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 [index13, 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, index13) {
|
|
1227
|
+
for (let scan = index13; scan < pattern2.length; scan++) {
|
|
1228
1228
|
if (IsOpenParen(pattern2, scan))
|
|
1229
|
-
return [
|
|
1229
|
+
return [index13, scan];
|
|
1230
1230
|
}
|
|
1231
|
-
return [
|
|
1231
|
+
return [index13, pattern2.length];
|
|
1232
1232
|
}
|
|
1233
1233
|
const expressions = [];
|
|
1234
|
-
for (let
|
|
1235
|
-
if (IsOpenParen(pattern,
|
|
1236
|
-
const [start, end] = Group(pattern,
|
|
1234
|
+
for (let index13 = 0; index13 < pattern.length; index13++) {
|
|
1235
|
+
if (IsOpenParen(pattern, index13)) {
|
|
1236
|
+
const [start, end] = Group(pattern, index13);
|
|
1237
1237
|
const range = pattern.slice(start, end + 1);
|
|
1238
1238
|
expressions.push(TemplateLiteralParse(range));
|
|
1239
|
-
|
|
1239
|
+
index13 = end;
|
|
1240
1240
|
} else {
|
|
1241
|
-
const [start, end] = Range(pattern,
|
|
1241
|
+
const [start, end] = Range(pattern, index13);
|
|
1242
1242
|
const range = pattern.slice(start, end);
|
|
1243
1243
|
if (range.length > 0)
|
|
1244
1244
|
expressions.push(TemplateLiteralParse(range));
|
|
1245
|
-
|
|
1245
|
+
index13 = 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, index13) => IntoBooleanResult(Visit3(right.parameters[index13], 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, index13) => IntoBooleanResult(Visit3(right.parameters[index13], 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, index13) => Visit3(schema, right.items[index13]) === 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;
|
|
@@ -4521,6 +4521,19 @@ var init_schema3 = __esm({
|
|
|
4521
4521
|
}
|
|
4522
4522
|
});
|
|
4523
4523
|
|
|
4524
|
+
// src/server/helpers/email.ts
|
|
4525
|
+
function normalizeEmail(email) {
|
|
4526
|
+
return email.trim().toLowerCase();
|
|
4527
|
+
}
|
|
4528
|
+
function normalizeOptionalEmail(email) {
|
|
4529
|
+
return typeof email === "string" ? normalizeEmail(email) : email;
|
|
4530
|
+
}
|
|
4531
|
+
var init_email = __esm({
|
|
4532
|
+
"src/server/helpers/email.ts"() {
|
|
4533
|
+
"use strict";
|
|
4534
|
+
}
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4524
4537
|
// src/server/entities/schema.ts
|
|
4525
4538
|
import { createSchema } from "@spfn/core/db";
|
|
4526
4539
|
var authSchema;
|
|
@@ -4965,9 +4978,59 @@ var init_verification_codes = __esm({
|
|
|
4965
4978
|
}
|
|
4966
4979
|
});
|
|
4967
4980
|
|
|
4981
|
+
// src/server/entities/signup-link-tokens.ts
|
|
4982
|
+
import { text as text7, index as index7, uniqueIndex as uniqueIndex2 } from "drizzle-orm/pg-core";
|
|
4983
|
+
import { id as id7, timestamps as timestamps6, utcTimestamp as utcTimestamp5 } from "@spfn/core/db";
|
|
4984
|
+
var signupLinkTokens;
|
|
4985
|
+
var init_signup_link_tokens = __esm({
|
|
4986
|
+
"src/server/entities/signup-link-tokens.ts"() {
|
|
4987
|
+
"use strict";
|
|
4988
|
+
init_schema4();
|
|
4989
|
+
signupLinkTokens = authSchema.table(
|
|
4990
|
+
"signup_link_tokens",
|
|
4991
|
+
{
|
|
4992
|
+
id: id7(),
|
|
4993
|
+
// Normalized email address the link was issued for
|
|
4994
|
+
email: text7("email").notNull(),
|
|
4995
|
+
// SHA-256 of the emailed token, base64url
|
|
4996
|
+
// The token itself (32 random bytes) is never stored
|
|
4997
|
+
tokenHash: text7("token_hash").notNull(),
|
|
4998
|
+
// Relative path to return the user to after signup completes
|
|
4999
|
+
// Validated as a relative path on the way in; never an absolute URL
|
|
5000
|
+
returnPath: text7("return_path"),
|
|
5001
|
+
// Link expiry — SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES from creation
|
|
5002
|
+
expiresAt: utcTimestamp5("expires_at").notNull(),
|
|
5003
|
+
// Set when the link is exchanged for a setup session
|
|
5004
|
+
// Non-null means the link is spent; it is one-time regardless of expiry
|
|
5005
|
+
consumedAt: utcTimestamp5("consumed_at"),
|
|
5006
|
+
// Set when a newer link for the same email replaced this one
|
|
5007
|
+
// A superseded row is refused even if it has not expired or been consumed
|
|
5008
|
+
supersededAt: utcTimestamp5("superseded_at"),
|
|
5009
|
+
// SHA-256 of the setup session secret, written at consume time
|
|
5010
|
+
// The secret lives only in the caller's HttpOnly cookie
|
|
5011
|
+
setupSecretHash: text7("setup_secret_hash"),
|
|
5012
|
+
// Setup session expiry — SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES from consume
|
|
5013
|
+
setupExpiresAt: utcTimestamp5("setup_expires_at"),
|
|
5014
|
+
// Terminal: the password was set and the account created
|
|
5015
|
+
completedAt: utcTimestamp5("completed_at"),
|
|
5016
|
+
...timestamps6()
|
|
5017
|
+
},
|
|
5018
|
+
(table) => [
|
|
5019
|
+
// Lookup path for confirming a link
|
|
5020
|
+
uniqueIndex2("signup_link_token_hash_idx").on(table.tokenHash),
|
|
5021
|
+
// Lookup path for setting a password
|
|
5022
|
+
// Unique so a setup secret can never address two rows
|
|
5023
|
+
uniqueIndex2("signup_link_setup_secret_hash_idx").on(table.setupSecretHash),
|
|
5024
|
+
// Supersede-on-resend scans the live rows for one address
|
|
5025
|
+
index7("signup_link_email_idx").on(table.email, table.expiresAt)
|
|
5026
|
+
]
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
});
|
|
5030
|
+
|
|
4968
5031
|
// src/server/entities/user-invitations.ts
|
|
4969
|
-
import { text as
|
|
4970
|
-
import { id as
|
|
5032
|
+
import { text as text8, index as index8 } from "drizzle-orm/pg-core";
|
|
5033
|
+
import { id as id8, timestamps as timestamps7, enumText as enumText5, utcTimestamp as utcTimestamp6, typedJsonb as typedJsonb2, foreignKey as foreignKey5 } from "@spfn/core/db";
|
|
4971
5034
|
var userInvitations;
|
|
4972
5035
|
var init_user_invitations = __esm({
|
|
4973
5036
|
"src/server/entities/user-invitations.ts"() {
|
|
@@ -4980,14 +5043,14 @@ var init_user_invitations = __esm({
|
|
|
4980
5043
|
"user_invitations",
|
|
4981
5044
|
{
|
|
4982
5045
|
// Primary key
|
|
4983
|
-
id:
|
|
5046
|
+
id: id8(),
|
|
4984
5047
|
// Target email address for the invitation
|
|
4985
5048
|
// Will become the user's email upon acceptance
|
|
4986
|
-
email:
|
|
5049
|
+
email: text8("email").notNull(),
|
|
4987
5050
|
// Unique invitation token (UUID v4)
|
|
4988
5051
|
// Used in invitation URL: /auth/invite/{token}
|
|
4989
5052
|
// Single-use token that expires after acceptance
|
|
4990
|
-
token:
|
|
5053
|
+
token: text8("token").notNull().unique(),
|
|
4991
5054
|
// Role to be assigned when invitation is accepted
|
|
4992
5055
|
// Foreign key to roles table
|
|
4993
5056
|
roleId: foreignKey5("role", () => roles.id),
|
|
@@ -5004,15 +5067,15 @@ var init_user_invitations = __esm({
|
|
|
5004
5067
|
// Expiration timestamp (default: 7 days from creation)
|
|
5005
5068
|
// Invitation cannot be accepted after this time
|
|
5006
5069
|
// Background job should update status to 'expired'
|
|
5007
|
-
expiresAt:
|
|
5070
|
+
expiresAt: utcTimestamp6("expires_at").notNull(),
|
|
5008
5071
|
// Timestamp when invitation was accepted
|
|
5009
5072
|
// null = not yet accepted
|
|
5010
5073
|
// Used for: audit trail, analytics
|
|
5011
|
-
acceptedAt:
|
|
5074
|
+
acceptedAt: utcTimestamp6("accepted_at"),
|
|
5012
5075
|
// Timestamp when invitation was cancelled
|
|
5013
5076
|
// null = not cancelled
|
|
5014
5077
|
// Used for: audit trail
|
|
5015
|
-
cancelledAt:
|
|
5078
|
+
cancelledAt: utcTimestamp6("cancelled_at"),
|
|
5016
5079
|
// Additional metadata (JSONB)
|
|
5017
5080
|
// Use cases:
|
|
5018
5081
|
// - Custom welcome message
|
|
@@ -5021,26 +5084,26 @@ var init_user_invitations = __esm({
|
|
|
5021
5084
|
// - Custom fields for app-specific data
|
|
5022
5085
|
// Example: { message: "Welcome!", department: "Engineering" }
|
|
5023
5086
|
metadata: typedJsonb2("metadata"),
|
|
5024
|
-
...
|
|
5087
|
+
...timestamps7()
|
|
5025
5088
|
},
|
|
5026
5089
|
(table) => [
|
|
5027
5090
|
// Indexes for query optimization
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5091
|
+
index8("invitations_token_idx").on(table.token),
|
|
5092
|
+
index8("invitations_email_idx").on(table.email),
|
|
5093
|
+
index8("invitations_status_idx").on(table.status),
|
|
5094
|
+
index8("invitations_invited_by_idx").on(table.invitedBy),
|
|
5095
|
+
index8("invitations_expires_at_idx").on(table.expiresAt),
|
|
5033
5096
|
// For cleanup jobs
|
|
5034
|
-
|
|
5097
|
+
index8("invitations_role_id_idx").on(table.roleId)
|
|
5035
5098
|
]
|
|
5036
5099
|
);
|
|
5037
5100
|
}
|
|
5038
5101
|
});
|
|
5039
5102
|
|
|
5040
5103
|
// src/server/entities/account-deletion-requests.ts
|
|
5041
|
-
import { text as
|
|
5104
|
+
import { text as text9, index as index9, uniqueIndex as uniqueIndex3 } from "drizzle-orm/pg-core";
|
|
5042
5105
|
import { sql as sql2 } from "drizzle-orm";
|
|
5043
|
-
import { id as
|
|
5106
|
+
import { id as id9, timestamps as timestamps8, enumText as enumText6, utcTimestamp as utcTimestamp7, optionalForeignKey } from "@spfn/core/db";
|
|
5044
5107
|
var accountDeletionRequests;
|
|
5045
5108
|
var init_account_deletion_requests = __esm({
|
|
5046
5109
|
"src/server/entities/account-deletion-requests.ts"() {
|
|
@@ -5051,18 +5114,18 @@ var init_account_deletion_requests = __esm({
|
|
|
5051
5114
|
accountDeletionRequests = authSchema.table(
|
|
5052
5115
|
"account_deletion_requests",
|
|
5053
5116
|
{
|
|
5054
|
-
id:
|
|
5117
|
+
id: id9(),
|
|
5055
5118
|
// Foreign key to users table. `set null` (optionalForeignKey default) so this
|
|
5056
5119
|
// row survives a hard-delete purge of the user it refers to.
|
|
5057
5120
|
userId: optionalForeignKey("user", () => users.id),
|
|
5058
5121
|
// Snapshot of the user's public UUID at request time — stays readable even
|
|
5059
5122
|
// after userId is nulled out or the account is anonymized.
|
|
5060
|
-
userPublicId:
|
|
5123
|
+
userPublicId: text9("user_public_id").notNull(),
|
|
5061
5124
|
// When the deletion was requested
|
|
5062
|
-
requestedAt:
|
|
5125
|
+
requestedAt: utcTimestamp7("requested_at").notNull().defaultNow(),
|
|
5063
5126
|
// When the purge job is allowed to run (requestedAt + grace period; equals
|
|
5064
5127
|
// requestedAt itself for immediate/zero-grace deletions)
|
|
5065
|
-
purgeScheduledAt:
|
|
5128
|
+
purgeScheduledAt: utcTimestamp7("purge_scheduled_at").notNull(),
|
|
5066
5129
|
// Request lifecycle status
|
|
5067
5130
|
// - pending: awaiting purgeScheduledAt (or immediate purge)
|
|
5068
5131
|
// - cancelled: recovered before purge
|
|
@@ -5071,20 +5134,20 @@ var init_account_deletion_requests = __esm({
|
|
|
5071
5134
|
// Who initiated the request
|
|
5072
5135
|
requestedBy: enumText6("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
|
|
5073
5136
|
// Optional free-text reason (self-service UI, admin note, DSR reference, ...)
|
|
5074
|
-
reason:
|
|
5075
|
-
cancelledAt:
|
|
5076
|
-
completedAt:
|
|
5137
|
+
reason: text9("reason"),
|
|
5138
|
+
cancelledAt: utcTimestamp7("cancelled_at"),
|
|
5139
|
+
completedAt: utcTimestamp7("completed_at"),
|
|
5077
5140
|
// Purge strategy actually executed (set on completion; null while pending)
|
|
5078
5141
|
purgeStrategy: enumText6("purge_strategy", PURGE_STRATEGIES),
|
|
5079
|
-
...
|
|
5142
|
+
...timestamps8()
|
|
5080
5143
|
},
|
|
5081
5144
|
(table) => [
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5145
|
+
index9("account_deletion_requests_user_id_idx").on(table.userId),
|
|
5146
|
+
index9("account_deletion_requests_status_idx").on(table.status),
|
|
5147
|
+
index9("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
|
|
5148
|
+
index9("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
|
|
5086
5149
|
// Partial unique index: at most one pending request per user at a time.
|
|
5087
|
-
|
|
5150
|
+
uniqueIndex3("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
|
|
5088
5151
|
]
|
|
5089
5152
|
);
|
|
5090
5153
|
}
|
|
@@ -5236,8 +5299,8 @@ var init_rbac = __esm({
|
|
|
5236
5299
|
});
|
|
5237
5300
|
|
|
5238
5301
|
// src/server/entities/permissions.ts
|
|
5239
|
-
import { text as
|
|
5240
|
-
import { id as
|
|
5302
|
+
import { text as text10, boolean as boolean4, index as index10 } from "drizzle-orm/pg-core";
|
|
5303
|
+
import { id as id10, timestamps as timestamps9, enumText as enumText7, typedJsonb as typedJsonb3 } from "@spfn/core/db";
|
|
5241
5304
|
var permissions;
|
|
5242
5305
|
var init_permissions = __esm({
|
|
5243
5306
|
"src/server/entities/permissions.ts"() {
|
|
@@ -5248,7 +5311,7 @@ var init_permissions = __esm({
|
|
|
5248
5311
|
"permissions",
|
|
5249
5312
|
{
|
|
5250
5313
|
// Primary key
|
|
5251
|
-
id:
|
|
5314
|
+
id: id10(),
|
|
5252
5315
|
// Permission identifier
|
|
5253
5316
|
// Format: resource:action or namespace:resource:action
|
|
5254
5317
|
// Examples:
|
|
@@ -5256,15 +5319,15 @@ var init_permissions = __esm({
|
|
|
5256
5319
|
// - Namespaced: 'auth:user:delete', 'cms:post:publish'
|
|
5257
5320
|
// Must be unique across all permissions
|
|
5258
5321
|
// Used in: permission checks, role assignments, API guards
|
|
5259
|
-
name:
|
|
5322
|
+
name: text10("name").notNull().unique(),
|
|
5260
5323
|
// Display name for UI
|
|
5261
5324
|
// Human-readable name shown in admin panels
|
|
5262
5325
|
// Example: "Delete Users", "Publish Posts"
|
|
5263
|
-
displayName:
|
|
5326
|
+
displayName: text10("display_name").notNull(),
|
|
5264
5327
|
// Permission description
|
|
5265
5328
|
// Detailed explanation of what this permission allows
|
|
5266
5329
|
// Example: "Allows deletion of user accounts from the system"
|
|
5267
|
-
description:
|
|
5330
|
+
description: text10("description"),
|
|
5268
5331
|
// Category for grouping
|
|
5269
5332
|
// Used for: organizing permissions in UI, filtering
|
|
5270
5333
|
// Built-in categories: auth, user, rbac, system
|
|
@@ -5300,22 +5363,22 @@ var init_permissions = __esm({
|
|
|
5300
5363
|
// - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
|
|
5301
5364
|
// Example: { icon: 'trash', color: 'red', requiresMfa: true }
|
|
5302
5365
|
metadata: typedJsonb3("metadata"),
|
|
5303
|
-
...
|
|
5366
|
+
...timestamps9()
|
|
5304
5367
|
},
|
|
5305
5368
|
(table) => [
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5369
|
+
index10("permissions_name_idx").on(table.name),
|
|
5370
|
+
index10("permissions_category_idx").on(table.category),
|
|
5371
|
+
index10("permissions_is_system_idx").on(table.isSystem),
|
|
5372
|
+
index10("permissions_is_active_idx").on(table.isActive),
|
|
5373
|
+
index10("permissions_is_builtin_idx").on(table.isBuiltin)
|
|
5311
5374
|
]
|
|
5312
5375
|
);
|
|
5313
5376
|
}
|
|
5314
5377
|
});
|
|
5315
5378
|
|
|
5316
5379
|
// src/server/entities/role-permissions.ts
|
|
5317
|
-
import { index as
|
|
5318
|
-
import { id as
|
|
5380
|
+
import { index as index11, unique } from "drizzle-orm/pg-core";
|
|
5381
|
+
import { id as id11, timestamps as timestamps10, foreignKey as foreignKey6 } from "@spfn/core/db";
|
|
5319
5382
|
var rolePermissions;
|
|
5320
5383
|
var init_role_permissions = __esm({
|
|
5321
5384
|
"src/server/entities/role-permissions.ts"() {
|
|
@@ -5327,7 +5390,7 @@ var init_role_permissions = __esm({
|
|
|
5327
5390
|
"role_permissions",
|
|
5328
5391
|
{
|
|
5329
5392
|
// Primary key
|
|
5330
|
-
id:
|
|
5393
|
+
id: id11(),
|
|
5331
5394
|
// Role reference
|
|
5332
5395
|
// Foreign key to roles table
|
|
5333
5396
|
// Cascade delete: when role is deleted, all role-permission mappings are removed
|
|
@@ -5340,12 +5403,12 @@ var init_role_permissions = __esm({
|
|
|
5340
5403
|
// Used for: granting permissions to roles
|
|
5341
5404
|
// Example: user:delete permission → [Admin, Superadmin]
|
|
5342
5405
|
permissionId: foreignKey6("permission", () => permissions.id, { onDelete: "cascade" }),
|
|
5343
|
-
...
|
|
5406
|
+
...timestamps10()
|
|
5344
5407
|
},
|
|
5345
5408
|
(table) => [
|
|
5346
5409
|
// Indexes for query performance
|
|
5347
|
-
|
|
5348
|
-
|
|
5410
|
+
index11("role_permissions_role_id_idx").on(table.roleId),
|
|
5411
|
+
index11("role_permissions_permission_id_idx").on(table.permissionId),
|
|
5349
5412
|
// Unique constraint: one role-permission pair only
|
|
5350
5413
|
unique("role_permissions_unique").on(table.roleId, table.permissionId)
|
|
5351
5414
|
]
|
|
@@ -5354,8 +5417,8 @@ var init_role_permissions = __esm({
|
|
|
5354
5417
|
});
|
|
5355
5418
|
|
|
5356
5419
|
// src/server/entities/user-permissions.ts
|
|
5357
|
-
import { boolean as boolean5, text as
|
|
5358
|
-
import { id as
|
|
5420
|
+
import { boolean as boolean5, text as text11, index as index12, unique as unique2 } from "drizzle-orm/pg-core";
|
|
5421
|
+
import { id as id12, timestamps as timestamps11, utcTimestamp as utcTimestamp8, foreignKey as foreignKey7 } from "@spfn/core/db";
|
|
5359
5422
|
var userPermissions;
|
|
5360
5423
|
var init_user_permissions = __esm({
|
|
5361
5424
|
"src/server/entities/user-permissions.ts"() {
|
|
@@ -5367,7 +5430,7 @@ var init_user_permissions = __esm({
|
|
|
5367
5430
|
"user_permissions",
|
|
5368
5431
|
{
|
|
5369
5432
|
// Primary key
|
|
5370
|
-
id:
|
|
5433
|
+
id: id12(),
|
|
5371
5434
|
// User reference
|
|
5372
5435
|
// Foreign key to users table
|
|
5373
5436
|
// Cascade delete: when user is deleted, all overrides are removed
|
|
@@ -5390,19 +5453,19 @@ var init_user_permissions = __esm({
|
|
|
5390
5453
|
// Reason for grant/revocation
|
|
5391
5454
|
// Used for: audit trail, compliance documentation
|
|
5392
5455
|
// Example: "Temporary access for project X", "Security incident - restricted"
|
|
5393
|
-
reason:
|
|
5456
|
+
reason: text11("reason"),
|
|
5394
5457
|
// Expiration timestamp (optional)
|
|
5395
5458
|
// null: Permanent override (remains until manually removed)
|
|
5396
5459
|
// timestamp: Permission expires at this time (auto-revoked by background job)
|
|
5397
5460
|
// Use case: Time-limited elevated access, temporary restrictions
|
|
5398
|
-
expiresAt:
|
|
5399
|
-
...
|
|
5461
|
+
expiresAt: utcTimestamp8("expires_at"),
|
|
5462
|
+
...timestamps11()
|
|
5400
5463
|
},
|
|
5401
5464
|
(table) => [
|
|
5402
5465
|
// Indexes for query performance
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5466
|
+
index12("user_permissions_user_id_idx").on(table.userId),
|
|
5467
|
+
index12("user_permissions_permission_id_idx").on(table.permissionId),
|
|
5468
|
+
index12("user_permissions_expires_at_idx").on(table.expiresAt),
|
|
5406
5469
|
// Unique constraint: one user-permission pair only
|
|
5407
5470
|
unique2("user_permissions_unique").on(table.userId, table.permissionId)
|
|
5408
5471
|
]
|
|
@@ -5412,7 +5475,7 @@ var init_user_permissions = __esm({
|
|
|
5412
5475
|
|
|
5413
5476
|
// src/server/entities/auth-metadata.ts
|
|
5414
5477
|
import { sql as sql3 } from "drizzle-orm";
|
|
5415
|
-
import { text as
|
|
5478
|
+
import { text as text12, timestamp } from "drizzle-orm/pg-core";
|
|
5416
5479
|
var authMetadata;
|
|
5417
5480
|
var init_auth_metadata = __esm({
|
|
5418
5481
|
"src/server/entities/auth-metadata.ts"() {
|
|
@@ -5422,9 +5485,9 @@ var init_auth_metadata = __esm({
|
|
|
5422
5485
|
"auth_metadata",
|
|
5423
5486
|
{
|
|
5424
5487
|
// Metadata key (primary key)
|
|
5425
|
-
key:
|
|
5488
|
+
key: text12("key").primaryKey(),
|
|
5426
5489
|
// Metadata value
|
|
5427
|
-
value:
|
|
5490
|
+
value: text12("value").notNull(),
|
|
5428
5491
|
// Last updated timestamp — stamped by the database on insert and on update
|
|
5429
5492
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
|
|
5430
5493
|
}
|
|
@@ -5433,8 +5496,8 @@ var init_auth_metadata = __esm({
|
|
|
5433
5496
|
});
|
|
5434
5497
|
|
|
5435
5498
|
// src/server/entities/ops-tokens.ts
|
|
5436
|
-
import { text as
|
|
5437
|
-
import { id as
|
|
5499
|
+
import { text as text13 } from "drizzle-orm/pg-core";
|
|
5500
|
+
import { id as id13, timestamps as timestamps12, utcTimestamp as utcTimestamp9 } from "@spfn/core/db";
|
|
5438
5501
|
var opsTokens;
|
|
5439
5502
|
var init_ops_tokens = __esm({
|
|
5440
5503
|
"src/server/entities/ops-tokens.ts"() {
|
|
@@ -5443,22 +5506,22 @@ var init_ops_tokens = __esm({
|
|
|
5443
5506
|
opsTokens = authSchema.table(
|
|
5444
5507
|
"ops_tokens",
|
|
5445
5508
|
{
|
|
5446
|
-
id:
|
|
5509
|
+
id: id13(),
|
|
5447
5510
|
// Operator-facing label ("ci-deploy", "rayim-laptop")
|
|
5448
|
-
name:
|
|
5511
|
+
name: text13("name").notNull(),
|
|
5449
5512
|
// SHA-256 hex of the token secret. Lookup key — the secret never lands
|
|
5450
5513
|
// here, and the unique constraint doubles as the lookup index.
|
|
5451
|
-
tokenHash:
|
|
5514
|
+
tokenHash: text13("token_hash").notNull().unique(),
|
|
5452
5515
|
// Granted scopes as permission strings ('waitlist:read', ...).
|
|
5453
5516
|
// '*' grants every scope.
|
|
5454
|
-
scopes:
|
|
5517
|
+
scopes: text13("scopes").array().notNull(),
|
|
5455
5518
|
// null = the token does not expire
|
|
5456
|
-
expiresAt:
|
|
5519
|
+
expiresAt: utcTimestamp9("expires_at"),
|
|
5457
5520
|
// null = active; a timestamp revokes the token permanently
|
|
5458
|
-
revokedAt:
|
|
5521
|
+
revokedAt: utcTimestamp9("revoked_at"),
|
|
5459
5522
|
// Last successful verification, updated fire-and-forget
|
|
5460
|
-
lastUsedAt:
|
|
5461
|
-
...
|
|
5523
|
+
lastUsedAt: utcTimestamp9("last_used_at"),
|
|
5524
|
+
...timestamps12()
|
|
5462
5525
|
}
|
|
5463
5526
|
);
|
|
5464
5527
|
}
|
|
@@ -5474,6 +5537,7 @@ var init_entities = __esm({
|
|
|
5474
5537
|
init_user_public_keys();
|
|
5475
5538
|
init_user_social_accounts();
|
|
5476
5539
|
init_verification_codes();
|
|
5540
|
+
init_signup_link_tokens();
|
|
5477
5541
|
init_user_invitations();
|
|
5478
5542
|
init_account_deletion_requests();
|
|
5479
5543
|
init_roles();
|
|
@@ -5486,7 +5550,7 @@ var init_entities = __esm({
|
|
|
5486
5550
|
});
|
|
5487
5551
|
|
|
5488
5552
|
// src/server/repositories/users.repository.ts
|
|
5489
|
-
import { eq, and } from "drizzle-orm";
|
|
5553
|
+
import { eq, and, sql as sql4 } from "drizzle-orm";
|
|
5490
5554
|
import { BaseRepository } from "@spfn/core/db";
|
|
5491
5555
|
import { EntityNotFoundError, NotFoundError } from "@spfn/core/errors";
|
|
5492
5556
|
var UsersRepository, usersRepository;
|
|
@@ -5494,13 +5558,14 @@ var init_users_repository = __esm({
|
|
|
5494
5558
|
"src/server/repositories/users.repository.ts"() {
|
|
5495
5559
|
"use strict";
|
|
5496
5560
|
init_entities();
|
|
5561
|
+
init_email();
|
|
5497
5562
|
UsersRepository = class extends BaseRepository {
|
|
5498
5563
|
/**
|
|
5499
5564
|
* ID로 사용자 조회
|
|
5500
5565
|
* Read replica 사용
|
|
5501
5566
|
*/
|
|
5502
|
-
async findById(
|
|
5503
|
-
const result = await this.readDb.select().from(users).where(eq(users.id,
|
|
5567
|
+
async findById(id14) {
|
|
5568
|
+
const result = await this.readDb.select().from(users).where(eq(users.id, id14)).limit(1);
|
|
5504
5569
|
return result[0] ?? null;
|
|
5505
5570
|
}
|
|
5506
5571
|
/**
|
|
@@ -5510,8 +5575,8 @@ var init_users_repository = __esm({
|
|
|
5510
5575
|
* 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
|
|
5511
5576
|
* 계속 사용할 것.
|
|
5512
5577
|
*/
|
|
5513
|
-
async findByIdOnPrimary(
|
|
5514
|
-
const result = await this.db.select().from(users).where(eq(users.id,
|
|
5578
|
+
async findByIdOnPrimary(id14) {
|
|
5579
|
+
const result = await this.db.select().from(users).where(eq(users.id, id14)).limit(1);
|
|
5515
5580
|
return result[0] ?? null;
|
|
5516
5581
|
}
|
|
5517
5582
|
/**
|
|
@@ -5519,7 +5584,25 @@ var init_users_repository = __esm({
|
|
|
5519
5584
|
* Read replica 사용
|
|
5520
5585
|
*/
|
|
5521
5586
|
async findByEmail(email) {
|
|
5522
|
-
const result = await this.readDb.select().from(users).where(eq(users.email, email)).limit(1);
|
|
5587
|
+
const result = await this.readDb.select().from(users).where(eq(users.email, normalizeEmail(email))).limit(1);
|
|
5588
|
+
return result[0] ?? null;
|
|
5589
|
+
}
|
|
5590
|
+
/**
|
|
5591
|
+
* 이메일로 사용자 조회 — 저장된 형태와 무관하게 찾는다.
|
|
5592
|
+
*
|
|
5593
|
+
* `findByEmail` asks whether a row holds this exact address; this asks
|
|
5594
|
+
* whether any row *is* this address, whatever form it was written in. The
|
|
5595
|
+
* difference matters to a caller that answers "no" by creating an account:
|
|
5596
|
+
* a lookup that misses a row stored in another form would make a second
|
|
5597
|
+
* account for a person who already has one, and the unique constraint
|
|
5598
|
+
* cannot object because the two stored strings differ.
|
|
5599
|
+
*
|
|
5600
|
+
* Folding in the predicate means no index on `email` applies, so this is for
|
|
5601
|
+
* the few addresses a caller decides about — admin seeding — not for a
|
|
5602
|
+
* request path. Write primary: the answer decides whether to insert.
|
|
5603
|
+
*/
|
|
5604
|
+
async findByEmailInAnyStoredForm(email) {
|
|
5605
|
+
const result = await this.db.select().from(users).where(sql4`lower(btrim(${users.email})) = ${normalizeEmail(email)}`).limit(1);
|
|
5523
5606
|
return result[0] ?? null;
|
|
5524
5607
|
}
|
|
5525
5608
|
/**
|
|
@@ -5564,13 +5647,13 @@ var init_users_repository = __esm({
|
|
|
5564
5647
|
*
|
|
5565
5648
|
* roleId가 null인 유저는 role: null 반환
|
|
5566
5649
|
*/
|
|
5567
|
-
async findByIdWithRole(
|
|
5650
|
+
async findByIdWithRole(id14) {
|
|
5568
5651
|
const result = await this.readDb.select({
|
|
5569
5652
|
user: users,
|
|
5570
5653
|
roleName: roles.name,
|
|
5571
5654
|
roleDisplayName: roles.displayName,
|
|
5572
5655
|
rolePriority: roles.priority
|
|
5573
|
-
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id,
|
|
5656
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id14)).limit(1);
|
|
5574
5657
|
const row = result[0];
|
|
5575
5658
|
if (!row) {
|
|
5576
5659
|
return null;
|
|
@@ -5585,14 +5668,69 @@ var init_users_repository = __esm({
|
|
|
5585
5668
|
* Write primary 사용
|
|
5586
5669
|
*/
|
|
5587
5670
|
async create(data) {
|
|
5588
|
-
return await this._create(users, data);
|
|
5671
|
+
return await this._create(users, { ...data, email: normalizeOptionalEmail(data.email) });
|
|
5672
|
+
}
|
|
5673
|
+
/**
|
|
5674
|
+
* User ids grouped by an address two or more rows share once folded.
|
|
5675
|
+
*
|
|
5676
|
+
* The whole comparison happens in the database and only the colliding groups
|
|
5677
|
+
* come back, so the size of the answer is the size of the problem rather
|
|
5678
|
+
* than the size of the table. `users.email` is unique, so a group of more
|
|
5679
|
+
* than one can only be rows that differ by capitalization or padding —
|
|
5680
|
+
* exactly the ones a rewrite cannot decide between.
|
|
5681
|
+
*
|
|
5682
|
+
* Every member id is returned, including a row already holding the
|
|
5683
|
+
* canonical form, because the operator has to compare the accounts against
|
|
5684
|
+
* each other to settle which is real.
|
|
5685
|
+
*
|
|
5686
|
+
* Write primary: the caller is about to rewrite rows and a replica could
|
|
5687
|
+
* still be showing the pre-fix state.
|
|
5688
|
+
*/
|
|
5689
|
+
async findEmailConflictGroups() {
|
|
5690
|
+
const rows = await this.db.select({ ids: sql4`array_agg(${users.id} ORDER BY ${users.id})` }).from(users).where(sql4`${users.email} IS NOT NULL`).groupBy(sql4`lower(btrim(${users.email}))`).having(sql4`count(*) > 1`);
|
|
5691
|
+
return rows.map((row) => row.ids.map(Number));
|
|
5692
|
+
}
|
|
5693
|
+
/**
|
|
5694
|
+
* Fold every stored address to canonical form, leaving the given ids alone.
|
|
5695
|
+
*
|
|
5696
|
+
* One statement rather than a row at a time: the rewrite is the same
|
|
5697
|
+
* expression the detection uses, so the database can do it in place. A
|
|
5698
|
+
* legacy install with a large users table therefore pays one update instead
|
|
5699
|
+
* of a round trip per row on the boot path, and no list of addresses is ever
|
|
5700
|
+
* carried through the application.
|
|
5701
|
+
*
|
|
5702
|
+
* The excluded ids travel as a single array parameter, so the count of
|
|
5703
|
+
* conflicts cannot run into the protocol's limit on bind parameters.
|
|
5704
|
+
*
|
|
5705
|
+
* One statement also means all or nothing. `users.email` is unique, so if an
|
|
5706
|
+
* instance still running the old code registers a canonical address in the
|
|
5707
|
+
* moment between the conflict query and this update, the update aborts and
|
|
5708
|
+
* nothing is folded on this boot. The next boot sees that pair as a conflict
|
|
5709
|
+
* and folds everything else, so the repair is deferred rather than lost.
|
|
5710
|
+
*
|
|
5711
|
+
* `lower(btrim(...))` is the SQL spelling of `normalizeEmail`. The two agree
|
|
5712
|
+
* on every address this package's validation accepts (ASCII, no interior
|
|
5713
|
+
* whitespace); an address outside that set — reachable only by an app
|
|
5714
|
+
* writing to the repository directly — may fold differently in a database
|
|
5715
|
+
* whose collation lower-cases non-ASCII letters.
|
|
5716
|
+
*
|
|
5717
|
+
* @param excludedIds - Rows to leave untouched, normally the conflict groups
|
|
5718
|
+
* @returns How many rows were rewritten
|
|
5719
|
+
*/
|
|
5720
|
+
async normalizeEmailsExcept(excludedIds) {
|
|
5721
|
+
const keepConflicts = excludedIds.length > 0 ? sql4` AND NOT (${users.id} = ANY(string_to_array(${excludedIds.join(",")}, ',')::bigint[]))` : sql4``;
|
|
5722
|
+
const pending = sql4`${users.email} IS NOT NULL AND ${users.email} <> lower(btrim(${users.email}))${keepConflicts}`;
|
|
5723
|
+
const [counted] = await this.db.select({ rows: sql4`count(*)` }).from(users).where(pending);
|
|
5724
|
+
await this.db.update(users).set({ email: sql4`lower(btrim(${users.email}))` }).where(pending);
|
|
5725
|
+
return Number(counted?.rows ?? 0);
|
|
5589
5726
|
}
|
|
5590
5727
|
/**
|
|
5591
5728
|
* 사용자 정보 업데이트
|
|
5592
5729
|
* Write primary 사용
|
|
5593
5730
|
*/
|
|
5594
|
-
async updateById(
|
|
5595
|
-
const
|
|
5731
|
+
async updateById(id14, data) {
|
|
5732
|
+
const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
|
|
5733
|
+
const result = await this.db.update(users).set(patch).where(eq(users.id, id14)).returning();
|
|
5596
5734
|
return result[0] ?? null;
|
|
5597
5735
|
}
|
|
5598
5736
|
/**
|
|
@@ -5604,10 +5742,10 @@ var init_users_repository = __esm({
|
|
|
5604
5742
|
* status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
|
|
5605
5743
|
* Write primary 사용
|
|
5606
5744
|
*/
|
|
5607
|
-
async reactivateFromPendingDeletion(
|
|
5745
|
+
async reactivateFromPendingDeletion(id14) {
|
|
5608
5746
|
const result = await this.db.update(users).set({ status: "active" }).where(
|
|
5609
5747
|
and(
|
|
5610
|
-
eq(users.id,
|
|
5748
|
+
eq(users.id, id14),
|
|
5611
5749
|
eq(users.status, "pending_deletion")
|
|
5612
5750
|
)
|
|
5613
5751
|
).returning();
|
|
@@ -5617,32 +5755,32 @@ var init_users_repository = __esm({
|
|
|
5617
5755
|
* 비밀번호 업데이트
|
|
5618
5756
|
* Write primary 사용
|
|
5619
5757
|
*/
|
|
5620
|
-
async updatePassword(
|
|
5758
|
+
async updatePassword(id14, passwordHash, clearPasswordChangeRequired = true) {
|
|
5621
5759
|
const updateData = {
|
|
5622
5760
|
passwordHash
|
|
5623
5761
|
};
|
|
5624
5762
|
if (clearPasswordChangeRequired) {
|
|
5625
5763
|
updateData.passwordChangeRequired = false;
|
|
5626
5764
|
}
|
|
5627
|
-
const result = await this.db.update(users).set(updateData).where(eq(users.id,
|
|
5765
|
+
const result = await this.db.update(users).set(updateData).where(eq(users.id, id14)).returning();
|
|
5628
5766
|
return result[0] ?? null;
|
|
5629
5767
|
}
|
|
5630
5768
|
/**
|
|
5631
5769
|
* 마지막 로그인 시간 업데이트
|
|
5632
5770
|
* Write primary 사용
|
|
5633
5771
|
*/
|
|
5634
|
-
async updateLastLogin(
|
|
5772
|
+
async updateLastLogin(id14) {
|
|
5635
5773
|
const result = await this.db.update(users).set({
|
|
5636
5774
|
lastLoginAt: /* @__PURE__ */ new Date()
|
|
5637
|
-
}).where(eq(users.id,
|
|
5775
|
+
}).where(eq(users.id, id14)).returning();
|
|
5638
5776
|
return result[0] ?? null;
|
|
5639
5777
|
}
|
|
5640
5778
|
/**
|
|
5641
5779
|
* 사용자 삭제
|
|
5642
5780
|
* Write primary 사용
|
|
5643
5781
|
*/
|
|
5644
|
-
async deleteById(
|
|
5645
|
-
const result = await this.db.delete(users).where(eq(users.id,
|
|
5782
|
+
async deleteById(id14) {
|
|
5783
|
+
const result = await this.db.delete(users).where(eq(users.id, id14)).returning();
|
|
5646
5784
|
return result[0] ?? null;
|
|
5647
5785
|
}
|
|
5648
5786
|
/**
|
|
@@ -5759,7 +5897,7 @@ var init_users_repository = __esm({
|
|
|
5759
5897
|
|
|
5760
5898
|
// src/server/repositories/keys.repository.ts
|
|
5761
5899
|
import { BaseRepository as BaseRepository2 } from "@spfn/core/db";
|
|
5762
|
-
import { eq as eq2, and as and2, or, isNull, lt, ne, desc, sql as
|
|
5900
|
+
import { eq as eq2, and as and2, or, isNull, lt, ne, desc, sql as sql5 } from "drizzle-orm";
|
|
5763
5901
|
var LAST_USED_THROTTLE_MS, KeysRepository, keysRepository;
|
|
5764
5902
|
var init_keys_repository = __esm({
|
|
5765
5903
|
"src/server/repositories/keys.repository.ts"() {
|
|
@@ -5990,31 +6128,31 @@ var init_keys_repository = __esm({
|
|
|
5990
6128
|
* stored, so it answers "since when has this device been on this release"
|
|
5991
6129
|
* rather than "when was it last seen", which lastUsedAt already answers.
|
|
5992
6130
|
*/
|
|
5993
|
-
async updateLastUsedById(
|
|
6131
|
+
async updateLastUsedById(id14, identity) {
|
|
5994
6132
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
5995
6133
|
const lastUsedIsStale = or(
|
|
5996
6134
|
isNull(userPublicKeys.lastUsedAt),
|
|
5997
6135
|
lt(userPublicKeys.lastUsedAt, staleBefore)
|
|
5998
6136
|
);
|
|
5999
6137
|
if (!identity) {
|
|
6000
|
-
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id,
|
|
6138
|
+
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id14), lastUsedIsStale));
|
|
6001
6139
|
return;
|
|
6002
6140
|
}
|
|
6003
|
-
const identityChanged =
|
|
6141
|
+
const identityChanged = sql5`(
|
|
6004
6142
|
${userPublicKeys.clientKind} IS DISTINCT FROM ${identity.kind}
|
|
6005
6143
|
OR ${userPublicKeys.clientVersion} IS DISTINCT FROM ${identity.version}
|
|
6006
6144
|
OR ${userPublicKeys.clientContractVersion} IS DISTINCT FROM ${identity.contractVersion}
|
|
6007
6145
|
)`;
|
|
6008
6146
|
const now = /* @__PURE__ */ new Date();
|
|
6009
|
-
const nowParam =
|
|
6147
|
+
const nowParam = sql5`${now.toISOString()}::timestamptz`;
|
|
6010
6148
|
await this.db.update(userPublicKeys).set({
|
|
6011
6149
|
lastUsedAt: now,
|
|
6012
6150
|
clientKind: identity.kind,
|
|
6013
6151
|
clientVersion: identity.version,
|
|
6014
6152
|
clientContractVersion: identity.contractVersion,
|
|
6015
|
-
clientSeenAt:
|
|
6153
|
+
clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
6016
6154
|
}).where(and2(
|
|
6017
|
-
eq2(userPublicKeys.id,
|
|
6155
|
+
eq2(userPublicKeys.id, id14),
|
|
6018
6156
|
or(lastUsedIsStale, identityChanged)
|
|
6019
6157
|
));
|
|
6020
6158
|
}
|
|
@@ -6053,8 +6191,8 @@ var init_verification_codes_repository = __esm({
|
|
|
6053
6191
|
* ID로 인증 코드 조회
|
|
6054
6192
|
* Read replica 사용
|
|
6055
6193
|
*/
|
|
6056
|
-
async findById(
|
|
6057
|
-
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id,
|
|
6194
|
+
async findById(id14) {
|
|
6195
|
+
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id14)).limit(1);
|
|
6058
6196
|
return result[0] ?? null;
|
|
6059
6197
|
}
|
|
6060
6198
|
/**
|
|
@@ -6068,22 +6206,22 @@ var init_verification_codes_repository = __esm({
|
|
|
6068
6206
|
* 인증 코드 사용 처리
|
|
6069
6207
|
* Write primary 사용
|
|
6070
6208
|
*/
|
|
6071
|
-
async markAsUsed(
|
|
6209
|
+
async markAsUsed(id14) {
|
|
6072
6210
|
const result = await this.db.update(verificationCodes).set({
|
|
6073
6211
|
usedAt: /* @__PURE__ */ new Date()
|
|
6074
|
-
}).where(eq3(verificationCodes.id,
|
|
6212
|
+
}).where(eq3(verificationCodes.id, id14)).returning();
|
|
6075
6213
|
return result[0] ?? null;
|
|
6076
6214
|
}
|
|
6077
6215
|
/**
|
|
6078
6216
|
* 시도 횟수 증가
|
|
6079
6217
|
* Write primary 사용
|
|
6080
6218
|
*/
|
|
6081
|
-
async incrementAttempts(
|
|
6082
|
-
const code = await this.findById(
|
|
6219
|
+
async incrementAttempts(id14) {
|
|
6220
|
+
const code = await this.findById(id14);
|
|
6083
6221
|
if (!code) return null;
|
|
6084
6222
|
const result = await this.db.update(verificationCodes).set({
|
|
6085
6223
|
attempts: code.attempts + 1
|
|
6086
|
-
}).where(eq3(verificationCodes.id,
|
|
6224
|
+
}).where(eq3(verificationCodes.id, id14)).returning();
|
|
6087
6225
|
return result[0] ?? null;
|
|
6088
6226
|
}
|
|
6089
6227
|
/**
|
|
@@ -6127,27 +6265,136 @@ var init_verification_codes_repository = __esm({
|
|
|
6127
6265
|
}
|
|
6128
6266
|
});
|
|
6129
6267
|
|
|
6130
|
-
// src/server/repositories/
|
|
6268
|
+
// src/server/repositories/signup-link-tokens.repository.ts
|
|
6131
6269
|
import { BaseRepository as BaseRepository4 } from "@spfn/core/db";
|
|
6132
|
-
import { eq as eq4,
|
|
6270
|
+
import { eq as eq4, and as and4, isNull as isNull3 } from "drizzle-orm";
|
|
6271
|
+
var SignupLinkTokensRepository, signupLinkTokensRepository;
|
|
6272
|
+
var init_signup_link_tokens_repository = __esm({
|
|
6273
|
+
"src/server/repositories/signup-link-tokens.repository.ts"() {
|
|
6274
|
+
"use strict";
|
|
6275
|
+
init_signup_link_tokens();
|
|
6276
|
+
SignupLinkTokensRepository = class extends BaseRepository4 {
|
|
6277
|
+
/**
|
|
6278
|
+
* Create a signup link row.
|
|
6279
|
+
* Write primary.
|
|
6280
|
+
*/
|
|
6281
|
+
async create(data) {
|
|
6282
|
+
const result = await this.db.insert(signupLinkTokens).values(data).returning();
|
|
6283
|
+
return result[0];
|
|
6284
|
+
}
|
|
6285
|
+
/**
|
|
6286
|
+
* Find a row by the hash of an emailed token, in any state.
|
|
6287
|
+
*
|
|
6288
|
+
* Deliberately unfiltered: the caller decides why a link is refused, and a
|
|
6289
|
+
* row filtered out here would be indistinguishable from an unknown token in
|
|
6290
|
+
* the logs.
|
|
6291
|
+
*
|
|
6292
|
+
* Read replica.
|
|
6293
|
+
*/
|
|
6294
|
+
async findByTokenHash(tokenHash) {
|
|
6295
|
+
const result = await this.readDb.select().from(signupLinkTokens).where(eq4(signupLinkTokens.tokenHash, tokenHash)).limit(1);
|
|
6296
|
+
return result[0] ?? null;
|
|
6297
|
+
}
|
|
6298
|
+
/**
|
|
6299
|
+
* Find a row by the hash of a setup session secret, in any state.
|
|
6300
|
+
* Read replica.
|
|
6301
|
+
*/
|
|
6302
|
+
async findBySetupSecretHash(setupSecretHash) {
|
|
6303
|
+
const result = await this.readDb.select().from(signupLinkTokens).where(eq4(signupLinkTokens.setupSecretHash, setupSecretHash)).limit(1);
|
|
6304
|
+
return result[0] ?? null;
|
|
6305
|
+
}
|
|
6306
|
+
/**
|
|
6307
|
+
* Consume a link and open a setup session on it, but only if it is still
|
|
6308
|
+
* unconsumed and not superseded.
|
|
6309
|
+
*
|
|
6310
|
+
* @returns the updated row, or null if another request claimed it first
|
|
6311
|
+
*/
|
|
6312
|
+
async claimLink(id14, setupSecretHash, setupExpiresAt) {
|
|
6313
|
+
const result = await this.db.update(signupLinkTokens).set({
|
|
6314
|
+
consumedAt: /* @__PURE__ */ new Date(),
|
|
6315
|
+
setupSecretHash,
|
|
6316
|
+
setupExpiresAt
|
|
6317
|
+
}).where(
|
|
6318
|
+
and4(
|
|
6319
|
+
eq4(signupLinkTokens.id, id14),
|
|
6320
|
+
isNull3(signupLinkTokens.consumedAt),
|
|
6321
|
+
isNull3(signupLinkTokens.supersededAt)
|
|
6322
|
+
)
|
|
6323
|
+
).returning();
|
|
6324
|
+
return result[0] ?? null;
|
|
6325
|
+
}
|
|
6326
|
+
/**
|
|
6327
|
+
* Mark a setup session as completed, but only if it has not completed
|
|
6328
|
+
* already.
|
|
6329
|
+
*
|
|
6330
|
+
* @returns the updated row, or null if another request completed it first
|
|
6331
|
+
*/
|
|
6332
|
+
async claimSetupSession(id14) {
|
|
6333
|
+
const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
6334
|
+
and4(
|
|
6335
|
+
eq4(signupLinkTokens.id, id14),
|
|
6336
|
+
isNull3(signupLinkTokens.completedAt),
|
|
6337
|
+
isNull3(signupLinkTokens.supersededAt)
|
|
6338
|
+
)
|
|
6339
|
+
).returning();
|
|
6340
|
+
return result[0] ?? null;
|
|
6341
|
+
}
|
|
6342
|
+
/**
|
|
6343
|
+
* Supersede every live link for an address, so requesting a new one
|
|
6344
|
+
* invalidates the previous one and any setup session opened from it.
|
|
6345
|
+
*
|
|
6346
|
+
* Write primary.
|
|
6347
|
+
*
|
|
6348
|
+
* @returns number of rows superseded
|
|
6349
|
+
*/
|
|
6350
|
+
async supersedeLiveForEmail(email) {
|
|
6351
|
+
const result = await this.db.update(signupLinkTokens).set({ supersededAt: /* @__PURE__ */ new Date() }).where(
|
|
6352
|
+
and4(
|
|
6353
|
+
eq4(signupLinkTokens.email, email),
|
|
6354
|
+
isNull3(signupLinkTokens.supersededAt),
|
|
6355
|
+
isNull3(signupLinkTokens.completedAt)
|
|
6356
|
+
)
|
|
6357
|
+
).returning();
|
|
6358
|
+
return result.length;
|
|
6359
|
+
}
|
|
6360
|
+
/**
|
|
6361
|
+
* Delete every signup link row for an address (account destruction).
|
|
6362
|
+
*
|
|
6363
|
+
* Rows key on the email text and carry no user FK, so destruction has to
|
|
6364
|
+
* clean them up by address the way verification codes are cleaned up.
|
|
6365
|
+
*
|
|
6366
|
+
* Write primary.
|
|
6367
|
+
*/
|
|
6368
|
+
async deleteByEmail(email) {
|
|
6369
|
+
const result = await this.db.delete(signupLinkTokens).where(eq4(signupLinkTokens.email, email)).returning();
|
|
6370
|
+
return result.length;
|
|
6371
|
+
}
|
|
6372
|
+
};
|
|
6373
|
+
signupLinkTokensRepository = new SignupLinkTokensRepository();
|
|
6374
|
+
}
|
|
6375
|
+
});
|
|
6376
|
+
|
|
6377
|
+
// src/server/repositories/roles.repository.ts
|
|
6378
|
+
import { BaseRepository as BaseRepository5 } from "@spfn/core/db";
|
|
6379
|
+
import { eq as eq5, asc } from "drizzle-orm";
|
|
6133
6380
|
var RolesRepository, rolesRepository;
|
|
6134
6381
|
var init_roles_repository = __esm({
|
|
6135
6382
|
"src/server/repositories/roles.repository.ts"() {
|
|
6136
6383
|
"use strict";
|
|
6137
6384
|
init_roles();
|
|
6138
|
-
RolesRepository = class extends
|
|
6385
|
+
RolesRepository = class extends BaseRepository5 {
|
|
6139
6386
|
/**
|
|
6140
6387
|
* ID로 역할 조회
|
|
6141
6388
|
*/
|
|
6142
|
-
async findById(
|
|
6143
|
-
const result = await this.readDb.select().from(roles).where(
|
|
6389
|
+
async findById(id14) {
|
|
6390
|
+
const result = await this.readDb.select().from(roles).where(eq5(roles.id, id14)).limit(1);
|
|
6144
6391
|
return result[0] ?? null;
|
|
6145
6392
|
}
|
|
6146
6393
|
/**
|
|
6147
6394
|
* Name으로 역할 조회
|
|
6148
6395
|
*/
|
|
6149
6396
|
async findByName(name) {
|
|
6150
|
-
const result = await this.readDb.select().from(roles).where(
|
|
6397
|
+
const result = await this.readDb.select().from(roles).where(eq5(roles.name, name)).limit(1);
|
|
6151
6398
|
return result[0] ?? null;
|
|
6152
6399
|
}
|
|
6153
6400
|
/**
|
|
@@ -6160,7 +6407,7 @@ var init_roles_repository = __esm({
|
|
|
6160
6407
|
* 활성 역할만 조회
|
|
6161
6408
|
*/
|
|
6162
6409
|
async findActive() {
|
|
6163
|
-
return this.readDb.select().from(roles).where(
|
|
6410
|
+
return this.readDb.select().from(roles).where(eq5(roles.isActive, true)).orderBy(asc(roles.priority));
|
|
6164
6411
|
}
|
|
6165
6412
|
/**
|
|
6166
6413
|
* 역할 생성
|
|
@@ -6171,15 +6418,15 @@ var init_roles_repository = __esm({
|
|
|
6171
6418
|
/**
|
|
6172
6419
|
* 역할 업데이트
|
|
6173
6420
|
*/
|
|
6174
|
-
async updateById(
|
|
6175
|
-
const result = await this.db.update(roles).set(data).where(
|
|
6421
|
+
async updateById(id14, data) {
|
|
6422
|
+
const result = await this.db.update(roles).set(data).where(eq5(roles.id, id14)).returning();
|
|
6176
6423
|
return result[0] ?? null;
|
|
6177
6424
|
}
|
|
6178
6425
|
/**
|
|
6179
6426
|
* 역할 삭제
|
|
6180
6427
|
*/
|
|
6181
|
-
async deleteById(
|
|
6182
|
-
const result = await this.db.delete(roles).where(
|
|
6428
|
+
async deleteById(id14) {
|
|
6429
|
+
const result = await this.db.delete(roles).where(eq5(roles.id, id14)).returning();
|
|
6183
6430
|
return result[0] ?? null;
|
|
6184
6431
|
}
|
|
6185
6432
|
};
|
|
@@ -6188,26 +6435,26 @@ var init_roles_repository = __esm({
|
|
|
6188
6435
|
});
|
|
6189
6436
|
|
|
6190
6437
|
// src/server/repositories/permissions.repository.ts
|
|
6191
|
-
import { BaseRepository as
|
|
6192
|
-
import { asc as asc2, eq as
|
|
6438
|
+
import { BaseRepository as BaseRepository6 } from "@spfn/core/db";
|
|
6439
|
+
import { asc as asc2, eq as eq6, inArray } from "drizzle-orm";
|
|
6193
6440
|
var PermissionsRepository, permissionsRepository;
|
|
6194
6441
|
var init_permissions_repository = __esm({
|
|
6195
6442
|
"src/server/repositories/permissions.repository.ts"() {
|
|
6196
6443
|
"use strict";
|
|
6197
6444
|
init_permissions();
|
|
6198
|
-
PermissionsRepository = class extends
|
|
6445
|
+
PermissionsRepository = class extends BaseRepository6 {
|
|
6199
6446
|
/**
|
|
6200
6447
|
* ID로 권한 조회
|
|
6201
6448
|
*/
|
|
6202
|
-
async findById(
|
|
6203
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
6449
|
+
async findById(id14) {
|
|
6450
|
+
const result = await this.readDb.select().from(permissions).where(eq6(permissions.id, id14)).limit(1);
|
|
6204
6451
|
return result[0] ?? null;
|
|
6205
6452
|
}
|
|
6206
6453
|
/**
|
|
6207
6454
|
* Name으로 권한 조회
|
|
6208
6455
|
*/
|
|
6209
6456
|
async findByName(name) {
|
|
6210
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
6457
|
+
const result = await this.readDb.select().from(permissions).where(eq6(permissions.name, name)).limit(1);
|
|
6211
6458
|
return result[0] ?? null;
|
|
6212
6459
|
}
|
|
6213
6460
|
/**
|
|
@@ -6227,13 +6474,13 @@ var init_permissions_repository = __esm({
|
|
|
6227
6474
|
* 활성 권한만 조회
|
|
6228
6475
|
*/
|
|
6229
6476
|
async findActive() {
|
|
6230
|
-
return this.readDb.select().from(permissions).where(
|
|
6477
|
+
return this.readDb.select().from(permissions).where(eq6(permissions.isActive, true)).orderBy(asc2(permissions.name));
|
|
6231
6478
|
}
|
|
6232
6479
|
/**
|
|
6233
6480
|
* 카테고리별 권한 조회
|
|
6234
6481
|
*/
|
|
6235
6482
|
async findByCategory(category) {
|
|
6236
|
-
return this.readDb.select().from(permissions).where(
|
|
6483
|
+
return this.readDb.select().from(permissions).where(eq6(permissions.category, category)).orderBy(asc2(permissions.name));
|
|
6237
6484
|
}
|
|
6238
6485
|
/**
|
|
6239
6486
|
* 권한 생성
|
|
@@ -6251,15 +6498,15 @@ var init_permissions_repository = __esm({
|
|
|
6251
6498
|
/**
|
|
6252
6499
|
* 권한 업데이트
|
|
6253
6500
|
*/
|
|
6254
|
-
async updateById(
|
|
6255
|
-
const result = await this.db.update(permissions).set(data).where(
|
|
6501
|
+
async updateById(id14, data) {
|
|
6502
|
+
const result = await this.db.update(permissions).set(data).where(eq6(permissions.id, id14)).returning();
|
|
6256
6503
|
return result[0] ?? null;
|
|
6257
6504
|
}
|
|
6258
6505
|
/**
|
|
6259
6506
|
* 권한 삭제
|
|
6260
6507
|
*/
|
|
6261
|
-
async deleteById(
|
|
6262
|
-
const result = await this.db.delete(permissions).where(
|
|
6508
|
+
async deleteById(id14) {
|
|
6509
|
+
const result = await this.db.delete(permissions).where(eq6(permissions.id, id14)).returning();
|
|
6263
6510
|
return result[0] ?? null;
|
|
6264
6511
|
}
|
|
6265
6512
|
};
|
|
@@ -6268,25 +6515,25 @@ var init_permissions_repository = __esm({
|
|
|
6268
6515
|
});
|
|
6269
6516
|
|
|
6270
6517
|
// src/server/repositories/role-permissions.repository.ts
|
|
6271
|
-
import { BaseRepository as
|
|
6272
|
-
import { and as
|
|
6518
|
+
import { BaseRepository as BaseRepository7 } from "@spfn/core/db";
|
|
6519
|
+
import { and as and5, eq as eq7 } from "drizzle-orm";
|
|
6273
6520
|
var RolePermissionsRepository, rolePermissionsRepository;
|
|
6274
6521
|
var init_role_permissions_repository = __esm({
|
|
6275
6522
|
"src/server/repositories/role-permissions.repository.ts"() {
|
|
6276
6523
|
"use strict";
|
|
6277
6524
|
init_role_permissions();
|
|
6278
|
-
RolePermissionsRepository = class extends
|
|
6525
|
+
RolePermissionsRepository = class extends BaseRepository7 {
|
|
6279
6526
|
/**
|
|
6280
6527
|
* 역할 ID로 모든 권한 조회
|
|
6281
6528
|
*/
|
|
6282
6529
|
async findByRoleId(roleId) {
|
|
6283
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
6530
|
+
return this.readDb.select().from(rolePermissions).where(eq7(rolePermissions.roleId, roleId));
|
|
6284
6531
|
}
|
|
6285
6532
|
/**
|
|
6286
6533
|
* 권한 ID로 모든 역할 조회
|
|
6287
6534
|
*/
|
|
6288
6535
|
async findByPermissionId(permissionId) {
|
|
6289
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
6536
|
+
return this.readDb.select().from(rolePermissions).where(eq7(rolePermissions.permissionId, permissionId));
|
|
6290
6537
|
}
|
|
6291
6538
|
/**
|
|
6292
6539
|
* 역할-권한 매핑 생성
|
|
@@ -6306,9 +6553,9 @@ var init_role_permissions_repository = __esm({
|
|
|
6306
6553
|
*/
|
|
6307
6554
|
async deleteByRoleIdAndPermissionId(roleId, permissionId) {
|
|
6308
6555
|
const result = await this.db.delete(rolePermissions).where(
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6556
|
+
and5(
|
|
6557
|
+
eq7(rolePermissions.roleId, roleId),
|
|
6558
|
+
eq7(rolePermissions.permissionId, permissionId)
|
|
6312
6559
|
)
|
|
6313
6560
|
).returning();
|
|
6314
6561
|
return result[0] ?? null;
|
|
@@ -6317,7 +6564,7 @@ var init_role_permissions_repository = __esm({
|
|
|
6317
6564
|
* 역할의 모든 권한 매핑 삭제
|
|
6318
6565
|
*/
|
|
6319
6566
|
async deleteByRoleId(roleId) {
|
|
6320
|
-
const result = await this.db.delete(rolePermissions).where(
|
|
6567
|
+
const result = await this.db.delete(rolePermissions).where(eq7(rolePermissions.roleId, roleId)).returning();
|
|
6321
6568
|
return result.length;
|
|
6322
6569
|
}
|
|
6323
6570
|
/**
|
|
@@ -6338,19 +6585,19 @@ var init_role_permissions_repository = __esm({
|
|
|
6338
6585
|
});
|
|
6339
6586
|
|
|
6340
6587
|
// src/server/repositories/user-permissions.repository.ts
|
|
6341
|
-
import { BaseRepository as
|
|
6342
|
-
import { eq as
|
|
6588
|
+
import { BaseRepository as BaseRepository8 } from "@spfn/core/db";
|
|
6589
|
+
import { eq as eq8, and as and6, or as or2, isNull as isNull4, isNotNull, lt as lt3, gt as gt2 } from "drizzle-orm";
|
|
6343
6590
|
var UserPermissionsRepository, userPermissionsRepository;
|
|
6344
6591
|
var init_user_permissions_repository = __esm({
|
|
6345
6592
|
"src/server/repositories/user-permissions.repository.ts"() {
|
|
6346
6593
|
"use strict";
|
|
6347
6594
|
init_user_permissions();
|
|
6348
|
-
UserPermissionsRepository = class extends
|
|
6595
|
+
UserPermissionsRepository = class extends BaseRepository8 {
|
|
6349
6596
|
/**
|
|
6350
6597
|
* 사용자 ID로 모든 권한 오버라이드 조회
|
|
6351
6598
|
*/
|
|
6352
6599
|
async findByUserId(userId) {
|
|
6353
|
-
return this.readDb.select().from(userPermissions).where(
|
|
6600
|
+
return this.readDb.select().from(userPermissions).where(eq8(userPermissions.userId, userId));
|
|
6354
6601
|
}
|
|
6355
6602
|
/**
|
|
6356
6603
|
* 사용자 ID로 유효한 권한 오버라이드만 조회
|
|
@@ -6359,10 +6606,10 @@ var init_user_permissions_repository = __esm({
|
|
|
6359
6606
|
async findValidByUserId(userId) {
|
|
6360
6607
|
const now = /* @__PURE__ */ new Date();
|
|
6361
6608
|
return this.readDb.select().from(userPermissions).where(
|
|
6362
|
-
|
|
6363
|
-
|
|
6609
|
+
and6(
|
|
6610
|
+
eq8(userPermissions.userId, userId),
|
|
6364
6611
|
or2(
|
|
6365
|
-
|
|
6612
|
+
isNull4(userPermissions.expiresAt),
|
|
6366
6613
|
gt2(userPermissions.expiresAt, now)
|
|
6367
6614
|
)
|
|
6368
6615
|
)
|
|
@@ -6373,9 +6620,9 @@ var init_user_permissions_repository = __esm({
|
|
|
6373
6620
|
*/
|
|
6374
6621
|
async findByUserIdAndPermissionId(userId, permissionId) {
|
|
6375
6622
|
const result = await this.readDb.select().from(userPermissions).where(
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6623
|
+
and6(
|
|
6624
|
+
eq8(userPermissions.userId, userId),
|
|
6625
|
+
eq8(userPermissions.permissionId, permissionId)
|
|
6379
6626
|
)
|
|
6380
6627
|
).limit(1);
|
|
6381
6628
|
return result[0] ?? null;
|
|
@@ -6389,8 +6636,8 @@ var init_user_permissions_repository = __esm({
|
|
|
6389
6636
|
/**
|
|
6390
6637
|
* 사용자 권한 오버라이드 업데이트
|
|
6391
6638
|
*/
|
|
6392
|
-
async updateById(
|
|
6393
|
-
const result = await this.db.update(userPermissions).set(data).where(
|
|
6639
|
+
async updateById(id14, data) {
|
|
6640
|
+
const result = await this.db.update(userPermissions).set(data).where(eq8(userPermissions.id, id14)).returning();
|
|
6394
6641
|
return result[0] ?? null;
|
|
6395
6642
|
}
|
|
6396
6643
|
/**
|
|
@@ -6398,9 +6645,9 @@ var init_user_permissions_repository = __esm({
|
|
|
6398
6645
|
*/
|
|
6399
6646
|
async deleteByUserIdAndPermissionId(userId, permissionId) {
|
|
6400
6647
|
const result = await this.db.delete(userPermissions).where(
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6648
|
+
and6(
|
|
6649
|
+
eq8(userPermissions.userId, userId),
|
|
6650
|
+
eq8(userPermissions.permissionId, permissionId)
|
|
6404
6651
|
)
|
|
6405
6652
|
).returning();
|
|
6406
6653
|
return result[0] ?? null;
|
|
@@ -6409,7 +6656,7 @@ var init_user_permissions_repository = __esm({
|
|
|
6409
6656
|
* 사용자의 모든 권한 오버라이드 삭제
|
|
6410
6657
|
*/
|
|
6411
6658
|
async deleteByUserId(userId) {
|
|
6412
|
-
const result = await this.db.delete(userPermissions).where(
|
|
6659
|
+
const result = await this.db.delete(userPermissions).where(eq8(userPermissions.userId, userId)).returning();
|
|
6413
6660
|
return result.length;
|
|
6414
6661
|
}
|
|
6415
6662
|
/**
|
|
@@ -6418,7 +6665,7 @@ var init_user_permissions_repository = __esm({
|
|
|
6418
6665
|
async deleteExpired() {
|
|
6419
6666
|
const now = /* @__PURE__ */ new Date();
|
|
6420
6667
|
const result = await this.db.delete(userPermissions).where(
|
|
6421
|
-
|
|
6668
|
+
and6(
|
|
6422
6669
|
isNotNull(userPermissions.expiresAt),
|
|
6423
6670
|
lt3(userPermissions.expiresAt, now)
|
|
6424
6671
|
)
|
|
@@ -6431,33 +6678,33 @@ var init_user_permissions_repository = __esm({
|
|
|
6431
6678
|
});
|
|
6432
6679
|
|
|
6433
6680
|
// src/server/repositories/user-profiles.repository.ts
|
|
6434
|
-
import { BaseRepository as
|
|
6435
|
-
import { eq as
|
|
6681
|
+
import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
|
|
6682
|
+
import { eq as eq9 } from "drizzle-orm";
|
|
6436
6683
|
var UserProfilesRepository, userProfilesRepository;
|
|
6437
6684
|
var init_user_profiles_repository = __esm({
|
|
6438
6685
|
"src/server/repositories/user-profiles.repository.ts"() {
|
|
6439
6686
|
"use strict";
|
|
6440
6687
|
init_user_profiles();
|
|
6441
|
-
UserProfilesRepository = class extends
|
|
6688
|
+
UserProfilesRepository = class extends BaseRepository9 {
|
|
6442
6689
|
/**
|
|
6443
6690
|
* ID로 프로필 조회
|
|
6444
6691
|
*/
|
|
6445
|
-
async findById(
|
|
6446
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
6692
|
+
async findById(id14) {
|
|
6693
|
+
const result = await this.readDb.select().from(userProfiles).where(eq9(userProfiles.id, id14)).limit(1);
|
|
6447
6694
|
return result[0] ?? null;
|
|
6448
6695
|
}
|
|
6449
6696
|
/**
|
|
6450
6697
|
* User ID로 locale만 조회 (경량)
|
|
6451
6698
|
*/
|
|
6452
6699
|
async findLocaleByUserId(userId) {
|
|
6453
|
-
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(
|
|
6700
|
+
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1);
|
|
6454
6701
|
return result[0]?.locale || "en";
|
|
6455
6702
|
}
|
|
6456
6703
|
/**
|
|
6457
6704
|
* User ID로 프로필 조회
|
|
6458
6705
|
*/
|
|
6459
6706
|
async findByUserId(userId) {
|
|
6460
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
6707
|
+
const result = await this.readDb.select().from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1);
|
|
6461
6708
|
return result[0] ?? null;
|
|
6462
6709
|
}
|
|
6463
6710
|
/**
|
|
@@ -6469,29 +6716,29 @@ var init_user_profiles_repository = __esm({
|
|
|
6469
6716
|
/**
|
|
6470
6717
|
* 프로필 업데이트 (by ID)
|
|
6471
6718
|
*/
|
|
6472
|
-
async updateById(
|
|
6473
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
6719
|
+
async updateById(id14, data) {
|
|
6720
|
+
const result = await this.db.update(userProfiles).set(data).where(eq9(userProfiles.id, id14)).returning();
|
|
6474
6721
|
return result[0] ?? null;
|
|
6475
6722
|
}
|
|
6476
6723
|
/**
|
|
6477
6724
|
* 프로필 업데이트 (by User ID)
|
|
6478
6725
|
*/
|
|
6479
6726
|
async updateByUserId(userId, data) {
|
|
6480
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
6727
|
+
const result = await this.db.update(userProfiles).set(data).where(eq9(userProfiles.userId, userId)).returning();
|
|
6481
6728
|
return result[0] ?? null;
|
|
6482
6729
|
}
|
|
6483
6730
|
/**
|
|
6484
6731
|
* 프로필 삭제 (by ID)
|
|
6485
6732
|
*/
|
|
6486
|
-
async deleteById(
|
|
6487
|
-
const result = await this.db.delete(userProfiles).where(
|
|
6733
|
+
async deleteById(id14) {
|
|
6734
|
+
const result = await this.db.delete(userProfiles).where(eq9(userProfiles.id, id14)).returning();
|
|
6488
6735
|
return result[0] ?? null;
|
|
6489
6736
|
}
|
|
6490
6737
|
/**
|
|
6491
6738
|
* 프로필 삭제 (by User ID)
|
|
6492
6739
|
*/
|
|
6493
6740
|
async deleteByUserId(userId) {
|
|
6494
|
-
const result = await this.db.delete(userProfiles).where(
|
|
6741
|
+
const result = await this.db.delete(userProfiles).where(eq9(userProfiles.userId, userId)).returning();
|
|
6495
6742
|
return result[0] ?? null;
|
|
6496
6743
|
}
|
|
6497
6744
|
/**
|
|
@@ -6533,7 +6780,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6533
6780
|
metadata: userProfiles.metadata,
|
|
6534
6781
|
createdAt: userProfiles.createdAt,
|
|
6535
6782
|
updatedAt: userProfiles.updatedAt
|
|
6536
|
-
}).from(userProfiles).where(
|
|
6783
|
+
}).from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
6537
6784
|
if (!profile) {
|
|
6538
6785
|
return null;
|
|
6539
6786
|
}
|
|
@@ -6561,8 +6808,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6561
6808
|
});
|
|
6562
6809
|
|
|
6563
6810
|
// src/server/repositories/invitations.repository.ts
|
|
6564
|
-
import { eq as
|
|
6565
|
-
import { BaseRepository as
|
|
6811
|
+
import { eq as eq10, and as and7, lt as lt4, desc as desc2, sql as sql6 } from "drizzle-orm";
|
|
6812
|
+
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
6566
6813
|
var InvitationsRepository, invitationsRepository;
|
|
6567
6814
|
var init_invitations_repository = __esm({
|
|
6568
6815
|
"src/server/repositories/invitations.repository.ts"() {
|
|
@@ -6570,19 +6817,20 @@ var init_invitations_repository = __esm({
|
|
|
6570
6817
|
init_users();
|
|
6571
6818
|
init_roles();
|
|
6572
6819
|
init_user_invitations();
|
|
6573
|
-
|
|
6820
|
+
init_email();
|
|
6821
|
+
InvitationsRepository = class extends BaseRepository10 {
|
|
6574
6822
|
/**
|
|
6575
6823
|
* ID로 초대 조회
|
|
6576
6824
|
*/
|
|
6577
|
-
async findById(
|
|
6578
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
6825
|
+
async findById(id14) {
|
|
6826
|
+
const result = await this.readDb.select().from(userInvitations).where(eq10(userInvitations.id, id14)).limit(1);
|
|
6579
6827
|
return result[0] ?? null;
|
|
6580
6828
|
}
|
|
6581
6829
|
/**
|
|
6582
6830
|
* Token으로 초대 조회
|
|
6583
6831
|
*/
|
|
6584
6832
|
async findByToken(token) {
|
|
6585
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
6833
|
+
const result = await this.readDb.select().from(userInvitations).where(eq10(userInvitations.token, token)).limit(1);
|
|
6586
6834
|
return result[0] ?? null;
|
|
6587
6835
|
}
|
|
6588
6836
|
/**
|
|
@@ -6590,9 +6838,9 @@ var init_invitations_repository = __esm({
|
|
|
6590
6838
|
*/
|
|
6591
6839
|
async findPendingByEmail(email) {
|
|
6592
6840
|
const result = await this.readDb.select().from(userInvitations).where(
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6841
|
+
and7(
|
|
6842
|
+
eq10(userInvitations.email, normalizeEmail(email)),
|
|
6843
|
+
eq10(userInvitations.status, "pending")
|
|
6596
6844
|
)
|
|
6597
6845
|
).limit(1);
|
|
6598
6846
|
return result[0] ?? null;
|
|
@@ -6601,24 +6849,24 @@ var init_invitations_repository = __esm({
|
|
|
6601
6849
|
* 초대자 ID로 모든 초대 조회
|
|
6602
6850
|
*/
|
|
6603
6851
|
async findByInvitedBy(invitedBy) {
|
|
6604
|
-
return this.readDb.select().from(userInvitations).where(
|
|
6852
|
+
return this.readDb.select().from(userInvitations).where(eq10(userInvitations.invitedBy, invitedBy));
|
|
6605
6853
|
}
|
|
6606
6854
|
/**
|
|
6607
6855
|
* 상태별 초대 조회
|
|
6608
6856
|
*/
|
|
6609
6857
|
async findByStatus(status) {
|
|
6610
|
-
return this.readDb.select().from(userInvitations).where(
|
|
6858
|
+
return this.readDb.select().from(userInvitations).where(eq10(userInvitations.status, status));
|
|
6611
6859
|
}
|
|
6612
6860
|
/**
|
|
6613
6861
|
* 초대 생성
|
|
6614
6862
|
*/
|
|
6615
6863
|
async create(data) {
|
|
6616
|
-
return await this._create(userInvitations, data);
|
|
6864
|
+
return await this._create(userInvitations, { ...data, email: normalizeEmail(data.email) });
|
|
6617
6865
|
}
|
|
6618
6866
|
/**
|
|
6619
6867
|
* 초대 상태 업데이트
|
|
6620
6868
|
*/
|
|
6621
|
-
async updateStatus(
|
|
6869
|
+
async updateStatus(id14, status, timestamp2) {
|
|
6622
6870
|
const updates = {
|
|
6623
6871
|
status
|
|
6624
6872
|
};
|
|
@@ -6629,14 +6877,14 @@ var init_invitations_repository = __esm({
|
|
|
6629
6877
|
updates.cancelledAt = timestamp2;
|
|
6630
6878
|
}
|
|
6631
6879
|
}
|
|
6632
|
-
const result = await this.db.update(userInvitations).set(updates).where(
|
|
6880
|
+
const result = await this.db.update(userInvitations).set(updates).where(eq10(userInvitations.id, id14)).returning();
|
|
6633
6881
|
return result[0] ?? null;
|
|
6634
6882
|
}
|
|
6635
6883
|
/**
|
|
6636
6884
|
* 초대 삭제
|
|
6637
6885
|
*/
|
|
6638
|
-
async deleteById(
|
|
6639
|
-
const result = await this.db.delete(userInvitations).where(
|
|
6886
|
+
async deleteById(id14) {
|
|
6887
|
+
const result = await this.db.delete(userInvitations).where(eq10(userInvitations.id, id14)).returning();
|
|
6640
6888
|
return result[0] ?? null;
|
|
6641
6889
|
}
|
|
6642
6890
|
/**
|
|
@@ -6645,8 +6893,8 @@ var init_invitations_repository = __esm({
|
|
|
6645
6893
|
async updateExpiredInvitations() {
|
|
6646
6894
|
const now = /* @__PURE__ */ new Date();
|
|
6647
6895
|
const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
|
|
6648
|
-
|
|
6649
|
-
|
|
6896
|
+
and7(
|
|
6897
|
+
eq10(userInvitations.status, "pending"),
|
|
6650
6898
|
lt4(userInvitations.expiresAt, now)
|
|
6651
6899
|
)
|
|
6652
6900
|
).returning();
|
|
@@ -6678,7 +6926,7 @@ var init_invitations_repository = __esm({
|
|
|
6678
6926
|
id: users.id,
|
|
6679
6927
|
email: users.email
|
|
6680
6928
|
}
|
|
6681
|
-
}).from(userInvitations).innerJoin(roles,
|
|
6929
|
+
}).from(userInvitations).innerJoin(roles, eq10(userInvitations.roleId, roles.id)).innerJoin(users, eq10(userInvitations.invitedBy, users.id)).where(eq10(userInvitations.token, token)).limit(1);
|
|
6682
6930
|
return result[0] ?? null;
|
|
6683
6931
|
}
|
|
6684
6932
|
/**
|
|
@@ -6689,13 +6937,13 @@ var init_invitations_repository = __esm({
|
|
|
6689
6937
|
const offset = (page - 1) * limit;
|
|
6690
6938
|
const conditions = [];
|
|
6691
6939
|
if (status) {
|
|
6692
|
-
conditions.push(
|
|
6940
|
+
conditions.push(eq10(userInvitations.status, status));
|
|
6693
6941
|
}
|
|
6694
6942
|
if (invitedBy) {
|
|
6695
|
-
conditions.push(
|
|
6943
|
+
conditions.push(eq10(userInvitations.invitedBy, invitedBy));
|
|
6696
6944
|
}
|
|
6697
|
-
const whereClause = conditions.length > 0 ?
|
|
6698
|
-
const countResult = await this.readDb.select({ count:
|
|
6945
|
+
const whereClause = conditions.length > 0 ? and7(...conditions) : void 0;
|
|
6946
|
+
const countResult = await this.readDb.select({ count: sql6`count(*)` }).from(userInvitations).where(whereClause);
|
|
6699
6947
|
const total = Number(countResult[0]?.count || 0);
|
|
6700
6948
|
const results = await this.readDb.select({
|
|
6701
6949
|
id: userInvitations.id,
|
|
@@ -6719,7 +6967,7 @@ var init_invitations_repository = __esm({
|
|
|
6719
6967
|
id: users.id,
|
|
6720
6968
|
email: users.email
|
|
6721
6969
|
}
|
|
6722
|
-
}).from(userInvitations).innerJoin(roles,
|
|
6970
|
+
}).from(userInvitations).innerJoin(roles, eq10(userInvitations.roleId, roles.id)).innerJoin(users, eq10(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc2(userInvitations.createdAt)).limit(limit).offset(offset);
|
|
6723
6971
|
return {
|
|
6724
6972
|
invitations: results,
|
|
6725
6973
|
total,
|
|
@@ -6731,30 +6979,31 @@ var init_invitations_repository = __esm({
|
|
|
6731
6979
|
/**
|
|
6732
6980
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
6733
6981
|
*/
|
|
6734
|
-
async updateById(
|
|
6735
|
-
const
|
|
6982
|
+
async updateById(id14, data) {
|
|
6983
|
+
const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
|
|
6984
|
+
const result = await this.db.update(userInvitations).set(patch).where(eq10(userInvitations.id, id14)).returning();
|
|
6736
6985
|
return result[0] ?? null;
|
|
6737
6986
|
}
|
|
6738
6987
|
/**
|
|
6739
6988
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
6740
6989
|
*/
|
|
6741
|
-
async resend(
|
|
6990
|
+
async resend(id14, newExpiresAt) {
|
|
6742
6991
|
const result = await this.db.update(userInvitations).set({
|
|
6743
6992
|
status: "pending",
|
|
6744
6993
|
expiresAt: newExpiresAt
|
|
6745
|
-
}).where(
|
|
6994
|
+
}).where(eq10(userInvitations.id, id14)).returning();
|
|
6746
6995
|
return result[0] ?? null;
|
|
6747
6996
|
}
|
|
6748
6997
|
/**
|
|
6749
6998
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
6750
6999
|
*/
|
|
6751
|
-
async cancel(
|
|
7000
|
+
async cancel(id14, cancelledBy, reason, currentMetadata) {
|
|
6752
7001
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
6753
7002
|
const result = await this.db.update(userInvitations).set({
|
|
6754
7003
|
status: "cancelled",
|
|
6755
7004
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
6756
7005
|
metadata: newMetadata
|
|
6757
|
-
}).where(
|
|
7006
|
+
}).where(eq10(userInvitations.id, id14)).returning();
|
|
6758
7007
|
return result[0] ?? null;
|
|
6759
7008
|
}
|
|
6760
7009
|
};
|
|
@@ -6966,6 +7215,33 @@ var init_schema5 = __esm({
|
|
|
6966
7215
|
})
|
|
6967
7216
|
},
|
|
6968
7217
|
// ============================================================================
|
|
7218
|
+
// Verified-email signup
|
|
7219
|
+
// ============================================================================
|
|
7220
|
+
SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES: {
|
|
7221
|
+
...envNumber({
|
|
7222
|
+
description: "How long an emailed signup confirmation link stays valid. Long enough to survive a mail delay, short enough that a link left in an inbox stops working.",
|
|
7223
|
+
default: 30,
|
|
7224
|
+
required: false,
|
|
7225
|
+
examples: [15, 30, 60]
|
|
7226
|
+
})
|
|
7227
|
+
},
|
|
7228
|
+
SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES: {
|
|
7229
|
+
...envNumber({
|
|
7230
|
+
description: "How long the password-setup session opened by a confirmation link stays valid. Covers one sitting at the password form, not an abandoned tab.",
|
|
7231
|
+
default: 15,
|
|
7232
|
+
required: false,
|
|
7233
|
+
examples: [10, 15, 30]
|
|
7234
|
+
})
|
|
7235
|
+
},
|
|
7236
|
+
SPFN_AUTH_SIGNUP_CONFIRM_PATH: {
|
|
7237
|
+
...envString({
|
|
7238
|
+
description: "App page the emailed confirmation link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/signup/email/confirm; it is a page in your app, not an API route.",
|
|
7239
|
+
default: "/signup/confirm",
|
|
7240
|
+
required: false,
|
|
7241
|
+
examples: ["/signup/confirm", "/auth/confirm", "/join/verify"]
|
|
7242
|
+
})
|
|
7243
|
+
},
|
|
7244
|
+
// ============================================================================
|
|
6969
7245
|
// API Configuration
|
|
6970
7246
|
// ============================================================================
|
|
6971
7247
|
SPFN_API_URL: {
|
|
@@ -7374,15 +7650,15 @@ var init_token_cipher = __esm({
|
|
|
7374
7650
|
});
|
|
7375
7651
|
|
|
7376
7652
|
// src/server/repositories/social-accounts.repository.ts
|
|
7377
|
-
import { eq as
|
|
7378
|
-
import { BaseRepository as
|
|
7653
|
+
import { eq as eq11, and as and8 } from "drizzle-orm";
|
|
7654
|
+
import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
|
|
7379
7655
|
var SocialAccountsRepository, socialAccountsRepository;
|
|
7380
7656
|
var init_social_accounts_repository = __esm({
|
|
7381
7657
|
"src/server/repositories/social-accounts.repository.ts"() {
|
|
7382
7658
|
"use strict";
|
|
7383
7659
|
init_entities();
|
|
7384
7660
|
init_token_cipher();
|
|
7385
|
-
SocialAccountsRepository = class extends
|
|
7661
|
+
SocialAccountsRepository = class extends BaseRepository11 {
|
|
7386
7662
|
/**
|
|
7387
7663
|
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
7388
7664
|
*
|
|
@@ -7410,10 +7686,10 @@ var init_social_accounts_repository = __esm({
|
|
|
7410
7686
|
if (refresh?.needsRotation) {
|
|
7411
7687
|
heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
|
|
7412
7688
|
}
|
|
7413
|
-
await this.db.update(userSocialAccounts).set(heal).where(
|
|
7414
|
-
|
|
7415
|
-
access?.needsRotation && account.accessToken !== null ?
|
|
7416
|
-
refresh?.needsRotation && account.refreshToken !== null ?
|
|
7689
|
+
await this.db.update(userSocialAccounts).set(heal).where(and8(
|
|
7690
|
+
eq11(userSocialAccounts.id, account.id),
|
|
7691
|
+
access?.needsRotation && account.accessToken !== null ? eq11(userSocialAccounts.accessToken, account.accessToken) : void 0,
|
|
7692
|
+
refresh?.needsRotation && account.refreshToken !== null ? eq11(userSocialAccounts.refreshToken, account.refreshToken) : void 0
|
|
7417
7693
|
));
|
|
7418
7694
|
} catch {
|
|
7419
7695
|
}
|
|
@@ -7430,9 +7706,9 @@ var init_social_accounts_repository = __esm({
|
|
|
7430
7706
|
*/
|
|
7431
7707
|
async findByProviderAndProviderId(provider, providerUserId) {
|
|
7432
7708
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7709
|
+
and8(
|
|
7710
|
+
eq11(userSocialAccounts.provider, provider),
|
|
7711
|
+
eq11(userSocialAccounts.providerUserId, providerUserId)
|
|
7436
7712
|
)
|
|
7437
7713
|
).limit(1);
|
|
7438
7714
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -7442,7 +7718,7 @@ var init_social_accounts_repository = __esm({
|
|
|
7442
7718
|
* Read replica 사용
|
|
7443
7719
|
*/
|
|
7444
7720
|
async findByUserId(userId) {
|
|
7445
|
-
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
7721
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(eq11(userSocialAccounts.userId, userId));
|
|
7446
7722
|
return Promise.all(result.map((account) => this.decryptAccount(account)));
|
|
7447
7723
|
}
|
|
7448
7724
|
/**
|
|
@@ -7451,9 +7727,9 @@ var init_social_accounts_repository = __esm({
|
|
|
7451
7727
|
*/
|
|
7452
7728
|
async findByUserIdAndProvider(userId, provider) {
|
|
7453
7729
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7730
|
+
and8(
|
|
7731
|
+
eq11(userSocialAccounts.userId, userId),
|
|
7732
|
+
eq11(userSocialAccounts.provider, provider)
|
|
7457
7733
|
)
|
|
7458
7734
|
).limit(1);
|
|
7459
7735
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -7479,11 +7755,11 @@ var init_social_accounts_repository = __esm({
|
|
|
7479
7755
|
* 토큰 정보 업데이트
|
|
7480
7756
|
* Write primary 사용
|
|
7481
7757
|
*/
|
|
7482
|
-
async updateTokens(
|
|
7758
|
+
async updateTokens(id14, data) {
|
|
7483
7759
|
const accounts = await this.db.select({
|
|
7484
7760
|
provider: userSocialAccounts.provider,
|
|
7485
7761
|
providerUserId: userSocialAccounts.providerUserId
|
|
7486
|
-
}).from(userSocialAccounts).where(
|
|
7762
|
+
}).from(userSocialAccounts).where(eq11(userSocialAccounts.id, id14)).limit(1);
|
|
7487
7763
|
const account = accounts[0];
|
|
7488
7764
|
if (!account) {
|
|
7489
7765
|
return null;
|
|
@@ -7497,15 +7773,15 @@ var init_social_accounts_repository = __esm({
|
|
|
7497
7773
|
...data,
|
|
7498
7774
|
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
7499
7775
|
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
|
|
7500
|
-
}).where(
|
|
7776
|
+
}).where(eq11(userSocialAccounts.id, id14)).returning();
|
|
7501
7777
|
return this.decryptAccount(result[0] ?? null);
|
|
7502
7778
|
}
|
|
7503
7779
|
/**
|
|
7504
7780
|
* 소셜 계정 삭제
|
|
7505
7781
|
* Write primary 사용
|
|
7506
7782
|
*/
|
|
7507
|
-
async deleteById(
|
|
7508
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
7783
|
+
async deleteById(id14) {
|
|
7784
|
+
const result = await this.db.delete(userSocialAccounts).where(eq11(userSocialAccounts.id, id14)).returning();
|
|
7509
7785
|
return result[0] ?? null;
|
|
7510
7786
|
}
|
|
7511
7787
|
/**
|
|
@@ -7514,9 +7790,9 @@ var init_social_accounts_repository = __esm({
|
|
|
7514
7790
|
*/
|
|
7515
7791
|
async deleteByUserIdAndProvider(userId, provider) {
|
|
7516
7792
|
const result = await this.db.delete(userSocialAccounts).where(
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7793
|
+
and8(
|
|
7794
|
+
eq11(userSocialAccounts.userId, userId),
|
|
7795
|
+
eq11(userSocialAccounts.provider, provider)
|
|
7520
7796
|
)
|
|
7521
7797
|
).returning();
|
|
7522
7798
|
return result[0] ?? null;
|
|
@@ -7529,7 +7805,7 @@ var init_social_accounts_repository = __esm({
|
|
|
7529
7805
|
* Write primary 사용
|
|
7530
7806
|
*/
|
|
7531
7807
|
async deleteAllByUserId(userId) {
|
|
7532
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
7808
|
+
const result = await this.db.delete(userSocialAccounts).where(eq11(userSocialAccounts.userId, userId)).returning();
|
|
7533
7809
|
return result.length;
|
|
7534
7810
|
}
|
|
7535
7811
|
};
|
|
@@ -7538,19 +7814,19 @@ var init_social_accounts_repository = __esm({
|
|
|
7538
7814
|
});
|
|
7539
7815
|
|
|
7540
7816
|
// src/server/repositories/auth-metadata.repository.ts
|
|
7541
|
-
import { BaseRepository as
|
|
7542
|
-
import { eq as
|
|
7817
|
+
import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
|
|
7818
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
7543
7819
|
var AuthMetadataRepository, authMetadataRepository;
|
|
7544
7820
|
var init_auth_metadata_repository = __esm({
|
|
7545
7821
|
"src/server/repositories/auth-metadata.repository.ts"() {
|
|
7546
7822
|
"use strict";
|
|
7547
7823
|
init_auth_metadata();
|
|
7548
|
-
AuthMetadataRepository = class extends
|
|
7824
|
+
AuthMetadataRepository = class extends BaseRepository12 {
|
|
7549
7825
|
/**
|
|
7550
7826
|
* 키로 값 조회
|
|
7551
7827
|
*/
|
|
7552
7828
|
async get(key) {
|
|
7553
|
-
const result = await this.readDb.select().from(authMetadata).where(
|
|
7829
|
+
const result = await this.readDb.select().from(authMetadata).where(eq12(authMetadata.key, key)).limit(1);
|
|
7554
7830
|
return result[0]?.value ?? null;
|
|
7555
7831
|
}
|
|
7556
7832
|
/**
|
|
@@ -7573,20 +7849,20 @@ var init_auth_metadata_repository = __esm({
|
|
|
7573
7849
|
});
|
|
7574
7850
|
|
|
7575
7851
|
// src/server/repositories/account-deletion-requests.repository.ts
|
|
7576
|
-
import { eq as
|
|
7577
|
-
import { BaseRepository as
|
|
7852
|
+
import { eq as eq13, and as and9, lte } from "drizzle-orm";
|
|
7853
|
+
import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
|
|
7578
7854
|
var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
|
|
7579
7855
|
var init_account_deletion_requests_repository = __esm({
|
|
7580
7856
|
"src/server/repositories/account-deletion-requests.repository.ts"() {
|
|
7581
7857
|
"use strict";
|
|
7582
7858
|
init_account_deletion_requests();
|
|
7583
|
-
AccountDeletionRequestsRepository = class extends
|
|
7859
|
+
AccountDeletionRequestsRepository = class extends BaseRepository13 {
|
|
7584
7860
|
/**
|
|
7585
7861
|
* ID로 요청 조회
|
|
7586
7862
|
* Read replica 사용
|
|
7587
7863
|
*/
|
|
7588
|
-
async findById(
|
|
7589
|
-
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
7864
|
+
async findById(id14) {
|
|
7865
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(eq13(accountDeletionRequests.id, id14)).limit(1);
|
|
7590
7866
|
return result[0] ?? null;
|
|
7591
7867
|
}
|
|
7592
7868
|
/**
|
|
@@ -7595,9 +7871,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7595
7871
|
*/
|
|
7596
7872
|
async findPendingByUserId(userId) {
|
|
7597
7873
|
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7874
|
+
and9(
|
|
7875
|
+
eq13(accountDeletionRequests.userId, userId),
|
|
7876
|
+
eq13(accountDeletionRequests.status, "pending")
|
|
7601
7877
|
)
|
|
7602
7878
|
).limit(1);
|
|
7603
7879
|
return result[0] ?? null;
|
|
@@ -7611,9 +7887,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7611
7887
|
*/
|
|
7612
7888
|
async findPendingByUserIdOnPrimary(userId) {
|
|
7613
7889
|
const result = await this.db.select().from(accountDeletionRequests).where(
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7890
|
+
and9(
|
|
7891
|
+
eq13(accountDeletionRequests.userId, userId),
|
|
7892
|
+
eq13(accountDeletionRequests.status, "pending")
|
|
7617
7893
|
)
|
|
7618
7894
|
).limit(1);
|
|
7619
7895
|
return result[0] ?? null;
|
|
@@ -7624,8 +7900,8 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7624
7900
|
*/
|
|
7625
7901
|
async findDueForPurge(now) {
|
|
7626
7902
|
return this.readDb.select().from(accountDeletionRequests).where(
|
|
7627
|
-
|
|
7628
|
-
|
|
7903
|
+
and9(
|
|
7904
|
+
eq13(accountDeletionRequests.status, "pending"),
|
|
7629
7905
|
lte(accountDeletionRequests.purgeScheduledAt, now)
|
|
7630
7906
|
)
|
|
7631
7907
|
);
|
|
@@ -7645,14 +7921,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7645
7921
|
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
7646
7922
|
* Write primary 사용
|
|
7647
7923
|
*/
|
|
7648
|
-
async markCancelled(
|
|
7924
|
+
async markCancelled(id14) {
|
|
7649
7925
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
7650
7926
|
status: "cancelled",
|
|
7651
7927
|
cancelledAt: /* @__PURE__ */ new Date()
|
|
7652
7928
|
}).where(
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7929
|
+
and9(
|
|
7930
|
+
eq13(accountDeletionRequests.id, id14),
|
|
7931
|
+
eq13(accountDeletionRequests.status, "pending")
|
|
7656
7932
|
)
|
|
7657
7933
|
).returning();
|
|
7658
7934
|
return result[0] ?? null;
|
|
@@ -7667,15 +7943,15 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7667
7943
|
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
7668
7944
|
* Write primary 사용
|
|
7669
7945
|
*/
|
|
7670
|
-
async markCompleted(
|
|
7946
|
+
async markCompleted(id14, purgeStrategy) {
|
|
7671
7947
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
7672
7948
|
status: "completed",
|
|
7673
7949
|
completedAt: /* @__PURE__ */ new Date(),
|
|
7674
7950
|
purgeStrategy
|
|
7675
7951
|
}).where(
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7952
|
+
and9(
|
|
7953
|
+
eq13(accountDeletionRequests.id, id14),
|
|
7954
|
+
eq13(accountDeletionRequests.status, "pending")
|
|
7679
7955
|
)
|
|
7680
7956
|
).returning();
|
|
7681
7957
|
return result[0] ?? null;
|
|
@@ -7686,14 +7962,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7686
7962
|
});
|
|
7687
7963
|
|
|
7688
7964
|
// src/server/repositories/ops-tokens.repository.ts
|
|
7689
|
-
import { and as
|
|
7690
|
-
import { BaseRepository as
|
|
7965
|
+
import { and as and10, desc as desc3, eq as eq14, isNull as isNull5 } from "drizzle-orm";
|
|
7966
|
+
import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
|
|
7691
7967
|
var OpsTokensRepository, opsTokensRepository;
|
|
7692
7968
|
var init_ops_tokens_repository = __esm({
|
|
7693
7969
|
"src/server/repositories/ops-tokens.repository.ts"() {
|
|
7694
7970
|
"use strict";
|
|
7695
7971
|
init_ops_tokens();
|
|
7696
|
-
OpsTokensRepository = class extends
|
|
7972
|
+
OpsTokensRepository = class extends BaseRepository14 {
|
|
7697
7973
|
/**
|
|
7698
7974
|
* Lookup by the secret's hash — the verification path.
|
|
7699
7975
|
*
|
|
@@ -7703,7 +7979,7 @@ var init_ops_tokens_repository = __esm({
|
|
|
7703
7979
|
* and revocation is documented as taking effect immediately.
|
|
7704
7980
|
*/
|
|
7705
7981
|
async findByTokenHash(tokenHash) {
|
|
7706
|
-
const result = await this.db.select().from(opsTokens).where(
|
|
7982
|
+
const result = await this.db.select().from(opsTokens).where(eq14(opsTokens.tokenHash, tokenHash)).limit(1);
|
|
7707
7983
|
return result[0] ?? null;
|
|
7708
7984
|
}
|
|
7709
7985
|
async create(data) {
|
|
@@ -7718,13 +7994,13 @@ var init_ops_tokens_repository = __esm({
|
|
|
7718
7994
|
* token is already revoked — the first revocation's timestamp is never
|
|
7719
7995
|
* overwritten.
|
|
7720
7996
|
*/
|
|
7721
|
-
async revokeById(
|
|
7722
|
-
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
7997
|
+
async revokeById(id14) {
|
|
7998
|
+
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and10(eq14(opsTokens.id, id14), isNull5(opsTokens.revokedAt))).returning();
|
|
7723
7999
|
return result[0] ?? null;
|
|
7724
8000
|
}
|
|
7725
8001
|
/** Fire-and-forget from the verification path. */
|
|
7726
|
-
async updateLastUsedById(
|
|
7727
|
-
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
8002
|
+
async updateLastUsedById(id14) {
|
|
8003
|
+
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq14(opsTokens.id, id14));
|
|
7728
8004
|
}
|
|
7729
8005
|
};
|
|
7730
8006
|
opsTokensRepository = new OpsTokensRepository();
|
|
@@ -7738,6 +8014,7 @@ var init_repositories = __esm({
|
|
|
7738
8014
|
init_users_repository();
|
|
7739
8015
|
init_keys_repository();
|
|
7740
8016
|
init_verification_codes_repository();
|
|
8017
|
+
init_signup_link_tokens_repository();
|
|
7741
8018
|
init_roles_repository();
|
|
7742
8019
|
init_permissions_repository();
|
|
7743
8020
|
init_role_permissions_repository();
|
|
@@ -7839,7 +8116,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
7839
8116
|
}
|
|
7840
8117
|
async function setRolePermissions(roleId, permissionIds) {
|
|
7841
8118
|
const roleIdNum = Number(roleId);
|
|
7842
|
-
const permissionIdNums = permissionIds.map((
|
|
8119
|
+
const permissionIdNums = permissionIds.map((id14) => Number(id14));
|
|
7843
8120
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
7844
8121
|
}
|
|
7845
8122
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -7859,7 +8136,7 @@ async function getRolePermissions(roleId) {
|
|
|
7859
8136
|
}
|
|
7860
8137
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
7861
8138
|
const perms = await Promise.all(
|
|
7862
|
-
permissionIds.map((
|
|
8139
|
+
permissionIds.map((id14) => permissionsRepository.findById(id14))
|
|
7863
8140
|
);
|
|
7864
8141
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
7865
8142
|
}
|
|
@@ -7879,6 +8156,9 @@ import { defineRouter as defineRouter6 } from "@spfn/core/route";
|
|
|
7879
8156
|
// src/server/routes/auth/index.ts
|
|
7880
8157
|
init_schema3();
|
|
7881
8158
|
|
|
8159
|
+
// src/server/helpers/index.ts
|
|
8160
|
+
init_email();
|
|
8161
|
+
|
|
7882
8162
|
// src/server/helpers/password.ts
|
|
7883
8163
|
import * as bcrypt from "@node-rs/bcrypt";
|
|
7884
8164
|
import { env } from "@spfn/auth/config";
|
|
@@ -8055,6 +8335,7 @@ function getKeyId(c) {
|
|
|
8055
8335
|
// src/server/routes/auth/index.ts
|
|
8056
8336
|
init_types();
|
|
8057
8337
|
import { KeyNotFoundError } from "@spfn/auth/errors";
|
|
8338
|
+
import { ValidationError as ValidationError9 } from "@spfn/core/errors";
|
|
8058
8339
|
|
|
8059
8340
|
// src/server/services/auth.service.ts
|
|
8060
8341
|
init_repositories();
|
|
@@ -8070,6 +8351,7 @@ import {
|
|
|
8070
8351
|
} from "@spfn/auth/errors";
|
|
8071
8352
|
|
|
8072
8353
|
// src/server/lib/config.ts
|
|
8354
|
+
init_email();
|
|
8073
8355
|
import { env as env4 } from "@spfn/auth/config";
|
|
8074
8356
|
function getCookieSuffix() {
|
|
8075
8357
|
const port = process.env.SPFN_PORT;
|
|
@@ -8091,6 +8373,10 @@ var COOKIE_NAMES = {
|
|
|
8091
8373
|
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
8092
8374
|
get OAUTH_CSRF() {
|
|
8093
8375
|
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
8376
|
+
},
|
|
8377
|
+
/** Password-setup session for verified-email signup — temporary, single-purpose */
|
|
8378
|
+
get SIGNUP_SETUP() {
|
|
8379
|
+
return `spfn_signup_setup${getCookieSuffix()}`;
|
|
8094
8380
|
}
|
|
8095
8381
|
};
|
|
8096
8382
|
function matchOAuthCsrfCookies(cookies) {
|
|
@@ -8135,7 +8421,7 @@ function getAuthConfig() {
|
|
|
8135
8421
|
async function runBeforeRegister(context) {
|
|
8136
8422
|
const { beforeRegister } = globalConfig;
|
|
8137
8423
|
if (beforeRegister) {
|
|
8138
|
-
await beforeRegister(context);
|
|
8424
|
+
await beforeRegister({ ...context, email: normalizeOptionalEmail(context.email) });
|
|
8139
8425
|
}
|
|
8140
8426
|
}
|
|
8141
8427
|
function getSessionTtl(override) {
|
|
@@ -8178,11 +8464,15 @@ var authLogger = {
|
|
|
8178
8464
|
};
|
|
8179
8465
|
|
|
8180
8466
|
// src/server/services/verification.service.ts
|
|
8467
|
+
init_email();
|
|
8181
8468
|
init_repositories();
|
|
8182
8469
|
var ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES = 60;
|
|
8183
8470
|
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
8184
8471
|
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
8185
8472
|
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
8473
|
+
function normalizeVerificationTarget(target, targetType) {
|
|
8474
|
+
return targetType === "email" ? normalizeEmail(target) : target.trim();
|
|
8475
|
+
}
|
|
8186
8476
|
function generateVerificationCode() {
|
|
8187
8477
|
return crypto4.randomInt(0, 1e6).toString().padStart(6, "0");
|
|
8188
8478
|
}
|
|
@@ -8290,23 +8580,28 @@ async function sendAccountExistsNotice(target, targetType) {
|
|
|
8290
8580
|
log.error("Failed to send account-exists notice", { target, error: result.error });
|
|
8291
8581
|
}
|
|
8292
8582
|
}
|
|
8583
|
+
async function noticeAccountExistsOnce(target, targetType) {
|
|
8584
|
+
const recentNotice = await verificationCodesRepository.findValidByTargetAndPurpose(target, "registration");
|
|
8585
|
+
if (recentNotice) {
|
|
8586
|
+
return;
|
|
8587
|
+
}
|
|
8588
|
+
const dedupeExpiresAt = new Date(Date.now() + ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES * 6e4);
|
|
8589
|
+
await verificationCodesRepository.invalidatePreviousCodes(target, "registration");
|
|
8590
|
+
await verificationCodesRepository.create({
|
|
8591
|
+
target,
|
|
8592
|
+
targetType,
|
|
8593
|
+
code: generateVerificationCode(),
|
|
8594
|
+
purpose: "registration",
|
|
8595
|
+
expiresAt: dedupeExpiresAt,
|
|
8596
|
+
attempts: 0
|
|
8597
|
+
});
|
|
8598
|
+
await sendAccountExistsNotice(target, targetType);
|
|
8599
|
+
}
|
|
8293
8600
|
async function sendVerificationCodeService(params) {
|
|
8294
|
-
const {
|
|
8601
|
+
const { targetType, purpose } = params;
|
|
8602
|
+
const target = normalizeVerificationTarget(params.target, targetType);
|
|
8295
8603
|
if (purpose === "registration" && await accountExistsForTarget(target, targetType)) {
|
|
8296
|
-
|
|
8297
|
-
if (!recentNotice) {
|
|
8298
|
-
const dedupeExpiresAt = new Date(Date.now() + ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES * 6e4);
|
|
8299
|
-
await verificationCodesRepository.invalidatePreviousCodes(target, purpose);
|
|
8300
|
-
await verificationCodesRepository.create({
|
|
8301
|
-
target,
|
|
8302
|
-
targetType,
|
|
8303
|
-
code: generateVerificationCode(),
|
|
8304
|
-
purpose,
|
|
8305
|
-
expiresAt: dedupeExpiresAt,
|
|
8306
|
-
attempts: 0
|
|
8307
|
-
});
|
|
8308
|
-
await sendAccountExistsNotice(target, targetType);
|
|
8309
|
-
}
|
|
8604
|
+
await noticeAccountExistsOnce(target, targetType);
|
|
8310
8605
|
return {
|
|
8311
8606
|
success: true,
|
|
8312
8607
|
expiresAt: new Date(Date.now() + VERIFICATION_CODE_EXPIRY_MINUTES * 6e4).toISOString()
|
|
@@ -8325,7 +8620,8 @@ async function sendVerificationCodeService(params) {
|
|
|
8325
8620
|
};
|
|
8326
8621
|
}
|
|
8327
8622
|
async function verifyCodeService(params) {
|
|
8328
|
-
const {
|
|
8623
|
+
const { targetType, code, purpose } = params;
|
|
8624
|
+
const target = normalizeVerificationTarget(params.target, targetType);
|
|
8329
8625
|
const validation = await validateVerificationCode(target, code, purpose);
|
|
8330
8626
|
if (!validation.valid) {
|
|
8331
8627
|
throw new InvalidVerificationCodeError({ message: validation.error || "Invalid verification code" });
|
|
@@ -8675,8 +8971,8 @@ async function verifyReauthCredential(user, params) {
|
|
|
8675
8971
|
throw new VerificationTokenTargetMismatchError();
|
|
8676
8972
|
}
|
|
8677
8973
|
}
|
|
8678
|
-
async function sendDeletionEmail(to, subject,
|
|
8679
|
-
const result = await sendEmail2({ to, subject, text:
|
|
8974
|
+
async function sendDeletionEmail(to, subject, text14) {
|
|
8975
|
+
const result = await sendEmail2({ to, subject, text: text14 });
|
|
8680
8976
|
if (!result.success) {
|
|
8681
8977
|
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
8682
8978
|
}
|
|
@@ -8923,7 +9219,8 @@ async function sweepDuePurges(now = /* @__PURE__ */ new Date()) {
|
|
|
8923
9219
|
|
|
8924
9220
|
// src/server/services/auth.service.ts
|
|
8925
9221
|
async function registerService(params) {
|
|
8926
|
-
const { email,
|
|
9222
|
+
const { email, verificationToken } = params;
|
|
9223
|
+
const phone = params.phone?.trim();
|
|
8927
9224
|
const tokenPayload = validateVerificationToken(verificationToken);
|
|
8928
9225
|
if (!tokenPayload) {
|
|
8929
9226
|
throw new InvalidVerificationTokenError2();
|
|
@@ -8931,7 +9228,7 @@ async function registerService(params) {
|
|
|
8931
9228
|
if (tokenPayload.purpose !== "registration") {
|
|
8932
9229
|
throw new VerificationTokenPurposeMismatchError2({ expected: "registration", actual: tokenPayload.purpose });
|
|
8933
9230
|
}
|
|
8934
|
-
const providedTarget = email
|
|
9231
|
+
const providedTarget = email ? normalizeEmail(email) : phone;
|
|
8935
9232
|
if (tokenPayload.target !== providedTarget) {
|
|
8936
9233
|
throw new VerificationTokenTargetMismatchError2();
|
|
8937
9234
|
}
|
|
@@ -8939,6 +9236,13 @@ async function registerService(params) {
|
|
|
8939
9236
|
if (tokenPayload.targetType !== providedTargetType) {
|
|
8940
9237
|
throw new VerificationTokenTargetMismatchError2();
|
|
8941
9238
|
}
|
|
9239
|
+
return await createVerifiedAccount({ ...params, phone });
|
|
9240
|
+
}
|
|
9241
|
+
async function createVerifiedAccount(params) {
|
|
9242
|
+
const { email, phone, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
9243
|
+
if (!publicKey || !keyId || !fingerprint) {
|
|
9244
|
+
throw new ValidationError3({ message: "Device key material is required to register" });
|
|
9245
|
+
}
|
|
8942
9246
|
const existingUser = await usersRepository.findByEmailOrPhone(email, phone);
|
|
8943
9247
|
if (existingUser) {
|
|
8944
9248
|
const identifierType = email ? "email" : "phone";
|
|
@@ -9070,6 +9374,158 @@ async function changePasswordService(params) {
|
|
|
9070
9374
|
await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
|
|
9071
9375
|
}
|
|
9072
9376
|
|
|
9377
|
+
// src/server/services/signup-link.service.ts
|
|
9378
|
+
import crypto5 from "crypto";
|
|
9379
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
9380
|
+
import { InvalidSignupLinkError, InvalidSignupSetupSessionError } from "@spfn/auth/errors";
|
|
9381
|
+
import { sendEmail as sendEmail3 } from "@spfn/notification/server";
|
|
9382
|
+
init_repositories();
|
|
9383
|
+
var CREDENTIAL_BYTES = 32;
|
|
9384
|
+
function mintCredential() {
|
|
9385
|
+
const secret = crypto5.randomBytes(CREDENTIAL_BYTES).toString("base64url");
|
|
9386
|
+
return { secret, hash: hashCredential(secret) };
|
|
9387
|
+
}
|
|
9388
|
+
function hashCredential(secret) {
|
|
9389
|
+
return crypto5.createHash("sha256").update(secret).digest("base64url");
|
|
9390
|
+
}
|
|
9391
|
+
function isSafeReturnPath(returnPath) {
|
|
9392
|
+
if (!returnPath.startsWith("/")) {
|
|
9393
|
+
return false;
|
|
9394
|
+
}
|
|
9395
|
+
if (returnPath.startsWith("//") || returnPath.includes("\\")) {
|
|
9396
|
+
return false;
|
|
9397
|
+
}
|
|
9398
|
+
if (returnPath.includes("..")) {
|
|
9399
|
+
return false;
|
|
9400
|
+
}
|
|
9401
|
+
return !/^\/[^/?#]*:/.test(returnPath);
|
|
9402
|
+
}
|
|
9403
|
+
function buildConfirmUrl(token) {
|
|
9404
|
+
const appUrl = (env7.NEXT_PUBLIC_SPFN_APP_URL || env7.SPFN_APP_URL || "").replace(/\/$/, "");
|
|
9405
|
+
const path = env7.SPFN_AUTH_SIGNUP_CONFIRM_PATH || "/signup/confirm";
|
|
9406
|
+
return `${appUrl}${path}?token=${encodeURIComponent(token)}`;
|
|
9407
|
+
}
|
|
9408
|
+
async function sendSignupLinkEmail(email, confirmUrl, expiresInMinutes) {
|
|
9409
|
+
const result = await sendEmail3({
|
|
9410
|
+
to: email,
|
|
9411
|
+
template: "signup-link",
|
|
9412
|
+
data: { confirmUrl, expiresInMinutes }
|
|
9413
|
+
});
|
|
9414
|
+
if (!result.success) {
|
|
9415
|
+
authLogger.email.error("Failed to send signup link email", {
|
|
9416
|
+
email,
|
|
9417
|
+
error: result.error
|
|
9418
|
+
});
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
async function requestSignupLinkService(params) {
|
|
9422
|
+
const email = params.email.trim();
|
|
9423
|
+
const returnPath = params.returnPath;
|
|
9424
|
+
const ttlMinutes = env7.SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES ?? 30;
|
|
9425
|
+
const expiresAt = new Date(Date.now() + ttlMinutes * 6e4);
|
|
9426
|
+
const existingUser = await usersRepository.findByEmail(email);
|
|
9427
|
+
if (existingUser) {
|
|
9428
|
+
await noticeAccountExistsOnce(email, "email");
|
|
9429
|
+
return { success: true, expiresAt: expiresAt.toISOString() };
|
|
9430
|
+
}
|
|
9431
|
+
await signupLinkTokensRepository.supersedeLiveForEmail(email);
|
|
9432
|
+
const { secret, hash: hash2 } = mintCredential();
|
|
9433
|
+
await signupLinkTokensRepository.create({
|
|
9434
|
+
email,
|
|
9435
|
+
tokenHash: hash2,
|
|
9436
|
+
returnPath: returnPath ?? null,
|
|
9437
|
+
expiresAt
|
|
9438
|
+
});
|
|
9439
|
+
await sendSignupLinkEmail(email, buildConfirmUrl(secret), ttlMinutes);
|
|
9440
|
+
return { success: true, expiresAt: expiresAt.toISOString() };
|
|
9441
|
+
}
|
|
9442
|
+
function linkRefusalReason(row) {
|
|
9443
|
+
if (!row) {
|
|
9444
|
+
return "unknown token";
|
|
9445
|
+
}
|
|
9446
|
+
if (row.completedAt) {
|
|
9447
|
+
return "signup already completed";
|
|
9448
|
+
}
|
|
9449
|
+
if (row.supersededAt) {
|
|
9450
|
+
return "superseded by a newer request";
|
|
9451
|
+
}
|
|
9452
|
+
if (row.consumedAt) {
|
|
9453
|
+
return "link already used";
|
|
9454
|
+
}
|
|
9455
|
+
if (/* @__PURE__ */ new Date() > new Date(row.expiresAt)) {
|
|
9456
|
+
return "link expired";
|
|
9457
|
+
}
|
|
9458
|
+
return null;
|
|
9459
|
+
}
|
|
9460
|
+
async function confirmSignupLinkService(params) {
|
|
9461
|
+
const row = await signupLinkTokensRepository.findByTokenHash(hashCredential(params.token));
|
|
9462
|
+
const refusal = linkRefusalReason(row);
|
|
9463
|
+
if (refusal || !row) {
|
|
9464
|
+
authLogger.service.warn("Signup link refused", { reason: refusal });
|
|
9465
|
+
throw new InvalidSignupLinkError();
|
|
9466
|
+
}
|
|
9467
|
+
if (await usersRepository.findByEmail(row.email)) {
|
|
9468
|
+
authLogger.service.warn("Signup link refused", { reason: "account created meanwhile" });
|
|
9469
|
+
throw new InvalidSignupLinkError();
|
|
9470
|
+
}
|
|
9471
|
+
const setupTtlMinutes = env7.SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES ?? 15;
|
|
9472
|
+
const setupExpiresAt = new Date(Date.now() + setupTtlMinutes * 6e4);
|
|
9473
|
+
const setup = mintCredential();
|
|
9474
|
+
const claimed = await signupLinkTokensRepository.claimLink(row.id, setup.hash, setupExpiresAt);
|
|
9475
|
+
if (!claimed) {
|
|
9476
|
+
authLogger.service.warn("Signup link refused", { reason: "lost the claim race" });
|
|
9477
|
+
throw new InvalidSignupLinkError();
|
|
9478
|
+
}
|
|
9479
|
+
return {
|
|
9480
|
+
email: claimed.email,
|
|
9481
|
+
returnPath: claimed.returnPath,
|
|
9482
|
+
setupSecret: setup.secret,
|
|
9483
|
+
setupExpiresAt: setupExpiresAt.toISOString()
|
|
9484
|
+
};
|
|
9485
|
+
}
|
|
9486
|
+
function setupRefusalReason(row) {
|
|
9487
|
+
if (!row) {
|
|
9488
|
+
return "unknown setup session";
|
|
9489
|
+
}
|
|
9490
|
+
if (row.completedAt) {
|
|
9491
|
+
return "setup session already used";
|
|
9492
|
+
}
|
|
9493
|
+
if (row.supersededAt) {
|
|
9494
|
+
return "superseded by a newer request";
|
|
9495
|
+
}
|
|
9496
|
+
if (!row.setupExpiresAt || /* @__PURE__ */ new Date() > new Date(row.setupExpiresAt)) {
|
|
9497
|
+
return "setup session expired";
|
|
9498
|
+
}
|
|
9499
|
+
return null;
|
|
9500
|
+
}
|
|
9501
|
+
async function completeSignupService(params) {
|
|
9502
|
+
if (!params.setupSecret) {
|
|
9503
|
+
throw new InvalidSignupSetupSessionError();
|
|
9504
|
+
}
|
|
9505
|
+
const row = await signupLinkTokensRepository.findBySetupSecretHash(hashCredential(params.setupSecret));
|
|
9506
|
+
const refusal = setupRefusalReason(row);
|
|
9507
|
+
if (refusal || !row) {
|
|
9508
|
+
authLogger.service.warn("Signup setup session refused", { reason: refusal });
|
|
9509
|
+
throw new InvalidSignupSetupSessionError();
|
|
9510
|
+
}
|
|
9511
|
+
const claimed = await signupLinkTokensRepository.claimSetupSession(row.id);
|
|
9512
|
+
if (!claimed) {
|
|
9513
|
+
authLogger.service.warn("Signup setup session refused", { reason: "lost the claim race" });
|
|
9514
|
+
throw new InvalidSignupSetupSessionError();
|
|
9515
|
+
}
|
|
9516
|
+
return await createVerifiedAccount({
|
|
9517
|
+
email: claimed.email,
|
|
9518
|
+
password: params.password,
|
|
9519
|
+
publicKey: params.publicKey,
|
|
9520
|
+
keyId: params.keyId,
|
|
9521
|
+
fingerprint: params.fingerprint,
|
|
9522
|
+
algorithm: params.algorithm,
|
|
9523
|
+
deviceName: params.deviceName,
|
|
9524
|
+
platform: params.platform,
|
|
9525
|
+
metadata: params.metadata
|
|
9526
|
+
});
|
|
9527
|
+
}
|
|
9528
|
+
|
|
9073
9529
|
// src/server/services/rbac.service.ts
|
|
9074
9530
|
init_repositories();
|
|
9075
9531
|
init_rbac();
|
|
@@ -9210,6 +9666,29 @@ async function syncMappings(allMappings, rolesByName, permsByName) {
|
|
|
9210
9666
|
}
|
|
9211
9667
|
}
|
|
9212
9668
|
|
|
9669
|
+
// src/server/services/email-normalization.service.ts
|
|
9670
|
+
init_repositories();
|
|
9671
|
+
var BACKFILL_KEY = "auth:email_normalization";
|
|
9672
|
+
async function normalizeStoredEmails() {
|
|
9673
|
+
if (await authMetadataRepository.get(BACKFILL_KEY)) {
|
|
9674
|
+
return { normalized: 0, conflicts: [] };
|
|
9675
|
+
}
|
|
9676
|
+
const conflicts = await usersRepository.findEmailConflictGroups();
|
|
9677
|
+
const normalized = await usersRepository.normalizeEmailsExcept(conflicts.flat());
|
|
9678
|
+
if (normalized > 0) {
|
|
9679
|
+
authLogger.service.info(`\u2709\uFE0F Normalized ${normalized} stored email address(es)`);
|
|
9680
|
+
}
|
|
9681
|
+
if (conflicts.length > 0) {
|
|
9682
|
+
authLogger.service.error(
|
|
9683
|
+
`${conflicts.length} email group(s) differ only by capitalization and cannot be normalized automatically. The accounts are untouched and the ones stored in mixed case cannot sign in until this is resolved.`,
|
|
9684
|
+
{ conflictingUserIds: conflicts }
|
|
9685
|
+
);
|
|
9686
|
+
return { normalized, conflicts };
|
|
9687
|
+
}
|
|
9688
|
+
await authMetadataRepository.set(BACKFILL_KEY, "done");
|
|
9689
|
+
return { normalized, conflicts };
|
|
9690
|
+
}
|
|
9691
|
+
|
|
9213
9692
|
// src/server/services/permission.service.ts
|
|
9214
9693
|
init_repositories();
|
|
9215
9694
|
import { ForbiddenError } from "@spfn/core/errors";
|
|
@@ -9224,7 +9703,7 @@ async function getUserPermissions(userId) {
|
|
|
9224
9703
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
9225
9704
|
if (permIds.length > 0) {
|
|
9226
9705
|
const rolePerms = await Promise.all(
|
|
9227
|
-
permIds.map((
|
|
9706
|
+
permIds.map((id14) => permissionsRepository.findById(id14))
|
|
9228
9707
|
);
|
|
9229
9708
|
for (const perm of rolePerms) {
|
|
9230
9709
|
if (perm && perm.isActive) {
|
|
@@ -9299,10 +9778,10 @@ init_role_service();
|
|
|
9299
9778
|
|
|
9300
9779
|
// src/server/services/invitation.service.ts
|
|
9301
9780
|
init_repositories();
|
|
9302
|
-
import
|
|
9781
|
+
import crypto6 from "crypto";
|
|
9303
9782
|
import { BadRequestError, NotFoundError as NotFoundError3, ConflictError } from "@spfn/core/errors";
|
|
9304
9783
|
function generateInvitationToken() {
|
|
9305
|
-
return
|
|
9784
|
+
return crypto6.randomUUID();
|
|
9306
9785
|
}
|
|
9307
9786
|
function calculateExpiresAt(days = 7) {
|
|
9308
9787
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
@@ -9437,20 +9916,20 @@ async function acceptInvitation(params) {
|
|
|
9437
9916
|
async function listInvitations(params) {
|
|
9438
9917
|
return await invitationsRepository.list(params);
|
|
9439
9918
|
}
|
|
9440
|
-
async function cancelInvitation(
|
|
9441
|
-
const invitation = await invitationsRepository.findById(
|
|
9919
|
+
async function cancelInvitation(id14, cancelledBy, reason) {
|
|
9920
|
+
const invitation = await invitationsRepository.findById(id14);
|
|
9442
9921
|
if (!invitation) {
|
|
9443
9922
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
9444
9923
|
}
|
|
9445
9924
|
if (invitation.status !== "pending") {
|
|
9446
9925
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
9447
9926
|
}
|
|
9448
|
-
await invitationsRepository.cancel(
|
|
9927
|
+
await invitationsRepository.cancel(id14, cancelledBy, reason, invitation.metadata);
|
|
9449
9928
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
9450
9929
|
}
|
|
9451
|
-
async function deleteInvitation(
|
|
9452
|
-
await invitationsRepository.deleteById(
|
|
9453
|
-
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${
|
|
9930
|
+
async function deleteInvitation(id14) {
|
|
9931
|
+
await invitationsRepository.deleteById(id14);
|
|
9932
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id14}`);
|
|
9454
9933
|
}
|
|
9455
9934
|
async function expireOldInvitations() {
|
|
9456
9935
|
const count = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -9459,8 +9938,8 @@ async function expireOldInvitations() {
|
|
|
9459
9938
|
}
|
|
9460
9939
|
return count;
|
|
9461
9940
|
}
|
|
9462
|
-
async function resendInvitation(
|
|
9463
|
-
const invitation = await invitationsRepository.findById(
|
|
9941
|
+
async function resendInvitation(id14, expiresInDays = 7) {
|
|
9942
|
+
const invitation = await invitationsRepository.findById(id14);
|
|
9464
9943
|
if (!invitation) {
|
|
9465
9944
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
9466
9945
|
}
|
|
@@ -9468,7 +9947,7 @@ async function resendInvitation(id13, expiresInDays = 7) {
|
|
|
9468
9947
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
9469
9948
|
}
|
|
9470
9949
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
9471
|
-
const updated = await invitationsRepository.resend(
|
|
9950
|
+
const updated = await invitationsRepository.resend(id14, newExpiresAt);
|
|
9472
9951
|
if (!updated) {
|
|
9473
9952
|
throw new Error("Failed to update invitation");
|
|
9474
9953
|
}
|
|
@@ -9622,7 +10101,7 @@ async function updateUserProfileService(userId, params) {
|
|
|
9622
10101
|
|
|
9623
10102
|
// src/server/services/oauth.service.ts
|
|
9624
10103
|
init_repositories();
|
|
9625
|
-
import { env as
|
|
10104
|
+
import { env as env12 } from "@spfn/auth/config";
|
|
9626
10105
|
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
9627
10106
|
import {
|
|
9628
10107
|
AccountDisabledError as AccountDisabledError2,
|
|
@@ -9631,21 +10110,21 @@ import {
|
|
|
9631
10110
|
} from "@spfn/auth/errors";
|
|
9632
10111
|
|
|
9633
10112
|
// src/server/lib/oauth/google.ts
|
|
9634
|
-
import { env as
|
|
10113
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
9635
10114
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
9636
10115
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
9637
10116
|
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
9638
10117
|
function isGoogleOAuthEnabled() {
|
|
9639
|
-
return !!(
|
|
10118
|
+
return !!(env8.SPFN_AUTH_GOOGLE_CLIENT_ID && env8.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
9640
10119
|
}
|
|
9641
10120
|
function getGoogleOAuthConfig() {
|
|
9642
|
-
const clientId =
|
|
9643
|
-
const clientSecret =
|
|
10121
|
+
const clientId = env8.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
10122
|
+
const clientSecret = env8.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
9644
10123
|
if (!clientId || !clientSecret) {
|
|
9645
10124
|
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
9646
10125
|
}
|
|
9647
|
-
const baseUrl =
|
|
9648
|
-
const redirectUri =
|
|
10126
|
+
const baseUrl = env8.NEXT_PUBLIC_SPFN_APP_URL || env8.SPFN_APP_URL;
|
|
10127
|
+
const redirectUri = env8.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
9649
10128
|
return {
|
|
9650
10129
|
clientId,
|
|
9651
10130
|
clientSecret,
|
|
@@ -9653,7 +10132,7 @@ function getGoogleOAuthConfig() {
|
|
|
9653
10132
|
};
|
|
9654
10133
|
}
|
|
9655
10134
|
function getDefaultScopes() {
|
|
9656
|
-
const envScopes =
|
|
10135
|
+
const envScopes = env8.SPFN_AUTH_GOOGLE_SCOPES;
|
|
9657
10136
|
if (envScopes) {
|
|
9658
10137
|
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9659
10138
|
}
|
|
@@ -9731,9 +10210,9 @@ async function refreshAccessToken(refreshToken) {
|
|
|
9731
10210
|
|
|
9732
10211
|
// src/server/lib/oauth/state.ts
|
|
9733
10212
|
import * as jose from "jose";
|
|
9734
|
-
import { env as
|
|
10213
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
9735
10214
|
async function getStateKey() {
|
|
9736
|
-
const secret =
|
|
10215
|
+
const secret = env9.SPFN_AUTH_SESSION_SECRET;
|
|
9737
10216
|
const encoder = new TextEncoder();
|
|
9738
10217
|
const data = encoder.encode(`oauth-state:${secret}`);
|
|
9739
10218
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -9782,8 +10261,8 @@ var registry2 = /* @__PURE__ */ new Map();
|
|
|
9782
10261
|
function registerOAuthProvider(provider) {
|
|
9783
10262
|
registry2.set(provider.id, provider);
|
|
9784
10263
|
}
|
|
9785
|
-
function getOAuthProvider(
|
|
9786
|
-
return registry2.get(
|
|
10264
|
+
function getOAuthProvider(id14) {
|
|
10265
|
+
return registry2.get(id14);
|
|
9787
10266
|
}
|
|
9788
10267
|
function getRegisteredProviders() {
|
|
9789
10268
|
return [...registry2.values()];
|
|
@@ -9836,14 +10315,14 @@ async function verifySignature(params) {
|
|
|
9836
10315
|
init_token_cipher();
|
|
9837
10316
|
|
|
9838
10317
|
// src/server/lib/oauth/google-provider.ts
|
|
9839
|
-
import { env as
|
|
10318
|
+
import { env as env10 } from "@spfn/auth/config";
|
|
9840
10319
|
import { NativeSignInUnsupportedError } from "@spfn/auth/errors";
|
|
9841
10320
|
var GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
|
|
9842
10321
|
var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
9843
10322
|
function getGoogleNativeAudiences() {
|
|
9844
|
-
const ids = (
|
|
9845
|
-
if (
|
|
9846
|
-
ids.push(
|
|
10323
|
+
const ids = (env10.SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10324
|
+
if (env10.SPFN_AUTH_GOOGLE_CLIENT_ID) {
|
|
10325
|
+
ids.push(env10.SPFN_AUTH_GOOGLE_CLIENT_ID);
|
|
9847
10326
|
}
|
|
9848
10327
|
return ids;
|
|
9849
10328
|
}
|
|
@@ -9905,13 +10384,13 @@ registerOAuthProvider(googleProvider);
|
|
|
9905
10384
|
|
|
9906
10385
|
// src/server/lib/oauth/apple-provider.ts
|
|
9907
10386
|
import { createHash as createHash2 } from "crypto";
|
|
9908
|
-
import { env as
|
|
10387
|
+
import { env as env11 } from "@spfn/auth/config";
|
|
9909
10388
|
import { ValidationError as ValidationError4 } from "@spfn/core/errors";
|
|
9910
10389
|
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError2 } from "@spfn/auth/errors";
|
|
9911
10390
|
var APPLE_JWKS_URI = "https://appleid.apple.com/auth/keys";
|
|
9912
10391
|
var APPLE_ISSUER = "https://appleid.apple.com";
|
|
9913
10392
|
function getAppleClientIds() {
|
|
9914
|
-
return (
|
|
10393
|
+
return (env11.SPFN_AUTH_APPLE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
9915
10394
|
}
|
|
9916
10395
|
function hashNonce(rawNonce) {
|
|
9917
10396
|
return createHash2("sha256").update(rawNonce).digest("hex");
|
|
@@ -10624,8 +11103,8 @@ async function oauthCallbackService(params) {
|
|
|
10624
11103
|
algorithm: stateData.algorithm
|
|
10625
11104
|
});
|
|
10626
11105
|
await updateLastLoginService(userId);
|
|
10627
|
-
const appUrl =
|
|
10628
|
-
const callbackPath =
|
|
11106
|
+
const appUrl = env12.NEXT_PUBLIC_SPFN_APP_URL || env12.SPFN_APP_URL;
|
|
11107
|
+
const callbackPath = env12.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
10629
11108
|
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
10630
11109
|
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
10631
11110
|
userId: String(userId),
|
|
@@ -10753,7 +11232,7 @@ function buildRedirectUrl(baseUrl, params) {
|
|
|
10753
11232
|
return `${url.pathname}${url.search}`;
|
|
10754
11233
|
}
|
|
10755
11234
|
function buildOAuthErrorUrl(error) {
|
|
10756
|
-
const errorUrl =
|
|
11235
|
+
const errorUrl = env12.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
10757
11236
|
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
10758
11237
|
}
|
|
10759
11238
|
function isOAuthProviderEnabled(provider) {
|
|
@@ -10919,8 +11398,8 @@ async function verifyOpsTokenService(token) {
|
|
|
10919
11398
|
scopes: record.scopes
|
|
10920
11399
|
};
|
|
10921
11400
|
}
|
|
10922
|
-
async function revokeOpsTokenService(
|
|
10923
|
-
return await opsTokensRepository.revokeById(
|
|
11401
|
+
async function revokeOpsTokenService(id14) {
|
|
11402
|
+
return await opsTokensRepository.revokeById(id14);
|
|
10924
11403
|
}
|
|
10925
11404
|
async function listOpsTokensService() {
|
|
10926
11405
|
return await opsTokensRepository.list();
|
|
@@ -10932,6 +11411,7 @@ import { Transactional } from "@spfn/core/db";
|
|
|
10932
11411
|
import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
10933
11412
|
|
|
10934
11413
|
// src/server/lib/rate-limit-keys.ts
|
|
11414
|
+
init_email();
|
|
10935
11415
|
import { createHash as createHash5 } from "crypto";
|
|
10936
11416
|
import { getClientIp } from "@spfn/core/middleware";
|
|
10937
11417
|
async function readJsonBody(c) {
|
|
@@ -10943,7 +11423,7 @@ async function readJsonBody(c) {
|
|
|
10943
11423
|
}
|
|
10944
11424
|
function accountKey(body) {
|
|
10945
11425
|
if (typeof body.email === "string" && body.email.trim()) {
|
|
10946
|
-
return `email:${body.email
|
|
11426
|
+
return `email:${normalizeEmail(body.email)}`;
|
|
10947
11427
|
}
|
|
10948
11428
|
if (typeof body.phone === "string" && body.phone.trim()) {
|
|
10949
11429
|
return `phone:${body.phone.trim()}`;
|
|
@@ -10955,7 +11435,7 @@ function targetKey(body) {
|
|
|
10955
11435
|
return void 0;
|
|
10956
11436
|
}
|
|
10957
11437
|
const type = typeof body.targetType === "string" ? body.targetType : "target";
|
|
10958
|
-
const value = type === "email" ? body.target
|
|
11438
|
+
const value = type === "email" ? normalizeEmail(body.target) : body.target.trim();
|
|
10959
11439
|
return `${type}:${value}`;
|
|
10960
11440
|
}
|
|
10961
11441
|
function byIpAndAccount(options = {}) {
|
|
@@ -11055,6 +11535,54 @@ var register = route.post("/_auth/register").input({
|
|
|
11055
11535
|
const { body } = await c.data();
|
|
11056
11536
|
return await registerService(body);
|
|
11057
11537
|
});
|
|
11538
|
+
var requestSignupLink = route.post("/_auth/signup/email").input({
|
|
11539
|
+
body: Type.Object({
|
|
11540
|
+
email: EmailSchema,
|
|
11541
|
+
returnPath: Type.Optional(Type.String({
|
|
11542
|
+
maxLength: 512,
|
|
11543
|
+
description: "Relative path within the app to return to after signup. Absolute URLs are rejected."
|
|
11544
|
+
}))
|
|
11545
|
+
})
|
|
11546
|
+
}).use([rateLimitPolicy("auth-signup-link", { limit: 5, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 20 }) })]).skip(["auth"]).handler(async (c) => {
|
|
11547
|
+
const { body } = await c.data();
|
|
11548
|
+
if (body.returnPath !== void 0 && !isSafeReturnPath(body.returnPath)) {
|
|
11549
|
+
throw new ValidationError9({ message: "returnPath must be a relative path within the app" });
|
|
11550
|
+
}
|
|
11551
|
+
return await requestSignupLinkService(body);
|
|
11552
|
+
});
|
|
11553
|
+
var confirmSignupLink = route.post("/_auth/signup/email/confirm").input({
|
|
11554
|
+
body: Type.Object({
|
|
11555
|
+
token: Type.String({
|
|
11556
|
+
minLength: 16,
|
|
11557
|
+
maxLength: 256,
|
|
11558
|
+
description: "Token from the confirmation link"
|
|
11559
|
+
})
|
|
11560
|
+
})
|
|
11561
|
+
}).use([rateLimitPolicy("auth-signup-confirm", { limit: 10, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
11562
|
+
const { body } = await c.data();
|
|
11563
|
+
return await confirmSignupLinkService(body);
|
|
11564
|
+
});
|
|
11565
|
+
var completeSignup = route.post("/_auth/signup/password").input({
|
|
11566
|
+
body: Type.Object({
|
|
11567
|
+
password: PasswordSchema,
|
|
11568
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
11569
|
+
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
11570
|
+
}))
|
|
11571
|
+
})
|
|
11572
|
+
}).interceptor({
|
|
11573
|
+
body: Type.Object({
|
|
11574
|
+
setupSecret: Type.String({ description: "Password-setup session secret, from the HttpOnly cookie" }),
|
|
11575
|
+
publicKey: Type.String({ description: "Client public key" }),
|
|
11576
|
+
keyId: Type.String({ description: "Key identifier" }),
|
|
11577
|
+
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
11578
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
11579
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
11580
|
+
platform: Type.Optional(PlatformSchema)
|
|
11581
|
+
})
|
|
11582
|
+
}).use([rateLimitPolicy("auth-signup-password", { limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
11583
|
+
const { body } = await c.data();
|
|
11584
|
+
return await completeSignupService(body);
|
|
11585
|
+
});
|
|
11058
11586
|
var login = route.post("/_auth/login").input({
|
|
11059
11587
|
body: Type.Object({
|
|
11060
11588
|
email: Type.Optional(EmailSchema),
|
|
@@ -11227,13 +11755,13 @@ var CanonicalJsonError = class extends Error {
|
|
|
11227
11755
|
var INT64_MIN = -(2n ** 63n);
|
|
11228
11756
|
var INT64_MAX = 2n ** 63n - 1n;
|
|
11229
11757
|
function parseCanonicalJson(bytes) {
|
|
11230
|
-
let
|
|
11758
|
+
let text14;
|
|
11231
11759
|
try {
|
|
11232
|
-
|
|
11760
|
+
text14 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
11233
11761
|
} catch {
|
|
11234
11762
|
throw new CanonicalJsonError("INVALID_UTF8");
|
|
11235
11763
|
}
|
|
11236
|
-
const parser = new Parser(
|
|
11764
|
+
const parser = new Parser(text14);
|
|
11237
11765
|
const value = parser.parseValue();
|
|
11238
11766
|
parser.skipWhitespace();
|
|
11239
11767
|
if (!parser.atEnd()) {
|
|
@@ -11254,8 +11782,8 @@ function isCanonicalBytes(bytes, value) {
|
|
|
11254
11782
|
return true;
|
|
11255
11783
|
}
|
|
11256
11784
|
var Parser = class {
|
|
11257
|
-
constructor(
|
|
11258
|
-
this.text =
|
|
11785
|
+
constructor(text14) {
|
|
11786
|
+
this.text = text14;
|
|
11259
11787
|
}
|
|
11260
11788
|
pos = 0;
|
|
11261
11789
|
atEnd() {
|
|
@@ -13168,7 +13696,7 @@ var deleteCookie = (c, name, opt) => {
|
|
|
13168
13696
|
init_types();
|
|
13169
13697
|
init_schema3();
|
|
13170
13698
|
import { Transactional as Transactional3 } from "@spfn/core/db";
|
|
13171
|
-
import { ValidationError as
|
|
13699
|
+
import { ValidationError as ValidationError10 } from "@spfn/core/errors";
|
|
13172
13700
|
import { rateLimitPolicy as rateLimitPolicy4 } from "@spfn/core/middleware";
|
|
13173
13701
|
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
13174
13702
|
var providerParams = Type.Object({
|
|
@@ -13288,10 +13816,10 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
|
13288
13816
|
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
13289
13817
|
const { body } = await c.data();
|
|
13290
13818
|
if (!isGoogleOAuthEnabled()) {
|
|
13291
|
-
throw new
|
|
13819
|
+
throw new ValidationError10({ message: "Google OAuth is not configured" });
|
|
13292
13820
|
}
|
|
13293
13821
|
if (!body.state) {
|
|
13294
|
-
throw new
|
|
13822
|
+
throw new ValidationError10({
|
|
13295
13823
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
13296
13824
|
});
|
|
13297
13825
|
}
|
|
@@ -13386,7 +13914,7 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
|
|
|
13386
13914
|
const { params, body } = await c.data();
|
|
13387
13915
|
const provider = requireEnabledProvider(params.provider);
|
|
13388
13916
|
if (!body.state) {
|
|
13389
|
-
throw new
|
|
13917
|
+
throw new ValidationError10({
|
|
13390
13918
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
13391
13919
|
});
|
|
13392
13920
|
}
|
|
@@ -13687,6 +14215,10 @@ var mainAuthRouter = defineRouter6({
|
|
|
13687
14215
|
sendVerificationCode,
|
|
13688
14216
|
verifyCode,
|
|
13689
14217
|
register,
|
|
14218
|
+
// Verified-email signup routes
|
|
14219
|
+
requestSignupLink,
|
|
14220
|
+
confirmSignupLink,
|
|
14221
|
+
completeSignup,
|
|
13690
14222
|
login,
|
|
13691
14223
|
logout,
|
|
13692
14224
|
rotateKey,
|
|
@@ -13746,11 +14278,11 @@ init_types();
|
|
|
13746
14278
|
init_schema3();
|
|
13747
14279
|
|
|
13748
14280
|
// src/server/lib/crypto.ts
|
|
13749
|
-
import
|
|
14281
|
+
import crypto7 from "crypto";
|
|
13750
14282
|
import jwt3 from "jsonwebtoken";
|
|
13751
14283
|
function generateKeyPairES256() {
|
|
13752
|
-
const keyId =
|
|
13753
|
-
const { privateKey, publicKey } =
|
|
14284
|
+
const keyId = crypto7.randomUUID();
|
|
14285
|
+
const { privateKey, publicKey } = crypto7.generateKeyPairSync("ec", {
|
|
13754
14286
|
namedCurve: "P-256",
|
|
13755
14287
|
// ES256
|
|
13756
14288
|
publicKeyEncoding: {
|
|
@@ -13764,7 +14296,7 @@ function generateKeyPairES256() {
|
|
|
13764
14296
|
});
|
|
13765
14297
|
const privateKeyB64 = privateKey.toString("base64");
|
|
13766
14298
|
const publicKeyB64 = publicKey.toString("base64");
|
|
13767
|
-
const fingerprint =
|
|
14299
|
+
const fingerprint = crypto7.createHash("sha256").update(publicKey).digest("hex");
|
|
13768
14300
|
return {
|
|
13769
14301
|
privateKey: privateKeyB64,
|
|
13770
14302
|
publicKey: publicKeyB64,
|
|
@@ -13774,8 +14306,8 @@ function generateKeyPairES256() {
|
|
|
13774
14306
|
};
|
|
13775
14307
|
}
|
|
13776
14308
|
function generateKeyPairRS256() {
|
|
13777
|
-
const keyId =
|
|
13778
|
-
const { privateKey, publicKey } =
|
|
14309
|
+
const keyId = crypto7.randomUUID();
|
|
14310
|
+
const { privateKey, publicKey } = crypto7.generateKeyPairSync("rsa", {
|
|
13779
14311
|
modulusLength: 2048,
|
|
13780
14312
|
publicKeyEncoding: {
|
|
13781
14313
|
type: "spki",
|
|
@@ -13788,7 +14320,7 @@ function generateKeyPairRS256() {
|
|
|
13788
14320
|
});
|
|
13789
14321
|
const privateKeyB64 = privateKey.toString("base64");
|
|
13790
14322
|
const publicKeyB64 = publicKey.toString("base64");
|
|
13791
|
-
const fingerprint =
|
|
14323
|
+
const fingerprint = crypto7.createHash("sha256").update(publicKey).digest("hex");
|
|
13792
14324
|
return {
|
|
13793
14325
|
privateKey: privateKeyB64,
|
|
13794
14326
|
publicKey: publicKeyB64,
|
|
@@ -13803,7 +14335,7 @@ function generateKeyPair(algorithm = "ES256") {
|
|
|
13803
14335
|
function generateClientToken(payload, privateKeyB64, algorithm, options) {
|
|
13804
14336
|
try {
|
|
13805
14337
|
const privateKeyDER = Buffer.from(privateKeyB64, "base64");
|
|
13806
|
-
const privateKeyObject =
|
|
14338
|
+
const privateKeyObject = crypto7.createPrivateKey({
|
|
13807
14339
|
key: privateKeyDER,
|
|
13808
14340
|
format: "der",
|
|
13809
14341
|
type: "pkcs8"
|
|
@@ -13847,10 +14379,10 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
13847
14379
|
|
|
13848
14380
|
// src/server/lib/session.ts
|
|
13849
14381
|
import * as jose2 from "jose";
|
|
13850
|
-
import { env as
|
|
14382
|
+
import { env as env13 } from "@spfn/auth/config";
|
|
13851
14383
|
import { env as coreEnv } from "@spfn/core/config";
|
|
13852
14384
|
async function getSessionSecretKey() {
|
|
13853
|
-
const secret =
|
|
14385
|
+
const secret = env13.SPFN_AUTH_SESSION_SECRET;
|
|
13854
14386
|
const encoder = new TextEncoder();
|
|
13855
14387
|
const data = encoder.encode(secret);
|
|
13856
14388
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -13932,14 +14464,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
13932
14464
|
}
|
|
13933
14465
|
|
|
13934
14466
|
// src/server/setup.ts
|
|
13935
|
-
import { env as
|
|
14467
|
+
import { env as env14 } from "@spfn/auth/config";
|
|
13936
14468
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
13937
14469
|
init_repositories();
|
|
13938
14470
|
function parseAdminAccounts() {
|
|
13939
14471
|
const accounts = [];
|
|
13940
|
-
if (
|
|
14472
|
+
if (env14.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
13941
14473
|
try {
|
|
13942
|
-
const accountsJson =
|
|
14474
|
+
const accountsJson = env14.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
13943
14475
|
const parsed = JSON.parse(accountsJson);
|
|
13944
14476
|
if (!Array.isArray(parsed)) {
|
|
13945
14477
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -13966,11 +14498,11 @@ function parseAdminAccounts() {
|
|
|
13966
14498
|
return accounts;
|
|
13967
14499
|
}
|
|
13968
14500
|
}
|
|
13969
|
-
const adminEmails =
|
|
14501
|
+
const adminEmails = env14.SPFN_AUTH_ADMIN_EMAILS;
|
|
13970
14502
|
if (adminEmails) {
|
|
13971
14503
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
13972
|
-
const passwords = (
|
|
13973
|
-
const roles2 = (
|
|
14504
|
+
const passwords = (env14.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
14505
|
+
const roles2 = (env14.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
13974
14506
|
if (passwords.length !== emails.length) {
|
|
13975
14507
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
13976
14508
|
return accounts;
|
|
@@ -13992,8 +14524,8 @@ function parseAdminAccounts() {
|
|
|
13992
14524
|
}
|
|
13993
14525
|
return accounts;
|
|
13994
14526
|
}
|
|
13995
|
-
const adminEmail =
|
|
13996
|
-
const adminPassword =
|
|
14527
|
+
const adminEmail = env14.SPFN_AUTH_ADMIN_EMAIL;
|
|
14528
|
+
const adminPassword = env14.SPFN_AUTH_ADMIN_PASSWORD;
|
|
13997
14529
|
if (adminEmail && adminPassword) {
|
|
13998
14530
|
accounts.push({
|
|
13999
14531
|
email: adminEmail,
|
|
@@ -14016,7 +14548,7 @@ async function ensureAdminExists() {
|
|
|
14016
14548
|
for (const account of accounts) {
|
|
14017
14549
|
authLogger.setup.info(`Creating ${account.email} admin account(s)...`);
|
|
14018
14550
|
try {
|
|
14019
|
-
const existing = await usersRepository.
|
|
14551
|
+
const existing = await usersRepository.findByEmailInAnyStoredForm(account.email);
|
|
14020
14552
|
if (existing) {
|
|
14021
14553
|
authLogger.setup.info(`\u26A0\uFE0F Account already exists: ${account.email} (skipped)`);
|
|
14022
14554
|
skipped++;
|
|
@@ -14068,6 +14600,14 @@ function createAuthLifecycle(options = {}) {
|
|
|
14068
14600
|
*/
|
|
14069
14601
|
afterInfrastructure: async () => {
|
|
14070
14602
|
await initializeAuth(options);
|
|
14603
|
+
try {
|
|
14604
|
+
await normalizeStoredEmails();
|
|
14605
|
+
} catch (error) {
|
|
14606
|
+
authLogger.service.error(
|
|
14607
|
+
"Stored email normalization did not complete. Addresses stay as they are, so an account stored in mixed case cannot sign in until a later boot succeeds.",
|
|
14608
|
+
{ error }
|
|
14609
|
+
);
|
|
14610
|
+
}
|
|
14071
14611
|
await ensureAdminExists();
|
|
14072
14612
|
initOneTimeTokenManager(options.oneTimeToken);
|
|
14073
14613
|
}
|
|
@@ -14121,6 +14661,7 @@ export {
|
|
|
14121
14661
|
RolePermissionsRepository,
|
|
14122
14662
|
RolesRepository,
|
|
14123
14663
|
SOCIAL_PROVIDERS,
|
|
14664
|
+
SignupLinkTokensRepository,
|
|
14124
14665
|
SocialAccountsRepository,
|
|
14125
14666
|
TargetTypeSchema,
|
|
14126
14667
|
USER_STATUSES,
|
|
@@ -14155,9 +14696,11 @@ export {
|
|
|
14155
14696
|
cancelInvitation,
|
|
14156
14697
|
changePasswordService,
|
|
14157
14698
|
checkUsernameAvailableService,
|
|
14699
|
+
completeSignupService,
|
|
14158
14700
|
configureAuth,
|
|
14159
14701
|
configureDeletion,
|
|
14160
14702
|
configureOAuthTokenCipher,
|
|
14703
|
+
confirmSignupLinkService,
|
|
14161
14704
|
createAuthDeletionJobRouter,
|
|
14162
14705
|
createAuthDeletionPurgeJob,
|
|
14163
14706
|
createAuthLifecycle,
|
|
@@ -14228,6 +14771,7 @@ export {
|
|
|
14228
14771
|
isEncrypted,
|
|
14229
14772
|
isGoogleOAuthEnabled,
|
|
14230
14773
|
isOAuthProviderEnabled,
|
|
14774
|
+
isSafeReturnPath,
|
|
14231
14775
|
issueOneTimeTokenService,
|
|
14232
14776
|
issueOpsTokenService,
|
|
14233
14777
|
kakaoProvider,
|
|
@@ -14239,6 +14783,9 @@ export {
|
|
|
14239
14783
|
logoutService,
|
|
14240
14784
|
matchOAuthCsrfCookies,
|
|
14241
14785
|
naverProvider,
|
|
14786
|
+
normalizeEmail,
|
|
14787
|
+
normalizeOptionalEmail,
|
|
14788
|
+
normalizeStoredEmails,
|
|
14242
14789
|
oauthCallbackService,
|
|
14243
14790
|
oauthNativeService,
|
|
14244
14791
|
oauthStartService,
|
|
@@ -14259,6 +14806,7 @@ export {
|
|
|
14259
14806
|
registerService,
|
|
14260
14807
|
removePermissionFromRole,
|
|
14261
14808
|
requestAccountDeletionService,
|
|
14809
|
+
requestSignupLinkService,
|
|
14262
14810
|
requireAnyPermission,
|
|
14263
14811
|
requireEnabledProvider,
|
|
14264
14812
|
requireOpsScope,
|
|
@@ -14283,6 +14831,8 @@ export {
|
|
|
14283
14831
|
setRolePermissions,
|
|
14284
14832
|
shouldRefreshSession,
|
|
14285
14833
|
shouldRotateKey,
|
|
14834
|
+
signupLinkTokens,
|
|
14835
|
+
signupLinkTokensRepository,
|
|
14286
14836
|
socialAccountsRepository,
|
|
14287
14837
|
sweepDuePurges,
|
|
14288
14838
|
unsealSession,
|