@guard-rail/core 0.8.0 → 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.
Files changed (47) hide show
  1. package/README.md +62 -102
  2. package/dist/adapters/supabase/auth.d.ts +9 -15
  3. package/dist/adapters/supabase/auth.js +33 -42
  4. package/dist/adapters/supabase/auth.js.map +1 -1
  5. package/dist/adapters/supabase/confirm.js +2 -2
  6. package/dist/adapters/supabase/confirm.js.map +1 -1
  7. package/dist/adapters/supabase/members.js +13 -2
  8. package/dist/adapters/supabase/members.js.map +1 -1
  9. package/dist/core/adapter-types.d.ts +60 -28
  10. package/dist/core/config.d.ts +2 -1
  11. package/dist/core/config.js +4 -0
  12. package/dist/core/config.js.map +1 -1
  13. package/dist/core/defs.d.ts +11 -1
  14. package/dist/core/defs.js +11 -1
  15. package/dist/core/defs.js.map +1 -1
  16. package/dist/core/types.d.ts +6 -0
  17. package/dist/email/resend.d.ts +39 -10
  18. package/dist/email/resend.js +92 -49
  19. package/dist/email/resend.js.map +1 -1
  20. package/dist/index.client.d.ts +5 -4
  21. package/dist/index.client.js +9 -0
  22. package/dist/index.client.js.map +1 -1
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +6 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/react/Admin.js +105 -107
  27. package/dist/react/Admin.js.map +1 -1
  28. package/dist/react/AuthDrawer.d.ts +23 -0
  29. package/dist/react/AuthDrawer.js +165 -0
  30. package/dist/react/AuthDrawer.js.map +1 -0
  31. package/dist/react/AuthRedirectSync.js +1 -1
  32. package/dist/react/AuthRedirectSync.js.map +1 -1
  33. package/dist/react/Icons.d.ts +2 -1
  34. package/dist/react/Icons.js +2 -0
  35. package/dist/react/Icons.js.map +1 -1
  36. package/dist/react/Team.d.ts +7 -1
  37. package/dist/react/Team.js +23 -3
  38. package/dist/react/Team.js.map +1 -1
  39. package/dist/server/access.d.ts +3 -1
  40. package/dist/server/access.js +5 -3
  41. package/dist/server/access.js.map +1 -1
  42. package/dist/server/actions.d.ts +25 -4
  43. package/dist/server/actions.js +115 -39
  44. package/dist/server/actions.js.map +1 -1
  45. package/dist/server/wrapper.js +2 -1
  46. package/dist/server/wrapper.js.map +1 -1
  47. package/package.json +8 -10
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n setFieldValue,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\nexport const isAdmin = serverOnly(\n \"isAdmin()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const isEditor = serverOnly(\n \"isEditor()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const canEdit = serverOnly(\n \"canEdit()\",\n \"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail().\",\n);\nexport const resendInvite = serverOnly(\n \"resendInvite()\",\n \"Pass it as onInvite in withGuardrail(), which runs in your root layout.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n ResendInviteOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n} from \"./email/resend\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,WAAW;AAAA,EACtB;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\nexport const isAdmin = serverOnly(\n \"isAdmin()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const isEditor = serverOnly(\n \"isEditor()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const canEdit = serverOnly(\n \"canEdit()\",\n \"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail().\",\n);\nexport const resendInvite = serverOnly(\n \"resendInvite()\",\n \"Pass it as onInvite in withGuardrail(), which runs in your root layout.\",\n);\nexport const resendEmails = serverOnly(\n \"resendEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,WAAW;AAAA,EACtB;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  export { withGuardrail } from './server/wrapper.js';
2
- export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
2
+ export { createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, removeMember, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
3
3
  export { GRail } from './react/GRail/index.js';
4
4
  export { GRepeater, GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
5
5
  export { configureGuardrail } from './core/config.js';
6
6
  export { getRepeaterItemBySlug, listRepeaterItems } from './server/repeaterData.js';
7
7
  export { getFieldValue } from './server/fieldData.js';
8
8
  export { canEdit, isAdmin, isEditor } from './server/access.js';
9
- export { ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, resendInvite } from './email/resend.js';
9
+ export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent, resendEmails, resendInvite } from './email/resend.js';
10
10
  export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
11
11
  export { useGRail } from './react/Provider.js';
12
12
  export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
13
- export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
13
+ export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
14
14
  import 'react/jsx-runtime';
15
15
  import 'react';
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  listAssets,
9
9
  setPasswordAction,
10
10
  skipSetPasswordAction,
11
+ forgotPasswordAction,
12
+ sendPasswordResetAction,
11
13
  setFieldValue
12
14
  } from "./server/actions";
13
15
  import { GRail } from "./react/GRail";
@@ -19,7 +21,7 @@ import {
19
21
  } from "./server/repeaterData";
20
22
  import { getFieldValue } from "./server/fieldData";
21
23
  import { isAdmin, isEditor, canEdit } from "./server/access";
22
- import { resendInvite } from "./email/resend";
24
+ import { resendInvite, resendEmails } from "./email/resend";
23
25
  import {
24
26
  addRepeaterItem,
25
27
  removeRepeaterItem,
@@ -33,6 +35,7 @@ export {
33
35
  canEdit,
34
36
  configureGuardrail,
35
37
  createAssetUpload,
38
+ forgotPasswordAction,
36
39
  getFieldValue,
37
40
  getRepeaterItemBySlug,
38
41
  inviteMember,
@@ -44,7 +47,9 @@ export {
44
47
  removeMember,
45
48
  removeRepeaterItem,
46
49
  reorderRepeaterItems,
50
+ resendEmails,
47
51
  resendInvite,
52
+ sendPasswordResetAction,
48
53
  setFieldValue,
49
54
  setPasswordAction,
50
55
  skipSetPasswordAction,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n setFieldValue,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite } from \"./email/resend\";\nexport type {\n ResendInviteOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n} from \"./email/resend\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B,SAAS,SAAS,UAAU,eAAe;AAE3C,SAAS,oBAAoB;AAQ7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite, resendEmails } from \"./email/resend\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B,SAAS,SAAS,UAAU,eAAe;AAE3C,SAAS,cAAc,oBAAoB;AAU3C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;","names":[]}
@@ -2,19 +2,14 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import React, { useActionState, useEffect, useState } from "react";
4
4
  import { usePathname, useRouter } from "next/navigation";
5
- import {
6
- loginAction,
7
- logoutAction,
8
- setPasswordAction,
9
- skipSetPasswordAction
10
- } from "../server/actions";
5
+ import { loginAction, logoutAction } from "../server/actions";
11
6
  import { useGRail } from "./Provider";
12
7
  import { TeamDrawer } from "./Team";
8
+ import { AuthDrawer } from "./AuthDrawer";
13
9
  import { CloseIcon, LogOutIcon, PeopleIcon } from "./Icons";
14
10
  import { S, THEME_CSS, v } from "./styles";
15
11
  import { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from "../core/defs";
16
12
  const initialState = { email: "", password: "" };
17
- const initialPasswordState = {};
18
13
  function GRailAdmin(props) {
19
14
  const { isEditMode } = useGRail();
20
15
  if (!isEditMode) return null;
@@ -24,28 +19,33 @@ function GRailAdmin(props) {
24
19
  ] });
25
20
  }
26
21
  function AdminBody({ toolbarExtra }) {
27
- const { isAuth, isEditMode, needsPassword, role, auth } = useGRail();
22
+ const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();
28
23
  const [teamOpen, setTeamOpen] = useState(false);
24
+ const [forgotOpen, setForgotOpen] = useState(false);
25
+ const [errorDismissed, setErrorDismissed] = useState(false);
29
26
  const [formState, formAction, isPending] = useActionState(
30
27
  loginAction,
31
28
  initialState
32
29
  );
33
- const [passwordState, passwordFormAction, isSettingPassword] = useActionState(
34
- setPasswordAction,
35
- initialPasswordState
36
- );
37
30
  const router = useRouter();
38
31
  const pathname = usePathname();
39
32
  useEffect(() => {
40
33
  if (formState.success) router.refresh();
41
34
  }, [formState.success, router]);
42
- useEffect(() => {
43
- if (passwordState.success) router.refresh();
44
- }, [passwordState.success, router]);
45
- const skipSetPassword = async () => {
46
- await skipSetPasswordAction();
35
+ const dismissAuthError = () => {
36
+ document.cookie = `${GR_COOKIES.AUTH_ERROR}=; path=/; max-age=0; SameSite=Lax`;
37
+ setErrorDismissed(true);
47
38
  router.refresh();
48
39
  };
40
+ const linkErrorDrawer = authError && !errorDismissed ? /* @__PURE__ */ jsx(
41
+ AuthDrawer,
42
+ {
43
+ mode: "forgot",
44
+ title: "That link didn't work",
45
+ message: authError === "expired" ? "It has expired or was already used. Links work once. Enter your email to get a new one." : "We couldn't complete it. Enter your email to get a new link.",
46
+ onClose: dismissAuthError
47
+ }
48
+ ) : null;
49
49
  const exitEditMode = () => {
50
50
  document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;
51
51
  router.replace(pathname);
@@ -59,12 +59,76 @@ function AdminBody({ toolbarExtra }) {
59
59
  if (!isEditMode) return null;
60
60
  if (!isAuth) {
61
61
  if (!auth.password) {
62
- return /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("div", { style: S.bannerForm, children: [
62
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
63
+ /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("div", { style: S.bannerForm, children: [
64
+ /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
65
+ /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
66
+ !auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: "No sign-in method is configured for this site." })
67
+ ] }),
68
+ auth.signInUrl && /* @__PURE__ */ jsx("a", { href: auth.signInUrl, style: { ...S.primaryBtn, textDecoration: "none" }, children: "Sign in" }),
69
+ /* @__PURE__ */ jsx(
70
+ "button",
71
+ {
72
+ type: "button",
73
+ style: { ...S.iconBtn, color: v("muted") },
74
+ onClick: exitEditMode,
75
+ title: "Not editing? Dismiss",
76
+ "aria-label": "Dismiss",
77
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
78
+ }
79
+ )
80
+ ] }) }),
81
+ linkErrorDrawer
82
+ ] });
83
+ }
84
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
85
+ /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("form", { action: formAction, style: S.bannerForm, children: [
63
86
  /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
64
87
  /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
65
- !auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: "No sign-in method is configured for this site." })
88
+ formState.error && /* @__PURE__ */ jsx("span", { style: S.error, children: formState.error })
66
89
  ] }),
