Skip to content

Commit 68b9e20

Browse files
authored
Merge pull request #51 from indrazm/feat/installation-setup
Feat/installation setup
2 parents dcb0e90 + b757ffe commit 68b9e20

File tree

18 files changed

+369
-44
lines changed

18 files changed

+369
-44
lines changed

apps/admin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "admin",
33
"private": true,
4-
"version": "0.0.9",
4+
"version": "0.0.10",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

apps/admin/src/features/appSettings/hooks/useAppSettings.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,29 @@ export const useAppSettings = () => {
2323
},
2424
});
2525

26+
const {
27+
data: installationData,
28+
isLoading: isInstallationLoading,
29+
isError: isInstallationError,
30+
error: installationError,
31+
} = useQuery({
32+
queryKey: ["installationStatus"],
33+
queryFn: async () => {
34+
const response = await api.appSettings.getInstallationStatus();
35+
return response;
36+
},
37+
});
38+
2639
return {
2740
appSettings: data,
2841
isAppSettingsLoading: isLoading,
2942
isAppSettingsError: isError,
3043
appSettingsError: error,
3144
updateAppSettings: updateMutation.mutate,
3245
isUpdatingAppSettings: updateMutation.isPending,
46+
installationStatus: installationData,
47+
isInstallationLoading,
48+
isInstallationError,
49+
installationError,
3350
};
3451
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { useMutation } from "@tanstack/react-query";
2+
import { useNavigate } from "@tanstack/react-router";
3+
import { HTTPError } from "ky";
4+
import { useState } from "react";
5+
import toast from "react-hot-toast";
6+
import { api } from "../../../utils/api";
7+
8+
export const useRegister = () => {
9+
const navigate = useNavigate();
10+
const [name, setName] = useState("");
11+
const [username, setUsername] = useState("");
12+
const [email, setEmail] = useState("");
13+
const [password, setPassword] = useState("");
14+
const [validationErrors, setValidationErrors] = useState<
15+
Record<string, string>
16+
>({});
17+
18+
const {
19+
mutate: register,
20+
isPending,
21+
isError,
22+
error,
23+
} = useMutation({
24+
mutationKey: ["register"],
25+
mutationFn: async () => {
26+
// Clear previous validation errors
27+
setValidationErrors({});
28+
29+
// Validate input
30+
const errors: Record<string, string> = {};
31+
if (!name.trim()) errors.name = "Full name is required";
32+
if (!username.trim()) errors.username = "Username is required";
33+
if (!email.trim()) errors.email = "Email is required";
34+
if (!password.trim()) errors.password = "Password is required";
35+
36+
if (Object.keys(errors).length > 0) {
37+
setValidationErrors(errors);
38+
throw new Error("Validation failed");
39+
}
40+
41+
const res = await api.auth.registerAdmin({
42+
name: name.trim(),
43+
username: username.trim().toLowerCase(),
44+
email: email.trim().toLowerCase(),
45+
password: password.trim(),
46+
});
47+
return res;
48+
},
49+
onSuccess: async () => {
50+
toast.success("Admin account created successfully! Please log in.");
51+
navigate({ to: "/" });
52+
},
53+
onError: (error) => {
54+
if (error instanceof HTTPError) {
55+
if (error.response.status === 409) {
56+
toast.error("Username or email already exists");
57+
} else if (error.response.status === 400) {
58+
toast.error("Please fill in all required fields correctly");
59+
} else {
60+
toast.error("Failed to create admin account");
61+
}
62+
} else if (error.message !== "Validation failed") {
63+
toast.error("An unexpected error occurred");
64+
}
65+
},
66+
});
67+
68+
return {
69+
name,
70+
setName,
71+
username,
72+
setUsername,
73+
email,
74+
setEmail,
75+
password,
76+
setPassword,
77+
register,
78+
validationErrors,
79+
isPending,
80+
isError,
81+
error,
82+
};
83+
};

apps/admin/src/routes/index.tsx

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { Button, Input } from "@opencircle/ui";
22
import { createFileRoute } from "@tanstack/react-router";
33
import { Zap } from "lucide-react";
44
import { METADATA } from "../constants/metadata";
5-
5+
import { useAppSettings } from "../features/appSettings/hooks/useAppSettings";
66
import { useLogin } from "../features/auth/hooks/useLogin";
7+
import { useRegister } from "../features/auth/hooks/useRegister";
78

