@draftlab/auth 0.0.4 → 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.
@@ -1,10 +1,9 @@
1
- import { Layout } from "./base.js";
2
- import { FormAlert } from "./form.js";
1
+ import { Layout, renderToHTML } from "./base.js";
2
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
3
3
 
4
- //#region src/ui/password.ts
4
+ //#region src/ui/password.tsx
5
5
  /**
6
- * Default text copy for all password authentication UI screens.
7
- * All text can be customized via the copy prop.
6
+ * Default copy text for password authentication UI
8
7
  */
9
8
  const DEFAULT_COPY = {
10
9
  error_email_taken: "There is already an account with this email.",
@@ -32,7 +31,81 @@ const DEFAULT_COPY = {
32
31
  logo: "A"
33
32
  };
34
33
  /**
35
- * Creates a complete UI configuration for password-based authentication.
34
+ * FormAlert component for displaying error messages
35
+ */
36
+ const FormAlert = ({ message, color = "danger" }) => {
37
+ if (!message) return null;
38
+ return /* @__PURE__ */ jsxs("div", {
39
+ "data-component": "form-alert",
40
+ "data-color": color,
41
+ children: [/* @__PURE__ */ jsx("i", {
42
+ "data-slot": color === "success" ? "icon-success" : "icon-danger",
43
+ children: color === "success" ? /* @__PURE__ */ jsx("svg", {
44
+ fill: "none",
45
+ stroke: "currentColor",
46
+ viewBox: "0 0 24 24",
47
+ xmlns: "http://www.w3.org/2000/svg",
48
+ "aria-label": "Success",
49
+ role: "img",
50
+ children: /* @__PURE__ */ jsx("path", {
51
+ strokeLinecap: "round",
52
+ strokeLinejoin: "round",
53
+ strokeWidth: 2,
54
+ d: "M5 13l4 4L19 7"
55
+ })
56
+ }) : /* @__PURE__ */ jsx("svg", {
57
+ fill: "none",
58
+ stroke: "currentColor",
59
+ viewBox: "0 0 24 24",
60
+ xmlns: "http://www.w3.org/2000/svg",
61
+ "aria-label": "Error",
62
+ role: "img",
63
+ children: /* @__PURE__ */ jsx("path", {
64
+ strokeLinecap: "round",
65
+ strokeLinejoin: "round",
66
+ strokeWidth: 2,
67
+ d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.232 16.5c-.77.833.192 2.5 1.732 2.5z"
68
+ })
69
+ })
70
+ }), /* @__PURE__ */ jsx("span", {
71
+ "data-slot": "message",
72
+ children: message
73
+ })]
74
+ });
75
+ };
76
+ /**
77
+ * Input component with consistent styling
78
+ */
79
+ const Input = ({ type, name, placeholder, value, required, autoComplete,...props }) => /* @__PURE__ */ jsx("input", {
80
+ type,
81
+ name,
82
+ placeholder,
83
+ value,
84
+ required,
85
+ autoComplete,
86
+ "data-component": "input",
87
+ ...props
88
+ });
89
+ /**
90
+ * Button component with consistent styling
91
+ */
92
+ const Button = ({ type = "submit", children,...props }) => /* @__PURE__ */ jsx("button", {
93
+ type,
94
+ "data-component": "button",
95
+ ...props,
96
+ children
97
+ });
98
+ /**
99
+ * Link component with consistent styling
100
+ */
101
+ const Link = ({ href, children,...props }) => /* @__PURE__ */ jsx("a", {
102
+ href,
103
+ "data-component": "link",
104
+ ...props,
105
+ children
106
+ });
107
+ /**
108
+ * Creates a complete UI configuration for password-based authentication
36
109
  */
37
110
  const PasswordUI = (options) => {
38
111
  const copy = {
@@ -40,253 +113,284 @@ const PasswordUI = (options) => {
40
113
  ...options.copy
41
114
  };
42
115
  /**
43
- * Gets the appropriate error message for display.
116
+ * Gets the appropriate error message for display
44
117
  */
45
118
  const getErrorMessage = (error) => {
46
- if (!error?.type) return;
119
+ if (!error?.type) return void 0;
47
120
  if (error.type === "validation_error" && "message" in error && error.message) return error.message;
48
- return copy[`error_${error.type}`];
121
+ const errorKey = `error_${error.type}`;
122
+ return copy[errorKey];
123
+ };
124
+ /**
125
+ * Renders the login form with email and password inputs
126
+ */
127
+ const renderLogin = (form, error) => /* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsxs("form", {
128
+ "data-component": "form",
129
+ method: "post",
130
+ children: [
131
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
132
+ /* @__PURE__ */ jsx(Input, {
133
+ type: "email",
134
+ name: "email",
135
+ placeholder: copy.input_email,
136
+ value: form?.get("email")?.toString() || "",
137
+ autoComplete: "email",
138
+ required: true
139
+ }),
140
+ /* @__PURE__ */ jsx(Input, {
141
+ type: "password",
142
+ name: "password",
143
+ placeholder: copy.input_password,
144
+ autoComplete: "current-password",
145
+ required: true
146
+ }),
147
+ /* @__PURE__ */ jsx(Button, {
148
+ type: "submit",
149
+ children: copy.button_continue
150
+ }),
151
+ /* @__PURE__ */ jsxs("div", {
152
+ "data-component": "form-footer",
153
+ children: [/* @__PURE__ */ jsxs("span", { children: [
154
+ copy.register_prompt,
155
+ " ",
156
+ /* @__PURE__ */ jsx(Link, {
157
+ href: "./register",
158
+ children: copy.register
159
+ })
160
+ ] }), /* @__PURE__ */ jsx(Link, {
161
+ href: "./change",
162
+ children: copy.change_prompt
163
+ })]
164
+ })
165
+ ]
166
+ }) });
167
+ /**
168
+ * Renders the registration form based on current state
169
+ */
170
+ const renderRegister = (state, form, error) => {
171
+ const emailError = ["invalid_email", "email_taken"].includes(error?.type || "");
172
+ const passwordError = [
173
+ "invalid_password",
174
+ "password_mismatch",
175
+ "validation_error"
176
+ ].includes(error?.type || "");
177
+ return /* @__PURE__ */ jsx(Layout, { children: state.type === "start" ? /* @__PURE__ */ jsxs("form", {
178
+ "data-component": "form",
179
+ method: "post",
180
+ children: [
181
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
182
+ /* @__PURE__ */ jsx("input", {
183
+ name: "action",
184
+ type: "hidden",
185
+ value: "register"
186
+ }),
187
+ /* @__PURE__ */ jsx(Input, {
188
+ type: "email",
189
+ name: "email",
190
+ placeholder: copy.input_email,
191
+ value: emailError ? "" : form?.get("email")?.toString() || "",
192
+ autoComplete: "email",
193
+ required: true
194
+ }),
195
+ /* @__PURE__ */ jsx(Input, {
196
+ type: "password",
197
+ name: "password",
198
+ placeholder: copy.input_password,
199
+ value: passwordError ? "" : form?.get("password")?.toString() || "",
200
+ autoComplete: "new-password",
201
+ required: true
202
+ }),
203
+ /* @__PURE__ */ jsx(Input, {
204
+ type: "password",
205
+ name: "repeat",
206
+ placeholder: copy.input_repeat,
207
+ autoComplete: "new-password",
208
+ required: true
209
+ }),
210
+ /* @__PURE__ */ jsx(Button, {
211
+ type: "submit",
212
+ children: copy.button_continue
213
+ }),
214
+ /* @__PURE__ */ jsx("div", {
215
+ "data-component": "form-footer",
216
+ children: /* @__PURE__ */ jsxs("span", { children: [
217
+ copy.login_prompt,
218
+ " ",
219
+ /* @__PURE__ */ jsx(Link, {
220
+ href: "./authorize",
221
+ children: copy.login
222
+ })
223
+ ] })
224
+ })
225
+ ]
226
+ }) : /* @__PURE__ */ jsxs("form", {
227
+ "data-component": "form",
228
+ method: "post",
229
+ children: [
230
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
231
+ /* @__PURE__ */ jsx("input", {
232
+ name: "action",
233
+ type: "hidden",
234
+ value: "verify"
235
+ }),
236
+ /* @__PURE__ */ jsx(Input, {
237
+ type: "text",
238
+ name: "code",
239
+ placeholder: copy.input_code,
240
+ "aria-label": "6-digit verification code",
241
+ autoComplete: "one-time-code",
242
+ inputMode: "numeric",
243
+ maxLength: 6,
244
+ minLength: 6,
245
+ pattern: "[0-9]{6}",
246
+ autoFocus: true,
247
+ required: true
248
+ }),
249
+ /* @__PURE__ */ jsx(Button, {
250
+ type: "submit",
251
+ children: copy.button_continue
252
+ })
253
+ ]
254
+ }) });
255
+ };
256
+ /**
257
+ * Renders the password change form based on current state
258
+ */
259
+ const renderChange = (state, form, error) => {
260
+ const passwordError = [
261
+ "invalid_password",
262
+ "password_mismatch",
263
+ "validation_error"
264
+ ].includes(error?.type || "");
265
+ return /* @__PURE__ */ jsx(Layout, { children: state.type === "start" ? /* @__PURE__ */ jsxs("form", {
266
+ "data-component": "form",
267
+ method: "post",
268
+ children: [
269
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
270
+ /* @__PURE__ */ jsx("input", {
271
+ name: "action",
272
+ type: "hidden",
273
+ value: "code"
274
+ }),
275
+ /* @__PURE__ */ jsx(Input, {
276
+ type: "email",
277
+ name: "email",
278
+ placeholder: copy.input_email,
279
+ value: form?.get("email")?.toString() || "",
280
+ autoComplete: "email",
281
+ required: true
282
+ }),
283
+ /* @__PURE__ */ jsx(Button, {
284
+ type: "submit",
285
+ children: copy.button_continue
286
+ })
287
+ ]
288
+ }) : state.type === "code" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
289
+ "data-component": "form",
290
+ method: "post",
291
+ children: [
292
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
293
+ /* @__PURE__ */ jsx("input", {
294
+ name: "action",
295
+ type: "hidden",
296
+ value: "verify"
297
+ }),
298
+ /* @__PURE__ */ jsx(Input, {
299
+ type: "text",
300
+ name: "code",
301
+ placeholder: copy.input_code,
302
+ "aria-label": "6-digit verification code",
303
+ autoComplete: "one-time-code",
304
+ inputMode: "numeric",
305
+ maxLength: 6,
306
+ minLength: 6,
307
+ pattern: "[0-9]{6}",
308
+ autoFocus: true,
309
+ required: true
310
+ }),
311
+ /* @__PURE__ */ jsx(Button, {
312
+ type: "submit",
313
+ children: copy.button_continue
314
+ })
315
+ ]
316
+ }), /* @__PURE__ */ jsxs("form", {
317
+ method: "post",
318
+ children: [
319
+ /* @__PURE__ */ jsx("input", {
320
+ name: "action",
321
+ type: "hidden",
322
+ value: "code"
323
+ }),
324
+ /* @__PURE__ */ jsx("input", {
325
+ name: "email",
326
+ type: "hidden",
327
+ value: state.email
328
+ }),
329
+ /* @__PURE__ */ jsxs("div", {
330
+ "data-component": "form-footer",
331
+ children: [/* @__PURE__ */ jsxs("span", { children: [
332
+ copy.code_return,
333
+ " ",
334
+ /* @__PURE__ */ jsx(Link, {
335
+ href: "./authorize",
336
+ children: copy.login.toLowerCase()
337
+ })
338
+ ] }), /* @__PURE__ */ jsx(Button, {
339
+ type: "submit",
340
+ "data-component": "link",
341
+ children: copy.code_resend
342
+ })]
343
+ })
344
+ ]
345
+ })] }) : /* @__PURE__ */ jsxs("form", {
346
+ "data-component": "form",
347
+ method: "post",
348
+ children: [
349
+ /* @__PURE__ */ jsx(FormAlert, { message: getErrorMessage(error) }),
350
+ /* @__PURE__ */ jsx("input", {
351
+ name: "action",
352
+ type: "hidden",
353
+ value: "update"
354
+ }),
355
+ /* @__PURE__ */ jsx(Input, {
356
+ type: "password",
357
+ name: "password",
358
+ placeholder: copy.input_password,
359
+ value: passwordError ? "" : form?.get("password")?.toString() || "",
360
+ autoComplete: "new-password",
361
+ required: true
362
+ }),
363
+ /* @__PURE__ */ jsx(Input, {
364
+ type: "password",
365
+ name: "repeat",
366
+ placeholder: copy.input_repeat,
367
+ value: passwordError ? "" : form?.get("repeat")?.toString() || "",
368
+ autoComplete: "new-password",
369
+ required: true
370
+ }),
371
+ /* @__PURE__ */ jsx(Button, {
372
+ type: "submit",
373
+ children: copy.button_continue
374
+ })
375
+ ]
376
+ }) });
49
377
  };
