@extantape/payload-plugin-signup 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +120 -2
- package/dist/access.d.ts +11 -0
- package/dist/access.js +14 -0
- package/dist/collections.d.ts +10 -0
- package/dist/collections.js +147 -0
- package/dist/emails.d.ts +3 -0
- package/dist/emails.js +25 -0
- package/dist/endpoints.d.ts +2 -0
- package/dist/endpoints.js +137 -0
- package/dist/flow.d.ts +93 -0
- package/dist/flow.js +424 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.js +99 -0
- package/dist/types.d.ts +187 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +54 -0
- package/dist/util.js +116 -0
- package/package.json +62 -4
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Access, Field, Payload, PayloadRequest } from 'payload';
|
|
2
|
+
export type ID = string | number;
|
|
3
|
+
export type Req = Partial<PayloadRequest>;
|
|
4
|
+
/**
|
|
5
|
+
* How people get an account.
|
|
6
|
+
* - `open`: anyone who verifies their email gets an active account.
|
|
7
|
+
* - `approval`: verified requests wait as disabled accounts until staff approve or decline.
|
|
8
|
+
* - `invite-only`: the form refuses; accounts come only from the site's own invitations.
|
|
9
|
+
* - `closed`: the form refuses; nobody can ask.
|
|
10
|
+
*/
|
|
11
|
+
export type SignupMode = 'open' | 'approval' | 'invite-only' | 'closed';
|
|
12
|
+
export declare const MODES: SignupMode[];
|
|
13
|
+
/** A user's place in sign-up. Accounts are made on confirming, as `pending` (or `active` in open mode); accounts made any other way are `active`. */
|
|
14
|
+
export type SignupStatus = 'pending' | 'active';
|
|
15
|
+
/** A request's place in the queue. */
|
|
16
|
+
export type RequestStatus = 'unverified' | 'pending' | 'approved' | 'declined';
|
|
17
|
+
/** What a site check says about a request. Return nothing (or `{}`) to let it through. */
|
|
18
|
+
export type CheckResult = {
|
|
19
|
+
/** Refuse, with this message shown to the person. Nothing is stored. */
|
|
20
|
+
refuse?: string;
|
|
21
|
+
/** Let it through, but show this to staff in the queue (for example "minor"). */
|
|
22
|
+
flag?: string;
|
|
23
|
+
/** Extra values for the new user (for example a minor flag), merged after `toUser`. */
|
|
24
|
+
userData?: Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* `Set-Cookie` header values for the response (for example a "stay blocked" cookie), refusals included. Sign-up's
|
|
27
|
+
* endpoint sets them; `submitSignup` hands them back for a site's own form. Single-line strings only: anything
|
|
28
|
+
* else is a broken check, and refuses.
|
|
29
|
+
*/
|
|
30
|
+
setCookies?: string[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* `data` is the request's details (kept); `answers` are for checks only and never stored. `req` is the request
|
|
34
|
+
* itself, headers and all. A check typed to take less (a `Partial<PayloadRequest>`) fits too.
|
|
35
|
+
*/
|
|
36
|
+
export type Check = (args: {
|
|
37
|
+
data: Record<string, unknown>;
|
|
38
|
+
answers: Record<string, unknown>;
|
|
39
|
+
email: string;
|
|
40
|
+
req: PayloadRequest;
|
|
41
|
+
payload: Payload;
|
|
42
|
+
}) => CheckResult | void | Promise<CheckResult | void>;
|
|
43
|
+
/**
|
|
44
|
+
* A check run again when the person confirms, before the account is made, for what can change in the meantime
|
|
45
|
+
* (a domain blocked since, sign-up closed to a group). It sees the stored details and the request's flag, never
|
|
46
|
+
* the check-only answers, which were never stored. A refusal declines the request; a check that throws refuses
|
|
47
|
+
* for now and the link keeps working.
|
|
48
|
+
*/
|
|
49
|
+
export type ConfirmCheck = (args: {
|
|
50
|
+
data: Record<string, unknown>;
|
|
51
|
+
email: string;
|
|
52
|
+
flag: string | null;
|
|
53
|
+
req: PayloadRequest;
|
|
54
|
+
payload: Payload;
|
|
55
|
+
}) => CheckResult | void | Promise<CheckResult | void>;
|
|
56
|
+
export type Email = {
|
|
57
|
+
subject: string;
|
|
58
|
+
html: string;
|
|
59
|
+
text: string;
|
|
60
|
+
};
|
|
61
|
+
export type EmailContext = {
|
|
62
|
+
email: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
req: Req;
|
|
65
|
+
payload: Payload;
|
|
66
|
+
};
|
|
67
|
+
export type Emails = {
|
|
68
|
+
/** The link that proves they own the address. Sent for new requests (and again on a repeat). */
|
|
69
|
+
verify: (ctx: EmailContext & {
|
|
70
|
+
url: string;
|
|
71
|
+
hours: number;
|
|
72
|
+
}) => Email | Promise<Email>;
|
|
73
|
+
/** After verifying, in approval mode: "we'll be in touch". */
|
|
74
|
+
pending: (ctx: EmailContext) => Email | Promise<Email>;
|
|
75
|
+
/** Approved, or verified in open mode: "you're in". */
|
|
76
|
+
approved: (ctx: EmailContext & {
|
|
77
|
+
signInURL: string;
|
|
78
|
+
}) => Email | Promise<Email>;
|
|
79
|
+
/** Declined: polite, with the optional note staff wrote for them. */
|
|
80
|
+
declined: (ctx: EmailContext & {
|
|
81
|
+
note?: string;
|
|
82
|
+
}) => Email | Promise<Email>;
|
|
83
|
+
/** Someone asked with an address that already has an account: sent instead of a request, so the form never tells. */
|
|
84
|
+
alreadyHasAccount: (ctx: EmailContext & {
|
|
85
|
+
signInURL: string;
|
|
86
|
+
resetURL: string;
|
|
87
|
+
}) => Email | Promise<Email>;
|
|
88
|
+
};
|
|
89
|
+
/** What a rate limit is counting. */
|
|
90
|
+
export type LimitKind = 'address' | 'email' | 'site' | 'verify';
|
|
91
|
+
/** Returns true to allow. Throwing, or anything else, refuses (fail closed). */
|
|
92
|
+
export type RateLimit = (kind: LimitKind, key: string, req: Req) => boolean | Promise<boolean>;
|
|
93
|
+
export type SignupPluginOptions = {
|
|
94
|
+
/** The auth collection accounts are made in. Default: `config.admin.user`, else `users`. */
|
|
95
|
+
users?: string;
|
|
96
|
+
/** Slugs. Defaults: `signup-requests`, `signup-settings` (a global), `signup-attempts`. */
|
|
97
|
+
slugs?: {
|
|
98
|
+
requests?: string;
|
|
99
|
+
settings?: string;
|
|
100
|
+
attempts?: string;
|
|
101
|
+
};
|
|
102
|
+
/** The request's details: plain Payload fields, validated by Payload and shown to staff. Email and password are built in. */
|
|
103
|
+
fields?: Field[];
|
|
104
|
+
/** Run at submit, in order. A refusal stops the request; a check that throws refuses it (fail closed). */
|
|
105
|
+
checks?: Check[];
|
|
106
|
+
/** Run when the person confirms, in order, before the account is made. Optional; see `ConfirmCheck`. */
|
|
107
|
+
confirmChecks?: ConfirmCheck[];
|
|
108
|
+
/** Map a request's details onto the new user. Default: copies `name` when there is one. */
|
|
109
|
+
toUser?: (data: Record<string, unknown>) => Record<string, unknown>;
|
|
110
|
+
/** After approval (or verification in open mode), inside the same transaction: give roles, memberships. Throw to refuse. */
|
|
111
|
+
onApprove?: (args: {
|
|
112
|
+
user: Record<string, unknown>;
|
|
113
|
+
request: Record<string, unknown>;
|
|
114
|
+
req: Req;
|
|
115
|
+
payload: Payload;
|
|
116
|
+
}) => void | Promise<void>;
|
|
117
|
+
/** Email bodies. Plain-text defaults otherwise. */
|
|
118
|
+
emails?: Partial<Emails>;
|
|
119
|
+
/** Where links point. `verifyURL` gets the token; defaults use `serverURL`. */
|
|
120
|
+
urls?: {
|
|
121
|
+
verify?: (token: string) => string;
|
|
122
|
+
signIn?: string;
|
|
123
|
+
resetPassword?: string;
|
|
124
|
+
};
|
|
125
|
+
/** The site's rate limiter. Default: a built-in one that stores hashed keys only. */
|
|
126
|
+
rateLimit?: RateLimit;
|
|
127
|
+
/** Built-in limiter's limits per hour. Defaults: 5 per address, 3 per email, 200 site-wide, 20 verify attempts per address. */
|
|
128
|
+
limits?: Partial<Record<LimitKind, number>>;
|
|
129
|
+
/** The person's network address, for rate limits. Default: the first `x-forwarded-for` entry, else `x-real-ip`. */
|
|
130
|
+
clientAddress?: (req: Req) => string | null;
|
|
131
|
+
/** A human check, such as Turnstile. Returns true for a person; false or throwing refuses. */
|
|
132
|
+
verifyHuman?: (args: {
|
|
133
|
+
token: string | undefined;
|
|
134
|
+
address: string | null;
|
|
135
|
+
req: Req;
|
|
136
|
+
}) => boolean | Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* Who decides requests and changes the mode. Required: the plugin won't start without it, so a site can't leave the
|
|
139
|
+
* queue open to every signed-in member by accident. Only an answer of exactly `true`, for a signed-in user, counts;
|
|
140
|
+
* anything else, a query (`Where`) or a thrown error included, is a no. Typed as Payload's `Access` so existing rules fit.
|
|
141
|
+
*/
|
|
142
|
+
access: {
|
|
143
|
+
manage: Access;
|
|
144
|
+
};
|
|
145
|
+
/** A field that separates sites, such as `tenant`. The request takes its value from `scopeFor`. */
|
|
146
|
+
scopeField?: string;
|
|
147
|
+
/** Which site a request belongs to (with `scopeField`). Return null to refuse. */
|
|
148
|
+
scopeFor?: (req: Req) => ID | null | Promise<ID | null>;
|
|
149
|
+
/** Days a decided, abandoned or unverified request is kept. Default 90. */
|
|
150
|
+
retentionDays?: number;
|
|
151
|
+
/**
|
|
152
|
+
* Which of your fields the confirm page shows, by name, so the person can see it's their request. Default: none,
|
|
153
|
+
* only the email. Someone who mistypes their address sends the link to a stranger's inbox, so show only what's
|
|
154
|
+
* safe for a stranger to read. Hidden fields and fields with their own read access are never shown.
|
|
155
|
+
*/
|
|
156
|
+
confirmShows?: string[];
|
|
157
|
+
/** Hours a verification link works. Default 48. */
|
|
158
|
+
verifyHours?: number;
|
|
159
|
+
/** Minimum password length. Default 10. */
|
|
160
|
+
minPasswordLength?: number;
|
|
161
|
+
/** Overrides the admin setting when set, as a hard lock. Default: `process.env.SIGNUP_MODE`. */
|
|
162
|
+
forceMode?: SignupMode | null;
|
|
163
|
+
/** Admin group. Default 'Sign-up'. */
|
|
164
|
+
adminGroup?: string;
|
|
165
|
+
};
|
|
166
|
+
export type ResolvedSignup = Required<Pick<SignupPluginOptions, 'retentionDays' | 'verifyHours' | 'minPasswordLength' | 'adminGroup'>> & {
|
|
167
|
+
users: string;
|
|
168
|
+
requests: string;
|
|
169
|
+
settings: string;
|
|
170
|
+
attempts: string;
|
|
171
|
+
fields: Field[];
|
|
172
|
+
checks: Check[];
|
|
173
|
+
confirmChecks: ConfirmCheck[];
|
|
174
|
+
toUser: NonNullable<SignupPluginOptions['toUser']>;
|
|
175
|
+
onApprove?: SignupPluginOptions['onApprove'];
|
|
176
|
+
emails: Emails;
|
|
177
|
+
urls: SignupPluginOptions['urls'];
|
|
178
|
+
rateLimit?: RateLimit;
|
|
179
|
+
limits: Record<LimitKind, number>;
|
|
180
|
+
clientAddress: NonNullable<SignupPluginOptions['clientAddress']>;
|
|
181
|
+
verifyHuman?: SignupPluginOptions['verifyHuman'];
|
|
182
|
+
manage: Access;
|
|
183
|
+
scopeField?: string;
|
|
184
|
+
scopeFor?: SignupPluginOptions['scopeFor'];
|
|
185
|
+
forceMode: SignupMode | null;
|
|
186
|
+
confirmShows: string[];
|
|
187
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MODES = ['open', 'approval', 'invite-only', 'closed'];
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { APIError, type Payload, type PayloadRequest } from 'payload';
|
|
2
|
+
import type { ID, Req, ResolvedSignup } from './types.js';
|
|
3
|
+
/** A refusal the person sees, with a code for the site to branch on. */
|
|
4
|
+
export declare class SignupError extends APIError<{
|
|
5
|
+
code: string;
|
|
6
|
+
}> {
|
|
7
|
+
code: string;
|
|
8
|
+
/** `Set-Cookie` values from the site's checks, for the response that carries this refusal. */
|
|
9
|
+
setCookies?: string[];
|
|
10
|
+
constructor(code: string, message: string, status?: number);
|
|
11
|
+
}
|
|
12
|
+
export declare const signupOf: (payload: Payload) => ResolvedSignup;
|
|
13
|
+
export declare const normalizeEmail: (email: unknown) => string;
|
|
14
|
+
export declare const validEmail: (email: string) => boolean;
|
|
15
|
+
/** A random token for a link, and the hash that's stored instead of it. */
|
|
16
|
+
export declare const newToken: () => string;
|
|
17
|
+
export declare const hashToken: (secret: string, token: string) => string;
|
|
18
|
+
/** Keys for rate limits are stored hashed, never as raw addresses or emails. */
|
|
19
|
+
export declare const hashKey: (secret: string, kind: string, key: string) => string;
|
|
20
|
+
export declare const idOf: (v: unknown) => ID | null;
|
|
21
|
+
/**
|
|
22
|
+
* The default client address: `x-real-ip` when present (set by the host's proxy, such as Vercel's), else the last
|
|
23
|
+
* `x-forwarded-for` entry, which is the one a single trusted proxy appended. Earlier entries come from the visitor
|
|
24
|
+
* and can say anything. Behind any other arrangement of proxies, pass your own `clientAddress`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const defaultClientAddress: (req: Req) => string | null;
|
|
27
|
+
/** Run `fn` in a transaction, or inside the caller's when `req` already has one. */
|
|
28
|
+
export declare function transaction<T>(payload: Payload, req: Req | undefined, fn: (req: Req) => Promise<T>): Promise<T>;
|
|
29
|
+
/** The request Payload operations want, from a partial one. The copy keeps `headers`, a getter a spread would drop. */
|
|
30
|
+
export declare const asReq: (payload: Payload, req: Req) => PayloadRequest;
|
|
31
|
+
/**
|
|
32
|
+
* The request itself, never a copy: a spread drops the getters of the request Payload made (`headers` among them),
|
|
33
|
+
* so checks and limits would see no headers and no client address. A request Payload made is used as it is. A
|
|
34
|
+
* partial one (a site calling `submitSignup` from its own code) is completed in place by Payload's `createLocalReq`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function requestFor(payload: Payload, req?: Req): Promise<PayloadRequest>;
|
|
37
|
+
/**
|
|
38
|
+
* The `Set-Cookie` values a check returned. Only printable ASCII (space to `~`), as RFC 6265's cookie syntax allows:
|
|
39
|
+
* anything else (a line break, a control character, any character past `~`) is a broken check, and refuses before
|
|
40
|
+
* anything is stored or emailed, rather than failing later when the header is set.
|
|
41
|
+
*/
|
|
42
|
+
export declare function cookiesOf(value: unknown): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Run a check with a response-header list of its own, so it can't add headers to Sign-up's response through
|
|
45
|
+
* `req.responseHeaders` (Payload's router sends that list). Cookies go through `setCookies`, which are checked.
|
|
46
|
+
* The request's own list (an auth strategy's refreshed cookie, say) is put back as it was.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withOwnResponseHeaders<T>(req: PayloadRequest, fn: () => T | Promise<T>): Promise<T>;
|
|
49
|
+
/** Plain paragraphs as both text and simple HTML. */
|
|
50
|
+
export declare const plainEmail: (subject: string, paragraphs: string[]) => {
|
|
51
|
+
subject: string;
|
|
52
|
+
text: string;
|
|
53
|
+
html: string;
|
|
54
|
+
};
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { APIError, createLocalReq } from 'payload';
|
|
3
|
+
/** A refusal the person sees, with a code for the site to branch on. */
|
|
4
|
+
export class SignupError extends APIError {
|
|
5
|
+
code;
|
|
6
|
+
/** `Set-Cookie` values from the site's checks, for the response that carries this refusal. */
|
|
7
|
+
setCookies;
|
|
8
|
+
constructor(code, message, status = 400) {
|
|
9
|
+
super(message, status, { code }, true);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const signupOf = (payload) => {
|
|
14
|
+
const o = payload.config.custom?.signup;
|
|
15
|
+
if (!o)
|
|
16
|
+
throw new Error('Sign-up: the plugin is not configured on this site.');
|
|
17
|
+
return o;
|
|
18
|
+
};
|
|
19
|
+
export const normalizeEmail = (email) => (typeof email === 'string' ? email.trim().toLowerCase() : '');
|
|
20
|
+
export const validEmail = (email) => email.length <= 254 && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
|
|
21
|
+
/** A random token for a link, and the hash that's stored instead of it. */
|
|
22
|
+
export const newToken = () => randomBytes(32).toString('base64url');
|
|
23
|
+
export const hashToken = (secret, token) => createHash('sha256').update(`${secret}:signup:${token}`).digest('hex');
|
|
24
|
+
/** Keys for rate limits are stored hashed, never as raw addresses or emails. */
|
|
25
|
+
export const hashKey = (secret, kind, key) => createHash('sha256').update(`${secret}:signup-limit:${kind}:${key}`).digest('hex');
|
|
26
|
+
export const idOf = (v) => {
|
|
27
|
+
if (v === null || v === undefined || v === '')
|
|
28
|
+
return null;
|
|
29
|
+
if (typeof v === 'object')
|
|
30
|
+
return (v.id ?? null);
|
|
31
|
+
return v;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The default client address: `x-real-ip` when present (set by the host's proxy, such as Vercel's), else the last
|
|
35
|
+
* `x-forwarded-for` entry, which is the one a single trusted proxy appended. Earlier entries come from the visitor
|
|
36
|
+
* and can say anything. Behind any other arrangement of proxies, pass your own `clientAddress`.
|
|
37
|
+
*/
|
|
38
|
+
export const defaultClientAddress = (req) => {
|
|
39
|
+
const h = req.headers;
|
|
40
|
+
const real = h?.get?.('x-real-ip')?.trim();
|
|
41
|
+
if (real)
|
|
42
|
+
return real;
|
|
43
|
+
return h?.get?.('x-forwarded-for')?.split(',').at(-1)?.trim() || null;
|
|
44
|
+
};
|
|
45
|
+
/** Run `fn` in a transaction, or inside the caller's when `req` already has one. */
|
|
46
|
+
export async function transaction(payload, req, fn) {
|
|
47
|
+
// Copies of req share one context object, so work queued inside (emails) is seen outside.
|
|
48
|
+
if (req)
|
|
49
|
+
req.context ??= {};
|
|
50
|
+
const base = { ...(req ?? {}), headers: req?.headers, payload };
|
|
51
|
+
if (base.transactionID)
|
|
52
|
+
return fn(base);
|
|
53
|
+
const id = (await payload.db.beginTransaction?.()) ?? undefined;
|
|
54
|
+
const r = { ...base, transactionID: id };
|
|
55
|
+
try {
|
|
56
|
+
const out = await fn(r);
|
|
57
|
+
if (id)
|
|
58
|
+
await payload.db.commitTransaction(id);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (id)
|
|
63
|
+
await payload.db.rollbackTransaction(id).catch(() => undefined);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** The request Payload operations want, from a partial one. The copy keeps `headers`, a getter a spread would drop. */
|
|
68
|
+
export const asReq = (payload, req) => ({ ...req, headers: req.headers, payload });
|
|
69
|
+
/**
|
|
70
|
+
* The request itself, never a copy: a spread drops the getters of the request Payload made (`headers` among them),
|
|
71
|
+
* so checks and limits would see no headers and no client address. A request Payload made is used as it is. A
|
|
72
|
+
* partial one (a site calling `submitSignup` from its own code) is completed in place by Payload's `createLocalReq`.
|
|
73
|
+
*/
|
|
74
|
+
export async function requestFor(payload, req = {}) {
|
|
75
|
+
if (req.payload && req.payloadAPI && req.headers)
|
|
76
|
+
return req;
|
|
77
|
+
return createLocalReq({ req }, payload);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The `Set-Cookie` values a check returned. Only printable ASCII (space to `~`), as RFC 6265's cookie syntax allows:
|
|
81
|
+
* anything else (a line break, a control character, any character past `~`) is a broken check, and refuses before
|
|
82
|
+
* anything is stored or emailed, rather than failing later when the header is set.
|
|
83
|
+
*/
|
|
84
|
+
export function cookiesOf(value) {
|
|
85
|
+
if (value === undefined || value === null)
|
|
86
|
+
return [];
|
|
87
|
+
if (!Array.isArray(value) || !value.every((c) => typeof c === 'string' && /^[\x20-\x7e]+$/.test(c)))
|
|
88
|
+
throw new Error('Sign-up: a check returned setCookies that are not a list of printable ASCII strings.');
|
|
89
|
+
return [...value];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run a check with a response-header list of its own, so it can't add headers to Sign-up's response through
|
|
93
|
+
* `req.responseHeaders` (Payload's router sends that list). Cookies go through `setCookies`, which are checked.
|
|
94
|
+
* The request's own list (an auth strategy's refreshed cookie, say) is put back as it was.
|
|
95
|
+
*/
|
|
96
|
+
export async function withOwnResponseHeaders(req, fn) {
|
|
97
|
+
const had = Object.prototype.hasOwnProperty.call(req, 'responseHeaders');
|
|
98
|
+
const own = req.responseHeaders;
|
|
99
|
+
req.responseHeaders = new Headers();
|
|
100
|
+
try {
|
|
101
|
+
return await fn();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
if (had)
|
|
105
|
+
req.responseHeaders = own;
|
|
106
|
+
else
|
|
107
|
+
delete req.responseHeaders;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
111
|
+
/** Plain paragraphs as both text and simple HTML. */
|
|
112
|
+
export const plainEmail = (subject, paragraphs) => ({
|
|
113
|
+
subject,
|
|
114
|
+
text: paragraphs.join('\n\n'),
|
|
115
|
+
html: paragraphs.map((p) => `<p>${escape(p).replace(/(https?:\/\/\S+)/g, '<a href="$1">$1</a>')}</p>`).join('\n'),
|
|
116
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extantape/payload-plugin-signup",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Sign-up for Payload: open, approval, invite-only or closed, switched in the admin. Requests are verified by email and wait as disabled accounts for an approve or decline, with no account enumeration.",
|
|
5
|
+
"homepage": "https://github.com/ExtantApe/payload-plugin-signup#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/ExtantApe/payload-plugin-signup/issues"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"payload",
|
|
11
|
+
"payload-plugin",
|
|
12
|
+
"payloadcms",
|
|
13
|
+
"authentication",
|
|
14
|
+
"multi-tenant",
|
|
15
|
+
"signup",
|
|
16
|
+
"registration",
|
|
17
|
+
"approval",
|
|
18
|
+
"waitlist",
|
|
19
|
+
"invite-only",
|
|
20
|
+
"email-verification"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/ExtantApe/payload-plugin-signup.git"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"leak-check": "bash scripts/leak-check.sh",
|
|
49
|
+
"prepack": "npm run build",
|
|
50
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"payload": "^3.80.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@payloadcms/db-mongodb": "3.92.1",
|
|
57
|
+
"@types/node": "^24.0.0",
|
|
58
|
+
"graphql": "^16.14.2",
|
|
59
|
+
"mongodb-memory-server": "^11.3.0",
|
|
60
|
+
"payload": "3.92.1",
|
|
61
|
+
"typescript": "^5.9.0",
|
|
62
|
+
"vitest": "^3.2.0"
|
|
63
|
+
}
|
|
64
|
+
}
|