Refactor code structure for improved readability and maintainability
This commit is contained in:
68
dist/assets/index-BlY_enN1.js
vendored
68
dist/assets/index-BlY_enN1.js
vendored
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
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
4
dist/index.html
vendored
@@ -5,8 +5,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Siti Plugin Repo</title>
|
||||
<script type="module" crossorigin src="/assets/index-BlY_enN1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CgskK80a.css">
|
||||
<script type="module" crossorigin src="/assets/index-garE9QaM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CFgb3VsA.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
} from "./lib/licenseService.js";
|
||||
import { HOST, PATHS, PORT } from "./lib/config.js";
|
||||
import { ensureSchema } from "./lib/schema.js";
|
||||
import { authenticateUser, registerUser } from "./lib/userService.js";
|
||||
import { requireAuth } from "./middleware/auth.js";
|
||||
import { authenticateUser, registerUser, adminCreateUser, listUsers, getUserById } from "./lib/userService.js";
|
||||
import { requireAuth, requireAdmin } from "./middleware/auth.js";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -75,6 +75,41 @@ app.get("/api/auth/me", requireAuth, (req, res) => {
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
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({
|
||||
count: payload.length,
|
||||
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." });
|
||||
}
|
||||
|
||||
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(),
|
||||
note: body.note?.trim(),
|
||||
repo: repoEntry
|
||||
|
||||
@@ -7,10 +7,16 @@ async function createUsersTable() {
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
email VARCHAR(120) NOT NULL UNIQUE,
|
||||
is_admin TINYINT(1) NOT NULL DEFAULT 0,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) 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() {
|
||||
|
||||
@@ -10,6 +10,7 @@ function serializeUser(row) {
|
||||
username: row.username,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
isAdmin: Boolean(row.is_admin),
|
||||
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 }) {
|
||||
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 {
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO users (username, name, email, password_hash) VALUES (?, ?, ?, ?)`,
|
||||
[username, name, email, passwordHash]
|
||||
`INSERT INTO users (username, name, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)`,
|
||||
[username, name, email, passwordHash, isAdmin]
|
||||
);
|
||||
const user = await getUserById(result.insertId);
|
||||
const token = signToken(user.id);
|
||||
@@ -64,6 +67,37 @@ export async function authenticateUser(identifier, password) {
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,13 @@ export async function requireAuth(req, res, next) {
|
||||
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();
|
||||
}
|
||||
|
||||
32
src/App.css
32
src/App.css
@@ -448,6 +448,28 @@
|
||||
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) {
|
||||
.app {
|
||||
padding: 40px 6vw 56px;
|
||||
@@ -540,6 +562,16 @@
|
||||
color: #cbd5f5;
|
||||
}
|
||||
|
||||
.inline-field select {
|
||||
background: #1e1b4b;
|
||||
border-color: #312e81;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.checkbox-field {
|
||||
color: #cbd5f5;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select,
|
||||
.field textarea {
|
||||
|
||||
@@ -7,19 +7,30 @@ export default function LicenseManager() {
|
||||
const { user, token, authFetch, login, register: registerUser, loading: authLoading } = useAuth();
|
||||
const [licenses, setLicenses] = useState([]);
|
||||
const [plugins, setPlugins] = useState([]);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [selectedPluginId, setSelectedPluginId] = useState("");
|
||||
const [selectedOwnerId, setSelectedOwnerId] = useState("");
|
||||
const [label, setLabel] = useState("");
|
||||
const [note, setNote] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 [lastSync, setLastSync] = useState(null);
|
||||
const [formStatus, setFormStatus] = useState(null);
|
||||
const [userFormStatus, setUserFormStatus] = useState(null);
|
||||
const [verifyStatus, setVerifyStatus] = useState(null);
|
||||
const [verifying, setVerifying] = useState(false);
|
||||
const [verifyKey, setVerifyKey] = useState("");
|
||||
const [verifyHostname, setVerifyHostname] = useState("");
|
||||
const [newUserForm, setNewUserForm] = useState({
|
||||
username: "",
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
isAdmin: false
|
||||
});
|
||||
|
||||
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(
|
||||
async (showStatus = true) => {
|
||||
async (showStatus = true, overrideUserId) => {
|
||||
if (!token) {
|
||||
setLicenses([]);
|
||||
setLastSync(null);
|
||||
@@ -72,7 +125,14 @@ export default function LicenseManager() {
|
||||
}
|
||||
setRefreshing(true);
|
||||
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(() => ({}));
|
||||
if (response.status === 401) {
|
||||
throw new Error("Sessie verlopen, log opnieuw in.");
|
||||
@@ -90,7 +150,7 @@ export default function LicenseManager() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[authFetch, token]
|
||||
[authFetch, selectedOwnerId, token, user]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -125,7 +185,7 @@ export default function LicenseManager() {
|
||||
setFormStatus({ variant: "error", message: "Selecteer een plugin." });
|
||||
return;
|
||||
}
|
||||
setCreating(true);
|
||||
setCreatingLicense(true);
|
||||
try {
|
||||
const payload = {
|
||||
label:
|
||||
@@ -140,10 +200,14 @@ export default function LicenseManager() {
|
||||
baseUrl: selectedPlugin.baseUrl
|
||||
}
|
||||
};
|
||||
const requestBody = {
|
||||
...payload,
|
||||
userId: user?.isAdmin ? Number(selectedOwnerId || user.id) : undefined
|
||||
};
|
||||
const response = await authFetch("/api/licenses", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (response.status === 401) {
|
||||
@@ -159,7 +223,34 @@ export default function LicenseManager() {
|
||||
} catch (err) {
|
||||
setFormStatus({ variant: "error", message: err.message });
|
||||
} 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>Laatste update: {formatDateTime(lastSync)}</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>
|
||||
|
||||
{isLoadingState && <div className="state">Bezig met laden…</div>}
|
||||
@@ -283,8 +392,20 @@ export default function LicenseManager() {
|
||||
rows={3}
|
||||
/>
|
||||
</label>
|
||||
<button className="cta" type="submit" disabled={creating || !selectedPlugin}>
|
||||
{creating ? "Aanmaken…" : "Licentie aanmaken"}
|
||||
{user?.isAdmin && (
|
||||
<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>
|
||||
</form>
|
||||
{formStatus && (
|
||||
@@ -340,6 +461,69 @@ export default function LicenseManager() {
|
||||
</article>
|
||||
</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 ? (
|
||||
<section className="license-grid">
|
||||
{sortedLicenses.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user