PageSourceSearch

https://lumenize.com/assets/js/e8709bc5.5130449e.js

js lumenize.com collected 2026-10-02 23:05:27 UTC 15,674 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunk_lumenize_website=globalThis.webpackChunk_lumenize_website||[]).push([[8532],{52935(e,s,n){n.r(s),n.d(s,{assets:()=>l,contentTitle:()=>c,default:()=>h,frontMatter:()=>r,metadata:()=>t,toc:()=>a});const t=JSON.parse('{"id":"auth/subject-management","title":"Subject Management","description":"Admin endpoints and subject record schema","source":"@site/docs/auth/subject-management.mdx","sourceDirName":"auth","slug":"/auth/subject-management","permalink":"/docs/auth/subject-management","draft":false,"unlisted":false,"editUrl":"https://github.com/lumenize/lumenize/tree/main/website/docs/auth/subject-management.mdx","tags":[],"version":"current","frontMatter":{"title":"Subject Management","description":"Admin endpoints and subject record schema"},"sidebar":"docsSidebar","previous":{"title":"Endpoints","permalink":"/docs/auth/endpoints"},"next":{"title":"Delegation","permalink":"/docs/auth/delegation"}}');var i=n(62540),d=n(43023);const r={title:"Subject Management",description:"Admin endpoints and subject record schema"},c="Subject Management Endpoints",l={},a=[{value:"Subject Record",id:"subject-record",level:2},{value:"List Subjects",id:"list-subjects",level:2},{value:"Get Subject",id:"get-subject",level:2},{value:"Update Subject",id:"update-subject",level:2},{value:"Delete Subject",id:"delete-subject",level:2},{value:"Invite Subjects",id:"invite-subjects",level:2},{value:"Approve Subject",id:"approve-subject",level:2}];function o(e){const s={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(s.header,{children:(0,i.jsx)(s.h1,{id:"subject-management-endpoints",children:"Subject Management Endpoints"})}),"\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Endpoint"}),(0,i.jsx)(s.th,{children:"Method"}),(0,i.jsx)(s.th,{children:"Description"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.a,{href:"#list-subjects",children:(0,i.jsx)(s.code,{children:"{prefix}/subjects"})})}),(0,i.jsx)(s.td,{children:"GET"}),(0,i.jsx)(s.td,{children:"List subjects"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.a,{href:"#get-subject",children:(0,i.jsx)(s.code,{children:"{prefix}/subject/:id"})})}),(0,i.jsx)(s.td,{children:"GET"}),(0,i.jsx)(s.td,{children:"Get subject"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.a,{href:"#update-subject",children:(0,i.jsx)(s.code,{children:"{prefix}/subject/:id"})})}),(0,i.jsx)(s.td,{children:"PATCH"}),(0,i.jsx)(s.td,{children:"Update subject flags"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.a,{href:"#delete-subject",children:(0,i.jsx)(s.code,{children:"{prefix}/subject/:id"})})}),(0,i.jsx)(s.td,{children:"DELETE"}),(0,i.jsx)(s.td,{children:"Delete subject"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.a,{href:"#invite-subjects",children:(0,i.jsx)(s.code,{children:"{prefix}/invite"})})}),(0,i.jsx)(s.td,{children:"POST"}),(0,i.jsxs)(s.td,{children:["Invite subjects (bulk) \u2192 ",(0,i.jsx)(s.code,{children:"adminApproved"}),", send invite emails"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.a,{href:"#approve-subject",children:(0,i.jsx)(s.code,{children:"{prefix}/approve/:id"})})}),(0,i.jsx)(s.td,{children:"GET"}),(0,i.jsxs)(s.td,{children:["Approve subject \u2192 ",(0,i.jsx)(s.code,{children:"adminApproved"})," (from admin notification email)"]})]})]})]}),"\n",(0,i.jsxs)(s.p,{children:["All endpoints require ",(0,i.jsx)(s.strong,{children:"Admin"})," auth (Bearer token or refresh token cookie + ",(0,i.jsx)(s.code,{children:"isAdmin"}),"). You don't need to check ",(0,i.jsx)(s.code,{children:"emailVerified"})," or ",(0,i.jsx)(s.code,{children:"adminApproved"})," in your DO guards \u2014 subjects without both flags are blocked at the Worker level before any DO code runs. Your guards only need to check application-level concerns like ",(0,i.jsx)(s.code,{children:"isAdmin"})," or resource ownership."]}),"\n",(0,i.jsx)(s.h2,{id:"subject-record",children:"Subject Record"}),"\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Field"}),(0,i.jsx)(s.th,{children:"Type"}),(0,i.jsx)(s.th,{children:"Updatable"}),(0,i.jsx)(s.th,{children:"Description"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"sub"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"string"})}),(0,i.jsx)(s.td,{children:"No"}),(0,i.jsx)(s.td,{children:"Subject ID (UUID, per RFC 7519)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"email"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"string"})}),(0,i.jsx)(s.td,{children:"No"}),(0,i.jsx)(s.td,{children:"Email address (unique)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"emailVerified"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"boolean"})}),(0,i.jsx)(s.td,{children:"No"}),(0,i.jsx)(s.td,{children:"Subject clicked magic link or invite link"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"adminApproved"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"boolean"})}),(0,i.jsx)(s.td,{children:"Yes"}),(0,i.jsx)(s.td,{children:"Admin granted access (or subject is admin)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"isAdmin"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"boolean"})}),(0,i.jsx)(s.td,{children:"Yes"}),(0,i.jsxs)(s.td,{children:["Full admin access (implicitly satisfies ",(0,i.jsx)(s.code,{children:"adminApproved"}),")"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"authorizedActors"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"string[]"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.a,{href:"/docs/auth/delegation#add-authorized-actor",children:"POST/DELETE"})}),(0,i.jsxs)(s.td,{children:["Actor IDs authorized to act for this subject (",(0,i.jsx)(s.a,{href:"/docs/auth/delegation",children:"Delegation"}),")"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"createdAt"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"number"})}),(0,i.jsx)(s.td,{children:"No"}),(0,i.jsx)(s.td,{children:"Unix timestamp"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"lastLoginAt"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"number | null"})}),(0,i.jsx)(s.td,{children:"No"}),(0,i.jsx)(s.td,{children:"Unix timestamp of last login"})]})]})]}),"\n",(0,i.jsx)(s.h2,{id:"list-subjects",children:"List Subjects"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"GET {prefix}/subjects"})," \u2014 Admin required"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-typescript",metastring:"@check-example('packages/auth/test/for-docs/endpoints.test.ts')",children:"const response = await fetch('/auth/subjects', {\n  headers: { 'Authorization': `Bearer ${accessToken}` }\n});\nconst { subjects } = await response.json();\n"})}),"\n",(0,i.jsx)(s.p,{children:"Query parameters:"}),"\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Parameter"}),(0,i.jsx)(s.th,{children:"Type"}),(0,i.jsx)(s.th,{children:"Description"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"limit"})}),(0,i.jsx)(s.td,{children:"number"}),(0,i.jsx)(s.td,{children:"Max subjects to return (default: 50, max: 200)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"offset"})}),(0,i.jsx)(s.td,{children:"number"}),(0,i.jsx)(s.td,{children:"Skip this many subjects (for pagination)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"role"})}),(0,i.jsx)(s.td,{children:"string"}),(0,i.jsxs)(s.td,{children:["Filter by role: ",(0,i.jsx)(s.code,{children:"admin"})," or ",(0,i.jsx)(s.code,{children:"none"})]})]})]})]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-typescript",metastring:"@check-example('packages/auth/test/for-docs/endpoints.test.ts')",children:"// Get first 50 admins\nconst response = await fetch('/auth/subjects?role=admin&limit=50', {\n  headers: { 'Authorization': `Bearer ${accessToken}` }\n});\n"})}),"\n",(0,i.jsx)(s.h2,{id:"get-subject",children:"Get Subject"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"GET {prefix}/subject/:id"})," \u2014 Admin required"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-typescript",metastring:"@check-example('packages/auth/test/for-docs/endpoints.test.ts')",children:"const response = await fetch(`/auth/subject/${sub}`, {\n  headers: { 'Authorization': `Bearer ${accessToken}` }\n});\nconst { subject } = await response.json();\n"})}),"\n",(0,i.jsx)(s.h2,{id:"update-subject",children:"Update Subject"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"PATCH {prefix}/subject/:id"})," \u2014 Admin required"]}),"\n",(0,i.jsxs)(s.p,{children:["Updates ",(0,i.jsx)(s.code,{children:"isAdmin"})," and/or ",(0,i.jsx)(s.code,{children:"adminApproved"})," flags. Use ",(0,i.jsx)(s.a,{href:"/docs/auth/delegation#add-authorized-actor",children:"Add Authorized Actor"})," / ",(0,i.jsx)(s.a,{href:"/docs/auth/delegation#remove-authorized-actor",children:"Remove Authorized Actor"})," to manage delegation \u2014 ",(0,i.jsx)(s.code,{children:"authorizedActors"})," in the PATCH body is rejected with ",(0,i.jsx)(s.code,{children:"400"}),"."]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-typescript",metastring:"@check-example('packages/auth/test/for-docs/endpoints.test.ts')",children:"const response = await fetch(`/auth/subject/${sub}`, {\n  method: 'PATCH',\n  headers: {\n    'Authorization': `Bearer ${accessToken}`,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    isAdmin: true\n  })\n});\nconst { subject } = await response.json();\n"})}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Rules:"})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"Cannot modify yourself (prevents lockout)"}),"\n",(0,i.jsxs)(s.li,{children:["Cannot modify the ",(0,i.jsx)(s.a,{href:"/docs/auth/getting-started#bootstrap-your-first-admin",children:"bootstrap admin"})]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"delete-subject",children:"Delete Subject"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"DELETE {prefix}/subject/:id"})," \u2014 Admin required"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-typescript",metastring:"@check-example('packages/auth/test/for-docs/endpoints.test.ts')",children:"const response = await fetch(`/auth/subject/${sub}`, {\n  method: 'DELETE',\n  headers: { 'Authorization': `Bearer ${accessToken}` }\n});\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Returns ",(0,i.jsx)(s.code,{children:"204 No Content"})," on success."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Rules:"})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"Cannot delete yourself"}),"\n",(0,i.jsxs)(s.li,{children:["Cannot delete the ",(0,i.jsx)(s.a,{href:"/docs/auth/getting-started#bootstrap-your-first-admin",children:"bootstrap admin"})]}),"\n",(0,i.jsxs)(s.li,{children:["Deleting a subject removes all their ",(0,i.jsx)(s.a,{href:"/docs/auth/delegation",children:"delegation"})," relationships (both as principal and actor) via cascading delete"]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"invite-subjects",children:"Invite Subjects"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"POST {prefix}/invite"})," \u2014 Admin required"]}),"\n",(0,i.jsx)(s.p,{children:"Pre-approves subjects by sending invite emails:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-typescript",metastring:"@check-example('packages/auth/test/for-docs/endpoints.test.ts')",children:"const response = await fetch('/auth/invite', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${accessToken}`,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    emails: ['[email protected]', '[email protected]']\n  })\n});\nconst { invited, errors } = await response.json();\n// invited: ['[email protected]', '[email protected]']\n// errors: [] (or [{email, error}] for failures)\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Creates subject records with ",(0,i.jsx)(s.code,{children:"adminApproved: true"}),", ",(0,i.jsx)(s.code,{children:"emailVerified: false"})," and sends invite emails. When subjects click the invite link (",(0,i.jsx)(s.a,{href:"/docs/auth/endpoints#accept-invite",children:"Accept Invite"}),"), ",(0,i.jsx)(s.code,{children:"emailVerified"})," is set to ",(0,i.jsx)(s.code,{children:"true"})," and they gain immediate access."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Behavior:"})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["If email already exists and subject has ",(0,i.jsx)(s.code,{children:"emailVerified: true"})," \u2014 sets ",(0,i.jsx)(s.code,{children:"adminApproved: true"})," and sends invite email"]}),"\n",(0,i.jsxs)(s.li,{children:["If email already exists and subject has ",(0,i.jsx)(s.code,{children:"emailVerified: false"})," \u2014 re-sends invite"]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["In ",(0,i.jsx)(s.a,{href:"/docs/auth/testing#test-mode",children:"test mode"}),", append ",(0,i.jsx)(s.code,{children:"?_test=true"})," to get invite links in the response instead of sending emails."]}),"\n",(0,i.jsx)(s.h2,{id:"approve-subject",children:"Approve Subject"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"GET {prefix}/approve/:id"})," \u2014 Admin required"]}),"\n",(0,i.jsxs)(s.p,{children:["Linked from the admin notification email sent during ",(0,i.jsx)(s.a,{href:"/docs/auth/#self
1-signup-flow",children:"self-signup"}),". Works from email links because the DO accepts the refresh token cookie (no Bearer header needed)."]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Authenticated admin"}),": Sets ",(0,i.jsx)(s.code,{children:"adminApproved: true"}),', sends a "You\'ve been approved" email to the subject, redirects to ',(0,i.jsx)(s.code,{children:"env.LUMENIZE_AUTH_REDIRECT"}),"."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Not authenticated"}),": Redirects to ",(0,i.jsx)(s.code,{children:"env.LUMENIZE_AUTH_REDIRECT?error=login_required"})," so the app can prompt login and redirect back."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["Equivalent to ",(0,i.jsx)(s.code,{children:"PATCH /subject/:id"})," with ",(0,i.jsx)(s.code,{children:"{ adminApproved: true }"})," \u2014 this endpoint exists as a GET so it works as an email link."]})]})}function h(e={}){const{wrapper:s}={...(0,d.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(o,{...e})}):o(e)}},43023(e,s,n){n.d(s,{R:()=>r,x:()=>c});var t=n(63696);const i={},d=t.createContext(i);function r(e){const s=t.useContext(d);return t.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function c(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),t.createElement(d.Provider,{value:s},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.