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 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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
32
src/App.css
32
src/App.css
@@ -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 {
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user