@guard-rail/core 0.7.1 → 0.9.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/README.md +86 -41
- package/dist/adapters/supabase/auth.d.ts +13 -1
- package/dist/adapters/supabase/auth.js +42 -25
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/adapters/supabase/client.d.ts +16 -1
- package/dist/adapters/supabase/client.js +9 -0
- package/dist/adapters/supabase/client.js.map +1 -1
- package/dist/adapters/supabase/confirm.d.ts +5 -0
- package/dist/adapters/supabase/confirm.js +12 -1
- package/dist/adapters/supabase/confirm.js.map +1 -1
- package/dist/adapters/supabase/members.js +13 -2
- package/dist/adapters/supabase/members.js.map +1 -1
- package/dist/core/adapter-types.d.ts +80 -12
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.js +4 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/defs.d.ts +11 -1
- package/dist/core/defs.js +11 -1
- package/dist/core/defs.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/email/resend.d.ts +80 -0
- package/dist/email/resend.js +118 -0
- package/dist/email/resend.js.map +1 -0
- package/dist/index.client.d.ts +6 -3
- package/dist/index.client.js +14 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.js +105 -107
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthDrawer.d.ts +23 -0
- package/dist/react/AuthDrawer.js +165 -0
- package/dist/react/AuthDrawer.js.map +1 -0
- package/dist/react/AuthRedirectSync.js +1 -1
- package/dist/react/AuthRedirectSync.js.map +1 -1
- package/dist/react/Icons.d.ts +2 -1
- package/dist/react/Icons.js +2 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/react/Team.d.ts +7 -1
- package/dist/react/Team.js +23 -3
- package/dist/react/Team.js.map +1 -1
- package/dist/server/access.d.ts +3 -1
- package/dist/server/access.js +5 -3
- package/dist/server/access.js.map +1 -1
- package/dist/server/actions.d.ts +25 -4
- package/dist/server/actions.js +118 -11
- package/dist/server/actions.js.map +1 -1
- package/dist/server/wrapper.js +2 -1
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +8 -10
package/dist/react/Icons.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PencilIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M12 20h9\" />\n <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n\nexport const OpenIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n);\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PencilIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M12 20h9\" />\n <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n\nexport const OpenIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n);\n\nexport const KeyIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4\" />\n </svg>\n);\n"],"mappings":"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","names":[]}
|
package/dist/react/Team.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Keyframes cannot be expressed as inline styles, so the drawer ships the few
|
|
5
|
+
* it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and
|
|
6
|
+
* disabled under prefers-reduced-motion.
|
|
7
|
+
*/
|
|
8
|
+
declare const ANIMATION_CSS = "\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n";
|
|
3
9
|
declare function TeamDrawer({ onClose }: {
|
|
4
10
|
onClose: () => void;
|
|
5
11
|
}): react_jsx_runtime.JSX.Element;
|
|
6
12
|
|
|
7
|
-
export { TeamDrawer };
|
|
13
|
+
export { ANIMATION_CSS, TeamDrawer };
|
package/dist/react/Team.js
CHANGED
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
inviteMember,
|
|
6
6
|
listTeam,
|
|
7
7
|
removeMember,
|
|
8
|
+
sendPasswordResetAction,
|
|
8
9
|
updateMemberRole
|
|
9
10
|
} from "../server/actions";
|
|
10
11
|
import { useGRail } from "./Provider";
|
|
11
|
-
import { CloseIcon } from "./Icons";
|
|
12
|
+
import { CloseIcon, KeyIcon } from "./Icons";
|
|
12
13
|
import { S, v } from "./styles";
|
|
13
14
|
const ROLES = ["admin", "editor"];
|
|
14
15
|
const ANIMATION_CSS = `
|
|
@@ -39,6 +40,7 @@ function TeamDrawer({ onClose }) {
|
|
|
39
40
|
const [members, setMembers] = useState([]);
|
|
40
41
|
const [loading, setLoading] = useState(true);
|
|
41
42
|
const [error, setError] = useState(null);
|
|
43
|
+
const [notice, setNotice] = useState(null);
|
|
42
44
|
const [inviteEmail, setInviteEmail] = useState("");
|
|
43
45
|
const [inviteRole, setInviteRole] = useState("editor");
|
|
44
46
|
const [pending, startTransition] = useTransition();
|
|
@@ -51,8 +53,10 @@ function TeamDrawer({ onClose }) {
|
|
|
51
53
|
}, []);
|
|
52
54
|
const run = (fn) => startTransition(async () => {
|
|
53
55
|
setError(null);
|
|
56
|
+
setNotice(null);
|
|
54
57
|
const res = await fn();
|
|
55
58
|
if (!res.success) setError(res.error ?? "Something went wrong.");
|
|
59
|
+
else if (res.notice) setNotice(res.notice);
|
|
56
60
|
await refresh();
|
|
57
61
|
});
|
|
58
62
|
const [closing, setClosing] = useState(false);
|
|
@@ -102,6 +106,7 @@ function TeamDrawer({ onClose }) {
|
|
|
102
106
|
] }),
|
|
103
107
|
/* @__PURE__ */ jsxs("div", { style: S.drawerBody, children: [
|
|
104
108
|
error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: error }),
|
|
109
|
+
notice && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: v("muted"), marginBottom: 12 }, role: "status", children: notice }),
|
|
105
110
|
loading ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: v("muted") }, children: "Loading\u2026" }) : members.map((m) => /* @__PURE__ */ jsxs("div", { "data-gr-row": true, style: S.memberRow, children: [
|
|
106
111
|
/* @__PURE__ */ jsxs("span", { style: S.memberEmail, title: m.email, children: [
|
|
107
112
|
m.email,
|
|
@@ -120,6 +125,17 @@ function TeamDrawer({ onClose }) {
|
|
|
120
125
|
children: ROLES.map((r) => /* @__PURE__ */ jsx("option", { value: r, children: r }, r))
|
|
121
126
|
}
|
|
122
127
|
) : /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: m.role }),
|
|
128
|
+
isAdmin && /* @__PURE__ */ jsx(
|
|
129
|
+
"button",
|
|
130
|
+
{
|
|
131
|
+
style: { ...S.iconBtn, color: v("muted"), width: 22, height: 22 },
|
|
132
|
+
disabled: pending,
|
|
133
|
+
onClick: () => run(() => sendPasswordResetAction(m.email)),
|
|
134
|
+
"aria-label": `Send password reset to ${m.email}`,
|
|
135
|
+
title: "Send password reset",
|
|
136
|
+
children: /* @__PURE__ */ jsx(KeyIcon, {})
|
|
137
|
+
}
|
|
138
|
+
),
|
|
123
139
|
isAdmin && /* @__PURE__ */ jsx(
|
|
124
140
|
"button",
|
|
125
141
|
{
|
|
@@ -173,9 +189,12 @@ function TeamDrawer({ onClose }) {
|
|
|
173
189
|
}
|
|
174
190
|
),
|
|
175
191
|
/* @__PURE__ */ jsxs("p", { style: { fontSize: 11, color: v("subtle"), marginTop: 10 }, children: [
|
|
176
|
-
"
|
|
192
|
+
"New people get an email to set a password. Invites and resets go out through your ",
|
|
177
193
|
/* @__PURE__ */ jsx("code", { children: "onInvite" }),
|
|
178
|
-
"
|
|
194
|
+
" / ",
|
|
195
|
+
/* @__PURE__ */ jsx("code", { children: "onPasswordReset" }),
|
|
196
|
+
" ",
|
|
197
|
+
"handlers, or your auth provider's own emails if you haven't set them."
|
|
179
198
|
] })
|
|
180
199
|
] })
|
|
181
200
|
] })
|
|
@@ -185,6 +204,7 @@ function TeamDrawer({ onClose }) {
|
|
|
185
204
|
] });
|
|
186
205
|
}
|
|
187
206
|
export {
|
|
207
|
+
ANIMATION_CSS,
|
|
188
208
|
TeamDrawer
|
|
189
209
|
};
|
|
190
210
|
//# sourceMappingURL=Team.js.map
|
package/dist/react/Team.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useState, useTransition } from \"react\";\nimport {\n inviteMember,\n listTeam,\n removeMember,\n updateMemberRole,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { CloseIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport type { GRMember, GRRole } from \"../core/types\";\n\nconst ROLES: GRRole[] = [\"admin\", \"editor\"];\n\n/**\n * Keyframes cannot be expressed as inline styles, so the drawer ships the few\n * it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and\n * disabled under prefers-reduced-motion.\n */\nconst ANIMATION_CSS = `\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n`;\n\nexport function TeamDrawer({ onClose }: { onClose: () => void }) {\n const { role, email: currentEmail } = useGRail();\n const isAdmin = role === \"admin\";\n\n const [members, setMembers] = useState<GRMember[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState<GRRole>(\"editor\");\n const [pending, startTransition] = useTransition();\n\n const refresh = () =>\n listTeam().then((rows) => {\n setMembers(rows);\n setLoading(false);\n });\n\n useEffect(() => {\n void refresh();\n }, []);\n\n // Server actions re-check the role; this only decides what to render.\n // Always refresh: a failure can still have changed the roster (an invite\n // that saved but whose email failed, for instance).\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n await refresh();\n });\n\n /**\n * Unmounting on close skips the exit animation entirely. Stay mounted, play\n * it, and let the parent unmount once it finishes.\n */\n const [closing, setClosing] = useState(false);\n const requestClose = useCallback(() => {\n const reduced =\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduced) return onClose(); // No animation, so no animationend to wait for.\n setClosing(true);\n }, [onClose]);\n\n useEffect(() => {\n if (!closing) return;\n // Safety net if animationend never arrives (tab hidden, styles stripped).\n const t = window.setTimeout(onClose, 400);\n return () => window.clearTimeout(t);\n }, [closing, onClose]);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && requestClose();\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [requestClose]);\n\n return (\n <>\n {/* Hoisted to <head> — see the note on the theme <style> in Admin.tsx. */}\n <style href=\"guardrail-team-animation\" precedence=\"default\">\n {ANIMATION_CSS}\n </style>\n <div\n data-guardrail\n data-guardrail-scrim\n {...(closing ? { \"data-closing\": \"\" } : {})}\n style={S.scrim}\n onClick={requestClose}\n />\n <aside\n data-guardrail\n data-guardrail-team\n {...(closing ? { \"data-closing\": \"\" } : {})}\n onAnimationEnd={(e) => {\n // Row fade-ins bubble up here too; only the drawer's own exit counts.\n if (closing && e.target === e.currentTarget) onClose();\n }}\n style={S.drawer}\n role=\"dialog\"\n aria-label=\"Team\"\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Team</h2>\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={requestClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {loading ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : (\n members.map((m) => (\n <div key={m.email} data-gr-row style={S.memberRow}>\n <span style={S.memberEmail} title={m.email}>\n {m.email}\n {m.email === currentEmail && \" (you)\"}\n </span>\n\n {m.status === \"pending\" && <span style={S.badge}>Invited</span>}\n\n {isAdmin ? (\n <select\n style={S.select}\n value={m.role}\n disabled={pending}\n onChange={(e) =>\n run(() =>\n updateMemberRole(m.email, e.target.value as GRRole),\n )\n }\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>{m.role}</span>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => removeMember(m.email))}\n aria-label={`Remove ${m.email}`}\n title=\"Remove\"\n >\n <CloseIcon />\n </button>\n )}\n </div>\n ))\n )}\n\n {isAdmin && (\n <>\n <p style={S.sectionLabel}>Invite someone</p>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n run(async () => {\n const res = await inviteMember(inviteEmail, inviteRole);\n if (res.success) setInviteEmail(\"\");\n return res;\n });\n }}\n >\n <input\n style={S.field}\n type=\"email\"\n placeholder=\"name@company.com\"\n value={inviteEmail}\n onChange={(e) => setInviteEmail(e.target.value)}\n required\n />\n <div style={{ display: \"flex\", gap: 8 }}>\n <select\n style={{ ...S.select, flex: 1 }}\n value={inviteRole}\n onChange={(e) => setInviteRole(e.target.value as GRRole)}\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n <button style={S.primaryBtn} type=\"submit\" disabled={pending}>\n {pending ? \"Working…\" : \"Invite\"}\n </button>\n </div>\n </form>\n <p style={{ fontSize: 11, color: v(\"subtle\"), marginTop: 10 }}>\n Guardrail records the invite. Sending the email is your app's\n job — wire the <code>onInvite</code> handler.\n </p>\n </>\n )}\n </div>\n </aside>\n </>\n );\n}\n"],"mappings":";AAyGM,SAiFM,UAjFN,KAsBE,YAtBF;AAvGN,OAAO,SAAS,aAAa,WAAW,UAAU,qBAAqB;AACvE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,SAAS,GAAG,SAAS;AAGrB,MAAM,QAAkB,CAAC,SAAS,QAAQ;AAO1C,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBf,SAAS,WAAW,EAAE,QAAQ,GAA4B;AAC/D,QAAM,EAAE,MAAM,OAAO,aAAa,IAAI,SAAS;AAC/C,QAAM,UAAU,SAAS;AAEzB,QAAM,CAAC,SAAS,UAAU,IAAI,SAAqB,CAAC,CAAC;AACrD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,EAAE;AACjD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAiB,QAAQ;AAC7D,QAAM,CAAC,SAAS,eAAe,IAAI,cAAc;AAEjD,QAAM,UAAU,MACd,SAAS,EAAE,KAAK,CAAC,SAAS;AACxB,eAAW,IAAI;AACf,eAAW,KAAK;AAAA,EAClB,CAAC;AAEH,YAAU,MAAM;AACd,SAAK,QAAQ;AAAA,EACf,GAAG,CAAC,CAAC;AAKL,QAAM,MAAM,CAAC,OACX,gBAAgB,YAAY;AAC1B,aAAS,IAAI;AACb,UAAM,MAAM,MAAM,GAAG;AACrB,QAAI,CAAC,IAAI,QAAS,UAAS,IAAI,SAAS,uBAAuB;AAC/D,UAAM,QAAQ;AAAA,EAChB,CAAC;AAMH,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,eAAe,YAAY,MAAM;AACrC,UAAM,UACJ,OAAO,WAAW,eAClB,OAAO,WAAW,kCAAkC,EAAE;AACxD,QAAI,QAAS,QAAO,QAAQ;AAC5B,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,OAAO,CAAC;AAEZ,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AAEd,UAAM,IAAI,OAAO,WAAW,SAAS,GAAG;AACxC,WAAO,MAAM,OAAO,aAAa,CAAC;AAAA,EACpC,GAAG,CAAC,SAAS,OAAO,CAAC;AAErB,YAAU,MAAM;AACd,UAAM,QAAQ,CAAC,MAAqB,EAAE,QAAQ,YAAY,aAAa;AACvE,WAAO,iBAAiB,WAAW,KAAK;AACxC,WAAO,MAAM,OAAO,oBAAoB,WAAW,KAAK;AAAA,EAC1D,GAAG,CAAC,YAAY,CAAC;AAEjB,SACE,iCAEE;AAAA,wBAAC,WAAM,MAAK,4BAA2B,YAAW,WAC/C,yBACH;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,kBAAc;AAAA,QACd,wBAAoB;AAAA,QACnB,GAAI,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;AAAA,QACzC,OAAO,EAAE;AAAA,QACT,SAAS;AAAA;AAAA,IACX;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,kBAAc;AAAA,QACd,uBAAmB;AAAA,QAClB,GAAI,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;AAAA,QACzC,gBAAgB,CAAC,MAAM;AAErB,cAAI,WAAW,EAAE,WAAW,EAAE,cAAe,SAAQ;AAAA,QACvD;AAAA,QACA,OAAO,EAAE;AAAA,QACT,MAAK;AAAA,QACL,cAAW;AAAA,QAEX;AAAA,+BAAC,YAAO,OAAO,EAAE,YACf;AAAA,gCAAC,QAAG,OAAO,EAAE,aAAa,kBAAI;AAAA,YAC9B,oBAAC,YAAO,OAAO,EAAE,GAAG,EAAE,SAAS,OAAO,EAAE,OAAO,EAAE,GAAG,SAAS,cAAc,cAAW,SACpF,8BAAC,aAAU,GACb;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,YACX;AAAA,qBAAS,oBAAC,SAAI,OAAO,EAAE,GAAG,EAAE,OAAO,cAAc,GAAG,GAAI,iBAAM;AAAA,YAE9D,UACC,oBAAC,OAAE,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,OAAO,EAAE,GAAG,2BAAQ,IAEvD,QAAQ,IAAI,CAAC,MACX,qBAAC,SAAkB,eAAW,MAAC,OAAO,EAAE,WACtC;AAAA,mCAAC,UAAK,OAAO,EAAE,aAAa,OAAO,EAAE,OAClC;AAAA,kBAAE;AAAA,gBACF,EAAE,UAAU,gBAAgB;AAAA,iBAC/B;AAAA,cAEC,EAAE,WAAW,aAAa,oBAAC,UAAK,OAAO,EAAE,OAAO,qBAAO;AAAA,cAEvD,UACC;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAO,EAAE;AAAA,kBACT,OAAO,EAAE;AAAA,kBACT,UAAU;AAAA,kBACV,UAAU,CAAC,MACT;AAAA,oBAAI,MACF,iBAAiB,EAAE,OAAO,EAAE,OAAO,KAAe;AAAA,kBACpD;AAAA,kBAGD,gBAAM,IAAI,CAAC,MACV,oBAAC,YAAe,OAAO,GACpB,eADU,CAEb,CACD;AAAA;AAAA,cACH,IAEA,oBAAC,UAAK,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,OAAO,EAAE,GAAI,YAAE,MAAK;AAAA,cAG3D,WACC;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAO,EAAE,GAAG,EAAE,SAAS,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,QAAQ,GAAG;AAAA,kBAChE,UAAU;AAAA,kBACV,SAAS,MAAM,IAAI,MAAM,aAAa,EAAE,KAAK,CAAC;AAAA,kBAC9C,cAAY,UAAU,EAAE,KAAK;AAAA,kBAC7B,OAAM;AAAA,kBAEN,8BAAC,aAAU;AAAA;AAAA,cACb;AAAA,iBAtCM,EAAE,KAwCZ,CACD;AAAA,YAGF,WACC,iCACE;AAAA,kCAAC,OAAE,OAAO,EAAE,cAAc,4BAAc;AAAA,cACxC;AAAA,gBAAC;AAAA;AAAA,kBACC,UAAU,CAAC,MAAM;AACf,sBAAE,eAAe;AACjB,wBAAI,YAAY;AACd,4BAAM,MAAM,MAAM,aAAa,aAAa,UAAU;AACtD,0BAAI,IAAI,QAAS,gBAAe,EAAE;AAClC,6BAAO;AAAA,oBACT,CAAC;AAAA,kBACH;AAAA,kBAEA;AAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,OAAO,EAAE;AAAA,wBACT,MAAK;AAAA,wBACL,aAAY;AAAA,wBACZ,OAAO;AAAA,wBACP,UAAU,CAAC,MAAM,eAAe,EAAE,OAAO,KAAK;AAAA,wBAC9C,UAAQ;AAAA;AAAA,oBACV;AAAA,oBACA,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,EAAE,GACpC;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,OAAO,EAAE,GAAG,EAAE,QAAQ,MAAM,EAAE;AAAA,0BAC9B,OAAO;AAAA,0BACP,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAe;AAAA,0BAEtD,gBAAM,IAAI,CAAC,MACV,oBAAC,YAAe,OAAO,GACpB,eADU,CAEb,CACD;AAAA;AAAA,sBACH;AAAA,sBACA,oBAAC,YAAO,OAAO,EAAE,YAAY,MAAK,UAAS,UAAU,SAClD,oBAAU,kBAAa,UAC1B;AAAA,uBACF;AAAA;AAAA;AAAA,cACF;AAAA,cACA,qBAAC,OAAE,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,QAAQ,GAAG,WAAW,GAAG,GAAG;AAAA;AAAA,gBAE9C,oBAAC,UAAK,sBAAQ;AAAA,gBAAO;AAAA,iBACtC;AAAA,eACF;AAAA,aAEJ;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useState, useTransition } from \"react\";\nimport {\n inviteMember,\n listTeam,\n removeMember,\n sendPasswordResetAction,\n updateMemberRole,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { CloseIcon, KeyIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport type { GRMember, GRRole } from \"../core/types\";\n\nconst ROLES: GRRole[] = [\"admin\", \"editor\"];\n\n/**\n * Keyframes cannot be expressed as inline styles, so the drawer ships the few\n * it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and\n * disabled under prefers-reduced-motion.\n */\nexport const ANIMATION_CSS = `\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n`;\n\nexport function TeamDrawer({ onClose }: { onClose: () => void }) {\n const { role, email: currentEmail } = useGRail();\n const isAdmin = role === \"admin\";\n\n const [members, setMembers] = useState<GRMember[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [notice, setNotice] = useState<string | null>(null);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState<GRRole>(\"editor\");\n const [pending, startTransition] = useTransition();\n\n const refresh = () =>\n listTeam().then((rows) => {\n setMembers(rows);\n setLoading(false);\n });\n\n useEffect(() => {\n void refresh();\n }, []);\n\n // Server actions re-check the role; this only decides what to render.\n // Always refresh: a failure can still have changed the roster (an invite\n // that saved but whose email failed, for instance).\n const run = (fn: () => Promise<{ success: boolean; error?: string; notice?: string }>) =>\n startTransition(async () => {\n setError(null);\n setNotice(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n else if (res.notice) setNotice(res.notice);\n await refresh();\n });\n\n /**\n * Unmounting on close skips the exit animation entirely. Stay mounted, play\n * it, and let the parent unmount once it finishes.\n */\n const [closing, setClosing] = useState(false);\n const requestClose = useCallback(() => {\n const reduced =\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduced) return onClose(); // No animation, so no animationend to wait for.\n setClosing(true);\n }, [onClose]);\n\n useEffect(() => {\n if (!closing) return;\n // Safety net if animationend never arrives (tab hidden, styles stripped).\n const t = window.setTimeout(onClose, 400);\n return () => window.clearTimeout(t);\n }, [closing, onClose]);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && requestClose();\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [requestClose]);\n\n return (\n <>\n {/* Hoisted to <head> — see the note on the theme <style> in Admin.tsx. */}\n <style href=\"guardrail-team-animation\" precedence=\"default\">\n {ANIMATION_CSS}\n </style>\n <div\n data-guardrail\n data-guardrail-scrim\n {...(closing ? { \"data-closing\": \"\" } : {})}\n style={S.scrim}\n onClick={requestClose}\n />\n <aside\n data-guardrail\n data-guardrail-team\n {...(closing ? { \"data-closing\": \"\" } : {})}\n onAnimationEnd={(e) => {\n // Row fade-ins bubble up here too; only the drawer's own exit counts.\n if (closing && e.target === e.currentTarget) onClose();\n }}\n style={S.drawer}\n role=\"dialog\"\n aria-label=\"Team\"\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Team</h2>\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={requestClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n {notice && (\n <div style={{ fontSize: 12, color: v(\"muted\"), marginBottom: 12 }} role=\"status\">\n {notice}\n </div>\n )}\n\n {loading ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : (\n members.map((m) => (\n <div key={m.email} data-gr-row style={S.memberRow}>\n <span style={S.memberEmail} title={m.email}>\n {m.email}\n {m.email === currentEmail && \" (you)\"}\n </span>\n\n {m.status === \"pending\" && <span style={S.badge}>Invited</span>}\n\n {isAdmin ? (\n <select\n style={S.select}\n value={m.role}\n disabled={pending}\n onChange={(e) =>\n run(() =>\n updateMemberRole(m.email, e.target.value as GRRole),\n )\n }\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>{m.role}</span>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => sendPasswordResetAction(m.email))}\n aria-label={`Send password reset to ${m.email}`}\n title=\"Send password reset\"\n >\n <KeyIcon />\n </button>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => removeMember(m.email))}\n aria-label={`Remove ${m.email}`}\n title=\"Remove\"\n >\n <CloseIcon />\n </button>\n )}\n </div>\n ))\n )}\n\n {isAdmin && (\n <>\n <p style={S.sectionLabel}>Invite someone</p>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n run(async () => {\n const res = await inviteMember(inviteEmail, inviteRole);\n if (res.success) setInviteEmail(\"\");\n return res;\n });\n }}\n >\n <input\n style={S.field}\n type=\"email\"\n placeholder=\"name@company.com\"\n value={inviteEmail}\n onChange={(e) => setInviteEmail(e.target.value)}\n required\n />\n <div style={{ display: \"flex\", gap: 8 }}>\n <select\n style={{ ...S.select, flex: 1 }}\n value={inviteRole}\n onChange={(e) => setInviteRole(e.target.value as GRRole)}\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n <button style={S.primaryBtn} type=\"submit\" disabled={pending}>\n {pending ? \"Working…\" : \"Invite\"}\n </button>\n </div>\n </form>\n <p style={{ fontSize: 11, color: v(\"subtle\"), marginTop: 10 }}>\n New people get an email to set a password. Invites and resets go\n out through your <code>onInvite</code> / <code>onPasswordReset</code>{\" \"}\n handlers, or your auth provider's own emails if you haven't\n set them.\n </p>\n </>\n )}\n </div>\n </aside>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/server/access.d.ts
CHANGED
|
@@ -62,8 +62,9 @@ declare function canEdit(): Promise<boolean>;
|
|
|
62
62
|
*
|
|
63
63
|
* needsPassword is set by completeAuthRedirectAction the moment an
|
|
64
64
|
* invite/recovery link finishes — it forces chrome on regardless of the
|
|
65
|
-
* edit-mode cookie, since the whole point is to show the set-password
|
|
65
|
+
* edit-mode cookie, since the whole point is to show the set-password step
|
|
66
66
|
* right away, not require also having ?edit=true or an unsuppressed cookie.
|
|
67
|
+
* authError (a link that failed) does the same, to explain what happened.
|
|
67
68
|
*/
|
|
68
69
|
declare function resolveEditMode(viewer: GRViewer | null, cookieStore: {
|
|
69
70
|
get(name: string): {
|
|
@@ -72,6 +73,7 @@ declare function resolveEditMode(viewer: GRViewer | null, cookieStore: {
|
|
|
72
73
|
}): {
|
|
73
74
|
isEditMode: boolean;
|
|
74
75
|
needsPassword: boolean;
|
|
76
|
+
authError?: "expired" | "failed";
|
|
75
77
|
};
|
|
76
78
|
|
|
77
79
|
export { canEdit, isAdmin, isEditor, requireRole, resolveEditMode };
|
package/dist/server/access.js
CHANGED
|
@@ -19,9 +19,11 @@ async function canEdit() {
|
|
|
19
19
|
}
|
|
20
20
|
function resolveEditMode(viewer, cookieStore) {
|
|
21
21
|
const editCookie = cookieStore.get(GR_COOKIES.EDIT_MODE)?.value;
|
|
22
|
-
const needsPassword = cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === "true";
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
const needsPassword = viewer !== null && cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === "true";
|
|
23
|
+
const rawError = cookieStore.get(GR_COOKIES.AUTH_ERROR)?.value;
|
|
24
|
+
const authError = rawError === "expired" || rawError === "failed" ? rawError : void 0;
|
|
25
|
+
const isEditMode = needsPassword || authError !== void 0 || (editCookie === "false" ? false : editCookie === "true" || viewer !== null);
|
|
26
|
+
return { isEditMode, needsPassword, ...authError ? { authError } : {} };
|
|
25
27
|
}
|
|
26
28
|
export {
|
|
27
29
|
canEdit,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/access.ts"],"sourcesContent":["import { cookies } from \"next/headers\";\nimport { getCachedViewer } from \"./session\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport type { GRRole, GRViewer } from \"../core/types\";\n\n/**\n * Role checks, server-side only.\n *\n * Deliberately NOT in a \"use server\" file: every export of one of those is a\n * callable Server Action endpoint, which is the wrong shape for an\n * authorization helper — it should only ever run inside the server code that\n * calls it.\n *\n * The client's copy of the role (useGRail().role) decides what to *show*; it\n * never decides what is *allowed*. A hidden button is not authorization, so\n * any Server Action behind a custom admin UI must call one of these itself.\n */\n\n/**\n * Returns the signed-in viewer if their role is in `allowed`, otherwise null.\n * The building block for the two helpers below; reach for this directly only\n * when you need the viewer itself (their email, say), not just a yes/no.\n */\nexport async function requireRole(\n allowed: readonly GRRole[],\n): Promise<GRViewer | null> {\n const viewer = await getCachedViewer();\n if (!viewer || !viewer.role || !allowed.includes(viewer.role)) return null;\n return viewer;\n}\n\n/** True for a signed-in admin of this site — team management, and everything an editor can do. */\nexport async function isAdmin(): Promise<boolean> {\n return (await requireRole([\"admin\"])) !== null;\n}\n\n/**\n * True for anyone who may edit this site's content: an editor, or an admin\n * (admins can do everything editors can). The check for a custom drawer's\n * Server Action that changes content. Use isAdmin() instead for anything that\n * touches the team.\n */\nexport async function isEditor(): Promise<boolean> {\n return (await requireRole([\"admin\", \"editor\"])) !== null;\n}\n\n/**\n * True when this viewer may edit AND the site is currently in edit mode — the\n * one check for \"should I show editing UI / do edit-only work right now.\"\n * Admin vs editor doesn't matter; use isAdmin() when it does.\n *\n * For a custom drawer's trigger rendered in a Server Component. In a Client\n * Component, read `canEdit` from useGRail() instead — same answer, same rule.\n * Don't use it as the only authorization on a Server Action: edit mode is a\n * display preference (a cookie the visitor controls), so the action should\n * call isEditor() — this answers \"show it,\" that answers \"allow it.\"\n *\n * Reads cookies, which makes the calling route dynamic — nothing new in\n * practice, since withGuardrail() already does the same in the root layout.\n */\nexport async function canEdit(): Promise<boolean> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return false;\n return resolveEditMode(viewer, await cookies()).isEditMode;\n}\n\n/**\n * The one definition of \"is the site in edit mode,\" shared by withGuardrail()\n * (which hands it to the client as useGRail().isEditMode) and canEdit() so the\n * two can't drift apart.\n *\n * \"false\" is an explicit, deliberate suppression (the toolbar's exit button,\n * or ?edit=false, while still signed in) and wins outright — it lasts until\n * the next ?edit=true, and ends only the editing session, not the sign-in. Otherwise, a real\n * session is enough on its own — an authenticated editor's chrome persists for\n * as long as that session does, with no separate cookie timer to fall out of\n * sync with it. \"true\" only matters for someone not yet authenticated: a\n * short-lived grace window (see GR_EDIT_MODE_TTL_SECONDS) to actually reach\n * the sign-in form, not a standing invitation — the previous design kept this\n * cookie alive indefinitely once set, which meant a stray copy of it kept\n * showing the sign-in bar to whoever's browser it was sitting in, for as long\n * as that browser stayed open.\n *\n * needsPassword is set by completeAuthRedirectAction the moment an\n * invite/recovery link finishes — it forces chrome on regardless of the\n * edit-mode cookie, since the whole point is to show the set-password
|
|
1
|
+
{"version":3,"sources":["../../src/server/access.ts"],"sourcesContent":["import { cookies } from \"next/headers\";\nimport { getCachedViewer } from \"./session\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport type { GRRole, GRViewer } from \"../core/types\";\n\n/**\n * Role checks, server-side only.\n *\n * Deliberately NOT in a \"use server\" file: every export of one of those is a\n * callable Server Action endpoint, which is the wrong shape for an\n * authorization helper — it should only ever run inside the server code that\n * calls it.\n *\n * The client's copy of the role (useGRail().role) decides what to *show*; it\n * never decides what is *allowed*. A hidden button is not authorization, so\n * any Server Action behind a custom admin UI must call one of these itself.\n */\n\n/**\n * Returns the signed-in viewer if their role is in `allowed`, otherwise null.\n * The building block for the two helpers below; reach for this directly only\n * when you need the viewer itself (their email, say), not just a yes/no.\n */\nexport async function requireRole(\n allowed: readonly GRRole[],\n): Promise<GRViewer | null> {\n const viewer = await getCachedViewer();\n if (!viewer || !viewer.role || !allowed.includes(viewer.role)) return null;\n return viewer;\n}\n\n/** True for a signed-in admin of this site — team management, and everything an editor can do. */\nexport async function isAdmin(): Promise<boolean> {\n return (await requireRole([\"admin\"])) !== null;\n}\n\n/**\n * True for anyone who may edit this site's content: an editor, or an admin\n * (admins can do everything editors can). The check for a custom drawer's\n * Server Action that changes content. Use isAdmin() instead for anything that\n * touches the team.\n */\nexport async function isEditor(): Promise<boolean> {\n return (await requireRole([\"admin\", \"editor\"])) !== null;\n}\n\n/**\n * True when this viewer may edit AND the site is currently in edit mode — the\n * one check for \"should I show editing UI / do edit-only work right now.\"\n * Admin vs editor doesn't matter; use isAdmin() when it does.\n *\n * For a custom drawer's trigger rendered in a Server Component. In a Client\n * Component, read `canEdit` from useGRail() instead — same answer, same rule.\n * Don't use it as the only authorization on a Server Action: edit mode is a\n * display preference (a cookie the visitor controls), so the action should\n * call isEditor() — this answers \"show it,\" that answers \"allow it.\"\n *\n * Reads cookies, which makes the calling route dynamic — nothing new in\n * practice, since withGuardrail() already does the same in the root layout.\n */\nexport async function canEdit(): Promise<boolean> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return false;\n return resolveEditMode(viewer, await cookies()).isEditMode;\n}\n\n/**\n * The one definition of \"is the site in edit mode,\" shared by withGuardrail()\n * (which hands it to the client as useGRail().isEditMode) and canEdit() so the\n * two can't drift apart.\n *\n * \"false\" is an explicit, deliberate suppression (the toolbar's exit button,\n * or ?edit=false, while still signed in) and wins outright — it lasts until\n * the next ?edit=true, and ends only the editing session, not the sign-in. Otherwise, a real\n * session is enough on its own — an authenticated editor's chrome persists for\n * as long as that session does, with no separate cookie timer to fall out of\n * sync with it. \"true\" only matters for someone not yet authenticated: a\n * short-lived grace window (see GR_EDIT_MODE_TTL_SECONDS) to actually reach\n * the sign-in form, not a standing invitation — the previous design kept this\n * cookie alive indefinitely once set, which meant a stray copy of it kept\n * showing the sign-in bar to whoever's browser it was sitting in, for as long\n * as that browser stayed open.\n *\n * needsPassword is set by completeAuthRedirectAction the moment an\n * invite/recovery link finishes — it forces chrome on regardless of the\n * edit-mode cookie, since the whole point is to show the set-password step\n * right away, not require also having ?edit=true or an unsuppressed cookie.\n * authError (a link that failed) does the same, to explain what happened.\n */\nexport function resolveEditMode(\n viewer: GRViewer | null,\n cookieStore: { get(name: string): { value: string } | undefined },\n): { isEditMode: boolean; needsPassword: boolean; authError?: \"expired\" | \"failed\" } {\n const editCookie = cookieStore.get(GR_COOKIES.EDIT_MODE)?.value;\n // Only means anything with a session to set a password on. A leftover flag —\n // from a link whose session has since been signed out or expired — must not\n // be able to force the chrome on for someone nobody is signed in as, which no\n // \"off\" setting could undo.\n const needsPassword =\n viewer !== null && cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === \"true\";\n const rawError = cookieStore.get(GR_COOKIES.AUTH_ERROR)?.value;\n const authError = rawError === \"expired\" || rawError === \"failed\" ? rawError : undefined;\n // A failed link forces the chrome on for the same reason a pending password\n // does: the whole point is to tell the person right now, even if they arrived\n // with no ?edit=true and no session.\n const isEditMode =\n needsPassword ||\n authError !== undefined ||\n (editCookie === \"false\" ? false : editCookie === \"true\" || viewer !== null);\n return { isEditMode, needsPassword, ...(authError ? { authError } : {}) };\n}\n"],"mappings":"AAAA,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAqB3B,eAAsB,YACpB,SAC0B;AAC1B,QAAM,SAAS,MAAM,gBAAgB;AACrC,MAAI,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,SAAS,OAAO,IAAI,EAAG,QAAO;AACtE,SAAO;AACT;AAGA,eAAsB,UAA4B;AAChD,SAAQ,MAAM,YAAY,CAAC,OAAO,CAAC,MAAO;AAC5C;AAQA,eAAsB,WAA6B;AACjD,SAAQ,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC,MAAO;AACtD;AAgBA,eAAsB,UAA4B;AAChD,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,gBAAgB,QAAQ,MAAM,QAAQ,CAAC,EAAE;AAClD;AAyBO,SAAS,gBACd,QACA,aACmF;AACnF,QAAM,aAAa,YAAY,IAAI,WAAW,SAAS,GAAG;AAK1D,QAAM,gBACJ,WAAW,QAAQ,YAAY,IAAI,WAAW,cAAc,GAAG,UAAU;AAC3E,QAAM,WAAW,YAAY,IAAI,WAAW,UAAU,GAAG;AACzD,QAAM,YAAY,aAAa,aAAa,aAAa,WAAW,WAAW;AAI/E,QAAM,aACJ,iBACA,cAAc,WACb,eAAe,UAAU,QAAQ,eAAe,UAAU,WAAW;AACxE,SAAO,EAAE,YAAY,eAAe,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC,EAAG;AAC1E;","names":[]}
|
package/dist/server/actions.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { GRRole, GRAsset, GRMember, OverrideValue, SetPasswordState, ActionState
|
|
|
2
2
|
import { GRUploadTarget, AuthRedirectParams } from '../core/adapter-types.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
|
|
5
|
+
/** `notice` is for a success the admin should still read, e.g. "no email was sent because...". */
|
|
5
6
|
type Result = {
|
|
6
7
|
success: boolean;
|
|
7
8
|
error?: string;
|
|
9
|
+
notice?: string;
|
|
8
10
|
};
|
|
9
11
|
declare function saveOverride(row: OverrideRow): Promise<Result>;
|
|
10
12
|
/**
|
|
@@ -46,13 +48,26 @@ declare function listTeam(): Promise<GRMember[]>;
|
|
|
46
48
|
declare function inviteMember(email: string, role: GRRole): Promise<Result>;
|
|
47
49
|
declare function updateMemberRole(email: string, role: GRRole): Promise<Result>;
|
|
48
50
|
declare function removeMember(email: string): Promise<Result>;
|
|
51
|
+
/**
|
|
52
|
+
* "Forgot password?" from the sign-in bar. Called with no session, so it says
|
|
53
|
+
* the same thing whether or not the address belongs to anyone: a member gets an
|
|
54
|
+
* email, a stranger gets nothing, and neither can tell which happened from the
|
|
55
|
+
* response or how long it took. Only a malformed address is rejected, since
|
|
56
|
+
* that says nothing about anyone.
|
|
57
|
+
*/
|
|
58
|
+
declare function forgotPasswordAction(email: string): Promise<Result>;
|
|
59
|
+
/**
|
|
60
|
+
* An admin sending a password reset to someone on the team, from the Team
|
|
61
|
+
* drawer. Unlike the self-service request this reports what happened — an admin
|
|
62
|
+
* is allowed to know, and needs to when it didn't work.
|
|
63
|
+
*/
|
|
64
|
+
declare function sendPasswordResetAction(email: string): Promise<Result>;
|
|
49
65
|
declare function loginAction(_prev: ActionState, formData: FormData): Promise<ActionState>;
|
|
50
66
|
declare function logoutAction(): Promise<{
|
|
51
67
|
success: boolean;
|
|
52
68
|
}>;
|
|
53
69
|
/**
|
|
54
|
-
* Completes a session that arrived via an invite
|
|
55
|
-
* password-recovery redirect. Called by AuthRedirectSync (react/), the
|
|
70
|
+
* Completes a session that arrived via an invite or password-recovery link. Called by AuthRedirectSync (react/), the
|
|
56
71
|
* generic client component that notices this kind of link and hands off
|
|
57
72
|
* whatever it found in the URL — the actual token exchange is entirely the
|
|
58
73
|
* auth adapter's job (see GRAuthAdapter.completeAuthRedirect); core only
|
|
@@ -74,9 +89,15 @@ declare function completeAuthRedirectAction(params: AuthRedirectParams): Promise
|
|
|
74
89
|
* resolving them, which already happens independently of this.
|
|
75
90
|
*/
|
|
76
91
|
declare function setPasswordAction(_prev: SetPasswordState, formData: FormData): Promise<SetPasswordState>;
|
|
77
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Dismisses the set-password step without setting a password.
|
|
94
|
+
*
|
|
95
|
+
* @deprecated Guardrail's own UI no longer offers this: passwords are the only
|
|
96
|
+
* way back in, so the step can't be skipped. Kept so an app's own UI built on
|
|
97
|
+
* it keeps working.
|
|
98
|
+
*/
|
|
78
99
|
declare function skipSetPasswordAction(): Promise<{
|
|
79
100
|
success: boolean;
|
|
80
101
|
}>;
|
|
81
102
|
|
|
82
|
-
export { completeAuthRedirectAction, createAssetUpload, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, saveOverride, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };
|
|
103
|
+
export { completeAuthRedirectAction, createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, saveOverride, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };
|
package/dist/server/actions.js
CHANGED
|
@@ -6,13 +6,18 @@ import {
|
|
|
6
6
|
getDB,
|
|
7
7
|
getInviteHandler,
|
|
8
8
|
getMembers,
|
|
9
|
+
getPasswordResetHandler,
|
|
9
10
|
getStorage,
|
|
10
11
|
resolveSiteId
|
|
11
12
|
} from "../core/config";
|
|
12
13
|
import { getCachedViewer } from "./session";
|
|
13
14
|
import { requireRole } from "./access";
|
|
14
15
|
import { rethrowIfBailout } from "../core/bailout";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
GR_AUTH_ERROR_TTL_SECONDS,
|
|
18
|
+
GR_COOKIES,
|
|
19
|
+
GR_NEEDS_PASSWORD_TTL_SECONDS
|
|
20
|
+
} from "../core/defs";
|
|
16
21
|
const DENIED = { success: false, error: "Unauthorized." };
|
|
17
22
|
async function saveOverride(row) {
|
|
18
23
|
const viewer = await requireRole(["admin", "editor"]);
|
|
@@ -110,29 +115,43 @@ async function inviteMember(email, role) {
|
|
|
110
115
|
if (!res.success) return res;
|
|
111
116
|
}
|
|
112
117
|
const auth = getAuth();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
const onInvite = getInviteHandler();
|
|
119
|
+
const origin = await requestOrigin();
|
|
120
|
+
const made = auth.createInviteLink ? await auth.createInviteLink(normalized, { origin }) : null;
|
|
121
|
+
if (made && !made.success) {
|
|
122
|
+
revalidatePath("/", "layout");
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: `Invite saved, but the sign-in link couldn't be created: ${made.error}`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (!onInvite) {
|
|
117
129
|
revalidatePath("/", "layout");
|
|
118
|
-
return
|
|
130
|
+
return {
|
|
119
131
|
success: false,
|
|
120
|
-
error:
|
|
132
|
+
error: "Invite saved, but this site has nothing set up to email it. Add resendEmails() (or your own onInvite) to withGuardrail() \u2014 see the README."
|
|
121
133
|
};
|
|
122
134
|
}
|
|
123
135
|
try {
|
|
124
|
-
await
|
|
136
|
+
await onInvite({
|
|
125
137
|
email: normalized,
|
|
126
138
|
role: existing?.role ?? role,
|
|
127
139
|
siteId,
|
|
128
|
-
invitedBy: viewer.email ?? viewer.userId
|
|
140
|
+
invitedBy: viewer.email ?? viewer.userId,
|
|
141
|
+
...made ? { link: made.link, isNewUser: made.isNewUser } : {}
|
|
129
142
|
});
|
|
130
143
|
} catch (e) {
|
|
131
144
|
console.error("Guardrail: onInvite handler threw", e);
|
|
145
|
+
revalidatePath("/", "layout");
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: `Invite saved, but the email failed: ${e instanceof Error ? e.message : "unknown error"}`
|
|
149
|
+
};
|
|
132
150
|
}
|
|
133
151
|
revalidatePath("/", "layout");
|
|
134
|
-
return { success: true };
|
|
152
|
+
return made && !made.isNewUser ? { success: true, notice: ALREADY_HAS_ACCOUNT(normalized) } : { success: true };
|
|
135
153
|
}
|
|
154
|
+
const ALREADY_HAS_ACCOUNT = (email) => `${email} already has an account, so they were told they've been added rather than sent a link \u2014 they sign in with their existing password.`;
|
|
136
155
|
async function updateMemberRole(email, role) {
|
|
137
156
|
const viewer = await requireRole(["admin"]);
|
|
138
157
|
if (!viewer) return DENIED;
|
|
@@ -167,6 +186,74 @@ async function removeMember(email) {
|
|
|
167
186
|
if (res.success) revalidatePath("/", "layout");
|
|
168
187
|
return res;
|
|
169
188
|
}
|
|
189
|
+
const RESET_COOLDOWN_MS = 6e4;
|
|
190
|
+
const lastResetAt = /* @__PURE__ */ new Map();
|
|
191
|
+
const RESET_MIN_DURATION_MS = 600;
|
|
192
|
+
const EMAIL_PATTERN = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
193
|
+
async function dispatchPasswordReset(email, requestedBy) {
|
|
194
|
+
const auth = getAuth();
|
|
195
|
+
const handler = getPasswordResetHandler();
|
|
196
|
+
if (!auth.createPasswordResetLink) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
error: "this site's auth setup can't create reset links \u2014 give the Supabase adapter a serviceRoleKey."
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (!handler) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: "this site has no onPasswordReset handler \u2014 add resendEmails() (or your own) to withGuardrail()."
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const made = await auth.createPasswordResetLink(email, { origin: await requestOrigin() });
|
|
209
|
+
if (!made.success) return { success: false, error: made.error };
|
|
210
|
+
try {
|
|
211
|
+
await handler({ email, siteId: await resolveSiteId(), link: made.link, requestedBy });
|
|
212
|
+
return { success: true };
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error("Guardrail: onPasswordReset handler threw", e);
|
|
215
|
+
return { success: false, error: e instanceof Error ? e.message : "unknown error" };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function forgotPasswordAction(email) {
|
|
219
|
+
const started = Date.now();
|
|
220
|
+
const normalized = String(email ?? "").toLowerCase().trim();
|
|
221
|
+
if (!EMAIL_PATTERN.test(normalized)) {
|
|
222
|
+
return { success: false, error: "Enter a valid email address." };
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const siteId = await resolveSiteId();
|
|
226
|
+
const key = `${siteId}:${normalized}`;
|
|
227
|
+
const last = lastResetAt.get(key);
|
|
228
|
+
if (!last || started - last >= RESET_COOLDOWN_MS) {
|
|
229
|
+
lastResetAt.set(key, started);
|
|
230
|
+
if (lastResetAt.size > 500) {
|
|
231
|
+
for (const [k, t] of lastResetAt) if (started - t >= RESET_COOLDOWN_MS) lastResetAt.delete(k);
|
|
232
|
+
}
|
|
233
|
+
const members = getMembers();
|
|
234
|
+
const known = members.emailHasAccess ? await members.emailHasAccess(siteId, normalized) : true;
|
|
235
|
+
if (known) {
|
|
236
|
+
const res = await dispatchPasswordReset(normalized, "self");
|
|
237
|
+
if (!res.success) console.error("Guardrail: password reset failed:", res.error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
rethrowIfBailout(e);
|
|
242
|
+
console.error("Guardrail: password reset failed", e);
|
|
243
|
+
}
|
|
244
|
+
const wait = RESET_MIN_DURATION_MS - (Date.now() - started);
|
|
245
|
+
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
async function sendPasswordResetAction(email) {
|
|
249
|
+
const viewer = await requireRole(["admin"]);
|
|
250
|
+
if (!viewer) return DENIED;
|
|
251
|
+
const normalized = String(email ?? "").toLowerCase().trim();
|
|
252
|
+
const target = await getMembers().getMember(await resolveSiteId(), { email: normalized });
|
|
253
|
+
if (!target) return { success: false, error: "No such member." };
|
|
254
|
+
const res = await dispatchPasswordReset(normalized, viewer.email ?? viewer.userId);
|
|
255
|
+
return res.success ? { success: true, notice: `Password reset email sent to ${normalized}.` } : { success: false, error: `The reset email couldn't be sent: ${res.error}` };
|
|
256
|
+
}
|
|
170
257
|
async function requestOrigin() {
|
|
171
258
|
const h = await headers();
|
|
172
259
|
const origin = h.get("origin");
|
|
@@ -210,6 +297,8 @@ async function logoutAction() {
|
|
|
210
297
|
await auth.logout();
|
|
211
298
|
const cookieStore = await cookies();
|
|
212
299
|
cookieStore.delete(GR_COOKIES.EDIT_MODE);
|
|
300
|
+
cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);
|
|
301
|
+
cookieStore.delete(GR_COOKIES.AUTH_ERROR);
|
|
213
302
|
revalidatePath("/", "layout");
|
|
214
303
|
return { success: true };
|
|
215
304
|
}
|
|
@@ -217,15 +306,24 @@ async function completeAuthRedirectAction(params) {
|
|
|
217
306
|
const auth = getAuth();
|
|
218
307
|
if (!auth.completeAuthRedirect) return { success: false };
|
|
219
308
|
const res = await auth.completeAuthRedirect(params);
|
|
309
|
+
const cookieStore = await cookies();
|
|
220
310
|
if (res.success) {
|
|
311
|
+
cookieStore.delete(GR_COOKIES.AUTH_ERROR);
|
|
221
312
|
if (res.needsPassword) {
|
|
222
|
-
const cookieStore = await cookies();
|
|
223
313
|
cookieStore.set(GR_COOKIES.NEEDS_PASSWORD, "true", {
|
|
224
314
|
path: "/",
|
|
225
315
|
maxAge: GR_NEEDS_PASSWORD_TTL_SECONDS
|
|
226
316
|
});
|
|
227
317
|
}
|
|
228
318
|
revalidatePath("/", "layout");
|
|
319
|
+
} else {
|
|
320
|
+
console.error("Guardrail: couldn't complete an emailed link:", res.error);
|
|
321
|
+
cookieStore.set(
|
|
322
|
+
GR_COOKIES.AUTH_ERROR,
|
|
323
|
+
/expired|invalid|already|used|not found/i.test(res.error ?? "") ? "expired" : "failed",
|
|
324
|
+
{ path: "/", maxAge: GR_AUTH_ERROR_TTL_SECONDS }
|
|
325
|
+
);
|
|
326
|
+
revalidatePath("/", "layout");
|
|
229
327
|
}
|
|
230
328
|
return res;
|
|
231
329
|
}
|
|
@@ -237,6 +335,13 @@ async function setPasswordAction(_prev, formData) {
|
|
|
237
335
|
return { success: false, error: "This site's auth provider does not support setting a password." };
|
|
238
336
|
}
|
|
239
337
|
const password = String(formData.get("password") ?? "");
|
|
338
|
+
const confirm = formData.get("confirm");
|
|
339
|
+
if (password.length < 8) {
|
|
340
|
+
return { success: false, error: "Use at least 8 characters." };
|
|
341
|
+
}
|
|
342
|
+
if (confirm !== null && String(confirm) !== password) {
|
|
343
|
+
return { success: false, error: "The two passwords don't match." };
|
|
344
|
+
}
|
|
240
345
|
const res = await auth.setPassword(password);
|
|
241
346
|
if (res.success) {
|
|
242
347
|
const cookieStore = await cookies();
|
|
@@ -254,6 +359,7 @@ async function skipSetPasswordAction() {
|
|
|
254
359
|
export {
|
|
255
360
|
completeAuthRedirectAction,
|
|
256
361
|
createAssetUpload,
|
|
362
|
+
forgotPasswordAction,
|
|
257
363
|
inviteMember,
|
|
258
364
|
listAssets,
|
|
259
365
|
listTeam,
|
|
@@ -261,6 +367,7 @@ export {
|
|
|
261
367
|
logoutAction,
|
|
262
368
|
removeMember,
|
|
263
369
|
saveOverride,
|
|
370
|
+
sendPasswordResetAction,
|
|
264
371
|
setFieldValue,
|
|
265
372
|
setPasswordAction,
|
|
266
373
|
skipSetPasswordAction,
|