create-authenik8-app 2.4.11 → 2.4.13
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 +160 -57
- package/assets/logo.svg +53 -0
- package/dist/src/bin/index.js +199 -125
- package/dist/src/bin/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +18 -0
- package/dist/src/lib/args.d.ts.map +1 -0
- package/dist/src/lib/args.js +82 -0
- package/dist/src/lib/args.js.map +1 -0
- package/dist/src/lib/constants.d.ts +1 -0
- package/dist/src/lib/constants.d.ts.map +1 -1
- package/dist/src/lib/constants.js +10 -11
- package/dist/src/lib/constants.js.map +1 -1
- package/dist/src/lib/preflight.d.ts +3 -0
- package/dist/src/lib/preflight.d.ts.map +1 -0
- package/dist/src/lib/preflight.js +21 -0
- package/dist/src/lib/preflight.js.map +1 -0
- package/dist/src/lib/process.d.ts +6 -2
- package/dist/src/lib/process.d.ts.map +1 -1
- package/dist/src/lib/process.js +66 -7
- package/dist/src/lib/process.js.map +1 -1
- package/dist/src/lib/projectName.d.ts +2 -0
- package/dist/src/lib/projectName.d.ts.map +1 -0
- package/dist/src/lib/projectName.js +6 -0
- package/dist/src/lib/projectName.js.map +1 -0
- package/dist/src/lib/schemas.d.ts +192 -0
- package/dist/src/lib/schemas.d.ts.map +1 -0
- package/dist/src/lib/schemas.js +171 -0
- package/dist/src/lib/schemas.js.map +1 -0
- package/dist/src/lib/state.d.ts.map +1 -1
- package/dist/src/lib/state.js +20 -3
- package/dist/src/lib/state.js.map +1 -1
- package/dist/src/lib/types.d.ts +6 -13
- package/dist/src/lib/types.d.ts.map +1 -1
- package/dist/src/lib/ui.d.ts +8 -3
- package/dist/src/lib/ui.d.ts.map +1 -1
- package/dist/src/lib/ui.js +76 -112
- package/dist/src/lib/ui.js.map +1 -1
- package/dist/src/steps/configurePrisma.d.ts.map +1 -1
- package/dist/src/steps/configurePrisma.js +83 -9
- package/dist/src/steps/configurePrisma.js.map +1 -1
- package/dist/src/steps/createProject.d.ts +2 -1
- package/dist/src/steps/createProject.d.ts.map +1 -1
- package/dist/src/steps/createProject.js +149 -28
- package/dist/src/steps/createProject.js.map +1 -1
- package/dist/src/steps/finalSetup.d.ts +3 -3
- package/dist/src/steps/finalSetup.d.ts.map +1 -1
- package/dist/src/steps/finalSetup.js +24 -28
- package/dist/src/steps/finalSetup.js.map +1 -1
- package/dist/src/steps/installAuth.d.ts +1 -1
- package/dist/src/steps/installAuth.d.ts.map +1 -1
- package/dist/src/steps/installAuth.js +5 -4
- package/dist/src/steps/installAuth.js.map +1 -1
- package/dist/src/steps/installDeps.d.ts +11 -4
- package/dist/src/steps/installDeps.d.ts.map +1 -1
- package/dist/src/steps/installDeps.js +45 -77
- package/dist/src/steps/installDeps.js.map +1 -1
- package/dist/src/steps/prompts.d.ts +1 -1
- package/dist/src/steps/prompts.d.ts.map +1 -1
- package/dist/src/steps/prompts.js +42 -30
- package/dist/src/steps/prompts.js.map +1 -1
- package/dist/src/utils/jwk.d.ts +2 -0
- package/dist/src/utils/jwk.d.ts.map +1 -0
- package/dist/src/utils/jwk.js +44 -0
- package/dist/src/utils/jwk.js.map +1 -0
- package/dist/src/utils/output.d.ts +1 -1
- package/dist/src/utils/output.d.ts.map +1 -1
- package/dist/src/utils/output.js +75 -68
- package/dist/src/utils/output.js.map +1 -1
- package/package.json +17 -9
- package/templates/AGENT_IDENTITY.md +112 -0
- package/templates/THREAT_MODEL.md +27 -5
- package/templates/express-auth/README.md +13 -10
- package/templates/express-auth/docker-compose.yml +10 -0
- package/templates/express-auth/gitignore.template +6 -0
- package/templates/express-auth/package.json +13 -6
- package/templates/express-auth/src/app.ts +2 -0
- package/templates/express-auth/src/controllers/auth.controller.ts +2 -6
- package/templates/express-auth/src/controllers/protected.controller.ts +25 -10
- package/templates/express-auth/src/middleware/auth.middleware.ts +1 -0
- package/templates/express-auth/src/routes/protected.routes.ts +3 -1
- package/templates/express-auth/src/server.ts +6 -12
- package/templates/express-auth/src/utils/security.ts +119 -32
- package/templates/express-auth+/README.md +13 -10
- package/templates/express-auth+/docker-compose.yml +10 -0
- package/templates/express-auth+/gitignore.template +6 -0
- package/templates/express-auth+/package.json +13 -6
- package/templates/express-auth+/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/auth/controllers/password.controller.ts +3 -7
- package/templates/express-auth+/src/auth/controllers/protected.controller.ts +26 -11
- package/templates/express-auth+/src/auth/identity.adapter.ts +90 -0
- package/templates/express-auth+/src/auth/middleware/auth.middleware.ts +3 -4
- package/templates/express-auth+/src/auth/routes/protected.routes.ts +2 -2
- package/templates/express-auth+/src/oauth-providers/github/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/oauth-providers/google/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/oauth-providers/google-github/src/auth/auth.ts +3 -2
- package/templates/express-auth+/src/server.ts +5 -10
- package/templates/express-auth+/src/utils/security.ts +109 -28
- package/templates/express-base/README.md +13 -6
- package/templates/express-base/app.ts +2 -0
- package/templates/express-base/controllers/base.controller.ts +33 -19
- package/templates/express-base/docker-compose.yml +10 -0
- package/templates/express-base/gitignore.template +6 -0
- package/templates/express-base/package.json +11 -5
- package/templates/express-base/routes/base.routes.ts +1 -1
- package/templates/express-base/src/.env.example +7 -2
- package/templates/express-base/src/server.ts +6 -12
- package/templates/express-base/tsconfig.json +7 -40
- package/templates/express-base/utils/security.ts +111 -15
- package/templates/fullstack/.env.example +24 -0
- package/templates/fullstack/PRESET_CONTRACT.md +46 -0
- package/templates/fullstack/README.md +65 -0
- package/templates/fullstack/THREAT_MODEL.md +30 -0
- package/templates/fullstack/apps/api/package.json +48 -0
- package/templates/fullstack/apps/api/prisma/schema.prisma +124 -0
- package/templates/fullstack/apps/api/prisma/seed.ts +36 -0
- package/templates/fullstack/apps/api/prisma.config.ts +18 -0
- package/templates/fullstack/apps/api/src/app.ts +68 -0
- package/templates/fullstack/apps/api/src/auth/auth.controller.ts +125 -0
- package/templates/fullstack/apps/api/src/auth/auth.routes.ts +50 -0
- package/templates/fullstack/apps/api/src/auth/auth.service.ts +257 -0
- package/templates/fullstack/apps/api/src/auth/authenik8.ts +67 -0
- package/templates/fullstack/apps/api/src/auth/cookies.ts +36 -0
- package/templates/fullstack/apps/api/src/config/env.ts +72 -0
- package/templates/fullstack/apps/api/src/config/logger.ts +17 -0
- package/templates/fullstack/apps/api/src/config/mailer.ts +32 -0
- package/templates/fullstack/apps/api/src/config/origins.ts +27 -0
- package/templates/fullstack/apps/api/src/config/prisma.ts +9 -0
- package/templates/fullstack/apps/api/src/middleware/authenticate.ts +35 -0
- package/templates/fullstack/apps/api/src/middleware/csrf.ts +60 -0
- package/templates/fullstack/apps/api/src/middleware/origin.ts +12 -0
- package/templates/fullstack/apps/api/src/middleware/request-id.ts +9 -0
- package/templates/fullstack/apps/api/src/modules/admin/admin.controller.ts +17 -0
- package/templates/fullstack/apps/api/src/modules/admin/admin.routes.ts +14 -0
- package/templates/fullstack/apps/api/src/modules/admin/admin.service.ts +54 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.controller.ts +22 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.policy.ts +14 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.repository.ts +20 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.routes.ts +21 -0
- package/templates/fullstack/apps/api/src/modules/projects/project.service.ts +41 -0
- package/templates/fullstack/apps/api/src/modules/users/user.controller.ts +26 -0
- package/templates/fullstack/apps/api/src/modules/users/user.presenter.ts +13 -0
- package/templates/fullstack/apps/api/src/modules/users/user.routes.ts +21 -0
- package/templates/fullstack/apps/api/src/modules/users/user.service.ts +71 -0
- package/templates/fullstack/apps/api/src/openapi.ts +32 -0
- package/templates/fullstack/apps/api/src/server.ts +29 -0
- package/templates/fullstack/apps/api/src/types.ts +15 -0
- package/templates/fullstack/apps/api/src/utils/crypto.ts +4 -0
- package/templates/fullstack/apps/api/src/utils/http.ts +33 -0
- package/templates/fullstack/apps/api/src/utils/sealed-value.ts +37 -0
- package/templates/fullstack/apps/api/tests/project.policy.test.ts +26 -0
- package/templates/fullstack/apps/api/tests/registration.test.ts +69 -0
- package/templates/fullstack/apps/api/tests/security.test.ts +98 -0
- package/templates/fullstack/apps/api/tests/validation.test.ts +37 -0
- package/templates/fullstack/apps/api/tsconfig.json +16 -0
- package/templates/fullstack/apps/api/vitest.config.ts +9 -0
- package/templates/fullstack/apps/web/index.html +14 -0
- package/templates/fullstack/apps/web/package.json +31 -0
- package/templates/fullstack/apps/web/public/authenik8-logo.svg +53 -0
- package/templates/fullstack/apps/web/src/App.tsx +45 -0
- package/templates/fullstack/apps/web/src/auth/AuthProvider.tsx +53 -0
- package/templates/fullstack/apps/web/src/auth/RouteGuards.tsx +16 -0
- package/templates/fullstack/apps/web/src/auth/providers.ts +3 -0
- package/templates/fullstack/apps/web/src/components/AppShell.tsx +80 -0
- package/templates/fullstack/apps/web/src/components/AuthShell.tsx +26 -0
- package/templates/fullstack/apps/web/src/components/Page.tsx +30 -0
- package/templates/fullstack/apps/web/src/features/projects/ProjectPages.tsx +79 -0
- package/templates/fullstack/apps/web/src/main.tsx +24 -0
- package/templates/fullstack/apps/web/src/pages/DashboardPage.tsx +48 -0
- package/templates/fullstack/apps/web/src/pages/StatusPages.tsx +8 -0
- package/templates/fullstack/apps/web/src/pages/admin/AuditPage.tsx +19 -0
- package/templates/fullstack/apps/web/src/pages/admin/UsersPage.tsx +24 -0
- package/templates/fullstack/apps/web/src/pages/auth/LoginPage.tsx +51 -0
- package/templates/fullstack/apps/web/src/pages/auth/OAuthCallbackPage.tsx +26 -0
- package/templates/fullstack/apps/web/src/pages/auth/RecoveryPages.tsx +53 -0
- package/templates/fullstack/apps/web/src/pages/auth/RegisterPage.tsx +86 -0
- package/templates/fullstack/apps/web/src/pages/auth/VerifyEmailPage.tsx +24 -0
- package/templates/fullstack/apps/web/src/pages/settings/ProfilePage.tsx +34 -0
- package/templates/fullstack/apps/web/src/pages/settings/SecurityPage.tsx +57 -0
- package/templates/fullstack/apps/web/src/styles.css +289 -0
- package/templates/fullstack/apps/web/src/token-storage.test.ts +12 -0
- package/templates/fullstack/apps/web/tsconfig.app.json +19 -0
- package/templates/fullstack/apps/web/tsconfig.json +7 -0
- package/templates/fullstack/apps/web/tsconfig.node.json +10 -0
- package/templates/fullstack/apps/web/vite.config.ts +18 -0
- package/templates/fullstack/docker-compose.yml +35 -0
- package/templates/fullstack/docs/PRODUCTION.md +30 -0
- package/templates/fullstack/gitignore.template +8 -0
- package/templates/fullstack/package.json +39 -0
- package/templates/fullstack/packages/api-client/package.json +20 -0
- package/templates/fullstack/packages/api-client/src/index.ts +203 -0
- package/templates/fullstack/packages/api-client/tsconfig.json +14 -0
- package/templates/fullstack/packages/contracts/package.json +20 -0
- package/templates/fullstack/packages/contracts/src/index.ts +146 -0
- package/templates/fullstack/packages/contracts/tsconfig.json +13 -0
- package/templates/fullstack/packages/ui/package.json +22 -0
- package/templates/fullstack/packages/ui/src/index.tsx +33 -0
- package/templates/fullstack/packages/ui/tsconfig.json +14 -0
- package/templates/prisma/postgresql/.env.example +6 -2
- package/templates/prisma/postgresql/client.ts +17 -0
- package/templates/prisma/postgresql/prisma.config.ts +12 -0
- package/templates/prisma/postgresql/schema.prisma +0 -1
- package/templates/prisma/sqlite/.env.example +6 -2
- package/templates/prisma/sqlite/client.ts +16 -0
- package/templates/prisma/sqlite/prisma.config.ts +12 -0
- package/templates/prisma/sqlite/schema.prisma +0 -1
- package/dist/src/tests/templated-routes.test.d.ts +0 -2
- package/dist/src/tests/templated-routes.test.d.ts.map +0 -1
- package/dist/src/tests/templated-routes.test.js +0 -325
- package/dist/src/tests/templated-routes.test.js.map +0 -1
- package/dist/templates/THREAT_MODEL.md +0 -138
- package/dist/templates/express-auth/src/controllers/protected.controller.d.ts +0 -8
- package/dist/templates/express-auth/src/controllers/protected.controller.d.ts.map +0 -1
- package/dist/templates/express-auth/src/controllers/protected.controller.js +0 -50
- package/dist/templates/express-auth/src/controllers/protected.controller.js.map +0 -1
- package/dist/templates/express-auth/src/routes/protected.routes.d.ts +0 -2
- package/dist/templates/express-auth/src/routes/protected.routes.d.ts.map +0 -1
- package/dist/templates/express-auth/src/routes/protected.routes.js +0 -16
- package/dist/templates/express-auth/src/routes/protected.routes.js.map +0 -1
- package/dist/templates/express-auth/src/utils/security.d.ts +0 -8
- package/dist/templates/express-auth/src/utils/security.d.ts.map +0 -1
- package/dist/templates/express-auth/src/utils/security.js +0 -55
- package/dist/templates/express-auth/src/utils/security.js.map +0 -1
- package/dist/templates/express-base/controllers/base.controller.d.ts +0 -12
- package/dist/templates/express-base/controllers/base.controller.d.ts.map +0 -1
- package/dist/templates/express-base/controllers/base.controller.js +0 -77
- package/dist/templates/express-base/controllers/base.controller.js.map +0 -1
- package/dist/templates/express-base/routes/base.routes.d.ts +0 -2
- package/dist/templates/express-base/routes/base.routes.d.ts.map +0 -1
- package/dist/templates/express-base/routes/base.routes.js +0 -20
- package/dist/templates/express-base/routes/base.routes.js.map +0 -1
- package/dist/templates/express-base/utils/security.d.ts +0 -5
- package/dist/templates/express-base/utils/security.d.ts.map +0 -1
- package/dist/templates/express-base/utils/security.js +0 -40
- package/dist/templates/express-base/utils/security.js.map +0 -1
- package/dist/templates/package.json +0 -27
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { Activity, ArrowRight, CheckCircle2, FolderKanban, MonitorSmartphone, ShieldCheck } from "lucide-react";
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
import { Badge, Card, EmptyState, Spinner } from "@authenik8/ui";
|
|
5
|
+
import { accountApi, healthApi, projectApi } from "@authenik8/api-client";
|
|
6
|
+
import { PageHeader, ErrorNotice } from "../components/Page";
|
|
7
|
+
import { useAuth } from "../auth/AuthProvider";
|
|
8
|
+
|
|
9
|
+
export function DashboardPage() {
|
|
10
|
+
const { user } = useAuth();
|
|
11
|
+
const projects = useQuery({ queryKey: ["projects"], queryFn: projectApi.list });
|
|
12
|
+
const sessions = useQuery({ queryKey: ["sessions"], queryFn: accountApi.sessions });
|
|
13
|
+
const health = useQuery({ queryKey: ["health"], queryFn: healthApi.status, refetchInterval: 60_000 });
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<PageHeader title={`Good to see you, ${user?.name.split(" ")[0]}`} description="Your workspace and identity services at a glance." />
|
|
18
|
+
<div className="stat-grid">
|
|
19
|
+
<Card className="stat-card"><span className="stat-icon cyan"><FolderKanban size={19} /></span><div><span>Projects</span><strong>{projects.data?.projects.length ?? "-"}</strong></div></Card>
|
|
20
|
+
<Card className="stat-card"><span className="stat-icon violet"><MonitorSmartphone size={19} /></span><div><span>Active sessions</span><strong>{sessions.data?.sessions.length ?? "-"}</strong></div></Card>
|
|
21
|
+
<Card className="stat-card"><span className="stat-icon green"><ShieldCheck size={19} /></span><div><span>Email security</span><strong>{user?.verified ? "Verified" : "Pending"}</strong></div></Card>
|
|
22
|
+
<Card className="stat-card"><span className="stat-icon amber"><Activity size={19} /></span><div><span>API status</span><strong>{health.data?.status ?? "Checking"}</strong></div></Card>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div className="dashboard-grid">
|
|
26
|
+
<section className="panel">
|
|
27
|
+
<div className="panel-heading"><div><h2>Recent projects</h2><p>Latest workspace activity</p></div><Link className="text-link" to="/projects">View all <ArrowRight size={15} /></Link></div>
|
|
28
|
+
{projects.isPending ? <div className="panel-loading"><Spinner /></div> : projects.error ? <ErrorNotice error={projects.error} /> : projects.data!.projects.length === 0 ? (
|
|
29
|
+
<EmptyState icon={<FolderKanban />} title="No projects yet" action={<Link className="button button-primary" to="/projects/new">Create project</Link>}>Create the first owned resource in this workspace.</EmptyState>
|
|
30
|
+
) : (
|
|
31
|
+
<div className="table-wrap"><table><thead><tr><th>Name</th><th>Status</th><th>Updated</th></tr></thead><tbody>
|
|
32
|
+
{projects.data!.projects.slice(0, 5).map((project) => <tr key={project.id}><td><Link className="table-link" to={`/projects/${project.id}`}>{project.name}</Link></td><td><Badge tone={project.status === "ACTIVE" ? "success" : project.status === "ARCHIVED" ? "neutral" : "warning"}>{project.status}</Badge></td><td>{new Date(project.updatedAt).toLocaleDateString()}</td></tr>)}
|
|
33
|
+
</tbody></table></div>
|
|
34
|
+
)}
|
|
35
|
+
</section>
|
|
36
|
+
<aside className="panel security-panel">
|
|
37
|
+
<div className="panel-heading"><div><h2>Security posture</h2><p>Account checks</p></div></div>
|
|
38
|
+
<ul className="check-list">
|
|
39
|
+
<li><CheckCircle2 /><span><strong>Rotating session</strong><small>Refresh cookie protected</small></span></li>
|
|
40
|
+
<li className={user?.verified ? "" : "pending"}><CheckCircle2 /><span><strong>Email verification</strong><small>{user?.verified ? "Identity confirmed" : "Action required"}</small></span></li>
|
|
41
|
+
<li><CheckCircle2 /><span><strong>API authorization</strong><small>Role and owner policies active</small></span></li>
|
|
42
|
+
</ul>
|
|
43
|
+
<Link className="button button-secondary full-width" to="/settings/security">Review account security</Link>
|
|
44
|
+
</aside>
|
|
45
|
+
</div>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ArrowLeft, LockKeyhole, SearchX } from "lucide-react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
function StatusPage({ icon, title, message }: { icon: React.ReactNode; title: string; message: string }) {
|
|
5
|
+
return <main className="status-page"><div className="status-icon">{icon}</div><h1>{title}</h1><p>{message}</p><Link className="button button-primary" to="/"><ArrowLeft size={17} /> Return to dashboard</Link></main>;
|
|
6
|
+
}
|
|
7
|
+
export const ForbiddenPage = () => <StatusPage icon={<LockKeyhole />} title="Access denied" message="Your account does not have permission to view this area." />;
|
|
8
|
+
export const NotFoundPage = () => <StatusPage icon={<SearchX />} title="Page not found" message="The page you requested does not exist." />;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { Activity, RefreshCw } from "lucide-react";
|
|
3
|
+
import { Button, EmptyState, Spinner } from "@authenik8/ui";
|
|
4
|
+
import { adminApi } from "@authenik8/api-client";
|
|
5
|
+
import { ErrorNotice, PageHeader } from "../../components/Page";
|
|
6
|
+
|
|
7
|
+
export function AuditPage() {
|
|
8
|
+
const query = useQuery({ queryKey: ["audit"], queryFn: adminApi.audit });
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<PageHeader title="Audit trail" description="Recent privileged workspace actions." action={<Button variant="secondary" onClick={() => query.refetch()}><RefreshCw size={17} /> Refresh</Button>} />
|
|
12
|
+
<section className="panel">
|
|
13
|
+
{query.isPending ? <div className="panel-loading"><Spinner /></div> : query.error ? <ErrorNotice error={query.error} /> : query.data!.events.length === 0 ? <EmptyState icon={<Activity />} title="No audit events">Administrative changes will appear here.</EmptyState> : <div className="table-wrap"><table><thead><tr><th>Action</th><th>Actor</th><th>Target</th><th>Time</th></tr></thead><tbody>
|
|
14
|
+
{query.data!.events.map((event) => <tr key={event.id}><td><code>{event.action}</code></td><td>{event.actorEmail ?? "System"}</td><td>{event.targetType}{event.targetId ? ` / ${event.targetId.slice(0, 8)}` : ""}</td><td>{new Date(event.createdAt).toLocaleString()}</td></tr>)}
|
|
15
|
+
</tbody></table></div>}
|
|
16
|
+
</section>
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { MoreHorizontal, RefreshCw, Users } from "lucide-react";
|
|
3
|
+
import { Badge, Button, EmptyState, Spinner } from "@authenik8/ui";
|
|
4
|
+
import { adminApi } from "@authenik8/api-client";
|
|
5
|
+
import type { Role, UserStatus } from "@authenik8/contracts";
|
|
6
|
+
import { ErrorNotice, PageHeader } from "../../components/Page";
|
|
7
|
+
|
|
8
|
+
export function UsersPage() {
|
|
9
|
+
const client = useQueryClient();
|
|
10
|
+
const query = useQuery({ queryKey: ["admin-users"], queryFn: () => adminApi.users() });
|
|
11
|
+
const update = useMutation({ mutationFn: ({ id, role, status }: { id: string; role?: Role; status?: UserStatus }) => adminApi.updateUser(id, { role, status }), onSuccess: () => client.invalidateQueries({ queryKey: ["admin-users"] }) });
|
|
12
|
+
const revoke = useMutation({ mutationFn: adminApi.revokeSessions });
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<PageHeader title="Users" description="Roles, account status, and session control." action={<Button variant="secondary" onClick={() => query.refetch()}><RefreshCw size={17} /> Refresh</Button>} />
|
|
16
|
+
{(query.error || update.error || revoke.error) && <ErrorNotice error={query.error ?? update.error ?? revoke.error} />}
|
|
17
|
+
<section className="panel">
|
|
18
|
+
{query.isPending ? <div className="panel-loading"><Spinner /></div> : query.data!.items.length === 0 ? <EmptyState icon={<Users />} title="No users">Registered users will appear here.</EmptyState> : <div className="table-wrap"><table><thead><tr><th>User</th><th>Role</th><th>Status</th><th>Verified</th><th>Joined</th><th><span className="sr-only">Actions</span></th></tr></thead><tbody>
|
|
19
|
+
{query.data!.items.map((user) => <tr key={user.id}><td><strong>{user.name}</strong><small className="table-subtitle">{user.email}</small></td><td><select className="table-select" value={user.role} onChange={(event) => update.mutate({ id: user.id, role: event.target.value as Role })}><option value="USER">User</option><option value="ADMIN">Admin</option></select></td><td><select className="table-select" value={user.status} onChange={(event) => update.mutate({ id: user.id, status: event.target.value as UserStatus })}><option value="ACTIVE">Active</option><option value="SUSPENDED">Suspended</option></select></td><td><Badge tone={user.verified ? "success" : "warning"}>{user.verified ? "Verified" : "Pending"}</Badge></td><td>{new Date(user.createdAt).toLocaleDateString()}</td><td><Button className="icon-button" variant="ghost" onClick={() => revoke.mutate(user.id)} aria-label={`Revoke sessions for ${user.name}`} title="Revoke all sessions"><MoreHorizontal size={18} /></Button></td></tr>)}
|
|
20
|
+
</tbody></table></div>}
|
|
21
|
+
</section>
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState, type FormEvent } from "react";
|
|
2
|
+
import { Github, KeyRound } from "lucide-react";
|
|
3
|
+
import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
|
4
|
+
import { Button, Input } from "@authenik8/ui";
|
|
5
|
+
import { AuthShell } from "../../components/AuthShell";
|
|
6
|
+
import { ErrorNotice, Field } from "../../components/Page";
|
|
7
|
+
import { useAuth } from "../../auth/AuthProvider";
|
|
8
|
+
import { enabledOAuthProviders } from "../../auth/providers";
|
|
9
|
+
|
|
10
|
+
export function LoginPage() {
|
|
11
|
+
const { login } = useAuth();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const location = useLocation();
|
|
14
|
+
const [params] = useSearchParams();
|
|
15
|
+
const [email, setEmail] = useState("");
|
|
16
|
+
const [password, setPassword] = useState("");
|
|
17
|
+
const [error, setError] = useState<unknown>();
|
|
18
|
+
const [pending, setPending] = useState(false);
|
|
19
|
+
|
|
20
|
+
async function submit(event: FormEvent) {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
setPending(true);
|
|
23
|
+
setError(undefined);
|
|
24
|
+
try {
|
|
25
|
+
await login({ email, password });
|
|
26
|
+
const from = (location.state as { from?: string } | null)?.from;
|
|
27
|
+
navigate(from ?? "/", { replace: true });
|
|
28
|
+
} catch (caught) {
|
|
29
|
+
setError(caught);
|
|
30
|
+
} finally {
|
|
31
|
+
setPending(false);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AuthShell title="Welcome back" subtitle="Sign in to your workspace" footer={<>New to Authenik8? <Link to="/register">Create an account</Link></>}>
|
|
37
|
+
{params.has("oauthError") && <ErrorNotice error={new Error("OAuth sign-in could not be completed")} />}
|
|
38
|
+
{error !== undefined && <ErrorNotice error={error} />}
|
|
39
|
+
<form className="form-stack" onSubmit={submit}>
|
|
40
|
+
<Field label="Email address" htmlFor="email"><Input id="email" type="email" autoComplete="email" value={email} onChange={(event) => setEmail(event.target.value)} required /></Field>
|
|
41
|
+
<Field label="Password" htmlFor="password"><Input id="password" type="password" autoComplete="current-password" value={password} onChange={(event) => setPassword(event.target.value)} required /></Field>
|
|
42
|
+
<div className="form-row end"><Link to="/forgot-password">Forgot password?</Link></div>
|
|
43
|
+
<Button type="submit" disabled={pending}><KeyRound size={17} /> {pending ? "Signing in..." : "Sign in"}</Button>
|
|
44
|
+
</form>
|
|
45
|
+
{enabledOAuthProviders.length > 0 && <><div className="divider"><span>or continue with</span></div>
|
|
46
|
+
<div className="oauth-grid">
|
|
47
|
+
{enabledOAuthProviders.map((provider) => <Button key={provider} variant="secondary" onClick={() => window.location.assign(`/api/auth/oauth/${provider}`)}>{provider === "google" ? <span className="provider-g">G</span> : <Github size={18} />} {provider === "google" ? "Google" : "GitHub"}</Button>)}
|
|
48
|
+
</div></>}
|
|
49
|
+
</AuthShell>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
3
|
+
import { Spinner } from "@authenik8/ui";
|
|
4
|
+
import { useAuth } from "../../auth/AuthProvider";
|
|
5
|
+
import { AuthShell } from "../../components/AuthShell";
|
|
6
|
+
import { ErrorNotice } from "../../components/Page";
|
|
7
|
+
|
|
8
|
+
export function OAuthCallbackPage() {
|
|
9
|
+
const [params] = useSearchParams();
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
const { completeOAuth } = useAuth();
|
|
12
|
+
const started = useRef(false);
|
|
13
|
+
const [error, setError] = useState<unknown>();
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (started.current) return;
|
|
16
|
+
started.current = true;
|
|
17
|
+
const code = params.get("code");
|
|
18
|
+
if (!code) { setError(new Error("OAuth exchange code is missing")); return; }
|
|
19
|
+
completeOAuth(code).then(() => navigate("/", { replace: true })).catch(setError);
|
|
20
|
+
}, [completeOAuth, navigate, params]);
|
|
21
|
+
return (
|
|
22
|
+
<AuthShell title="Completing sign in" subtitle="Securing your new session" footer={<span />}>
|
|
23
|
+
{error ? <ErrorNotice error={error} /> : <div className="center-status"><Spinner label="Completing OAuth sign in" /></div>}
|
|
24
|
+
</AuthShell>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useState, type FormEvent } from "react";
|
|
2
|
+
import { ArrowLeft, Mail, RotateCcw } from "lucide-react";
|
|
3
|
+
import { Link, useSearchParams } from "react-router-dom";
|
|
4
|
+
import { Button, Input } from "@authenik8/ui";
|
|
5
|
+
import { authApi } from "@authenik8/api-client";
|
|
6
|
+
import { AuthShell } from "../../components/AuthShell";
|
|
7
|
+
import { ErrorNotice, Field, SuccessNotice } from "../../components/Page";
|
|
8
|
+
|
|
9
|
+
export function ForgotPasswordPage() {
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [result, setResult] = useState<{ message: string; devResetToken?: string }>();
|
|
12
|
+
const [error, setError] = useState<unknown>();
|
|
13
|
+
async function submit(event: FormEvent) {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
setError(undefined);
|
|
16
|
+
try { setResult(await authApi.forgotPassword(email)); } catch (caught) { setError(caught); }
|
|
17
|
+
}
|
|
18
|
+
return (
|
|
19
|
+
<AuthShell title="Reset your password" subtitle="We will send a short-lived recovery link" footer={<Link to="/login"><ArrowLeft size={15} /> Back to sign in</Link>}>
|
|
20
|
+
{error !== undefined && <ErrorNotice error={error} />}
|
|
21
|
+
{result ? <SuccessNotice>{result.message}{result.devResetToken && <> Development token: <code>{result.devResetToken}</code></>}</SuccessNotice> : (
|
|
22
|
+
<form className="form-stack" onSubmit={submit}>
|
|
23
|
+
<Field label="Email address" htmlFor="email"><Input id="email" type="email" autoComplete="email" value={email} onChange={(event) => setEmail(event.target.value)} required /></Field>
|
|
24
|
+
<Button type="submit"><Mail size={17} /> Send recovery link</Button>
|
|
25
|
+
</form>
|
|
26
|
+
)}
|
|
27
|
+
</AuthShell>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ResetPasswordPage() {
|
|
32
|
+
const [params] = useSearchParams();
|
|
33
|
+
const [password, setPassword] = useState("");
|
|
34
|
+
const [done, setDone] = useState(false);
|
|
35
|
+
const [error, setError] = useState<unknown>();
|
|
36
|
+
const token = params.get("token") ?? "";
|
|
37
|
+
async function submit(event: FormEvent) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
setError(undefined);
|
|
40
|
+
try { await authApi.resetPassword(token, password); setDone(true); } catch (caught) { setError(caught); }
|
|
41
|
+
}
|
|
42
|
+
return (
|
|
43
|
+
<AuthShell title="Choose a new password" subtitle="This link can be used once" footer={<Link to="/login">Return to sign in</Link>}>
|
|
44
|
+
{error !== undefined && <ErrorNotice error={error} />}
|
|
45
|
+
{done ? <SuccessNotice>Password updated. You can sign in now.</SuccessNotice> : (
|
|
46
|
+
<form className="form-stack" onSubmit={submit}>
|
|
47
|
+
<Field label="New password" htmlFor="password"><Input id="password" type="password" minLength={10} autoComplete="new-password" value={password} onChange={(event) => setPassword(event.target.value)} required /></Field>
|
|
48
|
+
<Button type="submit" disabled={!token}><RotateCcw size={17} /> Update password</Button>
|
|
49
|
+
</form>
|
|
50
|
+
)}
|
|
51
|
+
</AuthShell>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState, type FormEvent } from "react";
|
|
2
|
+
import { UserPlus } from "lucide-react";
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
import { registerSchema, type RegisterInput } from "@authenik8/contracts";
|
|
5
|
+
import { ApiError, authApi } from "@authenik8/api-client";
|
|
6
|
+
import { Button, Input } from "@authenik8/ui";
|
|
7
|
+
import { AuthShell } from "../../components/AuthShell";
|
|
8
|
+
import { ErrorNotice, Field, SuccessNotice } from "../../components/Page";
|
|
9
|
+
|
|
10
|
+
type RegistrationField = keyof RegisterInput;
|
|
11
|
+
type RegistrationErrors = Partial<Record<RegistrationField, string>>;
|
|
12
|
+
|
|
13
|
+
function validationErrors(error: { issues: Array<{ path: PropertyKey[]; message: string }> }): RegistrationErrors {
|
|
14
|
+
const fields: RegistrationErrors = {};
|
|
15
|
+
for (const issue of error.issues) {
|
|
16
|
+
const field = issue.path[0];
|
|
17
|
+
if ((field === "name" || field === "email" || field === "password") && !fields[field]) {
|
|
18
|
+
fields[field] = issue.message;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return fields;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function RegisterPage() {
|
|
25
|
+
const [form, setForm] = useState({ name: "", email: "", password: "" });
|
|
26
|
+
const [fieldErrors, setFieldErrors] = useState<RegistrationErrors>({});
|
|
27
|
+
const [error, setError] = useState<unknown>();
|
|
28
|
+
const [result, setResult] = useState<{ message: string; devVerificationToken?: string }>();
|
|
29
|
+
const [pending, setPending] = useState(false);
|
|
30
|
+
|
|
31
|
+
function update(field: RegistrationField, value: string) {
|
|
32
|
+
setForm((current) => ({ ...current, [field]: value }));
|
|
33
|
+
setFieldErrors((current) => ({ ...current, [field]: undefined }));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function submit(event: FormEvent) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
setError(undefined);
|
|
39
|
+
|
|
40
|
+
const validation = registerSchema.safeParse(form);
|
|
41
|
+
if (!validation.success) {
|
|
42
|
+
setFieldErrors(validationErrors(validation.error));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setFieldErrors({});
|
|
47
|
+
setPending(true);
|
|
48
|
+
try {
|
|
49
|
+
setResult(await authApi.register(validation.data));
|
|
50
|
+
} catch (caught) {
|
|
51
|
+
if (caught instanceof ApiError && caught.fields) {
|
|
52
|
+
setFieldErrors(Object.fromEntries(
|
|
53
|
+
Object.entries(caught.fields)
|
|
54
|
+
.filter(([field]) => field === "name" || field === "email" || field === "password")
|
|
55
|
+
.map(([field, messages]) => [field, messages[0]]),
|
|
56
|
+
));
|
|
57
|
+
}
|
|
58
|
+
setError(caught);
|
|
59
|
+
} finally {
|
|
60
|
+
setPending(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<AuthShell title="Create your account" subtitle="Start with a secure workspace" footer={<>Already registered? <Link to="/login">Sign in</Link></>}>
|
|
66
|
+
{error !== undefined && <ErrorNotice error={error} />}
|
|
67
|
+
{result ? (
|
|
68
|
+
<SuccessNotice>{result.message}{result.devVerificationToken && <> Development token: <code>{result.devVerificationToken}</code></>}</SuccessNotice>
|
|
69
|
+
) : (
|
|
70
|
+
<form className="form-stack" onSubmit={submit} noValidate>
|
|
71
|
+
<Field label="Full name" htmlFor="name" error={fieldErrors.name}>
|
|
72
|
+
<Input id="name" autoComplete="name" value={form.name} onChange={(event) => update("name", event.target.value)} aria-invalid={Boolean(fieldErrors.name)} required />
|
|
73
|
+
</Field>
|
|
74
|
+
<Field label="Email address" htmlFor="email" error={fieldErrors.email}>
|
|
75
|
+
<Input id="email" type="email" autoComplete="email" value={form.email} onChange={(event) => update("email", event.target.value)} aria-invalid={Boolean(fieldErrors.email)} required />
|
|
76
|
+
</Field>
|
|
77
|
+
<Field label="Password" htmlFor="password" error={fieldErrors.password}>
|
|
78
|
+
<Input id="password" type="password" autoComplete="new-password" value={form.password} onChange={(event) => update("password", event.target.value)} aria-invalid={Boolean(fieldErrors.password)} required />
|
|
79
|
+
</Field>
|
|
80
|
+
<p className="field-hint">Use 10 or more characters with uppercase, lowercase, and a number.</p>
|
|
81
|
+
<Button type="submit" disabled={pending}><UserPlus size={17} /> {pending ? "Creating..." : "Create account"}</Button>
|
|
82
|
+
</form>
|
|
83
|
+
)}
|
|
84
|
+
</AuthShell>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link, useSearchParams } from "react-router-dom";
|
|
3
|
+
import { Spinner } from "@authenik8/ui";
|
|
4
|
+
import { authApi } from "@authenik8/api-client";
|
|
5
|
+
import { AuthShell } from "../../components/AuthShell";
|
|
6
|
+
import { ErrorNotice, SuccessNotice } from "../../components/Page";
|
|
7
|
+
|
|
8
|
+
export function VerifyEmailPage() {
|
|
9
|
+
const [params] = useSearchParams();
|
|
10
|
+
const [status, setStatus] = useState<"loading" | "done" | "error">("loading");
|
|
11
|
+
const [error, setError] = useState<unknown>();
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const token = params.get("token");
|
|
14
|
+
if (!token) { setStatus("error"); setError(new Error("Verification token is missing")); return; }
|
|
15
|
+
authApi.verifyEmail(token).then(() => setStatus("done")).catch((caught) => { setError(caught); setStatus("error"); });
|
|
16
|
+
}, [params]);
|
|
17
|
+
return (
|
|
18
|
+
<AuthShell title="Email verification" subtitle="Confirming your account" footer={<Link to="/login">Continue to sign in</Link>}>
|
|
19
|
+
{status === "loading" && <div className="center-status"><Spinner label="Verifying email" /></div>}
|
|
20
|
+
{status === "done" && <SuccessNotice>Your email address is verified.</SuccessNotice>}
|
|
21
|
+
{status === "error" && <ErrorNotice error={error} />}
|
|
22
|
+
</AuthShell>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState, type FormEvent } from "react";
|
|
2
|
+
import { useMutation } from "@tanstack/react-query";
|
|
3
|
+
import { Save, UserRound } from "lucide-react";
|
|
4
|
+
import { Button, Input } from "@authenik8/ui";
|
|
5
|
+
import { accountApi } from "@authenik8/api-client";
|
|
6
|
+
import { useAuth } from "../../auth/AuthProvider";
|
|
7
|
+
import { ErrorNotice, Field, PageHeader, SuccessNotice } from "../../components/Page";
|
|
8
|
+
|
|
9
|
+
export function ProfilePage() {
|
|
10
|
+
const { user, setUser } = useAuth();
|
|
11
|
+
const [name, setName] = useState(user?.name ?? "");
|
|
12
|
+
const [saved, setSaved] = useState(false);
|
|
13
|
+
const mutation = useMutation({
|
|
14
|
+
mutationFn: () => accountApi.updateProfile({ name }),
|
|
15
|
+
onSuccess: ({ user: updated }) => { setUser(updated); setSaved(true); },
|
|
16
|
+
});
|
|
17
|
+
function submit(event: FormEvent) { event.preventDefault(); setSaved(false); mutation.mutate(); }
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<PageHeader title="Profile" description="Personal details used across your workspace." />
|
|
21
|
+
<div className="settings-layout">
|
|
22
|
+
<nav className="settings-nav"><a className="active" href="/settings/profile">Profile</a><a href="/settings/security">Security</a></nav>
|
|
23
|
+
<form className="form-panel" onSubmit={submit}>
|
|
24
|
+
<div className="form-section-heading"><span className="stat-icon cyan"><UserRound size={19} /></span><div><h2>Personal information</h2><p>Keep your workspace identity current.</p></div></div>
|
|
25
|
+
{mutation.error && <ErrorNotice error={mutation.error} />}
|
|
26
|
+
{saved && <SuccessNotice>Profile saved.</SuccessNotice>}
|
|
27
|
+
<Field label="Full name" htmlFor="name"><Input id="name" autoComplete="name" value={name} onChange={(event) => setName(event.target.value)} required /></Field>
|
|
28
|
+
<Field label="Email address" htmlFor="email"><Input id="email" type="email" value={user?.email ?? ""} disabled /></Field>
|
|
29
|
+
<div className="form-actions"><Button type="submit" disabled={mutation.isPending}><Save size={17} /> {mutation.isPending ? "Saving..." : "Save changes"}</Button></div>
|
|
30
|
+
</form>
|
|
31
|
+
</div>
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState, type FormEvent } from "react";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { Github, KeyRound, Link2, LogOut, MonitorSmartphone, ShieldCheck } from "lucide-react";
|
|
4
|
+
import { Badge, Button, Input, Spinner } from "@authenik8/ui";
|
|
5
|
+
import { accountApi, authApi } from "@authenik8/api-client";
|
|
6
|
+
import { ErrorNotice, Field, PageHeader, SuccessNotice } from "../../components/Page";
|
|
7
|
+
import { enabledOAuthProviders } from "../../auth/providers";
|
|
8
|
+
|
|
9
|
+
export function SecurityPage() {
|
|
10
|
+
const client = useQueryClient();
|
|
11
|
+
const sessions = useQuery({ queryKey: ["sessions"], queryFn: accountApi.sessions });
|
|
12
|
+
const providers = useQuery({ queryKey: ["providers"], queryFn: accountApi.providers });
|
|
13
|
+
const [passwords, setPasswords] = useState({ currentPassword: "", newPassword: "" });
|
|
14
|
+
const [passwordSaved, setPasswordSaved] = useState(false);
|
|
15
|
+
const passwordMutation = useMutation({ mutationFn: () => accountApi.changePassword(passwords), onSuccess: () => { setPasswords({ currentPassword: "", newPassword: "" }); setPasswordSaved(true); } });
|
|
16
|
+
const revoke = useMutation({ mutationFn: accountApi.revokeSession, onSuccess: () => client.invalidateQueries({ queryKey: ["sessions"] }) });
|
|
17
|
+
async function link(provider: "google" | "github") {
|
|
18
|
+
const result = await accountApi.startProviderLink(provider);
|
|
19
|
+
window.location.assign(result.url);
|
|
20
|
+
}
|
|
21
|
+
function submitPassword(event: FormEvent) { event.preventDefault(); setPasswordSaved(false); passwordMutation.mutate(); }
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<PageHeader title="Security" description="Credentials, providers, and active sessions." />
|
|
26
|
+
<div className="settings-layout">
|
|
27
|
+
<nav className="settings-nav"><a href="/settings/profile">Profile</a><a className="active" href="/settings/security">Security</a></nav>
|
|
28
|
+
<div className="settings-stack">
|
|
29
|
+
<form className="form-panel" onSubmit={submitPassword}>
|
|
30
|
+
<div className="form-section-heading"><span className="stat-icon violet"><KeyRound size={19} /></span><div><h2>Password</h2><p>Update your credential regularly.</p></div></div>
|
|
31
|
+
{passwordMutation.error && <ErrorNotice error={passwordMutation.error} />}{passwordSaved && <SuccessNotice>Password updated.</SuccessNotice>}
|
|
32
|
+
<div className="two-fields"><Field label="Current password" htmlFor="current-password"><Input id="current-password" type="password" autoComplete="current-password" value={passwords.currentPassword} onChange={(event) => setPasswords({ ...passwords, currentPassword: event.target.value })} required /></Field><Field label="New password" htmlFor="new-password"><Input id="new-password" type="password" minLength={10} autoComplete="new-password" value={passwords.newPassword} onChange={(event) => setPasswords({ ...passwords, newPassword: event.target.value })} required /></Field></div>
|
|
33
|
+
<div className="form-actions"><Button type="submit" disabled={passwordMutation.isPending}>Update password</Button></div>
|
|
34
|
+
</form>
|
|
35
|
+
|
|
36
|
+
<section className="panel settings-panel">
|
|
37
|
+
<div className="form-section-heading"><span className="stat-icon cyan"><Link2 size={19} /></span><div><h2>Linked providers</h2><p>External identities connected to this account.</p></div></div>
|
|
38
|
+
{providers.isPending ? <Spinner /> : providers.error ? <ErrorNotice error={providers.error} /> : <div className="provider-list">
|
|
39
|
+
{enabledOAuthProviders.map((provider) => {
|
|
40
|
+
const linked = providers.data!.providers.find((item) => item.provider === provider);
|
|
41
|
+
return <div className="provider-row" key={provider}><span className="provider-icon">{provider === "github" ? <Github size={20} /> : <span className="provider-g">G</span>}</span><div><strong>{provider === "github" ? "GitHub" : "Google"}</strong><small>{linked?.providerEmail ?? "Not connected"}</small></div>{linked ? <Badge tone="success">Connected</Badge> : <Button variant="secondary" onClick={() => void link(provider)}>Connect</Button>}</div>;
|
|
42
|
+
})}
|
|
43
|
+
</div>}
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<section className="panel settings-panel">
|
|
47
|
+
<div className="form-section-heading"><span className="stat-icon green"><MonitorSmartphone size={19} /></span><div><h2>Active sessions</h2><p>Devices with refresh access.</p></div></div>
|
|
48
|
+
{revoke.error && <ErrorNotice error={revoke.error} />}
|
|
49
|
+
{sessions.isPending ? <Spinner /> : sessions.error ? <ErrorNotice error={sessions.error} /> : <div className="session-list">
|
|
50
|
+
{sessions.data!.sessions.map((session) => <div className="session-row" key={session.id}><span className="device-icon"><ShieldCheck size={19} /></span><div><strong>{session.userAgent}</strong><small>{session.ipAddress} · Last used {new Date(session.lastUsedAt).toLocaleString()}</small></div>{session.current && <Badge tone="accent">Current</Badge>}<Button className="icon-button" variant="ghost" onClick={() => revoke.mutate(session.id)} aria-label="Revoke session" title="Revoke session"><LogOut size={18} /></Button></div>)}
|
|
51
|
+
</div>}
|
|
52
|
+
</section>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
}
|