Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-02-01 02:28:31 +00:00
parent 7b0ca40c4f
commit 73025c84c5
10 changed files with 413 additions and 87 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

68
dist/assets/index-garE9QaM.js vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@@ -5,8 +5,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Siti Plugin Repo</title> <title>Siti Plugin Repo</title>
<script type="module" crossorigin src="/assets/index-BlY_enN1.js"></script> <script type="module" crossorigin src="/assets/index-garE9QaM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CgskK80a.css"> <link rel="stylesheet" crossorigin href="/assets/index-CFgb3VsA.css">
</head> </head>
<body> <body>

View File

@@ -19,8 +19,8 @@ import {
} from "./lib/licenseService.js"; } from "./lib/licenseService.js";
import { HOST, PATHS, PORT } from "./lib/config.js"; import { HOST, PATHS, PORT } from "./lib/config.js";
import { ensureSchema } from "./lib/schema.js"; import { ensureSchema } from "./lib/schema.js";
import { authenticateUser, registerUser } from "./lib/userService.js"; import { authenticateUser, registerUser, adminCreateUser, listUsers, getUserById } from "./lib/userService.js";
import { requireAuth } from "./middleware/auth.js"; import { requireAuth, requireAdmin } from "./middleware/auth.js";
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
@@ -75,6 +75,41 @@ app.get("/api/auth/me", requireAuth, (req, res) => {
res.json({ user: req.user }); res.json({ user: req.user });
}); });
app.get("/api/admin/users", requireAuth, requireAdmin, async (_req, res) => {
try {
const users = await listUsers();
res.json({ count: users.length, items: users });
} catch (error) {
res.status(500).json({ error: "Kon gebruikers niet laden." });
}
});
app.post("/api/admin/users", requireAuth, requireAdmin, async (req, res) => {
try {
const { username, name, email, password, isAdmin } = req.body || {};
if (!username || !name || !email || !password) {
return res.status(400).json({ error: "Alle velden zijn verplicht." });
}
if (String(password).length < 8) {
return res.status(400).json({ error: "Wachtwoord moet minimaal 8 karakters zijn." });
}
const user = await adminCreateUser({
username: String(username).trim(),
name: String(name).trim(),
email: String(email).trim().toLowerCase(),
password: String(password),
isAdmin: Boolean(isAdmin)
});
res.status(201).json({ user });
} catch (error) {
if (error?.code === "ER_DUP_ENTRY") {
const field = error.meta === "EMAIL" ? "e-mailadres" : "gebruikersnaam";
return res.status(409).json({ error: `Dit ${field} is al in gebruik.` });
}
res.status(500).json({ error: "Gebruiker aanmaken mislukt." });
}
});
app.get("/api/plugins", async (_req, res) => { app.get("/api/plugins", async (_req, res) => {
try { try {
const repos = await readRepos(PATHS.reposFile); const repos = await readRepos(PATHS.reposFile);
@@ -145,7 +180,19 @@ app.get("/api/plugins/:owner/:repo", async (req, res) => {
app.get("/api/licenses", requireAuth, async (req, res) => { app.get("/api/licenses", requireAuth, async (req, res) => {
try { try {
const payload = await listLicensesByUser(req.user.id); let targetUserId = req.user.id;
if (req.user.isAdmin && req.query.userId) {
const parsed = Number(req.query.userId);
if (Number.isNaN(parsed)) {
return res.status(400).json({ error: "Ongeldige userId." });
}
const targetUser = await getUserById(parsed);
if (!targetUser) {
return res.status(404).json({ error: "Gebruiker niet gevonden." });
}
targetUserId = parsed;
}
const payload = await listLicensesByUser(targetUserId);
res.json({ res.json({
count: payload.length, count: payload.length,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
@@ -182,7 +229,20 @@ app.post("/api/licenses", requireAuth, async (req, res) => {
return res.status(400).json({ error: "Kon plugin gegevens niet ophalen." }); return res.status(400).json({ error: "Kon plugin gegevens niet ophalen." });
} }
const payload = await createLicense(req.user.id, { let ownerUserId = req.user.id;
if (req.user.isAdmin && body.userId) {
const parsed = Number(body.userId);
if (Number.isNaN(parsed)) {
return res.status(400).json({ error: "Ongeldige gebruiker." });
}
const target = await getUserById(parsed);
if (!target) {
return res.status(404).json({ error: "Gebruiker niet gevonden." });
}
ownerUserId = parsed;
}
const payload = await createLicense(ownerUserId, {
label: body.label?.trim(), label: body.label?.trim(),
note: body.note?.trim(), note: body.note?.trim(),
repo: repoEntry repo: repoEntry

View File

@@ -7,10 +7,16 @@ async function createUsersTable() {
username VARCHAR(50) NOT NULL UNIQUE, username VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(120) NOT NULL, name VARCHAR(120) NOT NULL,
email VARCHAR(120) NOT NULL UNIQUE, email VARCHAR(120) NOT NULL UNIQUE,
is_admin TINYINT(1) NOT NULL DEFAULT 0,
password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`); `);
const [columns] = await db.query(`SHOW COLUMNS FROM users LIKE 'is_admin'`);
if (columns.length === 0) {
await db.query(`ALTER TABLE users ADD COLUMN is_admin TINYINT(1) NOT NULL DEFAULT 0 AFTER email`);
}
} }
async function createLicensesTable() { async function createLicensesTable() {

View File

@@ -10,6 +10,7 @@ function serializeUser(row) {
username: row.username, username: row.username,
name: row.name, name: row.name,
email: row.email, email: row.email,
isAdmin: Boolean(row.is_admin),
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null createdAt: row.created_at ? new Date(row.created_at).toISOString() : null
}; };
} }
@@ -20,10 +21,12 @@ function signToken(userId) {
export async function registerUser({ username, name, email, password }) { export async function registerUser({ username, name, email, password }) {
const passwordHash = await bcrypt.hash(password, 10); const passwordHash = await bcrypt.hash(password, 10);
const [[{ count }]] = await db.query("SELECT COUNT(*) AS count FROM users");
const isAdmin = count === 0 ? 1 : 0;
try { try {
const [result] = await db.query( const [result] = await db.query(
`INSERT INTO users (username, name, email, password_hash) VALUES (?, ?, ?, ?)`, `INSERT INTO users (username, name, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)`,
[username, name, email, passwordHash] [username, name, email, passwordHash, isAdmin]
); );
const user = await getUserById(result.insertId); const user = await getUserById(result.insertId);
const token = signToken(user.id); const token = signToken(user.id);
@@ -64,6 +67,37 @@ export async function authenticateUser(identifier, password) {
} }
export async function getUserById(id) { export async function getUserById(id) {
const [rows] = await db.query(`SELECT id, username, name, email, created_at FROM users WHERE id = ? LIMIT 1`, [id]); const [rows] = await db.query(
`SELECT id, username, name, email, is_admin, created_at FROM users WHERE id = ? LIMIT 1`,
[id]
);
return serializeUser(rows[0]); return serializeUser(rows[0]);
} }
export async function listUsers() {
const [rows] = await db.query(
`SELECT id, username, name, email, is_admin, created_at FROM users ORDER BY created_at ASC`
);
return rows.map(serializeUser);
}
export async function adminCreateUser({ username, name, email, password, isAdmin = false }) {
const passwordHash = await bcrypt.hash(password, 10);
try {
const [result] = await db.query(
`INSERT INTO users (username, name, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)`,
[username, name, email, passwordHash, isAdmin ? 1 : 0]
);
return await getUserById(result.insertId);
} catch (error) {
if (error?.code === "ER_DUP_ENTRY") {
const message = error.sqlMessage || "Duplicate";
if (message.includes("username")) {
error.meta = "USERNAME";
} else if (message.includes("email")) {
error.meta = "EMAIL";
}
}
throw error;
}
}

View File

@@ -21,3 +21,13 @@ export async function requireAuth(req, res, next) {
return res.status(401).json({ error: "Ongeldige of verlopen token." }); return res.status(401).json({ error: "Ongeldige of verlopen token." });
} }
} }
export function requireAdmin(req, res, next) {
if (!req.user) {
return res.status(401).json({ error: "Inloggen vereist." });
}
if (!req.user.isAdmin) {
return res.status(403).json({ error: "Administratorrechten vereist." });
}
next();
}

View File

@@ -448,6 +448,28 @@
color: #4338ca; color: #4338ca;
} }
.inline-field {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
}
.inline-field select {
border-radius: 12px;
border: 1px solid #e2e8f0;
padding: 6px 10px;
font-size: 0.9rem;
}
.checkbox-field {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: #475569;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.app { .app {
padding: 40px 6vw 56px; padding: 40px 6vw 56px;
@@ -540,6 +562,16 @@
color: #cbd5f5; color: #cbd5f5;
} }
.inline-field select {
background: #1e1b4b;
border-color: #312e81;
color: #e2e8f0;
}
.checkbox-field {
color: #cbd5f5;
}
.field input, .field input,
.field select, .field select,
.field textarea { .field textarea {

View File

@@ -7,19 +7,30 @@ export default function LicenseManager() {
const { user, token, authFetch, login, register: registerUser, loading: authLoading } = useAuth(); const { user, token, authFetch, login, register: registerUser, loading: authLoading } = useAuth();
const [licenses, setLicenses] = useState([]); const [licenses, setLicenses] = useState([]);
const [plugins, setPlugins] = useState([]); const [plugins, setPlugins] = useState([]);
const [users, setUsers] = useState([]);
const [selectedPluginId, setSelectedPluginId] = useState(""); const [selectedPluginId, setSelectedPluginId] = useState("");
const [selectedOwnerId, setSelectedOwnerId] = useState("");
const [label, setLabel] = useState(""); const [label, setLabel] = useState("");
const [note, setNote] = useState(""); const [note, setNote] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [creating, setCreating] = useState(false); const [creatingLicense, setCreatingLicense] = useState(false);
const [creatingUser, setCreatingUser] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [lastSync, setLastSync] = useState(null); const [lastSync, setLastSync] = useState(null);
const [formStatus, setFormStatus] = useState(null); const [formStatus, setFormStatus] = useState(null);
const [userFormStatus, setUserFormStatus] = useState(null);
const [verifyStatus, setVerifyStatus] = useState(null); const [verifyStatus, setVerifyStatus] = useState(null);
const [verifying, setVerifying] = useState(false); const [verifying, setVerifying] = useState(false);
const [verifyKey, setVerifyKey] = useState(""); const [verifyKey, setVerifyKey] = useState("");
const [verifyHostname, setVerifyHostname] = useState(""); const [verifyHostname, setVerifyHostname] = useState("");
const [newUserForm, setNewUserForm] = useState({
username: "",
name: "",
email: "",
password: "",
isAdmin: false
});
const isAuthenticated = Boolean(user && token); const isAuthenticated = Boolean(user && token);
@@ -57,8 +68,50 @@ export default function LicenseManager() {
}; };
}, []); }, []);
useEffect(() => {
if (!isAuthenticated) {
setUsers([]);
setSelectedOwnerId("");
return;
}
if (user && !selectedOwnerId) {
setSelectedOwnerId(String(user.id));
}
}, [isAuthenticated, selectedOwnerId, user]);
useEffect(() => {
if (!isAuthenticated || !user) return;
if (!user.isAdmin) {
setUsers([user]);
return;
}
let cancelled = false;
async function loadUsers() {
try {
const response = await authFetch("/api/admin/users");
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.error || "Kon gebruikers niet laden.");
}
if (cancelled) return;
setUsers(data.items || []);
if (!selectedOwnerId && (data.items || []).length > 0) {
setSelectedOwnerId(String(data.items[0].id));
}
} catch (err) {
if (!cancelled) {
setFormStatus({ variant: "error", message: err.message });
}
}
}
loadUsers();
return () => {
cancelled = true;
};
}, [authFetch, isAuthenticated, selectedOwnerId, user]);
const refreshLicenses = useCallback( const refreshLicenses = useCallback(
async (showStatus = true) => { async (showStatus = true, overrideUserId) => {
if (!token) { if (!token) {
setLicenses([]); setLicenses([]);
setLastSync(null); setLastSync(null);
@@ -72,7 +125,14 @@ export default function LicenseManager() {
} }
setRefreshing(true); setRefreshing(true);
try { try {
const response = await authFetch("/api/licenses"); const ownerIdToUse =
overrideUserId ||
(user?.isAdmin ? selectedOwnerId || (user ? String(user.id) : "") : user ? String(user.id) : "");
let url = "/api/licenses";
if (user?.isAdmin && ownerIdToUse) {
url += `?userId=${ownerIdToUse}`;
}
const response = await authFetch(url);
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
if (response.status === 401) { if (response.status === 401) {
throw new Error("Sessie verlopen, log opnieuw in."); throw new Error("Sessie verlopen, log opnieuw in.");
@@ -90,7 +150,7 @@ export default function LicenseManager() {
setRefreshing(false); setRefreshing(false);
} }
}, },
[authFetch, token] [authFetch, selectedOwnerId, token, user]
); );
useEffect(() => { useEffect(() => {
@@ -125,7 +185,7 @@ export default function LicenseManager() {
setFormStatus({ variant: "error", message: "Selecteer een plugin." }); setFormStatus({ variant: "error", message: "Selecteer een plugin." });
return; return;
} }
setCreating(true); setCreatingLicense(true);
try { try {
const payload = { const payload = {
label: label:
@@ -140,10 +200,14 @@ export default function LicenseManager() {
baseUrl: selectedPlugin.baseUrl baseUrl: selectedPlugin.baseUrl
} }
}; };
const requestBody = {
...payload,
userId: user?.isAdmin ? Number(selectedOwnerId || user.id) : undefined
};
const response = await authFetch("/api/licenses", { const response = await authFetch("/api/licenses", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload) body: JSON.stringify(requestBody)
}); });
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
if (response.status === 401) { if (response.status === 401) {
@@ -159,7 +223,34 @@ export default function LicenseManager() {
} catch (err) { } catch (err) {
setFormStatus({ variant: "error", message: err.message }); setFormStatus({ variant: "error", message: err.message });
} finally { } finally {
setCreating(false); setCreatingLicense(false);
}
}
async function handleCreateUser(event) {
event.preventDefault();
setUserFormStatus(null);
setCreatingUser(true);
try {
const response = await authFetch("/api/admin/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newUserForm)
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.error || "Gebruiker aanmaken mislukt.");
}
setUserFormStatus({ variant: "success", message: "Gebruiker aangemaakt." });
setNewUserForm({ username: "", name: "", email: "", password: "", isAdmin: false });
setUsers((prev) => [...prev, data.user]);
if (!selectedOwnerId) {
setSelectedOwnerId(String(data.user.id));
}
} catch (err) {
setUserFormStatus({ variant: "error", message: err.message });
} finally {
setCreatingUser(false);
} }
} }
@@ -236,6 +327,24 @@ export default function LicenseManager() {
<span>Actieve licenties: {licenses.length}</span> <span>Actieve licenties: {licenses.length}</span>
<span>Laatste update: {formatDateTime(lastSync)}</span> <span>Laatste update: {formatDateTime(lastSync)}</span>
{user && <span>Ingelogd als: {user.email}</span>} {user && <span>Ingelogd als: {user.email}</span>}
{user?.isAdmin && users.length > 0 && (
<label className="inline-field">
<span>Licenties gebruiker</span>
<select
value={selectedOwnerId}
onChange={(event) => {
setSelectedOwnerId(event.target.value);
refreshLicenses(true, event.target.value);
}}
>
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.name} ({u.email})
</option>
))}
</select>
</label>
)}
</div> </div>
{isLoadingState && <div className="state">Bezig met laden</div>} {isLoadingState && <div className="state">Bezig met laden</div>}
@@ -283,8 +392,20 @@ export default function LicenseManager() {
rows={3} rows={3}
/> />
</label> </label>
<button className="cta" type="submit" disabled={creating || !selectedPlugin}> {user?.isAdmin && (
{creating ? "Aanmaken…" : "Licentie aanmaken"} <label className="field">
<span>Licentie voor gebruiker</span>
<select value={selectedOwnerId} onChange={(event) => setSelectedOwnerId(event.target.value)}>
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.name} ({u.email})
</option>
))}
</select>
</label>
)}
<button className="cta" type="submit" disabled={creatingLicense || !selectedPlugin}>
{creatingLicense ? "Aanmaken…" : "Licentie aanmaken"}
</button> </button>
</form> </form>
{formStatus && ( {formStatus && (
@@ -340,6 +461,69 @@ export default function LicenseManager() {
</article> </article>
</section> </section>
{user?.isAdmin && (
<section className="license-forms">
<article className="card">
<h2>Gebruiker toevoegen</h2>
<p className="hint">Admins kunnen extra gebruikers aanmaken en direct licenties toewijzen.</p>
<form className="form-grid" onSubmit={handleCreateUser}>
<label className="field">
<span>Gebruikersnaam</span>
<input
value={newUserForm.username}
onChange={(event) => setNewUserForm((prev) => ({ ...prev, username: event.target.value }))}
placeholder="gebruikersnaam"
/>
</label>
<label className="field">
<span>Naam</span>
<input
value={newUserForm.name}
onChange={(event) => setNewUserForm((prev) => ({ ...prev, name: event.target.value }))}
placeholder="Volledige naam"
/>
</label>
<label className="field">
<span>E-mail</span>
<input
type="email"
value={newUserForm.email}
onChange={(event) => setNewUserForm((prev) => ({ ...prev, email: event.target.value }))}
placeholder="naam@bedrijf.nl"
/>
</label>
<label className="field">
<span>Wachtwoord</span>
<input
type="password"
value={newUserForm.password}
onChange={(event) => setNewUserForm((prev) => ({ ...prev, password: event.target.value }))}
placeholder="Minimaal 8 karakters"
/>
</label>
<label className="checkbox-field">
<input
type="checkbox"
checked={newUserForm.isAdmin}
onChange={(event) =>
setNewUserForm((prev) => ({ ...prev, isAdmin: event.target.checked }))
}
/>
<span>Maak deze gebruiker admin</span>
</label>
<button className="cta" type="submit" disabled={creatingUser}>
{creatingUser ? "Bezig…" : "Gebruiker aanmaken"}
</button>
</form>
{userFormStatus && (
<div className={`state inline ${userFormStatus.variant === "error" ? "error" : "success"}`}>
{userFormStatus.message}
</div>
)}
</article>
</section>
)}
{isAuthenticated ? ( {isAuthenticated ? (
<section className="license-grid"> <section className="license-grid">
{sortedLicenses.length === 0 ? ( {sortedLicenses.length === 0 ? (