Skip to content

Commit 679d97d

Browse files
committed
Add authentication handling
1 parent d879f4b commit 679d97d

File tree

10 files changed

+423
-85
lines changed

10 files changed

+423
-85
lines changed

src/backend/api/user.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ var userMutex = sync.Mutex{}
2020

2121
// RegisterUserAPI registers the user management API endpoints with the PocketBase server.
2222
// It attaches three HTTP routes:
23-
// - GET /api/user/exists : Determines if any user accounts beyond the default exist.
24-
// - POST /api/user/create : Creates the first normal user and matching superuser when none exist.
25-
// - GET /api/user/is-authenticated : Checks if the current request is authenticated and if admin creation is allowed.
23+
// - GET /api/user/exists : Determines if any user accounts beyond the default exist.
24+
// - POST /api/user/create-admin-user : Creates the first normal user and matching superuser when none exist.
25+
// - GET /api/user/is-authenticated : Checks if the current request is authenticated and if admin creation is allowed.
2626
//
2727
// GET /api/user/exists responses:
2828
//
2929
// 200 OK - {"exists":true} if at least one user exists or the superuser state is non-default.
3030
// {"exists":false} if no users and only the initial default superuser remain.
3131
// 500 Error - On database count or fetch failures.
3232
//
33-
// POST /api/user/create expected form parameters:
33+
// POST /api/user/create-admin-user expected form parameters:
3434
//
3535
// email (string) Required. Valid email for new accounts.
3636
// password (string) Required. Minimum 10 characters.
@@ -75,7 +75,7 @@ func RegisterUserAPI(app *pocketbase.PocketBase, cfg configuration.AppConfig) {
7575

7676
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
7777
if cfg.General.InitialAdminRegistration {
78-
// Handler: POST /api/user/create
78+
// Handler: POST /api/user/create-admin-user
7979
// Purpose: Initializes the first normal user and a matching superuser when no users exist.
8080
// Parameters (form):
8181
// - email string (required): email address for new accounts.
@@ -86,7 +86,7 @@ func RegisterUserAPI(app *pocketbase.PocketBase, cfg configuration.AppConfig) {
8686
// 400: Bad request on missing or invalid parameters.
8787
// 409: Conflict if users already exist or superuser count mismatch.
8888
// 500: Internal error on database or transaction failures.
89-
se.Router.POST("/api/user/create", func(e *core.RequestEvent) error {
89+
se.Router.POST("/api/user/create-admin-user", func(e *core.RequestEvent) error {
9090
userMutex.Lock()
9191
defer userMutex.Unlock()
9292

src/backend/configuration/loader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func ParseAppConfig(data []byte) (AppConfig, error) {
3030
}
3131

3232
func sanitizeJSONC(data []byte) ([]byte, error) {
33-
appConfigJSONCReader := bytes.NewReader(config.AppConfigJSONC)
33+
appConfigJSONCReader := bytes.NewReader(data)
3434

3535
jsoncFilter, err := jsonc.New(appConfigJSONCReader, true, "")
3636
if err != nil {

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const [pageList, wellKnownPages, pageTree] = transformPageTree({
2525
title: "SolidJS",
2626
component: () => <span>SolidJS Page</span>,
2727
icon: SolidIcon,
28+
authenticationRequired: true,
2829
},
2930
tailwind: {
3031
title: "TailwindCSS",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import LoadingIcon from "~icons/svg-spinners/bouncing-ball";
2+
3+
import { JSX, Show, createSignal } from "solid-js";
4+
5+
import { RouteSectionProps } from "@solidjs/router";
6+
7+
import { createAdminUser } from "../../service/api/user";
8+
9+
const CreateAdminUser = (_: RouteSectionProps): JSX.Element => {
10+
const [email, setEmail] = createSignal("");
11+
const [password, setPassword] = createSignal("");
12+
const [passwordConfirm, setPasswordConfirm] = createSignal("");
13+
const [error, setError] = createSignal<string | null>(null);
14+
const [isLoading, setIsLoading] = createSignal(false);
15+
16+
const handleSubmit = async (e: Event) => {
17+
e.preventDefault();
18+
setError(null);
19+
setIsLoading(true);
20+
21+
if (password() !== passwordConfirm()) {
22+
setError("Passwords do not match");
23+
setIsLoading(false);
24+
return;
25+
}
26+
27+
const [_, createError] = await createAdminUser(
28+
email(),
29+
password(),
30+
passwordConfirm(),
31+
);
32+
33+
setIsLoading(false);
34+
35+
if (createError) {
36+
setError(createError.message || "Failed to create admin user");
37+
}
38+
};
39+
40+
return (
41+
<div class="flex flex-col gap-4">
42+
<h2 class="text-center text-2xl font-bold">Create Admin User</h2>
43+
<p class="text-center text-sm opacity-75">
44+
Welcome! Please create the first admin user to get started.
45+
</p>
46+
<form
47+
onSubmit={handleSubmit}
48+
class="flex flex-col gap-4"
49+
>
50+
<div class="form-control w-full">
51+
<label class="label">
52+
<span class="label-text">Email</span>
53+
</label>
54+
<input
55+
type="email"
56+
placeholder="[email protected]"
57+
class="input input-bordered w-full"
58+
value={email()}
59+
onInput={(e) => setEmail(e.currentTarget.value)}
60+
required
61+
disabled={isLoading()}
62+
/>
63+
</div>
64+
65+
<div class="form-control w-full">
66+
<label class="label">
67+
<span class="label-text">Password</span>
68+
</label>
69+
<input
70+
type="password"
71+
placeholder="Enter password"
72+
class="input input-bordered w-full"
73+
value={password()}
74+
onInput={(e) => setPassword(e.currentTarget.value)}
75+
required
76+
disabled={isLoading()}
77+
minlength="8"
78+
/>
79+
</div>
80+
81+
<div class="form-control w-full">
82+
<label class="label">
83+
<span class="label-text">Confirm Password</span>
84+
</label>
85+
<input
86+
type="password"
87+
placeholder="Confirm password"
88+
class="input input-bordered w-full"
89+
value={passwordConfirm()}
90+
onInput={(e) => setPasswordConfirm(e.currentTarget.value)}
91+
required
92+
disabled={isLoading()}
93+
minlength="8"
94+
/>
95+
</div>
96+
97+
<Show when={error()}>
98+
<div
99+
role="alert"
100+
class="alert alert-error text-sm"
101+
>
102+
<span>{error()}</span>
103+
</div>
104+
</Show>
105+
106+
<button
107+
type="submit"
108+
class="btn btn-primary mt-2 w-full"
109+
disabled={isLoading()}
110+
>
111+
{isLoading() ?
112+
<LoadingIcon />
113+
: "Create Admin"}
114+
</button>
115+
</form>
116+
</div>
117+
);
118+
};
119+
120+
export default CreateAdminUser;

src/pages/well-known/Layout.tsx

Lines changed: 95 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AnarchyIcon from "~icons/game-icons/anarchy";
22
import FistIcon from "~icons/game-icons/fist";
33
import HammerSickleIcon from "~icons/game-icons/hammer-sickle";
4+
import LogoutIcon from "~icons/hugeicons/logout-02";
45
import MenuIcon from "~icons/line-md/close-to-menu-alt-transition";
56
import GitHubIcon from "~icons/line-md/github-loop";
67
import XIcon from "~icons/line-md/menu-to-close-transition";
@@ -26,6 +27,7 @@ import { A, RouteSectionProps, useLocation } from "@solidjs/router";
2627
import { Observer } from "tailwindcss-intersect";
2728

2829
import { pageList } from "../..";
30+
import { isAuthenticated, logoutUser } from "../../service/api/user";
2931

3032
const getBreadcrumbs = () => {
3133
const location = useLocation();
@@ -82,66 +84,107 @@ const NavigationLinks = (props: {
8284
const location = useLocation();
8385

8486
return (
85-
<For each={pageList}>
86-
{(page) => {
87-
switch (props.kind) {
88-
case "desktop":
89-
return (
90-
<li>
87+
<>
88+
<For each={pageList}>
89+
{(page) => {
90+
switch (props.kind) {
91+
case "desktop":
92+
return (
93+
<li>
94+
<A
95+
href={page.path}
96+
activeClass="bg-base-200 font-medium"
97+
class={
98+
"hover:text-primary group text-xs-adjust rounded-lg py-1 transition-all duration-200 hover:scale-105 sm:py-2 sm:text-sm md:text-base"
99+
}
100+
end={true}
101+
onClick={props.onClick}
102+
>
103+
{page.icon && (
104+
<page.icon
105+
class={
106+
"mr-1 inline h-4 w-4 transition-transform duration-200 group-hover:-rotate-12 sm:h-5 sm:w-5"
107+
}
108+
/>
109+
)}
110+
{page.title}
111+
</A>
112+
</li>
113+
);
114+
case "mobile":
115+
return (
91116
<A
92117
href={page.path}
93-
activeClass="bg-base-200 font-medium"
94-
class={
95-
"hover:text-primary group text-xs-adjust rounded-lg py-1 transition-all duration-200 hover:scale-105 sm:py-2 sm:text-sm md:text-base"
96-
}
97-
end={true}
118+
class={`btn btn-ghost btn-xs-adjust sm:btn-sm my-1 justify-start ${
119+
location.pathname === page.path ?
120+
"bg-base-300 font-medium"
121+
: ""
122+
}`}
98123
onClick={props.onClick}
99124
>
100125
{page.icon && (
101-
<page.icon
102-
class={
103-
"mr-1 inline h-4 w-4 transition-transform duration-200 group-hover:-rotate-12 sm:h-5 sm:w-5"
104-
}
105-
/>
126+
<page.icon class="mr-1 inline h-4 w-4 sm:h-5 sm:w-5" />
127+
)}
128+
<span class="truncate">{page.title}</span>
129+
</A>
130+
);
131+
case "footer":
132+
return (
133+
<A
134+
href={page.path}
135+
class="link link-hover text-xs-adjust sm:text-sm"
136+
>
137+
{page.icon && (
138+
<page.icon class="mr-1 inline h-4 w-4 sm:h-5 sm:w-5" />
106139
)}
107140
{page.title}
108141
</A>
109-
</li>
110-
);
111-
case "mobile":
112-
return (
113-
<A
114-
href={page.path}
115-
class={`btn btn-ghost btn-xs-adjust sm:btn-sm my-1 justify-start ${
116-
location.pathname === page.path ?
117-
"bg-base-300 font-medium"
118-
: ""
119-
}`}
120-
onClick={props.onClick}
121-
>
122-
{page.icon && (
123-
<page.icon class="mr-1 inline h-4 w-4 sm:h-5 sm:w-5" />
124-
)}
125-
<span class="truncate">{page.title}</span>
126-
</A>
127-
);
128-
case "footer":
129-
return (
130-
<A
131-
href={page.path}
132-
class="link link-hover text-xs-adjust sm:text-sm"
133-
>
134-
{page.icon && (
135-
<page.icon class="mr-1 inline h-4 w-4 sm:h-5 sm:w-5" />
136-
)}
137-
{page.title}
138-
</A>
139-
);
140-
default:
141-
const _: never = props.kind;
142-
}
143-
}}
144-
</For>
142+
);
143+
default:
144+
const _: never = props.kind;
145+
}
146+
}}
147+
</For>
148+
{isAuthenticated() ?
149+
props.kind === "desktop" ?
150+
<li>
151+
<A
152+
href="/"
153+
activeClass="bg-base-200 font-medium"
154+
class={
155+
"hover:text-primary group text-xs-adjust rounded-lg py-1 transition-all duration-200 hover:scale-105 sm:py-2 sm:text-sm md:text-base"
156+
}
157+
end={true}
158+
onClick={logoutUser}
159+
>
160+
<LogoutIcon
161+
class={
162+
"mr-1 inline h-4 w-4 transition-transform duration-200 group-hover:-rotate-12 sm:h-5 sm:w-5"
163+
}
164+
/>
165+
Logout
166+
</A>
167+
</li>
168+
: props.kind === "mobile" ?
169+
<A
170+
href="/"
171+
class={"btn btn-ghost btn-xs-adjust sm:btn-sm my-1 justify-start"}
172+
onClick={props.onClick}
173+
>
174+
<LogoutIcon class="mr-1 inline h-4 w-4 sm:h-5 sm:w-5" />
175+
<span class="truncate">Logout</span>
176+
</A>
177+
: (props.kind as string) === "footer" ?
178+
<A
179+
href="/"
180+
class="link link-hover text-xs-adjust sm:text-sm"
181+
>
182+
<LogoutIcon class="mr-1 inline h-4 w-4 sm:h-5 sm:w-5" />
183+
Logout
184+
</A>
185+
: <></>
186+
: <></>}
187+
</>
145188
);
146189
};
147190

0 commit comments

Comments
 (0)