89
export const Route = createFileRoute("/")({
910
head: () => ({
@@ -39,7 +40,42 @@ export const Route = createFileRoute("/")({
3940
});
4041

4142
function RouteComponent() {
43+
const { installationStatus, isInstallationLoading } = useAppSettings();
4244
const { username, setUsername, password, setPassword, login } = useLogin();
45+
const {
46+
name,
47+
setName,
48+
username: regUsername,
49+
setUsername: setRegUsername,
50+
email,
51+
setEmail,
52+
password: regPassword,
53+
setPassword: setRegPassword,
54+
register,
55+
validationErrors,
56+
} = useRegister();
57+
58+
if (isInstallationLoading) {
59+
return (
60+
<main className="m-auto max-w-sm">
61+
<div className="flex h-screen flex-col justify-center gap-10">
62+
<section className="space-y-8 text-center">
63+
<section className="ml-2 flex items-center justify-center gap-2">
64+
<div className="flex h-6 w-6 items-center justify-center rounded-lg bg-foreground text-background">
65+
<Zap size={12} fill="currentColor" />
66+
</div>
67+
<h2 className="font-medium">Opencircle</h2>
68+
</section>
69+
<div className="space-y-2">
70+
<p className="text-foreground/50">Loading...</p>
71+
</div>
72+
</section>
73+
</div>
74+
</main>
75+
);
76+
}
77+
78+
const showRegisterForm = !installationStatus?.is_installed;
4379

4480
return (
4581
<main className="m-auto max-w-sm">
@@ -52,29 +88,114 @@ function RouteComponent() {
5288
<h2 className="font-medium">Opencircle</h2>
5389
</section>
5490
<div className="space-y-2">
55-
<p className="text-foreground/50">Sign in to Admin account</p>
91+
{showRegisterForm ? (
92+
<>
93+
<h1 className="font-medium text-2xl">Setup Admin Account</h1>
94+
<p className="text-foreground/50">
95+
Create your admin account to get started
96+
</p>
97+
<p className="font-medium text-amber-600 text-sm dark:text-amber-400">
98+
⚠️ This registration form appears only once - save your
99+
credentials!
100+
</p>
101+
</>
102+
) : (
103+
<p className="text-foreground/50">Sign in to Admin account</p>
104+
)}
56105
</div>
57106
</section>
58107
<div className="space-y-6 rounded-xl border border-border p-8 shadow-2xl">
59108
<section className="space-y-3">
60-
<section className="space-y-2">
61-
<Input
62-
placeholder="Username"
63-
value={username}
64-
onChange={(v) => setUsername(v.target.value)}
65-
/>
66-
</section>
67-
<section className="space-y-2">
68-
<Input
69-
placeholder="Password"
70-
type="password"
71-
value={password}
72-
onChange={(v) => setPassword(v.target.value)}
73-
/>
74-
</section>
75-
<Button radius="xl" className="mt-2 w-full" onClick={() => login()}>
76-
Login
77-
</Button>
109+
{showRegisterForm ? (
110+
<>
111+
<section className="space-y-2">
112+
<Input
113+
placeholder="Full Name"
114+
value={name}
115+
onChange={(v) => setName(v.target.value)}
116+
/>
117+
{validationErrors.name && (
118+
<p className="text-red-500 text-xs">
119+
{validationErrors.name}
120+
</p>
121+
)}
122+
</section>
123+
<section className="space-y-2">
124+
<Input
125+
placeholder="Username"
126+
value={regUsername}
127+
onChange={(v) =>
128+
setRegUsername(
129+
v.target.value.toLowerCase().replace(/\s/g, ""),
130+
)
131+
}
132+
/>
133+
{validationErrors.username && (
134+
<p className="text-red-500 text-xs">
135+
{validationErrors.username}
136+
</p>
137+
)}
138+
</section>
139+
<section className="space-y-2">
140+
<Input
141+
placeholder="Email"
142+
type="email"
143+
value={email}
144+
onChange={(v) => setEmail(v.target.value)}
145+
/>
146+
{validationErrors.email && (
147+
<p className="text-red-500 text-xs">
148+
{validationErrors.email}
149+
</p>
150+
)}
151+
</section>
152+
<section className="space-y-2">
153+
<Input
154+
placeholder="Password"
155+
type="password"
156+
value={regPassword}
157+
onChange={(v) => setRegPassword(v.target.value)}
158+
/>
159+
{validationErrors.password && (
160+
<p className="text-red-500 text-xs">
161+
{validationErrors.password}
162+
</p>
163+
)}
164+
</section>
165+
<Button
166+
radius="xl"
167+
className="mt-2 w-full"
168+
onClick={() => register()}
169+
>
170+
Create Admin Account
171+
</Button>
172+
</>
173+
) : (
174+
<>
175+
<section className="space-y-2">
176+
<Input
177+
placeholder="Username"
178+
value={username}
179+
onChange={(v) => setUsername(v.target.value)}
180+
/>
181+
</section>
182+
<section className="space-y-2">
183+
<Input
184+
placeholder="Password"
185+
type="password"
186+
value={password}
187+
onChange={(v) => setPassword(v.target.value)}
188+
/>
189+
</section>
190+
<Button
191+
radius="xl"
192+
className="mt-2 w-full"
193+
onClick={() => login()}
194+
>
195+
Login
196+
</Button>
197+
</>
198+
)}
78199
</section>{" "}
79200
</div>
80201
</div>

apps/api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "api"
3-
version = "0.0.9"
3+
version = "0.0.10"
44
description = "Add your description here"
55
readme = "README.md"
66
requires-python = ">=3.12"

apps/api/src/api/appsettings/api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from src.database.engine import get_session
55
from src.modules.appsettings import appsettings_methods
66
from src.modules.storages.storage_methods import upload_file
7+
from src.modules.user.user_methods import get_admin_count
78

89
router = APIRouter()
910

@@ -29,7 +30,7 @@ async def update_app_settings(settings_data: dict, db: Session = Depends(get_ses
2930
@router.post("/upload-logo")
3031
async def upload_logo(file: UploadFile = File(...), db: Session = Depends(get_session)):
3132
"""Upload app logo and update app settings."""
32-
if not file.content_type.startswith("image/"):
33+
if not file.content_type or not file.content_type.startswith("image/"):
3334
raise HTTPException(status_code=400, detail="File must be an image")
3435

3536
# Upload file to R2
@@ -48,3 +49,10 @@ async def get_app_settings_count(db: Session = Depends(get_session)):
4849
"""Get the count of app settings records (should be 1)."""
4950
count = appsettings_methods.get_app_settings_count(db)
5051
return {"count": count}
52+
53+
54+
@router.get("/installation-status")
55+
async def get_installation_status(db: Session = Depends(get_session)):
56+
"""Check if the application has been installed (has at least one admin user)."""
57+
admin_count = get_admin_count(db)
58+
return {"is_installed": admin_count >= 1, "admin_count": admin_count}

0 commit comments

Comments
 (0)