67
- auth.signInUrl && /* @__PURE__ */ jsx("a", { href: auth.signInUrl, style: { ...S.primaryBtn, textDecoration: "none" }, children: "Sign in" }),
90
+ /* @__PURE__ */ jsx(
91
+ "input",
92
+ {
93
+ name: "email",
94
+ type: "email",
95
+ autoComplete: "email",
96
+ placeholder: "Email",
97
+ defaultValue: formState.email,
98
+ required: true,
99
+ style: S.input
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx(
103
+ "input",
104
+ {
105
+ name: "password",
106
+ type: "password",
107
+ autoComplete: "current-password",
108
+ placeholder: "Password",
109
+ required: true,
110
+ style: S.input
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: isPending, style: S.primaryBtn, children: isPending ? "Authenticating\u2026" : "Sign In" }),
114
+ /* @__PURE__ */ jsx(
115
+ "button",
116
+ {
117
+ type: "button",
118
+ onClick: () => setForgotOpen(true),
119
+ style: {
120
+ background: "none",
121
+ border: "none",
122
+ cursor: "pointer",
123
+ color: v("muted"),
124
+ fontSize: 12,
125
+ textDecoration: "underline",
126
+ padding: "0 4px",
127
+ whiteSpace: "nowrap"
128
+ },
129
+ children: "Forgot password?"
130
+ }
131
+ ),
68
132
  /* @__PURE__ */ jsx(
69
133
  "button",
70
134
  {
@@ -76,98 +140,32 @@ function AdminBody({ toolbarExtra }) {
76
140
  children: /* @__PURE__ */ jsx(CloseIcon, {})
77
141
  }
78
142
  )
79
- ] }) });
80
- }
81
- return /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("form", { action: formAction, style: S.bannerForm, children: [
82
- /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
83
- /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
84
- formState.error && /* @__PURE__ */ jsx("span", { style: S.error, children: formState.error })
85
- ] }),
86
- /* @__PURE__ */ jsx(
87
- "input",
88
- {
89
- name: "email",
90
- type: "email",
91
- autoComplete: "email",
92
- placeholder: "Email",
93
- defaultValue: formState.email,
94
- required: true,
95
- style: S.input
96
- }
97
- ),
98
- /* @__PURE__ */ jsx(
99
- "input",
100
- {
101
- name: "password",
102
- type: "password",
103
- autoComplete: "current-password",
104
- placeholder: "Password",
105
- required: true,
106
- style: S.input
107
- }
108
- ),
109
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: isPending, style: S.primaryBtn, children: isPending ? "Authenticating\u2026" : "Sign In" }),
110
- /* @__PURE__ */ jsx(
111
- "button",
112
- {
113
- type: "button",
114
- style: { ...S.iconBtn, color: v("muted") },
115
- onClick: exitEditMode,
116
- title: "Not editing? Dismiss",
117
- "aria-label": "Dismiss",
118
- children: /* @__PURE__ */ jsx(CloseIcon, {})
119
- }
120
- )
121
- ] }) });
122
- }
123
- if (needsPassword) {
124
- return /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("form", { action: passwordFormAction, style: S.bannerForm, children: [
125
- /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
126
- /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
127
- passwordState.error ? /* @__PURE__ */ jsx("span", { style: S.error, children: passwordState.error }) : /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: "Set a password to sign in next time" })
128
- ] }),
129
- /* @__PURE__ */ jsx(
130
- "input",
131
- {
132
- name: "password",
133
- type: "password",
134
- placeholder: "New password",
135
- required: true,
136
- minLength: 6,
137
- style: S.input
138
- }
139
- ),
140
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: isSettingPassword, style: S.primaryBtn, children: isSettingPassword ? "Saving\u2026" : "Set Password" }),
141
- /* @__PURE__ */ jsx(
142
- "button",
143
- {
144
- type: "button",
145
- style: { ...S.iconBtn, color: v("muted") },
146
- onClick: skipSetPassword,
147
- title: "Skip for now",
148
- "aria-label": "Skip",
149
- children: /* @__PURE__ */ jsx(CloseIcon, {})
150
- }
151
- )
152
- ] }) });
143
+ ] }) }),
144
+ linkErrorDrawer ?? (forgotOpen && /* @__PURE__ */ jsx(AuthDrawer, { mode: "forgot", onClose: () => setForgotOpen(false) }))
145
+ ] });
153
146
  }
