@drawnagency/primitives 0.1.33 → 0.1.35

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.
@@ -49,6 +49,8 @@ export interface AuthProvider {
49
49
  getPasswordEnabled(): Promise<boolean>;
50
50
  setPasswordEnabled?(enabled: boolean): Promise<void>;
51
51
  handleCallback?(request: Request, ctx: AuthContext): Promise<Response>;
52
+ updatePassword?(password: string, ctx: AuthContext): Promise<void>;
53
+ resetPassword?(email: string): Promise<void>;
52
54
  }
53
55
  export interface CookieLike {
54
56
  get(name: string): {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"EditorLoginForm.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorLoginForm.tsx"],"names":[],"mappings":"AAKA,UAAU,KAAK;IACb,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,cAAc,CAAC,EAAE,CAAC,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC;IACzC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAgB,eAAe,CAAC,EAAE,YAAY,EAAE,cAAmB,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,2CA+G9F"}
1
+ {"version":3,"file":"EditorLoginForm.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorLoginForm.tsx"],"names":[],"mappings":"AAKA,UAAU,KAAK;IACb,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,cAAc,CAAC,EAAE,CAAC,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC;IACzC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAgB,eAAe,CAAC,EAAE,YAAY,EAAE,cAAmB,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,2CA0O9F"}
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ reason?: "invite" | "recovery" | null;
3
+ }
4
+ export declare function SetPasswordForm({ reason }: Props): import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=SetPasswordForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SetPasswordForm.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SetPasswordForm.tsx"],"names":[],"mappings":"AAIA,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI,CAAC;CACvC;AAED,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,2CA2FhD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
package/src/auth/types.ts CHANGED
@@ -47,6 +47,8 @@ export interface AuthProvider {
47
47
  setPasswordEnabled?(enabled: boolean): Promise<void>;
48
48
 
49
49
  handleCallback?(request: Request, ctx: AuthContext): Promise<Response>;
50
+ updatePassword?(password: string, ctx: AuthContext): Promise<void>;
51
+ resetPassword?(email: string): Promise<void>;
50
52
  }
51
53
 