50
378
  return {
51
379
  validatePassword: options.validatePassword,
52
380
  sendCode: options.sendCode,
53
381
  login: async (_req, form, error) => {
54
- const formContent = `
55
- <form data-component="form" method="post">
56
- ${FormAlert({ message: getErrorMessage(error) })}
57
-
58
- <input
59
- autocomplete="email"
60
- data-component="input"
61
- value="${form?.get("email")?.toString() || ""}"
62
- name="email"
63
- placeholder="${copy.input_email}"
64
- required
65
- type="email"
66
- />
67
-
68
- <input
69
- autocomplete="current-password"
70
- data-component="input"
71
- name="password"
72
- placeholder="${copy.input_password}"
73
- required
74
- type="password"
75
- />
76
-
77
- <button data-component="button" type="submit">
78
- ${copy.button_continue}
79
- </button>
80
-
81
- <div data-component="form-footer">
82
- <span>
83
- ${copy.register_prompt}
84
- <a data-component="link" href="./register">
85
- ${copy.register}
86
- </a>
87
- </span>
88
- <a data-component="link" href="./change">
89
- ${copy.change_prompt}
90
- </a>
91
- </div>
92
- </form>
93
- `;
94
- const html = Layout({ children: formContent });
382
+ const html = renderToHTML(renderLogin(form, error));
95
383
  return new Response(html, {
96
384
  status: error ? 401 : 200,
97
385
  headers: { "Content-Type": "text/html" }
98
386
  });
99
387
  },
100
388
  register: async (_req, state, form, error) => {
101
- const emailError = ["invalid_email", "email_taken"].includes(error?.type || "");
102
- const passwordError = [
103
- "invalid_password",
104
- "password_mismatch",
105
- "validation_error"
106
- ].includes(error?.type || "");
107
- let formContent = "";
108
- if (state.type === "start") formContent = `
109
- <form data-component="form" method="post">
110
- ${FormAlert({ message: getErrorMessage(error) })}
111
-
112
- <input name="action" type="hidden" value="register" />
113
-
114
- <input
115
- autocomplete="email"
116
- data-component="input"
117
- value="${emailError ? "" : form?.get("email")?.toString() || ""}"
118
- name="email"
119
- placeholder="${copy.input_email}"
120
- required
121
- type="email"
122
- />
123
-
124
- <input
125
- autocomplete="new-password"
126
- data-component="input"
127
- value="${passwordError ? "" : form?.get("password")?.toString() || ""}"
128
- name="password"
129
- placeholder="${copy.input_password}"
130
- required
131
- type="password"
132
- />
133
-
134
- <input
135
- autocomplete="new-password"
136
- data-component="input"
137
- name="repeat"
138
- placeholder="${copy.input_repeat}"
139
- required
140
- type="password"
141
- />
142
-
143
- <button data-component="button" type="submit">
144
- ${copy.button_continue}
145
- </button>
146
-
147
- <div data-component="form-footer">
148
- <span>
149
- ${copy.login_prompt}
150
- <a data-component="link" href="./authorize">
151
- ${copy.login}
152
- </a>
153
- </span>
154
- </div>
155
- </form>
156
- `;
157
- else if (state.type === "code") formContent = `
158
- <form data-component="form" method="post">
159
- ${FormAlert({ message: getErrorMessage(error) })}
160
-
161
- <input name="action" type="hidden" value="verify" />
162
-
163
- <input
164
- aria-label="6-digit verification code"
165
- autocomplete="one-time-code"
166
- data-component="input"
167
- inputmode="numeric"
168
- maxlength="6"
169
- minlength="6"
170
- name="code"
171
- pattern="[0-9]{6}"
172
- placeholder="${copy.input_code}"
173
- required
174
- type="text"
175
- />
176
-
177
- <button data-component="button" type="submit">
178
- ${copy.button_continue}
179
- </button>
180
- </form>
181
- `;
182
- const html = Layout({ children: formContent });
389
+ const html = renderToHTML(renderRegister(state, form, error));
183
390
  return new Response(html, { headers: { "Content-Type": "text/html" } });
184
391
  },
185
392
  change: async (_req, state, form, error) => {
186
- const passwordError = [
187
- "invalid_password",
188
- "password_mismatch",
189
- "validation_error"
190
- ].includes(error?.type || "");
191
- let formContent = "";
192
- let additionalForms = "";
193
- if (state.type === "start") formContent = `
194
- <form data-component="form" method="post">
195
- ${FormAlert({ message: getErrorMessage(error) })}
196
-
197
- <input name="action" type="hidden" value="code" />
198
-
199
- <input
200
- autocomplete="email"
201
- data-component="input"
202
- value="${form?.get("email")?.toString() || ""}"
203
- name="email"
204
- placeholder="${copy.input_email}"
205
- required
206
- type="email"
207
- />
208
-
209
- <button data-component="button" type="submit">
210
- ${copy.button_continue}
211
- </button>
212
- </form>
213
- `;
214
- else if (state.type === "code") {
215
- formContent = `
216
- <form data-component="form" method="post">
217
- ${FormAlert({ message: getErrorMessage(error) })}
218
-
219
- <input name="action" type="hidden" value="verify" />
220
-
221
- <input
222
- aria-label="6-digit verification code"
223
- autocomplete="one-time-code"
224
- data-component="input"
225
- inputmode="numeric"
226
- maxlength="6"
227
- minlength="6"
228
- name="code"
229
- pattern="[0-9]{6}"
230
- placeholder="${copy.input_code}"
231
- required
232
- type="text"
233
- />
234
-
235
- <button data-component="button" type="submit">
236
- ${copy.button_continue}
237
- </button>
238
- </form>
239
- `;
240
- additionalForms = `
241
- <form method="post">
242
- <input name="action" type="hidden" value="code" />
243
- <input name="email" type="hidden" value="${state.email}" />
244
-
245
- <div data-component="form-footer">
246
- <span>
247
- ${copy.code_return}
248
- <a data-component="link" href="./authorize">
249
- ${copy.login.toLowerCase()}
250
- </a>
251
- </span>
252
- <button data-component="link" type="submit">
253
- ${copy.code_resend}
254
- </button>
255
- </div>
256
- </form>
257
- `;
258
- } else if (state.type === "update") formContent = `
259
- <form data-component="form" method="post">
260
- ${FormAlert({ message: getErrorMessage(error) })}
261
-
262
- <input name="action" type="hidden" value="update" />
263
-
264
- <input
265
- autocomplete="new-password"
266
- data-component="input"
267
- value="${passwordError ? "" : form?.get("password")?.toString() || ""}"
268
- name="password"
269
- placeholder="${copy.input_password}"
270
- required
271
- type="password"
272
- />
273
-
274
- <input
275
- autocomplete="new-password"
276
- data-component="input"
277
- value="${passwordError ? "" : form?.get("repeat")?.toString() || ""}"
278
- name="repeat"
279
- placeholder="${copy.input_repeat}"
280
- required
281
- type="password"
282
- />
283
-
284
- <button data-component="button" type="submit">
285
- ${copy.button_continue}
286
- </button>
287
- </form>
288
- `;
289
- const html = Layout({ children: formContent + additionalForms });
393
+ const html = renderToHTML(renderChange(state, form, error));
290
394
  return new Response(html, {
291
395
  status: error ? 400 : 200,
292
396
  headers: { "Content-Type": "text/html" }