147
+ if (needsPassword) return /* @__PURE__ */ jsx(AuthDrawer, { mode: "set-password" });
154
148
  if (!role) {
155
- return /* @__PURE__ */ jsxs("div", { "data-guardrail": true, style: S.toolbar, children: [
156
- /* @__PURE__ */ jsx("span", { style: S.toolbarLabel, children: "No access to this site" }),
157
- /* @__PURE__ */ jsx("span", { style: S.divider }),
158
- /* @__PURE__ */ jsx(
159
- "button",
160
- {
161
- style: { ...S.iconBtn, color: v("muted") },
162
- onClick: signOut,
163
- title: "Sign out",
164
- "aria-label": "Sign out",
165
- children: /* @__PURE__ */ jsx(LogOutIcon, {})
166
- }
167
- )
149
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
150
+ /* @__PURE__ */ jsxs("div", { "data-guardrail": true, style: S.toolbar, children: [
151
+ /* @__PURE__ */ jsx("span", { style: S.toolbarLabel, children: "No access to this site" }),
152
+ /* @__PURE__ */ jsx("span", { style: S.divider }),
153
+ /* @__PURE__ */ jsx(
154
+ "button",
155
+ {
156
+ style: { ...S.iconBtn, color: v("muted") },
157
+ onClick: signOut,
158
+ title: "Sign out",
159
+ "aria-label": "Sign out",
160
+ children: /* @__PURE__ */ jsx(LogOutIcon, {})
161
+ }
162
+ )
163
+ ] }),
164
+ linkErrorDrawer
168
165
  ] });
169
166
  }
