@mohasinac/appkit 4.30.4 → 4.30.5
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/features/auth/actions/profile-actions.d.ts +17 -1
- package/dist/features/auth/actions/profile-actions.js +26 -4
- package/dist/features/auth/actions/user-slug.d.ts +37 -0
- package/dist/features/auth/actions/user-slug.js +124 -0
- package/dist/features/auth/repository/user.repository.d.ts +8 -0
- package/dist/features/auth/repository/user.repository.js +24 -5
- package/dist/features/auth/schemas/firestore.d.ts +22 -3
- package/dist/features/auth/schemas/firestore.js +3 -4
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/react/contexts/SessionContext.d.ts +6 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/dist/utils/id-generators.d.ts +0 -7
- package/dist/utils/id-generators.js +0 -9
- package/package.json +1 -1
|
@@ -23,7 +23,23 @@ export type UpdateProfileInput = {
|
|
|
23
23
|
export declare function updateUserProfile(userId: string, input: UpdateProfileInput): Promise<UserDocument>;
|
|
24
24
|
export declare function getUserProfile(userId: string): Promise<UserDocument | null>;
|
|
25
25
|
export declare function getUserSessions(userId: string): Promise<import("..").SessionDocument[]>;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a profile by slug, then document id, then Auth uid.
|
|
28
|
+
*
|
|
29
|
+
* ONE function because the page and `GET /api/profile/[userId]` had already
|
|
30
|
+
* diverged: the page fell back to `findByUid`, the route did not, so the same
|
|
31
|
+
* identifier resolved on one surface and 404'd on the other. Adding slug
|
|
32
|
+
* resolution to each of them separately would have made that three variants.
|
|
33
|
+
*
|
|
34
|
+
* Order matters. The slug is the canonical public identifier, so it is tried
|
|
35
|
+
* first; `id` is next because for every app-created account `id === uid`, which
|
|
36
|
+
* makes the third lookup a no-op for them. The uid fallback exists for seeded
|
|
37
|
+
* personas (whose document id is a `user-*` slug, not their uid) and for links
|
|
38
|
+
* shared before slugs existed — dropping it would 404 URLs already in the wild,
|
|
39
|
+
* including ones inside sent notification emails.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveProfileUser(identifier: string): Promise<UserDocument | null>;
|
|
42
|
+
export declare function getPublicUserProfile(userId: string): Promise<Pick<UserDocument, "id" | "slug" | "displayName" | "photoURL" | "role" | "createdAt" | "storeId" | "storeSlug" | "publicProfile" | "stats"> | null>;
|
|
27
43
|
/** Fetch approved reviews for a store. storeId === storeSlug in this project. */
|
|
28
44
|
export declare function getSellerReviews(storeId: string): Promise<Review[]>;
|
|
29
45
|
export declare function getProfileStoreProducts(storeId: string): Promise<import("../../products").ProductDocument[]>;
|
|
@@ -22,15 +22,37 @@ export async function getUserProfile(userId) {
|
|
|
22
22
|
export async function getUserSessions(userId) {
|
|
23
23
|
return sessionRepository.findAllByUser(userId);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a profile by slug, then document id, then Auth uid.
|
|
27
|
+
*
|
|
28
|
+
* ONE function because the page and `GET /api/profile/[userId]` had already
|
|
29
|
+
* diverged: the page fell back to `findByUid`, the route did not, so the same
|
|
30
|
+
* identifier resolved on one surface and 404'd on the other. Adding slug
|
|
31
|
+
* resolution to each of them separately would have made that three variants.
|
|
32
|
+
*
|
|
33
|
+
* Order matters. The slug is the canonical public identifier, so it is tried
|
|
34
|
+
* first; `id` is next because for every app-created account `id === uid`, which
|
|
35
|
+
* makes the third lookup a no-op for them. The uid fallback exists for seeded
|
|
36
|
+
* personas (whose document id is a `user-*` slug, not their uid) and for links
|
|
37
|
+
* shared before slugs existed — dropping it would 404 URLs already in the wild,
|
|
38
|
+
* including ones inside sent notification emails.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveProfileUser(identifier) {
|
|
41
|
+
const bySlug = await userRepository.findBySlug(identifier);
|
|
42
|
+
if (bySlug)
|
|
43
|
+
return bySlug;
|
|
44
|
+
const byId = await userRepository.findById(identifier);
|
|
45
|
+
if (byId)
|
|
46
|
+
return byId;
|
|
47
|
+
return userRepository.findByUid(identifier);
|
|
48
|
+
}
|
|
25
49
|
export async function getPublicUserProfile(userId) {
|
|
26
|
-
|
|
27
|
-
// Fallback: userId may be a Firebase Auth UID rather than the document slug
|
|
28
|
-
if (!user)
|
|
29
|
-
user = await userRepository.findByUid(userId);
|
|
50
|
+
const user = await resolveProfileUser(userId);
|
|
30
51
|
if (!user)
|
|
31
52
|
return null;
|
|
32
53
|
return {
|
|
33
54
|
id: user.id,
|
|
55
|
+
slug: user.slug,
|
|
34
56
|
displayName: user.displayName,
|
|
35
57
|
photoURL: user.photoURL,
|
|
36
58
|
role: user.role,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Reservation collection: doc id IS the slug, so the id is the uniqueness. */
|
|
2
|
+
export declare const USER_SLUGS_COLLECTION = "userSlugs";
|
|
3
|
+
/**
|
|
4
|
+
* The stem a slug is built from.
|
|
5
|
+
*
|
|
6
|
+
* 🛑 `displayName` ONLY. `generateUserId` used to append
|
|
7
|
+
* `email.split("@")[0].substring(0, 8)`, which put the local-part of a real
|
|
8
|
+
* email address into a public URL. That function is deleted; do not
|
|
9
|
+
* reintroduce an email- or phone-derived stem here.
|
|
10
|
+
*
|
|
11
|
+
* 🛑 …AND the caller must pass `email`, because deleting that function was not
|
|
12
|
+
* enough. TWO of the three signup paths — Google callback and session — set
|
|
13
|
+
*
|
|
14
|
+
* displayName: name ?? email.split("@")[0] ?? DEFAULT_DISPLAY_NAME
|
|
15
|
+
*
|
|
16
|
+
* so a Google account with no name, or any session-created user, already HOLDS
|
|
17
|
+
* its email local-part as its display name. Deriving a slug from displayName
|
|
18
|
+
* would publish it in the URL through the back door. When the two match, this
|
|
19
|
+
* falls back to `member` — a guard that does not depend on every caller
|
|
20
|
+
* remembering, which is the only kind that survives.
|
|
21
|
+
*/
|
|
22
|
+
export declare function userSlugBase(displayName: string | null | undefined, email?: string | null): string;
|
|
23
|
+
/**
|
|
24
|
+
* Claim a unique slug for `uid`, returning it.
|
|
25
|
+
*
|
|
26
|
+
* Idempotent per user: if `uid` already holds a reservation, that slug is
|
|
27
|
+
* returned rather than a second one being minted.
|
|
28
|
+
*
|
|
29
|
+
* The claim is a TRANSACTION because Firestore has no unique constraint — a
|
|
30
|
+
* read-then-write races two simultaneous signups onto the same slug, and the
|
|
31
|
+
* loser silently overwrites the winner's reservation.
|
|
32
|
+
*/
|
|
33
|
+
export declare function reserveUserSlug(uid: string, displayName: string | null | undefined,
|
|
34
|
+
/** Pass it. See `userSlugBase` — two signup paths put the local-part here. */
|
|
35
|
+
email?: string | null, opts?: {
|
|
36
|
+
maxAttempts?: number;
|
|
37
|
+
}): Promise<string | null>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Users are the only entity in the Slug Prefix System without a real slug —
|
|
3
|
+
* every profile URL was a raw Firebase uid. This mints one, uniquely,
|
|
4
|
+
* without moving the document key.
|
|
5
|
+
*
|
|
6
|
+
* WHAT: `reserveUserSlug(uid, displayName)` — transactional claim against a
|
|
7
|
+
* `userSlugs/{slug}` reservation collection, returning the slug it won.
|
|
8
|
+
*
|
|
9
|
+
* @tag domain:auth
|
|
10
|
+
* @tag layer:server-data
|
|
11
|
+
* @tag access:server-only
|
|
12
|
+
* @tag sideEffects:firestore-write
|
|
13
|
+
*/
|
|
14
|
+
import { getAdminDb } from "../../../providers/db-firebase/admin";
|
|
15
|
+
import { slugify } from "../../../utils";
|
|
16
|
+
import { serverLogger } from "../../../monitoring/server-logger";
|
|
17
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
18
|
+
/** Reservation collection: doc id IS the slug, so the id is the uniqueness. */
|
|
19
|
+
export const USER_SLUGS_COLLECTION = "userSlugs";
|
|
20
|
+
/**
|
|
21
|
+
* Slugs nobody may hold.
|
|
22
|
+
*
|
|
23
|
+
* `/profile/{slug}` has a `[tab]` child segment, so `overview` and `catalogue`
|
|
24
|
+
* are included even though a collision there is positional rather than fatal —
|
|
25
|
+
* a username that reads like a tab is confusing regardless. The rest are the
|
|
26
|
+
* words a URL-shaped identifier should never be able to impersonate.
|
|
27
|
+
*/
|
|
28
|
+
const RESERVED = new Set([
|
|
29
|
+
"admin", "administrator", "api", "app", "auth", "login", "logout", "register",
|
|
30
|
+
"signup", "signin", "new", "edit", "delete", "settings", "me", "self", "user",
|
|
31
|
+
"users", "profile", "profiles", "store", "stores", "seller", "sellers",
|
|
32
|
+
"support", "help", "about", "contact", "search", "null", "undefined", "root",
|
|
33
|
+
"system", "moderator", "staff", "team", "official", "letitrip",
|
|
34
|
+
// `/profile/{slug}/{tab}` values
|
|
35
|
+
"overview", "catalogue",
|
|
36
|
+
]);
|
|
37
|
+
/** Display names that carry no identity — `UserSchemaDefaults.DEFAULT_DISPLAY_NAME`. */
|
|
38
|
+
const EMPTY_NAMES = new Set(["", "user", "anonymous", "null", "undefined"]);
|
|
39
|
+
/** Longest a generated slug may be before the collision suffix. */
|
|
40
|
+
const MAX_BASE_LENGTH = 40;
|
|
41
|
+
/**
|
|
42
|
+
* The stem a slug is built from.
|
|
43
|
+
*
|
|
44
|
+
* 🛑 `displayName` ONLY. `generateUserId` used to append
|
|
45
|
+
* `email.split("@")[0].substring(0, 8)`, which put the local-part of a real
|
|
46
|
+
* email address into a public URL. That function is deleted; do not
|
|
47
|
+
* reintroduce an email- or phone-derived stem here.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 …AND the caller must pass `email`, because deleting that function was not
|
|
50
|
+
* enough. TWO of the three signup paths — Google callback and session — set
|
|
51
|
+
*
|
|
52
|
+
* displayName: name ?? email.split("@")[0] ?? DEFAULT_DISPLAY_NAME
|
|
53
|
+
*
|
|
54
|
+
* so a Google account with no name, or any session-created user, already HOLDS
|
|
55
|
+
* its email local-part as its display name. Deriving a slug from displayName
|
|
56
|
+
* would publish it in the URL through the back door. When the two match, this
|
|
57
|
+
* falls back to `member` — a guard that does not depend on every caller
|
|
58
|
+
* remembering, which is the only kind that survives.
|
|
59
|
+
*/
|
|
60
|
+
export function userSlugBase(displayName, email) {
|
|
61
|
+
const base = slugify(displayName ?? "").slice(0, MAX_BASE_LENGTH);
|
|
62
|
+
if (!base || EMPTY_NAMES.has(base))
|
|
63
|
+
return "member";
|
|
64
|
+
const localPart = (email ?? "").split("@")[0] ?? "";
|
|
65
|
+
if (localPart && slugify(localPart) === base)
|
|
66
|
+
return "member";
|
|
67
|
+
return base;
|
|
68
|
+
}
|
|
69
|
+
/** Whether a candidate may be claimed at all. */
|
|
70
|
+
function isClaimable(candidate) {
|
|
71
|
+
return candidate.length > 0 && !RESERVED.has(candidate);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Claim a unique slug for `uid`, returning it.
|
|
75
|
+
*
|
|
76
|
+
* Idempotent per user: if `uid` already holds a reservation, that slug is
|
|
77
|
+
* returned rather than a second one being minted.
|
|
78
|
+
*
|
|
79
|
+
* The claim is a TRANSACTION because Firestore has no unique constraint — a
|
|
80
|
+
* read-then-write races two simultaneous signups onto the same slug, and the
|
|
81
|
+
* loser silently overwrites the winner's reservation.
|
|
82
|
+
*/
|
|
83
|
+
export async function reserveUserSlug(uid, displayName,
|
|
84
|
+
/** Pass it. See `userSlugBase` — two signup paths put the local-part here. */
|
|
85
|
+
email, opts) {
|
|
86
|
+
const db = getAdminDb();
|
|
87
|
+
const base = userSlugBase(displayName, email);
|
|
88
|
+
const maxAttempts = opts?.maxAttempts ?? 50;
|
|
89
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
90
|
+
// `ravi-kumar`, then `ravi-kumar-2`, `ravi-kumar-3`… The suffix appears
|
|
91
|
+
// only when it has to, so the common case stays clean.
|
|
92
|
+
const candidate = attempt === 0 ? base : `${base}-${attempt + 1}`;
|
|
93
|
+
if (!isClaimable(candidate))
|
|
94
|
+
continue;
|
|
95
|
+
try {
|
|
96
|
+
const won = await db.runTransaction(async (tx) => {
|
|
97
|
+
const ref = db.collection(USER_SLUGS_COLLECTION).doc(candidate);
|
|
98
|
+
const snap = await tx.get(ref);
|
|
99
|
+
if (snap.exists) {
|
|
100
|
+
// Already ours — re-running signup or a backfill must not mint a second.
|
|
101
|
+
return snap.data()?.uid === uid;
|
|
102
|
+
}
|
|
103
|
+
tx.set(ref, { uid, createdAt: new Date() });
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
if (won)
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
void normalizeError(err);
|
|
111
|
+
serverLogger.warn("reserveUserSlug: transaction failed", {
|
|
112
|
+
uid,
|
|
113
|
+
candidate,
|
|
114
|
+
error: err instanceof Error ? err.message : String(err),
|
|
115
|
+
});
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Never throw: a signup must not fail because a display name is popular.
|
|
120
|
+
// The profile route still resolves by id and uid, so the account works — it
|
|
121
|
+
// just keeps a uid-shaped URL.
|
|
122
|
+
serverLogger.warn("reserveUserSlug: exhausted attempts", { uid, base, maxAttempts });
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
@@ -11,6 +11,14 @@ export declare class UserRepository extends BaseRepository<UserDocument> {
|
|
|
11
11
|
createWithId(id: string, data: Partial<UserDocument>): Promise<UserDocument>;
|
|
12
12
|
create(input: Omit<UserDocument, "id" | "createdAt" | "updatedAt">): Promise<UserDocument>;
|
|
13
13
|
findByUid(uid: string): Promise<UserDocument | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a public profile slug.
|
|
16
|
+
*
|
|
17
|
+
* Separate from `findById` because the slug is a FIELD, not the document key
|
|
18
|
+
* — `id === uid` is what keeps every auth-gated request an O(1) lookup from
|
|
19
|
+
* the verified token claim, so the key could not be repurposed for this.
|
|
20
|
+
*/
|
|
21
|
+
findBySlug(slug: string): Promise<UserDocument | null>;
|
|
14
22
|
findByEmail(email: string): Promise<UserDocument | null>;
|
|
15
23
|
findByPhone(phoneNumber: string): Promise<UserDocument | null>;
|
|
16
24
|
findByRole(role: UserRole): Promise<UserDocument[]>;
|
|
@@ -2,7 +2,7 @@ import { normalizeError } from "../../../errors/normalize";
|
|
|
2
2
|
import { DatabaseError } from "../../../errors";
|
|
3
3
|
import { BaseRepository, getFirestoreCount, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
4
4
|
import { USER_PII_FIELDS, USER_PII_INDEX_MAP, piiIndicesFor, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptPiiFields, encryptShippingConfig, piiBlindIndex, } from "../../../security";
|
|
5
|
-
import { USER_COLLECTION, USER_FIELDS,
|
|
5
|
+
import { USER_COLLECTION, USER_FIELDS, } from "../schemas";
|
|
6
6
|
export class UserRepository extends BaseRepository {
|
|
7
7
|
constructor() {
|
|
8
8
|
super(USER_COLLECTION);
|
|
@@ -61,10 +61,18 @@ export class UserRepository extends BaseRepository {
|
|
|
61
61
|
return super.createWithId(id, encrypted);
|
|
62
62
|
}
|
|
63
63
|
async create(input) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// A Firestore auto-id, NOT a derived one.
|
|
65
|
+
//
|
|
66
|
+
// This used to call `createUserId({firstName, lastName, email})`, which
|
|
67
|
+
// built `user-{first}-{last}-{emailPrefix8}` — putting the local-part of a
|
|
68
|
+
// real email address into the document id, and therefore into the public
|
|
69
|
+
// profile URL. That generator is deleted.
|
|
70
|
+
//
|
|
71
|
+
// No real signup path reaches here (all three call `createWithId` with the
|
|
72
|
+
// Firebase Auth uid, because `id === uid` is what makes an auth-gated
|
|
73
|
+
// request an O(1) lookup). The readable public identifier is `slug`, minted
|
|
74
|
+
// by `reserveUserSlug` from displayName only.
|
|
75
|
+
const id = this.db.collection(this.collection).doc().id;
|
|
68
76
|
const userData = {
|
|
69
77
|
...input,
|
|
70
78
|
createdAt: new Date(),
|
|
@@ -80,6 +88,16 @@ export class UserRepository extends BaseRepository {
|
|
|
80
88
|
async findByUid(uid) {
|
|
81
89
|
return this.findOneBy(USER_FIELDS.UID, uid);
|
|
82
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a public profile slug.
|
|
93
|
+
*
|
|
94
|
+
* Separate from `findById` because the slug is a FIELD, not the document key
|
|
95
|
+
* — `id === uid` is what keeps every auth-gated request an O(1) lookup from
|
|
96
|
+
* the verified token claim, so the key could not be repurposed for this.
|
|
97
|
+
*/
|
|
98
|
+
async findBySlug(slug) {
|
|
99
|
+
return this.findOneBy(USER_FIELDS.SLUG, slug);
|
|
100
|
+
}
|
|
83
101
|
async findByEmail(email) {
|
|
84
102
|
return this.findOneBy(USER_FIELDS.EMAIL_INDEX, piiBlindIndex(email));
|
|
85
103
|
}
|
|
@@ -262,6 +280,7 @@ export class UserRepository extends BaseRepository {
|
|
|
262
280
|
}
|
|
263
281
|
UserRepository.SIEVE_FIELDS = {
|
|
264
282
|
uid: { canFilter: true, canSort: false },
|
|
283
|
+
slug: { canFilter: true, canSort: false },
|
|
265
284
|
emailIndex: { canFilter: true, canSort: false },
|
|
266
285
|
displayName: { canFilter: true, canSort: true },
|
|
267
286
|
role: { canFilter: true, canSort: true },
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { UserRole } from "../types";
|
|
8
8
|
import type { BannedAction, EmployeeGroup, Permission } from "../permissions/constants";
|
|
9
|
-
import { type GenerateUserIdInput } from "../../../utils/id-generators";
|
|
10
9
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
11
10
|
/**
|
|
12
11
|
* Generic schema defaults for user-related data.
|
|
@@ -34,6 +33,25 @@ export interface AvatarMetadata {
|
|
|
34
33
|
}
|
|
35
34
|
export interface UserDocument extends BaseDocument {
|
|
36
35
|
uid: string;
|
|
36
|
+
/**
|
|
37
|
+
* Public URL identifier — `/profile/{slug}`.
|
|
38
|
+
*
|
|
39
|
+
* NOT the document key. `id === uid` is what makes every auth-gated request an
|
|
40
|
+
* O(1) lookup from the verified token claim, and Firebase assigns that uid, so
|
|
41
|
+
* the app could not override it for organic signups even if it wanted to.
|
|
42
|
+
*
|
|
43
|
+
* Derived from `displayName` ONLY. Never from email or phone: a slug is public,
|
|
44
|
+
* and the local-part of an email address in a URL is a PII leak (D1 keeps
|
|
45
|
+
* displayName unencrypted precisely so it CAN be used this way).
|
|
46
|
+
*
|
|
47
|
+
* Immutable once set — categories, brands and bundles all treat slug that way,
|
|
48
|
+
* and a mutable public slug means link rot plus a freed slug someone else can
|
|
49
|
+
* claim, which is an impersonation vector.
|
|
50
|
+
*
|
|
51
|
+
* Optional because documents created before this field existed have none;
|
|
52
|
+
* profile resolution falls back to id and then uid.
|
|
53
|
+
*/
|
|
54
|
+
slug?: string;
|
|
37
55
|
email: string | null;
|
|
38
56
|
phoneNumber: string | null;
|
|
39
57
|
phoneVerified?: boolean;
|
|
@@ -177,7 +195,7 @@ export interface SellerPayoutDetails {
|
|
|
177
195
|
}
|
|
178
196
|
export declare const USER_COLLECTION: "users";
|
|
179
197
|
export declare const DEFAULT_USER_DATA: Partial<UserDocument>;
|
|
180
|
-
export declare const USER_INDEXED_FIELDS: readonly ["email", "emailIndex", "phoneNumber", "phoneIndex", "role", "disabled", "emailVerified", "phoneVerified", "storeSlug", "storeStatus", "isTester"];
|
|
198
|
+
export declare const USER_INDEXED_FIELDS: readonly ["email", "emailIndex", "phoneNumber", "phoneIndex", "role", "disabled", "emailVerified", "phoneVerified", "storeSlug", "storeStatus", "isTester", "slug"];
|
|
181
199
|
export declare const USER_PUBLIC_FIELDS: readonly ["uid", "displayName", "photoURL", "avatarMetadata", "role", "createdAt", "storeSlug", "publicProfile", "stats"];
|
|
182
200
|
export declare const USER_UPDATABLE_FIELDS: readonly ["displayName", "photoURL"];
|
|
183
201
|
export type UserCreateInput = Omit<UserDocument, "uid" | "id" | "createdAt" | "updatedAt">;
|
|
@@ -202,10 +220,11 @@ export declare const userQueryHelpers: {
|
|
|
202
220
|
readonly active: () => readonly ["disabled", "==", false];
|
|
203
221
|
readonly disabled: () => readonly ["disabled", "==", true];
|
|
204
222
|
};
|
|
205
|
-
export declare function createUserId(input: GenerateUserIdInput): string;
|
|
206
223
|
export declare const USER_FIELDS: {
|
|
207
224
|
readonly ID: "id";
|
|
208
225
|
readonly UID: "uid";
|
|
226
|
+
/** Public URL identifier. Derived from displayName; never from PII. */
|
|
227
|
+
readonly SLUG: "slug";
|
|
209
228
|
readonly EMAIL: "email";
|
|
210
229
|
readonly PHONE_NUMBER: "phoneNumber";
|
|
211
230
|
readonly PHONE_VERIFIED: "phoneVerified";
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Canonical Firestore document interfaces, collection names, field constants,
|
|
5
5
|
* and query helpers for the auth feature (users, tokens, sessions).
|
|
6
6
|
*/
|
|
7
|
-
import { generateUserId, } from "../../../utils/id-generators";
|
|
8
7
|
// ============================================================================
|
|
9
8
|
// SCHEMA DEFAULTS
|
|
10
9
|
// ============================================================================
|
|
@@ -59,6 +58,7 @@ export const USER_INDEXED_FIELDS = [
|
|
|
59
58
|
"storeSlug",
|
|
60
59
|
"storeStatus",
|
|
61
60
|
"isTester",
|
|
61
|
+
"slug",
|
|
62
62
|
];
|
|
63
63
|
export const USER_PUBLIC_FIELDS = [
|
|
64
64
|
"uid",
|
|
@@ -84,13 +84,12 @@ export const userQueryHelpers = {
|
|
|
84
84
|
active: () => ["disabled", "==", false],
|
|
85
85
|
disabled: () => ["disabled", "==", true],
|
|
86
86
|
};
|
|
87
|
-
export function createUserId(input) {
|
|
88
|
-
return generateUserId(input);
|
|
89
|
-
}
|
|
90
87
|
// -- User field name constants ------------------------------------------------
|
|
91
88
|
export const USER_FIELDS = {
|
|
92
89
|
ID: "id",
|
|
93
90
|
UID: "uid",
|
|
91
|
+
/** Public URL identifier. Derived from displayName; never from PII. */
|
|
92
|
+
SLUG: "slug",
|
|
94
93
|
EMAIL: "email",
|
|
95
94
|
PHONE_NUMBER: "phoneNumber",
|
|
96
95
|
PHONE_VERIFIED: "phoneVerified",
|
package/dist/index.d.ts
CHANGED
|
@@ -1448,7 +1448,6 @@ export { USER_PUBLIC_FIELDS } from "./features/auth/schemas/index";
|
|
|
1448
1448
|
export { USER_UPDATABLE_FIELDS } from "./features/auth/schemas/index";
|
|
1449
1449
|
export { UserSchemaDefaults } from "./features/auth/schemas/index";
|
|
1450
1450
|
export { authUserSchema } from "./features/auth/schemas/index";
|
|
1451
|
-
export { createUserId } from "./features/auth/schemas/index";
|
|
1452
1451
|
export { forgotPasswordSchema } from "./features/auth/schemas/index";
|
|
1453
1452
|
export { loginSchema } from "./features/auth/schemas/index";
|
|
1454
1453
|
export { parseUserAgent } from "./features/auth/schemas/index";
|
package/dist/index.js
CHANGED
|
@@ -2727,8 +2727,6 @@ export { UserSchemaDefaults } from "./features/auth/schemas/index";
|
|
|
2727
2727
|
// authUserSchema - Model for auth user schema.
|
|
2728
2728
|
export { authUserSchema } from "./features/auth/schemas/index";
|
|
2729
2729
|
// [SCHEMA]-Schema / data-shape constant â€" Zod validator, default-value object, or Firestore collection/field name constant.
|
|
2730
|
-
// createUserId - Helper for create user id.
|
|
2731
|
-
export { createUserId } from "./features/auth/schemas/index";
|
|
2732
2730
|
// [SCHEMA]-Schema / data-shape constant â€" Zod validator, default-value object, or Firestore collection/field name constant.
|
|
2733
2731
|
// forgotPasswordSchema - Model for forgot password schema.
|
|
2734
2732
|
export { forgotPasswordSchema } from "./features/auth/schemas/index";
|
|
@@ -20,6 +20,12 @@ export interface SessionUser {
|
|
|
20
20
|
displayName: string | null;
|
|
21
21
|
photoURL: string | null;
|
|
22
22
|
phoneNumber: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Public profile slug, so the user's own "View public profile" links show the
|
|
25
|
+
* readable URL rather than their raw Firebase uid. Optional: accounts created
|
|
26
|
+
* before slugs existed have none, and `/profile/{uid}` still resolves.
|
|
27
|
+
*/
|
|
28
|
+
slug?: string;
|
|
23
29
|
role: string;
|
|
24
30
|
disabled?: boolean;
|
|
25
31
|
createdAt?: Date;
|
package/dist/server.d.ts
CHANGED
|
@@ -463,6 +463,8 @@ export { decryptPii } from "./security/index";
|
|
|
463
463
|
export { isPiiEncrypted } from "./security/index";
|
|
464
464
|
export { piiBlindIndex } from "./security/index";
|
|
465
465
|
export { piiIndicesFor } from "./security/index";
|
|
466
|
+
export { reserveUserSlug, userSlugBase, USER_SLUGS_COLLECTION } from "./features/auth/actions/user-slug";
|
|
467
|
+
export { resolveProfileUser } from "./features/auth/actions/profile-actions";
|
|
466
468
|
export { getPiiConfigError } from "./security/index";
|
|
467
469
|
export { encryptShippingAddress } from "./security/index";
|
|
468
470
|
export { decryptShippingAddress } from "./security/index";
|
package/dist/server.js
CHANGED
|
@@ -1295,6 +1295,8 @@ export { decryptPii } from "./security/index";
|
|
|
1295
1295
|
export { isPiiEncrypted } from "./security/index";
|
|
1296
1296
|
export { piiBlindIndex } from "./security/index";
|
|
1297
1297
|
export { piiIndicesFor } from "./security/index";
|
|
1298
|
+
export { reserveUserSlug, userSlugBase, USER_SLUGS_COLLECTION } from "./features/auth/actions/user-slug";
|
|
1299
|
+
export { resolveProfileUser } from "./features/auth/actions/profile-actions";
|
|
1298
1300
|
export { getPiiConfigError } from "./security/index";
|
|
1299
1301
|
export { encryptShippingAddress } from "./security/index";
|
|
1300
1302
|
export { decryptShippingAddress } from "./security/index";
|
|
@@ -12,13 +12,6 @@ export interface GenerateCategoryIdInput {
|
|
|
12
12
|
customId?: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function generateCategoryId(input: GenerateCategoryIdInput): string;
|
|
15
|
-
export interface GenerateUserIdInput {
|
|
16
|
-
firstName: string;
|
|
17
|
-
lastName: string;
|
|
18
|
-
email: string;
|
|
19
|
-
customId?: string;
|
|
20
|
-
}
|
|
21
|
-
export declare function generateUserId(input: GenerateUserIdInput): string;
|
|
22
15
|
export interface GenerateProductIdInput {
|
|
23
16
|
name: string;
|
|
24
17
|
category: string;
|
|
@@ -26,15 +26,6 @@ export function generateCategoryId(input) {
|
|
|
26
26
|
return `category-${nameSlug}-${slugify(input.rootName)}`;
|
|
27
27
|
return `category-${nameSlug}`;
|
|
28
28
|
}
|
|
29
|
-
export function generateUserId(input) {
|
|
30
|
-
if (input.customId?.trim())
|
|
31
|
-
return input.customId.trim();
|
|
32
|
-
const firstSlug = slugify(input.firstName);
|
|
33
|
-
const lastSlug = slugify(input.lastName);
|
|
34
|
-
const emailPrefix = input.email.split("@")[0].toLowerCase().substring(0, 8);
|
|
35
|
-
const emailSlug = slugify(emailPrefix);
|
|
36
|
-
return `user-${firstSlug}-${lastSlug}-${emailSlug}`;
|
|
37
|
-
}
|
|
38
29
|
export function generateProductId(input) {
|
|
39
30
|
if (input.customId?.trim())
|
|
40
31
|
return input.customId.trim();
|