najm-auth 6.3.1 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{NajmAuthClient-BygfBCpF.d.ts → NajmAuthClient-B1Gv8PIw.d.ts} +1 -1
- package/dist/client/edge.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/react/index.d.ts +2 -2
- package/dist/client/server/index.d.ts +4 -4
- package/dist/client/server/index.js +1 -1
- package/dist/client/server/react.d.ts +2 -2
- package/dist/{getSession-DBX8XpsR.d.ts → getSession-DAQCguP3.d.ts} +1 -1
- package/dist/index.d.ts +35 -7
- package/dist/index.js +97 -29
- package/dist/schema/pg.d.ts +2 -2
- package/dist/schema/pg.js +7 -3
- package/dist/schema/sqlite.d.ts +2 -2
- package/dist/schema/sqlite.js +5 -3
- package/dist/{types-CI2t8wpJ.d.ts → types-axhOL6D1.d.ts} +2 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as RetryConfig, a as RequestOptions, A as AuthUser, b as AuthClientConfig, L as LoginCredentials, c as LoginResult, O as OAuthProvider, d as OAuthLoginOptions, e as AuthState, f as AuthEvent, g as AuthEventHandler } from './types-
|
|
1
|
+
import { R as RetryConfig, a as RequestOptions, A as AuthUser, b as AuthClientConfig, L as LoginCredentials, c as LoginResult, O as OAuthProvider, d as OAuthLoginOptions, e as AuthState, f as AuthEvent, g as AuthEventHandler } from './types-axhOL6D1.js';
|
|
2
2
|
|
|
3
3
|
interface FetchClientConfig {
|
|
4
4
|
baseURL: string;
|
package/dist/client/edge.js
CHANGED
|
@@ -139,7 +139,7 @@ function isRecord(value) {
|
|
|
139
139
|
}
|
|
140
140
|
__name(isRecord, "isRecord");
|
|
141
141
|
function isValidUser(value) {
|
|
142
|
-
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.email === "string" && value.email.length > 0 && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
142
|
+
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && (value.email === null || typeof value.email === "string" && value.email.length > 0) && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
143
143
|
}
|
|
144
144
|
__name(isValidUser, "isValidUser");
|
|
145
145
|
function isStringArray(value) {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-
|
|
2
|
-
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-
|
|
3
|
-
export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-
|
|
1
|
+
export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-B1Gv8PIw.js';
|
|
2
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-axhOL6D1.js';
|
|
3
|
+
export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-axhOL6D1.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Decode a JWT token payload without verification.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, CSSProperties, ReactElement } from 'react';
|
|
3
|
-
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-
|
|
4
|
-
import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-
|
|
3
|
+
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-B1Gv8PIw.js';
|
|
4
|
+
import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-axhOL6D1.js';
|
|
5
5
|
|
|
6
6
|
interface AuthProviderProps {
|
|
7
7
|
client: NajmAuthClient;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { A as AuthUser, R as RetryConfig } from '../../types-
|
|
2
|
-
import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-
|
|
1
|
+
import { A as AuthUser, R as RetryConfig } from '../../types-axhOL6D1.js';
|
|
2
|
+
import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-B1Gv8PIw.js';
|
|
3
3
|
import { AuthProxyOptions, ProxySessionMode } from '../edge.js';
|
|
4
4
|
export { withAuthMiddleware } from '../edge.js';
|
|
5
|
-
import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-
|
|
6
|
-
export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-
|
|
5
|
+
import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-DAQCguP3.js';
|
|
6
|
+
export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-DAQCguP3.js';
|
|
7
7
|
import { S as SessionRecoveryFailure } from '../../sessionRecovery-D5Fa0yZ1.js';
|
|
8
8
|
export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../../sessionRecovery-D5Fa0yZ1.js';
|
|
9
9
|
import 'next/server';
|
|
@@ -134,7 +134,7 @@ function isRecord(value) {
|
|
|
134
134
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
135
135
|
}
|
|
136
136
|
function isValidUser(value) {
|
|
137
|
-
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.email === "string" && value.email.length > 0 && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
137
|
+
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && (value.email === null || typeof value.email === "string" && value.email.length > 0) && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
138
138
|
}
|
|
139
139
|
function isStringArray(value) {
|
|
140
140
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
package/dist/index.d.ts
CHANGED
|
@@ -379,7 +379,8 @@ interface TokenPair {
|
|
|
379
379
|
*/
|
|
380
380
|
interface AuthUser {
|
|
381
381
|
id: string;
|
|
382
|
-
|
|
382
|
+
/** Null for an account reached by phone only. */
|
|
383
|
+
email: string | null;
|
|
383
384
|
name?: string | null;
|
|
384
385
|
role?: string;
|
|
385
386
|
status?: string;
|
|
@@ -462,6 +463,7 @@ var users = {
|
|
|
462
463
|
notFound: "User not found",
|
|
463
464
|
idExists: "User ID already exists",
|
|
464
465
|
emailRequired: "Email is required",
|
|
466
|
+
emailOrPhoneRequired: "An email address or a phone number is required",
|
|
465
467
|
phoneExists: "Phone number already registered",
|
|
466
468
|
passwordRequired: "Password is required",
|
|
467
469
|
invalidEmail: "Invalid email format",
|
|
@@ -592,6 +594,7 @@ declare const AUTH_LOCALES: {
|
|
|
592
594
|
notFound: string;
|
|
593
595
|
idExists: string;
|
|
594
596
|
emailRequired: string;
|
|
597
|
+
emailOrPhoneRequired: string;
|
|
595
598
|
phoneExists: string;
|
|
596
599
|
passwordRequired: string;
|
|
597
600
|
invalidEmail: string;
|
|
@@ -667,7 +670,7 @@ interface SessionCookieData {
|
|
|
667
670
|
/** Must satisfy the AuthUser contract — see AuthUser in ../types. */
|
|
668
671
|
user: {
|
|
669
672
|
id: string;
|
|
670
|
-
email: string;
|
|
673
|
+
email: string | null;
|
|
671
674
|
name?: string | null;
|
|
672
675
|
role?: string;
|
|
673
676
|
status?: string;
|
|
@@ -744,6 +747,10 @@ declare class UserRepository {
|
|
|
744
747
|
getByEmailInsensitive(email: string): Promise<(User & {
|
|
745
748
|
role?: string | null;
|
|
746
749
|
}) | undefined>;
|
|
750
|
+
/** Login by phone reads the row it matched; the account may have no email. */
|
|
751
|
+
getByPhoneWithCredentials(phone: string): Promise<(User & {
|
|
752
|
+
role?: string | null;
|
|
753
|
+
}) | undefined>;
|
|
747
754
|
create(data: NewUser): Promise<User>;
|
|
748
755
|
/**
|
|
749
756
|
* Ids of everyone holding a role. Used to end sessions when the role's
|
|
@@ -779,11 +786,17 @@ declare class UserValidator {
|
|
|
779
786
|
/**
|
|
780
787
|
* Check if email already exists in database
|
|
781
788
|
*/
|
|
782
|
-
checkEmailUnique(email: string, excludeId?: string): Promise<void>;
|
|
789
|
+
checkEmailUnique(email: string | null | undefined, excludeId?: string): Promise<void>;
|
|
783
790
|
/**
|
|
784
791
|
* Check that a normalized phone number is not already taken.
|
|
785
792
|
*/
|
|
786
793
|
checkPhoneUnique(phone: string, excludeId?: string): Promise<void>;
|
|
794
|
+
/**
|
|
795
|
+
* An account is reached by its email, its phone, or both. Refused here with
|
|
796
|
+
* a message the caller can show; the `users_email_or_phone` check is the
|
|
797
|
+
* database's own copy of the rule.
|
|
798
|
+
*/
|
|
799
|
+
checkHasIdentity(email: string | null | undefined, phone: string | null | undefined): void;
|
|
787
800
|
/** The database remains authoritative when two validation reads both pass. */
|
|
788
801
|
writeUnique<T>(write: () => Promise<T>): Promise<T>;
|
|
789
802
|
checkCredentialUnchanged(updated: unknown): void;
|
|
@@ -1219,6 +1232,11 @@ declare class UserService {
|
|
|
1219
1232
|
* so, and a second call is harmless.
|
|
1220
1233
|
*/
|
|
1221
1234
|
private invalidateSessions;
|
|
1235
|
+
/**
|
|
1236
|
+
* Stored in the same normalized form login lookup resolves to, or phone
|
|
1237
|
+
* login would never find the row. Blank means no phone.
|
|
1238
|
+
*/
|
|
1239
|
+
private normalizePhone;
|
|
1222
1240
|
private sanitizeUser;
|
|
1223
1241
|
private sanitizeUsers;
|
|
1224
1242
|
private requireUser;
|
|
@@ -1240,6 +1258,10 @@ declare class UserService {
|
|
|
1240
1258
|
role?: string | null;
|
|
1241
1259
|
}) | undefined>;
|
|
1242
1260
|
findByPhone(phone: string): Promise<UserWithPermissions>;
|
|
1261
|
+
/** The credential row behind a phone login, for timing-safe authentication. */
|
|
1262
|
+
findCredentialsByPhone(phone: string): Promise<(User & {
|
|
1263
|
+
role?: string | null;
|
|
1264
|
+
}) | undefined>;
|
|
1243
1265
|
getAuthRecordById(id: string): Promise<User | undefined>;
|
|
1244
1266
|
create(data: Record<string, any>, options?: {
|
|
1245
1267
|
validatePasswordStrength?: boolean;
|
|
@@ -1599,7 +1621,8 @@ declare class TokenService {
|
|
|
1599
1621
|
|
|
1600
1622
|
declare const createUserDto: z.ZodObject<{
|
|
1601
1623
|
name: z.ZodOptional<z.ZodString>;
|
|
1602
|
-
email: z.ZodString
|
|
1624
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1625
|
+
phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1603
1626
|
password: z.ZodString;
|
|
1604
1627
|
roleId: z.ZodOptional<z.ZodString>;
|
|
1605
1628
|
image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
@@ -1612,7 +1635,8 @@ declare const createUserDto: z.ZodObject<{
|
|
|
1612
1635
|
}, z.core.$strip>;
|
|
1613
1636
|
declare const updateUserDto: z.ZodObject<{
|
|
1614
1637
|
name: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
1615
|
-
email: z.ZodOptional<z.ZodString
|
|
1638
|
+
email: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
1639
|
+
phone: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
1616
1640
|
password: z.ZodOptional<z.ZodString>;
|
|
1617
1641
|
roleId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
1618
1642
|
image: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
@@ -1837,9 +1861,13 @@ declare class PasswordSetupService {
|
|
|
1837
1861
|
type ProvisionUserInput = {
|
|
1838
1862
|
id?: string;
|
|
1839
1863
|
name?: string;
|
|
1840
|
-
|
|
1864
|
+
/**
|
|
1865
|
+
* At least one of `email` and `phone` is required. Blank counts as absent;
|
|
1866
|
+
* an account with no email is created without an invitation mail.
|
|
1867
|
+
*/
|
|
1868
|
+
email?: string | null;
|
|
1841
1869
|
/** Normalized through the configured identity preset before it is stored. */
|
|
1842
|
-
phone?: string;
|
|
1870
|
+
phone?: string | null;
|
|
1843
1871
|
role?: string;
|
|
1844
1872
|
roleId?: string;
|
|
1845
1873
|
image?: string | null;
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var AUTH_PERMISSIONS = Symbol.for("najm:auth:permissions");
|
|
|
18
18
|
var AUTH_ENCRYPTION_KEY = Symbol.for("najm:auth:encryption-key");
|
|
19
19
|
|
|
20
20
|
// src/schema/pg.ts
|
|
21
|
-
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex } from "drizzle-orm/pg-core";
|
|
21
|
+
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex, check } from "drizzle-orm/pg-core";
|
|
22
22
|
import { sql } from "drizzle-orm";
|
|
23
23
|
import { nanoid } from "nanoid";
|
|
24
24
|
|
|
@@ -49,7 +49,10 @@ var rolesTable = pgTable("roles", {
|
|
|
49
49
|
var usersTable = pgTable("users", {
|
|
50
50
|
...baseFields(8),
|
|
51
51
|
name: text("name"),
|
|
52
|
-
email
|
|
52
|
+
// An account is reached by its email, its phone, or both — never neither.
|
|
53
|
+
// Email is nullable so several phone-only accounts can coexist: a NOT NULL
|
|
54
|
+
// column forced them all to "" and the second one broke the unique index.
|
|
55
|
+
email: text("email").unique(),
|
|
53
56
|
emailVerified: boolean("email_verified").default(false),
|
|
54
57
|
phone: text("phone").unique(),
|
|
55
58
|
phoneVerified: boolean("phone_verified").default(false),
|
|
@@ -61,7 +64,8 @@ var usersTable = pgTable("users", {
|
|
|
61
64
|
failedLoginAttempts: integer("failed_login_attempts").default(0),
|
|
62
65
|
lockoutUntil: timestamp("lockout_until", { mode: "string" })
|
|
63
66
|
}, (table) => ({
|
|
64
|
-
roleIdx: index("users_role_id_idx").on(table.roleId)
|
|
67
|
+
roleIdx: index("users_role_id_idx").on(table.roleId),
|
|
68
|
+
emailOrPhone: check("users_email_or_phone", sql`${table.email} is not null or ${table.phone} is not null`)
|
|
65
69
|
}));
|
|
66
70
|
var oauthAccountsTable = pgTable("oauth_accounts", {
|
|
67
71
|
...baseFields(10),
|
|
@@ -137,7 +141,7 @@ var authSchema = {
|
|
|
137
141
|
};
|
|
138
142
|
|
|
139
143
|
// src/schema/sqlite.ts
|
|
140
|
-
import { sqliteTable, text as text2, integer as integer2, primaryKey as primaryKey2, uniqueIndex as uniqueIndex2, index as index2 } from "drizzle-orm/sqlite-core";
|
|
144
|
+
import { sqliteTable, text as text2, integer as integer2, primaryKey as primaryKey2, uniqueIndex as uniqueIndex2, index as index2, check as check2 } from "drizzle-orm/sqlite-core";
|
|
141
145
|
import { sql as sql2 } from "drizzle-orm";
|
|
142
146
|
import { nanoid as nanoid2 } from "nanoid";
|
|
143
147
|
var baseFields2 = /* @__PURE__ */ __name((idLength = 5) => ({
|
|
@@ -156,7 +160,8 @@ var rolesTable2 = sqliteTable("roles", {
|
|
|
156
160
|
var usersTable2 = sqliteTable("users", {
|
|
157
161
|
...baseFields2(8),
|
|
158
162
|
name: text2("name"),
|
|
159
|
-
email
|
|
163
|
+
// Nullable, with at least one of email or phone; see schema/pg.ts.
|
|
164
|
+
email: text2("email").unique(),
|
|
160
165
|
emailVerified: integer2("email_verified", { mode: "boolean" }).default(false),
|
|
161
166
|
phone: text2("phone").unique(),
|
|
162
167
|
phoneVerified: integer2("phone_verified", { mode: "boolean" }).default(false),
|
|
@@ -168,7 +173,8 @@ var usersTable2 = sqliteTable("users", {
|
|
|
168
173
|
failedLoginAttempts: integer2("failed_login_attempts").default(0),
|
|
169
174
|
lockoutUntil: text2("lockout_until")
|
|
170
175
|
}, (table) => ({
|
|
171
|
-
roleIdx: index2("users_role_id_idx").on(table.roleId)
|
|
176
|
+
roleIdx: index2("users_role_id_idx").on(table.roleId),
|
|
177
|
+
emailOrPhone: check2("users_email_or_phone", sql2`${table.email} is not null or ${table.phone} is not null`)
|
|
172
178
|
}));
|
|
173
179
|
var oauthAccountsTable2 = sqliteTable("oauth_accounts", {
|
|
174
180
|
...baseFields2(10),
|
|
@@ -385,7 +391,7 @@ function isRecord(value) {
|
|
|
385
391
|
}
|
|
386
392
|
__name(isRecord, "isRecord");
|
|
387
393
|
function isValidUser(value) {
|
|
388
|
-
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.email === "string" && value.email.length > 0 && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
394
|
+
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && (value.email === null || typeof value.email === "string" && value.email.length > 0) && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
389
395
|
}
|
|
390
396
|
__name(isValidUser, "isValidUser");
|
|
391
397
|
function isStringArray(value) {
|
|
@@ -682,6 +688,16 @@ var UserRepository = class UserRepository2 {
|
|
|
682
688
|
}).from(this.users).leftJoin(this.roles, eq2(this.users.roleId, this.roles.id)).where(sql3`lower(${this.users.email}) = ${email2.trim().toLowerCase()}`).limit(1);
|
|
683
689
|
return existingUser;
|
|
684
690
|
}
|
|
691
|
+
/** Login by phone reads the row it matched; the account may have no email. */
|
|
692
|
+
async getByPhoneWithCredentials(phone) {
|
|
693
|
+
const [existingUser] = await this.db.select({
|
|
694
|
+
...this.q.userSelection(),
|
|
695
|
+
password: this.users.password,
|
|
696
|
+
failedLoginAttempts: this.users.failedLoginAttempts,
|
|
697
|
+
lockoutUntil: this.users.lockoutUntil
|
|
698
|
+
}).from(this.users).leftJoin(this.roles, eq2(this.users.roleId, this.roles.id)).where(eq2(this.users.phone, phone)).limit(1);
|
|
699
|
+
return existingUser;
|
|
700
|
+
}
|
|
685
701
|
async create(data) {
|
|
686
702
|
const [newUser] = await this.db.insert(this.users).values(data).returning();
|
|
687
703
|
return newUser;
|
|
@@ -701,7 +717,7 @@ var UserRepository = class UserRepository2 {
|
|
|
701
717
|
}
|
|
702
718
|
/** The credential that authorized a delayed write must still own the account. */
|
|
703
719
|
async updateWithCredential(id, data, expected) {
|
|
704
|
-
const [updated] = await this.db.update(this.users).set(data).where(and(eq2(this.users.id, id), eq2(this.users.password, expected.password), eq2(this.users.email, expected.email), expected.status == null ? isNull(this.users.status) : eq2(this.users.status, expected.status), expected.emailVerified == null ? isNull(this.users.emailVerified) : eq2(this.users.emailVerified, expected.emailVerified))).returning();
|
|
720
|
+
const [updated] = await this.db.update(this.users).set(data).where(and(eq2(this.users.id, id), eq2(this.users.password, expected.password), expected.email == null ? isNull(this.users.email) : eq2(this.users.email, expected.email), expected.status == null ? isNull(this.users.status) : eq2(this.users.status, expected.status), expected.emailVerified == null ? isNull(this.users.emailVerified) : eq2(this.users.emailVerified, expected.emailVerified))).returning();
|
|
705
721
|
return updated;
|
|
706
722
|
}
|
|
707
723
|
async updateLastLogin(id) {
|
|
@@ -779,6 +795,21 @@ function isUniqueViolation(error) {
|
|
|
779
795
|
return false;
|
|
780
796
|
}
|
|
781
797
|
__name(isUniqueViolation, "isUniqueViolation");
|
|
798
|
+
function isMissingIdentityViolation(error) {
|
|
799
|
+
const visited = /* @__PURE__ */ new Set();
|
|
800
|
+
while (error && typeof error === "object" && !visited.has(error)) {
|
|
801
|
+
visited.add(error);
|
|
802
|
+
const candidate = error;
|
|
803
|
+
const constraint = candidate.constraint ?? candidate.constraint_name;
|
|
804
|
+
if (constraint === "users_email_or_phone")
|
|
805
|
+
return true;
|
|
806
|
+
if (typeof candidate.message === "string" && candidate.message.includes("users_email_or_phone"))
|
|
807
|
+
return true;
|
|
808
|
+
error = candidate.cause;
|
|
809
|
+
}
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
__name(isMissingIdentityViolation, "isMissingIdentityViolation");
|
|
782
813
|
|
|
783
814
|
// src/users/UserValidator.ts
|
|
784
815
|
var __decorate4 = function(decorators, target, key, desc) {
|
|
@@ -825,6 +856,16 @@ var UserValidator = class UserValidator2 {
|
|
|
825
856
|
Err(this.t("errors.phoneExists"), 409);
|
|
826
857
|
}
|
|
827
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* An account is reached by its email, its phone, or both. Refused here with
|
|
861
|
+
* a message the caller can show; the `users_email_or_phone` check is the
|
|
862
|
+
* database's own copy of the rule.
|
|
863
|
+
*/
|
|
864
|
+
checkHasIdentity(email2, phone) {
|
|
865
|
+
if (!email2 && !phone) {
|
|
866
|
+
Err(this.t("errors.emailOrPhoneRequired"), 400);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
828
869
|
/** The database remains authoritative when two validation reads both pass. */
|
|
829
870
|
async writeUnique(write) {
|
|
830
871
|
try {
|
|
@@ -832,6 +873,8 @@ var UserValidator = class UserValidator2 {
|
|
|
832
873
|
} catch (error) {
|
|
833
874
|
if (isUniqueViolation(error))
|
|
834
875
|
Err("User identity already exists", 409);
|
|
876
|
+
if (isMissingIdentityViolation(error))
|
|
877
|
+
Err(this.t("errors.emailOrPhoneRequired"), 400);
|
|
835
878
|
throw error;
|
|
836
879
|
}
|
|
837
880
|
}
|
|
@@ -1862,6 +1905,10 @@ var _g;
|
|
|
1862
1905
|
var _h;
|
|
1863
1906
|
var _j;
|
|
1864
1907
|
var E164_PHONE = /^\+[1-9]\d{7,14}$/;
|
|
1908
|
+
function normalizeEmail(value) {
|
|
1909
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
1910
|
+
}
|
|
1911
|
+
__name(normalizeEmail, "normalizeEmail");
|
|
1865
1912
|
var UserService = class UserService2 {
|
|
1866
1913
|
static {
|
|
1867
1914
|
__name(this, "UserService");
|
|
@@ -1897,6 +1944,18 @@ var UserService = class UserService2 {
|
|
|
1897
1944
|
async invalidateSessions(userId) {
|
|
1898
1945
|
await this.sessionInvalidation?.invalidateUser(userId);
|
|
1899
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Stored in the same normalized form login lookup resolves to, or phone
|
|
1949
|
+
* login would never find the row. Blank means no phone.
|
|
1950
|
+
*/
|
|
1951
|
+
normalizePhone(phone) {
|
|
1952
|
+
if (phone == null || typeof phone === "string" && !phone.trim())
|
|
1953
|
+
return null;
|
|
1954
|
+
const normalized = this.authConfig.identity?.resolve(phone) ?? normalizeAuthIdentifier(phone);
|
|
1955
|
+
if (!normalized || !E164_PHONE.test(normalized))
|
|
1956
|
+
Err5("A valid phone number is required", 400);
|
|
1957
|
+
return normalized;
|
|
1958
|
+
}
|
|
1900
1959
|
sanitizeUser(user) {
|
|
1901
1960
|
if (!user)
|
|
1902
1961
|
return user;
|
|
@@ -1960,6 +2019,10 @@ var UserService = class UserService2 {
|
|
|
1960
2019
|
async findByPhone(phone) {
|
|
1961
2020
|
return await this.userRepository.findByPhone(phone);
|
|
1962
2021
|
}
|
|
2022
|
+
/** The credential row behind a phone login, for timing-safe authentication. */
|
|
2023
|
+
async findCredentialsByPhone(phone) {
|
|
2024
|
+
return await this.userRepository.getByPhoneWithCredentials(phone);
|
|
2025
|
+
}
|
|
1963
2026
|
async getAuthRecordById(id) {
|
|
1964
2027
|
return await this.userRepository.getRawById(id);
|
|
1965
2028
|
}
|
|
@@ -1973,12 +2036,11 @@ var UserService = class UserService2 {
|
|
|
1973
2036
|
this.userValidator.validatePasswordStrength(password);
|
|
1974
2037
|
}
|
|
1975
2038
|
let userId = id || nanoid3(10);
|
|
1976
|
-
|
|
2039
|
+
const normalizedEmail = normalizeEmail(email2);
|
|
2040
|
+
const normalizedPhone = this.normalizePhone(phone);
|
|
2041
|
+
this.userValidator.checkHasIdentity(normalizedEmail, normalizedPhone);
|
|
2042
|
+
await this.userValidator.checkEmailUnique(normalizedEmail);
|
|
1977
2043
|
await this.userValidator.checkUserIdIsUnique(userId);
|
|
1978
|
-
const normalizedPhone = phone == null ? null : this.authConfig.identity?.resolve(phone) ?? normalizeAuthIdentifier(phone);
|
|
1979
|
-
if (phone != null && (!normalizedPhone || !E164_PHONE.test(normalizedPhone))) {
|
|
1980
|
-
Err5("A valid phone number is required", 400);
|
|
1981
|
-
}
|
|
1982
2044
|
if (normalizedPhone) {
|
|
1983
2045
|
await this.userValidator.checkPhoneUnique(normalizedPhone, userId);
|
|
1984
2046
|
}
|
|
@@ -1987,7 +2049,7 @@ var UserService = class UserService2 {
|
|
|
1987
2049
|
const userDetails = {
|
|
1988
2050
|
id: userId,
|
|
1989
2051
|
name: name || null,
|
|
1990
|
-
email:
|
|
2052
|
+
email: normalizedEmail,
|
|
1991
2053
|
image,
|
|
1992
2054
|
password: hashedPassword,
|
|
1993
2055
|
roleId: resolvedRoleId,
|
|
@@ -2006,7 +2068,17 @@ var UserService = class UserService2 {
|
|
|
2006
2068
|
*/
|
|
2007
2069
|
async update(id, data, options = {}) {
|
|
2008
2070
|
const { password, image } = data;
|
|
2009
|
-
|
|
2071
|
+
const touchesEmail = data.email !== void 0;
|
|
2072
|
+
const touchesPhone = data.phone !== void 0;
|
|
2073
|
+
const email2 = touchesEmail ? normalizeEmail(data.email) : void 0;
|
|
2074
|
+
const phone = touchesPhone ? this.normalizePhone(data.phone) : void 0;
|
|
2075
|
+
if (touchesEmail && !email2 || touchesPhone && !phone) {
|
|
2076
|
+
const current = this.requireUser(await this.userRepository.getRawById(id));
|
|
2077
|
+
this.userValidator.checkHasIdentity(touchesEmail ? email2 : current.email, touchesPhone ? phone : current.phone);
|
|
2078
|
+
}
|
|
2079
|
+
await this.userValidator.checkEmailUnique(email2, id);
|
|
2080
|
+
if (phone)
|
|
2081
|
+
await this.userValidator.checkPhoneUnique(phone, id);
|
|
2010
2082
|
let hashedPassword;
|
|
2011
2083
|
if (password) {
|
|
2012
2084
|
if (options.validatePasswordStrength === false) {
|
|
@@ -2021,15 +2093,10 @@ var UserService = class UserService2 {
|
|
|
2021
2093
|
image,
|
|
2022
2094
|
...hashedPassword && { password: hashedPassword }
|
|
2023
2095
|
};
|
|
2024
|
-
if (
|
|
2025
|
-
updateData.email =
|
|
2026
|
-
if (
|
|
2027
|
-
const phone = this.authConfig.identity?.resolve(data.phone) ?? normalizeAuthIdentifier(data.phone);
|
|
2028
|
-
if (!phone || !E164_PHONE.test(phone))
|
|
2029
|
-
Err5("A valid phone number is required", 400);
|
|
2030
|
-
await this.userValidator.checkPhoneUnique(phone, id);
|
|
2096
|
+
if (touchesEmail)
|
|
2097
|
+
updateData.email = email2;
|
|
2098
|
+
if (touchesPhone)
|
|
2031
2099
|
updateData.phone = phone;
|
|
2032
|
-
}
|
|
2033
2100
|
const cleanedUpdateData = clean(updateData);
|
|
2034
2101
|
const updatedUser = await this.userValidator.writeUnique(() => options.expectedCredential ? this.userRepository.updateWithCredential(id, cleanedUpdateData, options.expectedCredential) : this.userRepository.update(id, cleanedUpdateData));
|
|
2035
2102
|
if (options.expectedCredential)
|
|
@@ -2891,7 +2958,7 @@ var TokenService = class TokenService2 {
|
|
|
2891
2958
|
*/
|
|
2892
2959
|
async generateSetPasswordToken(userId, type, expiresIn, expectedEmail) {
|
|
2893
2960
|
const user = await this.tokenRepository.getCredentialState(userId);
|
|
2894
|
-
if (!user || expectedEmail !== void 0 && user.email.trim().toLowerCase() !== expectedEmail.trim().toLowerCase()) {
|
|
2961
|
+
if (!user || expectedEmail !== void 0 && (user.email ?? "").trim().toLowerCase() !== expectedEmail.trim().toLowerCase()) {
|
|
2895
2962
|
Err7(this.t("errors.invalidResetToken"), 401);
|
|
2896
2963
|
}
|
|
2897
2964
|
const credentialState = credentialFingerprint(this.config.jwt.refreshSecret, user);
|
|
@@ -3960,8 +4027,7 @@ var AuthService = class AuthService2 {
|
|
|
3960
4027
|
if (identifier && isEmailIdentifier(identifier)) {
|
|
3961
4028
|
user = await this.userService.findByEmailInsensitive(identifier);
|
|
3962
4029
|
} else if (identifier) {
|
|
3963
|
-
|
|
3964
|
-
user = phoneUser ? await this.userService.findByEmail(phoneUser.email) : void 0;
|
|
4030
|
+
user = await this.userService.findCredentialsByPhone(identifier);
|
|
3965
4031
|
}
|
|
3966
4032
|
if (user?.lockoutUntil && !this.isLockoutActive(user.lockoutUntil)) {
|
|
3967
4033
|
await this.userService.resetFailedAttempts(user.id);
|
|
@@ -4542,7 +4608,8 @@ var passwordField = z.string().min(8, "Password must be at least 8 characters").
|
|
|
4542
4608
|
var optionalDateField = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").nullable().optional();
|
|
4543
4609
|
var createUserDto = z.object({
|
|
4544
4610
|
name: z.string().max(100).optional(),
|
|
4545
|
-
email: emailField,
|
|
4611
|
+
email: emailField.nullish(),
|
|
4612
|
+
phone: z.string().trim().max(32).nullish(),
|
|
4546
4613
|
password: passwordField,
|
|
4547
4614
|
roleId: z.string().min(1).optional(),
|
|
4548
4615
|
image: z.string().nullish(),
|
|
@@ -7042,6 +7109,7 @@ var en_default = {
|
|
|
7042
7109
|
notFound: "User not found",
|
|
7043
7110
|
idExists: "User ID already exists",
|
|
7044
7111
|
emailRequired: "Email is required",
|
|
7112
|
+
emailOrPhoneRequired: "An email address or a phone number is required",
|
|
7045
7113
|
phoneExists: "Phone number already registered",
|
|
7046
7114
|
passwordRequired: "Password is required",
|
|
7047
7115
|
invalidEmail: "Invalid email format",
|
|
@@ -8374,7 +8442,7 @@ var authSeed = /* @__PURE__ */ __name((config) => ({
|
|
|
8374
8442
|
users: {
|
|
8375
8443
|
by: ["email"],
|
|
8376
8444
|
rows: /* @__PURE__ */ __name(async (seeded) => {
|
|
8377
|
-
|
|
8445
|
+
registerDto.pick({ email: true, password: true }).parse({
|
|
8378
8446
|
email: config.adminEmail,
|
|
8379
8447
|
password: config.adminPass
|
|
8380
8448
|
});
|
|
@@ -8392,7 +8460,7 @@ var authSeed = /* @__PURE__ */ __name((config) => ({
|
|
|
8392
8460
|
];
|
|
8393
8461
|
if (config.additionalUsers) {
|
|
8394
8462
|
for (const user of config.additionalUsers) {
|
|
8395
|
-
|
|
8463
|
+
registerDto.pick({ email: true, password: true }).parse({
|
|
8396
8464
|
email: user.email,
|
|
8397
8465
|
password: user.password
|
|
8398
8466
|
});
|
package/dist/schema/pg.d.ts
CHANGED
|
@@ -135,7 +135,7 @@ declare const usersTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
135
135
|
columnType: "PgText";
|
|
136
136
|
data: string;
|
|
137
137
|
driverParam: string;
|
|
138
|
-
notNull:
|
|
138
|
+
notNull: false;
|
|
139
139
|
hasDefault: false;
|
|
140
140
|
isPrimaryKey: false;
|
|
141
141
|
isAutoincrement: false;
|
|
@@ -1208,7 +1208,7 @@ declare const authSchema: {
|
|
|
1208
1208
|
columnType: "PgText";
|
|
1209
1209
|
data: string;
|
|
1210
1210
|
driverParam: string;
|
|
1211
|
-
notNull:
|
|
1211
|
+
notNull: false;
|
|
1212
1212
|
hasDefault: false;
|
|
1213
1213
|
isPrimaryKey: false;
|
|
1214
1214
|
isAutoincrement: false;
|
package/dist/schema/pg.js
CHANGED
|
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/schema/pg.ts
|
|
5
|
-
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex } from "drizzle-orm/pg-core";
|
|
5
|
+
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex, check } from "drizzle-orm/pg-core";
|
|
6
6
|
import { sql } from "drizzle-orm";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
|
|
@@ -33,7 +33,10 @@ var rolesTable = pgTable("roles", {
|
|
|
33
33
|
var usersTable = pgTable("users", {
|
|
34
34
|
...baseFields(8),
|
|
35
35
|
name: text("name"),
|
|
36
|
-
email
|
|
36
|
+
// An account is reached by its email, its phone, or both — never neither.
|
|
37
|
+
// Email is nullable so several phone-only accounts can coexist: a NOT NULL
|
|
38
|
+
// column forced them all to "" and the second one broke the unique index.
|
|
39
|
+
email: text("email").unique(),
|
|
37
40
|
emailVerified: boolean("email_verified").default(false),
|
|
38
41
|
phone: text("phone").unique(),
|
|
39
42
|
phoneVerified: boolean("phone_verified").default(false),
|
|
@@ -45,7 +48,8 @@ var usersTable = pgTable("users", {
|
|
|
45
48
|
failedLoginAttempts: integer("failed_login_attempts").default(0),
|
|
46
49
|
lockoutUntil: timestamp("lockout_until", { mode: "string" })
|
|
47
50
|
}, (table) => ({
|
|
48
|
-
roleIdx: index("users_role_id_idx").on(table.roleId)
|
|
51
|
+
roleIdx: index("users_role_id_idx").on(table.roleId),
|
|
52
|
+
emailOrPhone: check("users_email_or_phone", sql`${table.email} is not null or ${table.phone} is not null`)
|
|
49
53
|
}));
|
|
50
54
|
var oauthAccountsTable = pgTable("oauth_accounts", {
|
|
51
55
|
...baseFields(10),
|
package/dist/schema/sqlite.d.ts
CHANGED
|
@@ -144,7 +144,7 @@ declare const usersTable: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
144
144
|
columnType: "SQLiteText";
|
|
145
145
|
data: string;
|
|
146
146
|
driverParam: string;
|
|
147
|
-
notNull:
|
|
147
|
+
notNull: false;
|
|
148
148
|
hasDefault: false;
|
|
149
149
|
isPrimaryKey: false;
|
|
150
150
|
isAutoincrement: false;
|
|
@@ -1368,7 +1368,7 @@ declare const authSchema: {
|
|
|
1368
1368
|
columnType: "SQLiteText";
|
|
1369
1369
|
data: string;
|
|
1370
1370
|
driverParam: string;
|
|
1371
|
-
notNull:
|
|
1371
|
+
notNull: false;
|
|
1372
1372
|
hasDefault: false;
|
|
1373
1373
|
isPrimaryKey: false;
|
|
1374
1374
|
isAutoincrement: false;
|
package/dist/schema/sqlite.js
CHANGED
|
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/schema/sqlite.ts
|
|
5
|
-
import { sqliteTable, text, integer, primaryKey, uniqueIndex, index } from "drizzle-orm/sqlite-core";
|
|
5
|
+
import { sqliteTable, text, integer, primaryKey, uniqueIndex, index, check } from "drizzle-orm/sqlite-core";
|
|
6
6
|
import { sql } from "drizzle-orm";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
var baseFields = /* @__PURE__ */ __name((idLength = 5) => ({
|
|
@@ -21,7 +21,8 @@ var rolesTable = sqliteTable("roles", {
|
|
|
21
21
|
var usersTable = sqliteTable("users", {
|
|
22
22
|
...baseFields(8),
|
|
23
23
|
name: text("name"),
|
|
24
|
-
email
|
|
24
|
+
// Nullable, with at least one of email or phone; see schema/pg.ts.
|
|
25
|
+
email: text("email").unique(),
|
|
25
26
|
emailVerified: integer("email_verified", { mode: "boolean" }).default(false),
|
|
26
27
|
phone: text("phone").unique(),
|
|
27
28
|
phoneVerified: integer("phone_verified", { mode: "boolean" }).default(false),
|
|
@@ -33,7 +34,8 @@ var usersTable = sqliteTable("users", {
|
|
|
33
34
|
failedLoginAttempts: integer("failed_login_attempts").default(0),
|
|
34
35
|
lockoutUntil: text("lockout_until")
|
|
35
36
|
}, (table) => ({
|
|
36
|
-
roleIdx: index("users_role_id_idx").on(table.roleId)
|
|
37
|
+
roleIdx: index("users_role_id_idx").on(table.roleId),
|
|
38
|
+
emailOrPhone: check("users_email_or_phone", sql`${table.email} is not null or ${table.phone} is not null`)
|
|
37
39
|
}));
|
|
38
40
|
var oauthAccountsTable = sqliteTable("oauth_accounts", {
|
|
39
41
|
...baseFields(10),
|