170
167
  return /* @__PURE__ */ jsxs(Fragment, { children: [
168
+ linkErrorDrawer,
171
169
  teamOpen && /* @__PURE__ */ jsx(TeamDrawer, { onClose: () => setTeamOpen(false) }),
172
170
  /* @__PURE__ */ jsxs("div", { "data-guardrail": true, style: S.toolbar, children: [
173
171
  /* @__PURE__ */ jsx("span", { style: S.dot }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport {\n loginAction,\n logoutAction,\n setPasswordAction,\n skipSetPasswordAction,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { CloseIcon, LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, THEME_CSS, v } from \"./styles\";\nimport { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState, SetPasswordState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\nconst initialPasswordState: SetPasswordState = {};\n\n/**\n * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks up the same\n * light/dark variables from one place.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return (\n <>\n {/*\n precedence + href make React hoist this into <head> and dedupe it.\n Required, not cosmetic: this whole component renders as a sibling of\n <html> (withGuardrail can't reach inside the app's own layout), and\n React 19 rejects a bare <style> anywhere outside the main document.\n */}\n <style href=\"guardrail-theme\" precedence=\"default\">\n {THEME_CSS}\n </style>\n <AdminBody {...props} />\n </>\n );\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const [passwordState, passwordFormAction, isSettingPassword] = useActionState(\n setPasswordAction,\n initialPasswordState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n useEffect(() => {\n if (passwordState.success) router.refresh();\n }, [passwordState.success, router]);\n\n const skipSetPassword = async () => {\n await skipSetPasswordAction();\n router.refresh();\n };\n\n // Ends the editing session, not the sign-in: the overlay goes away and the\n // person's session stays, so ?edit=true brings it straight back with no\n // credentials. The sign-in form only returns once the session itself is\n // gone (expired, or ended elsewhere). Explicit \"false\" rather than deleting\n // the cookie: with a live session, deleting it would leave edit mode on —\n // see resolveEditMode. router.replace drops ?edit=true from the URL (a bare\n // refresh would leave it there), and refresh re-renders without chrome.\n //\n // Also what the sign-in bar's dismiss button does, for someone who was never\n // authenticated and doesn't want to be here at all.\n const exitEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // A real sign-out, kept only for the \"no access\" bar below: someone on the\n // wrong account has no other way to switch to the right one. logoutAction()\n // clears the edit-mode cookie itself — a client-side navigation right after\n // it used to race Next's own post-action route refresh and lose.\n const signOut = async () => {\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div data-guardrail style={S.banner}>\n <form action={formAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n );\n }\n\n // A redirect-carried session (an invite or password-recovery link) just\n // completed — they're genuinely signed in, but have no password yet.\n // Ahead of the role check below: getCachedViewer() already claims a\n // pending invite as a side effect of resolving them, so role is normally\n // already set by the time this renders — this is purely about the\n // password, which is orthogonal to that. Skippable rather than a hard\n // gate: magic-link sign-in already works with no password at all, so\n // someone can dismiss this and keep using invite/magic links indefinitely.\n if (needsPassword) {\n return (\n <div data-guardrail style={S.banner}>\n <form action={passwordFormAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {passwordState.error ? (\n <span style={S.error}>{passwordState.error}</span>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n Set a password to sign in next time\n </span>\n )}\n </div>\n <input\n name=\"password\"\n type=\"password\"\n placeholder=\"New password\"\n required\n minLength={6}\n style={S.input}\n />\n <button type=\"submit\" disabled={isSettingPassword} style={S.primaryBtn}>\n {isSettingPassword ? \"Saving…\" : \"Set Password\"}\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={skipSetPassword}\n title=\"Skip for now\"\n aria-label=\"Skip\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n );\n }\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Sign out\"\n aria-label=\"Sign out\"\n >\n <LogOutIcon />\n </button>\n </div>\n );\n }\n\n return (\n <>\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Exit edit mode\"\n aria-label=\"Exit edit mode\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { AuthDrawer } from \"./AuthDrawer\";\nimport { CloseIcon, LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, THEME_CSS, v } from \"./styles\";\nimport { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\n/**\n * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks up the same\n * light/dark variables from one place.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return (\n <>\n {/*\n precedence + href make React hoist this into <head> and dedupe it.\n Required, not cosmetic: this whole component renders as a sibling of\n <html> (withGuardrail can't reach inside the app's own layout), and\n React 19 rejects a bare <style> anywhere outside the main document.\n */}\n <style href=\"guardrail-theme\" precedence=\"default\">\n {THEME_CSS}\n </style>\n <AdminBody {...props} />\n </>\n );\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [forgotOpen, setForgotOpen] = useState(false);\n const [errorDismissed, setErrorDismissed] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n // The message for an emailed link that didn't work. The cookie behind it is\n // read on the server, so dismissing clears it there too (and refreshes), but\n // hides it right away without waiting for that.\n const dismissAuthError = () => {\n document.cookie = `${GR_COOKIES.AUTH_ERROR}=; path=/; max-age=0; SameSite=Lax`;\n setErrorDismissed(true);\n router.refresh();\n };\n const linkErrorDrawer =\n authError && !errorDismissed ? (\n <AuthDrawer\n mode=\"forgot\"\n title=\"That link didn't work\"\n message={\n authError === \"expired\"\n ? \"It has expired or was already used. Links work once. Enter your email to get a new one.\"\n : \"We couldn't complete it. Enter your email to get a new link.\"\n }\n onClose={dismissAuthError}\n />\n ) : null;\n\n // Ends the editing session, not the sign-in: the overlay goes away and the\n // person's session stays, so ?edit=true brings it straight back with no\n // credentials. The sign-in form only returns once the session itself is\n // gone (expired, or ended elsewhere). Explicit \"false\" rather than deleting\n // the cookie: with a live session, deleting it would leave edit mode on —\n // see resolveEditMode. router.replace drops ?edit=true from the URL (a bare\n // refresh would leave it there), and refresh re-renders without chrome.\n //\n // Also what the sign-in bar's dismiss button does, for someone who was never\n // authenticated and doesn't want to be here at all.\n const exitEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // A real sign-out, kept only for the \"no access\" bar below: someone on the\n // wrong account has no other way to switch to the right one. logoutAction()\n // clears the edit-mode cookie itself — a client-side navigation right after\n // it used to race Next's own post-action route refresh and lose.\n const signOut = async () => {\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <>\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n <div data-guardrail style={S.banner}>\n <form action={formAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n onClick={() => setForgotOpen(true)}\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n color: v(\"muted\"),\n fontSize: 12,\n textDecoration: \"underline\",\n padding: \"0 4px\",\n whiteSpace: \"nowrap\",\n }}\n >\n Forgot password?\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n {linkErrorDrawer ??\n (forgotOpen && <AuthDrawer mode=\"forgot\" onClose={() => setForgotOpen(false)} />)}\n </>\n );\n }\n\n // An invite or password-reset link just completed: they're signed in, but must\n // choose a password before anything else. Ahead of the role check below —\n // getCachedViewer() already claims a pending invite as a side effect of\n // resolving them, so the role is normally set by now; this is purely about the\n // password. Not skippable: a password is the only way back in once this\n // session ends, so the overlay and toolbar wait until one exists.\n if (needsPassword) return <AuthDrawer mode=\"set-password\" />;\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <>\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Sign out\"\n aria-label=\"Sign out\"\n >\n <LogOutIcon />\n </button>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n {linkErrorDrawer}\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Exit edit mode\"\n aria-label=\"Exit edit mode\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,23 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * The two password steps, in the same drawer the Team panel uses.
5
+ *
6
+ * Guardrail asks people for one credential, a password. Emailed links exist only
7
+ * to let someone choose or replace it, so this is where they land:
8
+ *
9
+ * - "set-password" — after an invite or reset link. Cannot be dismissed: until a
10
+ * password exists there is nothing to sign in with next time. The one way out
11
+ * is to sign out.
12
+ * - "forgot" — asks for an address and emails a reset link. Opened from the
13
+ * sign-in bar, and from the "that link didn't work" message with `message`.
14
+ */
15
+ declare function AuthDrawer({ mode, title, message, onClose, }: {
16
+ mode: "set-password" | "forgot";
17
+ title?: string;
18
+ /** Plain-language context shown above the form, e.g. why a link failed. */
19
+ message?: string;
20
+ onClose?: () => void;
21
+ }): react_jsx_runtime.JSX.Element;
22
+
23
+ export { AuthDrawer };
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useActionState, useEffect, useState, useTransition } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { forgotPasswordAction, logoutAction, setPasswordAction } from "../server/actions";
6
+ import { CloseIcon } from "./Icons";
7
+ import { S, v } from "./styles";
8
+ import { ANIMATION_CSS } from "./Team";
9
+ const initialPasswordState = {};
10
+ function AuthDrawer({
11
+ mode,
12
+ title,
13
+ message,
14
+ onClose
15
+ }) {
16
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
17
+ /* @__PURE__ */ jsx("style", { href: "guardrail-team-animation", precedence: "default", children: ANIMATION_CSS }),
18
+ /* @__PURE__ */ jsx(
19
+ "div",
20
+ {
21
+ "data-guardrail": true,
22
+ "data-guardrail-scrim": true,
23
+ style: S.scrim,
24
+ onClick: mode === "forgot" ? onClose : void 0
25
+ }
26
+ ),
27
+ /* @__PURE__ */ jsxs(
28
+ "aside",
29
+ {
30
+ "data-guardrail": true,
31
+ "data-guardrail-team": true,
32
+ style: S.drawer,
33
+ role: "dialog",
34
+ "aria-label": mode === "set-password" ? "Set your password" : "Forgot password",
35
+ children: [
36
+ /* @__PURE__ */ jsxs("header", { style: S.drawerHead, children: [
37
+ /* @__PURE__ */ jsx("h2", { style: S.drawerTitle, children: title ?? (mode === "set-password" ? "Set your password" : "Forgot your password?") }),
38
+ mode === "forgot" && onClose && /* @__PURE__ */ jsx("button", { style: { ...S.iconBtn, color: v("muted") }, onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
39
+ ] }),
40
+ /* @__PURE__ */ jsxs("div", { style: S.drawerBody, children: [
41
+ message && /* @__PURE__ */ jsx("p", { style: { fontSize: 13, lineHeight: 1.5, margin: "0 0 16px", color: v("fg") }, children: message }),
42
+ mode === "set-password" ? /* @__PURE__ */ jsx(SetPasswordForm, {}) : /* @__PURE__ */ jsx(ForgotPasswordForm, {})
43
+ ] })
44
+ ]
45
+ }
46
+ )
47
+ ] });
48
+ }
49
+ function SetPasswordForm() {
50
+ const router = useRouter();
51
+ const [state, formAction, isPending] = useActionState(setPasswordAction, initialPasswordState);
52
+ const [signingOut, startSignOut] = useTransition();
53
+ useEffect(() => {
54
+ if (state.success) router.refresh();
55
+ }, [state.success, router]);
56
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 13, lineHeight: 1.5, margin: "0 0 16px", color: v("muted") }, children: "Choose a password. It's how you'll sign in from now on." }),
58
+ /* @__PURE__ */ jsxs("form", { action: formAction, children: [
59
+ /* @__PURE__ */ jsx("label", { style: S.label, htmlFor: "gr-new-password", children: "New password" }),
60
+ /* @__PURE__ */ jsx(
61
+ "input",
62
+ {
63
+ id: "gr-new-password",
64
+ style: S.field,
65
+ name: "password",
66
+ type: "password",
67
+ autoComplete: "new-password",
68
+ minLength: 8,
69
+ required: true,
70
+ autoFocus: true
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsx("label", { style: S.label, htmlFor: "gr-confirm-password", children: "Confirm password" }),
74
+ /* @__PURE__ */ jsx(
75
+ "input",
76
+ {
77
+ id: "gr-confirm-password",
78
+ style: S.field,
79
+ name: "confirm",
80
+ type: "password",
81
+ autoComplete: "new-password",
82
+ minLength: 8,
83
+ required: true
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 11, color: v("subtle"), margin: "0 0 12px" }, children: "At least 8 characters." }),
87
+ state.error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: state.error }),
88
+ /* @__PURE__ */ jsx("button", { style: { ...S.primaryBtn, width: "100%" }, type: "submit", disabled: isPending || signingOut, children: isPending ? "Saving\u2026" : "Set password" })
89
+ ] }),
90
+ /* @__PURE__ */ jsx(
91
+ "button",
92
+ {
93
+ type: "button",
94
+ disabled: isPending || signingOut,
95
+ style: {
96
+ background: "none",
97
+ border: "none",
98
+ cursor: "pointer",
99
+ color: v("muted"),
100
+ fontSize: 12,
101
+ textDecoration: "underline",
102
+ padding: 0,
103
+ marginTop: 16
104
+ },
105
+ onClick: () => startSignOut(async () => {
106
+ await logoutAction();
107
+ router.refresh();
108
+ }),
109
+ children: "Cancel and sign out"
110
+ }
111
+ )
112
+ ] });
113
+ }
114
+ function ForgotPasswordForm() {
115
+ const [email, setEmail] = useState("");
116
+ const [status, setStatus] = useState("idle");
117
+ const [error, setError] = useState(null);
118
+ if (status === "sent") {
119
+ return /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, lineHeight: 1.5, margin: 0, color: v("fg") }, children: [
120
+ "If ",
121
+ /* @__PURE__ */ jsx("strong", { children: email.trim() }),
122
+ " has access to this site, a link to choose a new password is on its way. It works once, so open it on this device."
123
+ ] });
124
+ }
125
+ return /* @__PURE__ */ jsxs(
126
+ "form",
127
+ {
128
+ onSubmit: async (e) => {
129
+ e.preventDefault();
130
+ setStatus("sending");
131
+ setError(null);
132
+ const res = await forgotPasswordAction(email);
133
+ if (res.success) setStatus("sent");
134
+ else {
135
+ setError(res.error ?? "Something went wrong.");
136
+ setStatus("idle");
137
+ }
138
+ },
139
+ children: [
140
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 13, lineHeight: 1.5, margin: "0 0 16px", color: v("muted") }, children: "Enter your email and we'll send a link to choose a new password." }),
141
+ /* @__PURE__ */ jsx("label", { style: S.label, htmlFor: "gr-forgot-email", children: "Email" }),
142
+ /* @__PURE__ */ jsx(
143
+ "input",
144
+ {
145
+ id: "gr-forgot-email",
146
+ style: S.field,
147
+ type: "email",
148
+ autoComplete: "email",
149
+ placeholder: "name@company.com",
150
+ value: email,
151
+ onChange: (e) => setEmail(e.target.value),
152
+ required: true,
153
+ autoFocus: true
154
+ }
155
+ ),
156
+ error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: error }),
157
+ /* @__PURE__ */ jsx("button", { style: { ...S.primaryBtn, width: "100%" }, type: "submit", disabled: status === "sending", children: status === "sending" ? "Sending\u2026" : "Send reset link" })
158
+ ]
159
+ }
160
+ );
161
+ }
162
+ export {
163
+ AuthDrawer
164
+ };
165
+ //# sourceMappingURL=AuthDrawer.js.map