52
54
  export interface CookieLike {
@@ -32,7 +32,7 @@ export default function SplitContent({ imageId, src, srcset, alt, body, border,
32
32
  const handleBodyChange = (html: string) => {
33
33
  onChange?.({
34
34
  type: "split_content",
35
- content: { imageId, body: html },
35
+ content: { imageId, body: html, src, srcset, alt },
36
36
  options: { border, imagePosition },
37
37
  });
38
38
  };
@@ -20,6 +20,8 @@ export function EditorLoginForm({ capabilities, oauthProviders = [], next, serve
20
20
  const [password, setPassword] = useState("");
21
21
  const [error, setError] = useState<string | null>(serverError ?? null);
22
22
  const [loading, setLoading] = useState(false);
23
+ const [mode, setMode] = useState<"sign-in" | "forgot">("sign-in");
24
+ const [resetSent, setResetSent] = useState(false);
23
25
 
24
26
  async function handlePasswordSubmit(e: FormEvent) {
25
27
  e.preventDefault();
@@ -46,8 +48,36 @@ export function EditorLoginForm({ capabilities, oauthProviders = [], next, serve
46
48
 
47
49
  const data = await response.json().catch(() => ({}));
48
50
  window.location.assign(safeRedirect(data.redirectTo, "/edit"));
49
- } catch {
50
- setError("Network error");
51
+ } catch (err) {
52
+ console.error("sign-in request failed:", err);
53
+ setError("Unable to reach the server. Check your connection and try again.");
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ }
58
+
59
+ async function handleResetSubmit(e: FormEvent) {
60
+ e.preventDefault();
61
+ setLoading(true);
62
+ setError(null);
63
+
64
+ try {
65
+ const response = await fetch("/api/auth/reset-password", {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({ email }),
69
+ });
70
+
71
+ if (!response.ok) {
72
+ const data = await response.json().catch(() => ({}));
73
+ setError(data.error || "Failed to send reset link");
74
+ return;
75
+ }
76
+
77
+ setResetSent(true);
78
+ } catch (err) {
79
+ console.error("reset-password request failed:", err);
80
+ setError("Unable to reach the server. Check your connection and try again.");
51
81
  } finally {
52
82
  setLoading(false);
53
83
  }
@@ -57,6 +87,87 @@ export function EditorLoginForm({ capabilities, oauthProviders = [], next, serve
57
87
  window.location.href = `/api/auth/oauth?provider=${provider}`;
58
88
  }
59
89
 
90
+ function switchToForgot() {
91
+ setMode("forgot");
92
+ setError(null);
93
+ setResetSent(false);
94
+ }
95
+
96
+ function switchToSignIn() {
97
+ setMode("sign-in");
98
+ setError(null);
99
+ setResetSent(false);
100
+ }
101
+
102
+ if (mode === "forgot") {
103
+ return (
104
+ <div className="mx-auto max-w-sm space-y-6">
105
+ <div className="space-y-2 text-center">
106
+ <h2 className="text-lg font-semibold text-base-contrast">Reset your password</h2>
107
+ <p className="text-sm text-base-contrast-light">
108
+ Enter your email and we'll send you a link to reset your password.
109
+ </p>
110
+ </div>
111
+
112
+ {resetSent ? (
113
+ <div className="space-y-4">
114
+ <p className="text-center text-sm text-base-contrast-light">
115
+ If an account exists for <strong className="text-base-contrast">{email}</strong>, you'll receive
116
+ a password reset link shortly.
117
+ </p>
118
+ <Button
119
+ type="button"
120
+ variant="secondary"
121
+ size="md"
122
+ onClick={switchToSignIn}
123
+ className="w-full"
124
+ >
125
+ Back to sign in
126
+ </Button>
127
+ </div>
128
+ ) : (
129
+ <form onSubmit={handleResetSubmit} className="space-y-4">
130
+ <div>
131
+ <label htmlFor="reset-email" className="mb-1.5 block text-sm font-medium text-base-contrast">
132
+ Email
133
+ </label>
134
+ <input
135
+ id="reset-email"
136
+ type="email"
137
+ value={email}
138
+ onChange={(e) => setEmail(e.target.value)}
139
+ required
140
+ autoComplete="email"
141
+ className="w-full rounded border border-base-200 bg-base px-3 py-2 text-sm text-base-contrast focus:border-base-contrast focus:outline-none focus:ring-1 focus:ring-base-contrast"
142
+ />
143
+ </div>
144
+
145
+ {error && <p className="text-sm text-red-600">{error}</p>}
146
+
147
+ <Button
148
+ type="submit"
149
+ variant="primary"
150
+ size="md"
151
+ isLoading={loading}
152
+ loadingLabel="Sending..."
153
+ className="w-full"
154
+ >
155
+ Send reset link
156
+ </Button>
157
+
158
+ <button
159
+ type="button"
160
+ onClick={switchToSignIn}
161
+ className="block w-full text-center text-sm text-base-contrast-light hover:text-base-contrast"
162
+ >
163
+ Back to sign in
164
+ </button>
165
+ </form>
166
+ )}
167
+ </div>
168
+ );
169
+ }
170
+
60
171
  return (
61
172
  <div className="mx-auto max-w-sm space-y-6">
62
173
  {capabilities.oauth && oauthProviders.length > 0 && (
@@ -109,6 +220,18 @@ export function EditorLoginForm({ capabilities, oauthProviders = [], next, serve
109
220
  autoComplete="current-password"
110
221
  />
111
222
 
223
+ {capabilities.emailPassword && (
224
+ <div className="flex justify-end">
225
+ <button
226
+ type="button"
227
+ onClick={switchToForgot}
228
+ className="text-sm text-base-contrast-light hover:text-base-contrast"
229
+ >
230
+ Forgot password?
231
+ </button>
232
+ </div>
233
+ )}
234
+
112
235
  {error && (
113
236
  <p className="text-sm text-red-600">{error}</p>
114
237
  )}
@@ -0,0 +1,100 @@
1
+ import { useState, type FormEvent } from "react";
2
+ import { PasswordInput } from "../shared/PasswordInput";
3
+ import { Button } from "../shared/Button";
4
+
5
+ interface Props {
6
+ reason?: "invite" | "recovery" | null;
7
+ }
8
+
9
+ export function SetPasswordForm({ reason }: Props) {
10
+ const [password, setPassword] = useState("");
11
+ const [confirm, setConfirm] = useState("");
12
+ const [error, setError] = useState<string | null>(null);
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ const title = reason === "recovery" ? "Reset your password" : "Set your password";
16
+ const buttonLabel = reason === "recovery" ? "Reset Password" : "Set Password";
17
+
18
+ async function handleSubmit(e: FormEvent) {
19
+ e.preventDefault();
20
+ setError(null);
21
+
22
+ if (password.length < 8) {
23
+ setError("Password must be at least 8 characters");
24
+ return;
25
+ }
26
+ if (password !== confirm) {
27
+ setError("Passwords do not match");
28
+ return;
29
+ }
30
+
31
+ setLoading(true);
32
+ try {
33
+ const response = await fetch("/api/auth/set-password", {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({ password }),
37
+ });
38
+
39
+ if (!response.ok) {
40
+ if (response.status === 401) {
41
+ setError("Your session has expired. Please request a new link.");
42
+ return;
43
+ }
44
+ const data = await response.json().catch(() => ({}));
45
+ setError(data.error || "Failed to set password");
46
+ return;
47
+ }
48
+
49
+ window.location.assign("/edit");
50
+ } catch (err) {
51
+ console.error("set-password request failed:", err);
52
+ setError("Unable to reach the server. Check your connection and try again.");
53
+ } finally {
54
+ setLoading(false);
55
+ }
56
+ }
57
+
58
+ return (
59
+ <div className="mx-auto max-w-sm space-y-6">
60
+ <div className="space-y-2 text-center">
61
+ <h1 className="text-2xl font-bold text-base-contrast">{title}</h1>
62
+ <p className="text-sm text-base-contrast-light">
63
+ {reason === "invite"
64
+ ? "Create a password to complete your account setup."
65
+ : "Enter a new password for your account."}
66
+ </p>
67
+ </div>
68
+
69
+ <form onSubmit={handleSubmit} className="space-y-4">
70
+ <PasswordInput
71
+ label="New password"
72
+ value={password}
73
+ onChange={setPassword}
74
+ autoComplete="new-password"
75
+ showToggle
76
+ />
77
+
78
+ <PasswordInput
79
+ label="Confirm password"
80
+ value={confirm}
81
+ onChange={setConfirm}
82
+ autoComplete="new-password"
83
+ />
84
+
85
+ {error && <p className="text-sm text-red-600">{error}</p>}
86
+
87
+ <Button
88
+ type="submit"
89
+ variant="primary"
90
+ size="md"
91
+ isLoading={loading}
92
+ loadingLabel="Saving..."
93
+ className="w-full"
94
+ >
95
+ {buttonLabel}
96
+ </Button>
97
+ </form>
98
+ </div>
99
+ );
100
+ }