create admin
This commit is contained in:
parent
ad341d4208
commit
45bd4bf351
23 changed files with 404 additions and 12 deletions
|
@ -1,3 +1,4 @@
|
|||
import ChangeUsernameViewModel from "../models/account/ChangeUsernameViewModel";
|
||||
import LoginViewModel from "../models/account/LoginViewModel";
|
||||
import RegisterViewModel from "../models/account/RegisterViewModel";
|
||||
import Session from "../objects/Session";
|
||||
|
@ -37,7 +38,12 @@ export default class AccountController extends Controller {
|
|||
}
|
||||
|
||||
const username = registerViewModel.username.replaceAll("<", "<").replaceAll(">", ">");
|
||||
await UserService.CreateUser(0, username, registerViewModel.password);
|
||||
if (!await UserService.CreateUser(0, username, registerViewModel.password)) {
|
||||
registerViewModel.password = "";
|
||||
registerViewModel.message = "Sorry! That username is already taken.";
|
||||
|
||||
return this.view(registerViewModel);
|
||||
}
|
||||
|
||||
const user = await UserService.GetUserByUsername(username);
|
||||
if (!user) {
|
||||
|
@ -57,4 +63,22 @@ export default class AccountController extends Controller {
|
|||
|
||||
return this.redirectToAction("index", "home");
|
||||
}
|
||||
|
||||
public async ChangeUsername_Get(changeUsernameViewModel:ChangeUsernameViewModel) {
|
||||
return this.view(changeUsernameViewModel);
|
||||
}
|
||||
|
||||
public async ChangeUsername_Post(changeUsernameViewModel:ChangeUsernameViewModel) {
|
||||
if (typeof(changeUsernameViewModel.username) !== "string") {
|
||||
return this.badRequest();
|
||||
}
|
||||
|
||||
const user = await UserService.SaveUsername(this.session.userId, changeUsernameViewModel.username);
|
||||
if (!user) {
|
||||
changeUsernameViewModel.message = "Sorry! That username is already taken.";
|
||||
return this.view(changeUsernameViewModel);
|
||||
}
|
||||
|
||||
return this.redirectToAction("index", "home");
|
||||
}
|
||||
}
|
33
server/controller/AdminController.ts
Normal file
33
server/controller/AdminController.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import AdminIndexViewModel from "../models/admin/AdminIndexViewModel";
|
||||
import AdminPartiesViewModel from "../models/admin/AdminPartiesViewModel";
|
||||
import AdminUsersViewModel from "../models/admin/AdminUsersViewModel";
|
||||
import PartyService from "../services/PartyService";
|
||||
import UserService from "../services/UserService";
|
||||
import Controller from "./Controller";
|
||||
|
||||
export default class AdminController_Auth$Admin extends Controller {
|
||||
public async Index_Get() {
|
||||
const adminIndexViewModel: AdminIndexViewModel = {
|
||||
userCount: await UserService.GetUserCount(),
|
||||
partyCount: await PartyService.GetPartyCount()
|
||||
};
|
||||
|
||||
return this.view(adminIndexViewModel);
|
||||
}
|
||||
|
||||
public async Users_Get() {
|
||||
const adminUsersViewModel: AdminUsersViewModel = {
|
||||
users: await UserService.GetAll()
|
||||
};
|
||||
|
||||
return this.view(adminUsersViewModel);
|
||||
}
|
||||
|
||||
public async Parties_Get() {
|
||||
const adminPartiesViewModel: AdminPartiesViewModel = {
|
||||
parties: await PartyService.GetAll()
|
||||
};
|
||||
|
||||
return this.view(adminPartiesViewModel);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import { Console } from "hsconsole";
|
|||
import Session from "../objects/Session";
|
||||
import SessionUser from "../objects/SessionUser";
|
||||
import RequestCtx from "../objects/RequestCtx";
|
||||
import { UserLevel } from "../enums/UserLevel";
|
||||
|
||||
// prepare for ts-ignore :3
|
||||
// TODO: figure out some runtime field / type checking so
|
||||
|
@ -12,7 +13,19 @@ export default abstract class Controller {
|
|||
|
||||
public constructor() {
|
||||
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
||||
const controllerName = this.constructor.name.replace("Controller", "").toLowerCase();
|
||||
const rawControllerParts = this.constructor.name.split("_");
|
||||
const controllerName = rawControllerParts.splice(0, 1)[0].replace("Controller", "").toLowerCase();
|
||||
let controllerAuthLevel: UserLevel | undefined;
|
||||
|
||||
for (const prop of rawControllerParts) {
|
||||
if (prop.startsWith("Auth")) {
|
||||
const userLevel = prop.split("$")[1];
|
||||
// @ts-ignore
|
||||
controllerAuthLevel = UserLevel[userLevel];
|
||||
Console.printInfo(`Set Auth level requirement for ${this.constructor.name} to ${userLevel}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const method of methods) {
|
||||
if (method === "constructor" || method[0] !== method[0].toUpperCase()) { // * Anything that starts with lowercase we'll consider "private"
|
||||
continue;
|
||||
|
@ -23,6 +36,7 @@ export default abstract class Controller {
|
|||
const methodName = methodNameRaw.toLowerCase();
|
||||
const doAuth = !params.includes("AllowAnonymous");
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const controllerRequestHandler = this[method];
|
||||
const requestHandler = (req:FastifyRequest, res:FastifyReply) => {
|
||||
|
@ -30,6 +44,9 @@ export default abstract class Controller {
|
|||
if (doAuth && session === undefined) {
|
||||
return res.redirect(`/account/login?returnTo=${encodeURIComponent(req.url)}`);
|
||||
}
|
||||
if (session !== undefined && controllerAuthLevel !== undefined && controllerAuthLevel !== session.userLevel) {
|
||||
return res.status(403).send("Forbidden");
|
||||
}
|
||||
|
||||
const requestCtx = new RequestCtx(req, res, controllerName, methodName, session);
|
||||
controllerRequestHandler.bind(requestCtx)(req.method === "GET" ? req.query : req.body);
|
||||
|
@ -80,4 +97,5 @@ export default abstract class Controller {
|
|||
ok(message?:string) {}
|
||||
badRequest(message?:string) {}
|
||||
unauthorised(message?:string) {}
|
||||
forbidden(message?:string) {}
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
import { UserLevel } from "../enums/UserLevel";
|
||||
|
||||
export default class User {
|
||||
public Id:number;
|
||||
public UserLevel:UserLevel;
|
||||
public get UserLevelString() {
|
||||
return UserLevel[this.UserLevel];
|
||||
}
|
||||
public Username:string;
|
||||
public PasswordSalt:string;
|
||||
public PasswordHash:string;
|
||||
|
@ -12,9 +18,10 @@ export default class User {
|
|||
public DeletedDatetime?:Date;
|
||||
public IsDeleted:boolean;
|
||||
|
||||
public constructor(id?:number, username?:string, passwordSalt?:string, passwordHash?:string, apiKey?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) {
|
||||
if (typeof(id) == "number" && typeof(username) == "string" && typeof(passwordHash) == "string" && typeof(passwordSalt) == "string" && typeof(apiKey) == "string" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") {
|
||||
public constructor(id?:number, userLevel?:UserLevel, username?:string, passwordSalt?:string, passwordHash?:string, apiKey?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) {
|
||||
if (typeof(id) == "number" && typeof(userLevel) == "number" && typeof(username) == "string" && typeof(passwordHash) == "string" && typeof(passwordSalt) == "string" && typeof(apiKey) == "string" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") {
|
||||
this.Id = id;
|
||||
this.UserLevel = userLevel;
|
||||
this.Username = username;
|
||||
this.PasswordHash = passwordHash;
|
||||
this.PasswordSalt = passwordSalt;
|
||||
|
@ -28,6 +35,7 @@ export default class User {
|
|||
this.IsDeleted = isDeleted;
|
||||
} else {
|
||||
this.Id = Number.MIN_VALUE;
|
||||
this.UserLevel = UserLevel.Unknown;
|
||||
this.Username = "";
|
||||
this.PasswordHash = "";
|
||||
this.PasswordSalt = "";
|
||||
|
|
5
server/enums/UserLevel.ts
Normal file
5
server/enums/UserLevel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum UserLevel {
|
||||
Unknown = 0,
|
||||
User = 10,
|
||||
Admin = 999
|
||||
}
|
|
@ -40,6 +40,7 @@ import AccountController from "./controller/AccountController";
|
|||
import PartyController from "./controller/PartyController";
|
||||
import ApiController from "./controller/ApiController";
|
||||
import PartyService from "./services/PartyService";
|
||||
import AdminController_Auth$Admin from "./controller/AdminController";
|
||||
|
||||
Console.customHeader(`MultiProbe server started at ${new Date()}`);
|
||||
|
||||
|
@ -72,6 +73,7 @@ new HomeController();
|
|||
new AccountController();
|
||||
new PartyController();
|
||||
new ApiController();
|
||||
new AdminController_Auth$Admin();
|
||||
|
||||
// Websocket stuff
|
||||
|
||||
|
|
4
server/models/account/ChangeUsernameViewModel.ts
Normal file
4
server/models/account/ChangeUsernameViewModel.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default interface ChangeUsernameViewModel {
|
||||
username?: string
|
||||
message?: string
|
||||
}
|
4
server/models/admin/AdminIndexViewModel.ts
Normal file
4
server/models/admin/AdminIndexViewModel.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default interface AdminIndexViewModel {
|
||||
userCount: number,
|
||||
partyCount: number
|
||||
}
|
5
server/models/admin/AdminPartiesViewModel.ts
Normal file
5
server/models/admin/AdminPartiesViewModel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Party from "../../entities/Party";
|
||||
|
||||
export default interface AdminPartiesViewModel {
|
||||
parties: Array<Party>
|
||||
}
|
5
server/models/admin/AdminUsersViewModel.ts
Normal file
5
server/models/admin/AdminUsersViewModel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import User from "../../entities/User";
|
||||
|
||||
export default interface AdminUsersViewModel {
|
||||
users: Array<User>
|
||||
}
|
|
@ -57,4 +57,8 @@ export default class RequestCtx {
|
|||
unauthorised(message?:string) {
|
||||
return this.res.status(401).send(message ?? "");
|
||||
}
|
||||
|
||||
forbidden(message?:string) {
|
||||
return this.res.status(403).send(message ?? "");
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ export default abstract class Session {
|
|||
validPeriod.setTime(validPeriod.getTime() + Config.session.validity);
|
||||
const key = randomBytes(Config.session.length).toString("hex");
|
||||
|
||||
Session.Sessions.set(key, new SessionUser(user.Id, validPeriod));
|
||||
Session.Sessions.set(key, new SessionUser(user.Id, user.UserLevel, validPeriod));
|
||||
|
||||
res.setCookie("MP_SESSION", key, {
|
||||
path: "/",
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { UserLevel } from "../enums/UserLevel";
|
||||
|
||||
export default class SessionUser {
|
||||
public readonly userId:number;
|
||||
public readonly userLevel:UserLevel;
|
||||
public readonly validityPeriod:Date;
|
||||
|
||||
constructor(userId:number, validityPeriod:Date) {
|
||||
constructor(userId:number, userLevel:UserLevel, validityPeriod:Date) {
|
||||
this.userId = userId;
|
||||
this.userLevel = userLevel;
|
||||
this.validityPeriod = validityPeriod;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,19 @@ import User from "../entities/User";
|
|||
import RepoBase from "./RepoBase";
|
||||
|
||||
export default class PartyRepo {
|
||||
public static async selectAll() {
|
||||
const dbUser = await Database.Instance.query("SELECT * FROM Party WHERE IsDeleted = 0");
|
||||
const parties = new Array<Party>();
|
||||
|
||||
for (const row of dbUser) {
|
||||
const party = new Party();
|
||||
populatePartyFromDB(party, row);
|
||||
parties.push(party);
|
||||
}
|
||||
|
||||
return parties;
|
||||
}
|
||||
|
||||
public static async selectById(id:number) {
|
||||
const dbParty = await Database.Instance.query("SELECT * FROM Party WHERE Id = ? AND IsDeleted = 0 LIMIT 1", [id]);
|
||||
if (dbParty == null || dbParty.length === 0) {
|
||||
|
@ -42,6 +55,12 @@ export default class PartyRepo {
|
|||
}
|
||||
}
|
||||
|
||||
public static async selectPartyCount() {
|
||||
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `Party` LIMIT 1");
|
||||
|
||||
return countResult[0]["COUNT(Id)"];
|
||||
}
|
||||
|
||||
public static async insertUpdate(party:Party) {
|
||||
if (party.Id === Number.MIN_VALUE) {
|
||||
await Database.Instance.query("INSERT Party (Name, PartyRef, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
||||
|
|
|
@ -3,6 +3,19 @@ import User from "../entities/User";
|
|||
import RepoBase from "./RepoBase";
|
||||
|
||||
export default class UserRepo {
|
||||
public static async selectAll() {
|
||||
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE IsDeleted = 0");
|
||||
const users = new Array<User>();
|
||||
|
||||
for (const row of dbUser) {
|
||||
const user = new User();
|
||||
populateUserFromDB(user, row);
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
public static async selectById(id:number) {
|
||||
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE Id = ? LIMIT 1", [id]);
|
||||
if (dbUser == null || dbUser.length === 0) {
|
||||
|
@ -36,14 +49,20 @@ export default class UserRepo {
|
|||
}
|
||||
}
|
||||
|
||||
public static async selectUserCount() {
|
||||
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `User` LIMIT 1");
|
||||
|
||||
return countResult[0]["COUNT(Id)"];
|
||||
}
|
||||
|
||||
public static async insertUpdate(user:User) {
|
||||
if (user.Id === Number.MIN_VALUE) {
|
||||
await Database.Instance.query("INSERT User (Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
||||
user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted)
|
||||
await Database.Instance.query("INSERT User (UserLevelId, Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
||||
user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted)
|
||||
]);
|
||||
} else {
|
||||
await Database.Instance.query(`UPDATE User SET Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [
|
||||
user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id
|
||||
await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
|
||||
user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +70,7 @@ export default class UserRepo {
|
|||
|
||||
function populateUserFromDB(user:User, dbUser:any) {
|
||||
user.Id = dbUser.Id;
|
||||
user.UserLevel = dbUser.UserLevelId;
|
||||
user.Username = dbUser.Username;
|
||||
user.PasswordHash = dbUser.PasswordHash;
|
||||
user.PasswordSalt = dbUser.PasswordSalt;
|
||||
|
|
|
@ -42,6 +42,15 @@ export default abstract class PartyService {
|
|||
}
|
||||
}
|
||||
|
||||
public static async GetAll() {
|
||||
try {
|
||||
return await PartyRepo.selectAll();
|
||||
} catch (e) {
|
||||
Console.printError(`MultiProbe server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async GetPartyByPartyRef(partyRef:string) {
|
||||
try {
|
||||
return await PartyRepo.selectByPartyRef(partyRef);
|
||||
|
@ -81,4 +90,13 @@ export default abstract class PartyService {
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async GetPartyCount() {
|
||||
try {
|
||||
return await PartyRepo.selectPartyCount();
|
||||
} catch (e) {
|
||||
Console.printError(`MultiProbe server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,15 @@ export default class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
public static async GetAll() {
|
||||
try {
|
||||
return await UserRepo.selectAll();
|
||||
} catch (e) {
|
||||
Console.printError(`MultiProbe server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async GetUserByUsername(username:string) {
|
||||
try {
|
||||
return await UserRepo.selectByUsername(username);
|
||||
|
@ -62,6 +71,11 @@ export default class UserService {
|
|||
|
||||
public static async CreateUser(currentUserId:number, username:string, password:string) {
|
||||
try {
|
||||
const existingCheck = await UserRepo.selectByUsername(username);
|
||||
if (existingCheck) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = new User();
|
||||
user.Username = username;
|
||||
user.PasswordSalt = PasswordUtility.GenerateSalt();
|
||||
|
@ -70,6 +84,8 @@ export default class UserService {
|
|||
user.CreatedDatetime = new Date();
|
||||
|
||||
await UserRepo.insertUpdate(user);
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
Console.printError(`MultiProbe server service error:\n${e}`);
|
||||
throw e;
|
||||
|
@ -150,4 +166,39 @@ export default class UserService {
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async SaveUsername(currentUserId:number, username:string) {
|
||||
try {
|
||||
const existingCheck = await UserRepo.selectByUsername(username)
|
||||
if (existingCheck) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await UserRepo.selectById(currentUserId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
user.LastModifiedByUserId = currentUserId;
|
||||
user.LastModifiedDatetime = new Date();
|
||||
|
||||
user.Username = username;
|
||||
|
||||
await UserRepo.insertUpdate(user);
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
Console.printError(`MultiProbe server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async GetUserCount() {
|
||||
try {
|
||||
return await UserRepo.selectUserCount();
|
||||
} catch (e) {
|
||||
Console.printError(`MultiProbe server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
25
server/templates/account/changeusername.ejs
Normal file
25
server/templates/account/changeusername.ejs
Normal file
|
@ -0,0 +1,25 @@
|
|||
<%- include("../base/header", { title: "Login", userId: session.userId }) %>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="card my-auto" style="width: 25rem;">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-center">Change Username</h5>
|
||||
<% if (typeof(message) === "string") { %>
|
||||
<div class="alert alert-danger text-center" role="alert"><%= message %></div>
|
||||
<% } %>
|
||||
<form method="POST">
|
||||
<input class="form-control mt-3 mb-2" name="username" placeholder="Username" value="<%= typeof(username) === "undefined" ? "" : username %>" required autocomplete="one-time-code" />
|
||||
<div class="row mt-3">
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-danger d-block" href="/">Cancel</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input class="btn btn-primary ms-auto d-block" type="submit" value="Change" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include("../base/footer") %>
|
53
server/templates/admin/index.ejs
Normal file
53
server/templates/admin/index.ejs
Normal file
|
@ -0,0 +1,53 @@
|
|||
<%- include("../base/header", { title: "Admin Dashboard", userId: session.userId, isAdmin: true }) %>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item active"><a>Admin</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>Admin Dashboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-3 my-5">
|
||||
<div class="col mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Users</h4>
|
||||
<h3 class="card-text mb-0"><%= userCount %></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Parties</h4>
|
||||
<h3 class="card-text mb-0"><%= partyCount %></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Badges</h5>
|
||||
<h3 class="card-text mb-0"><%= 0 %></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a class="btn btn-primary btn-lg me-2" href="/admin/users">Manage Users</a>
|
||||
<a class="btn btn-primary btn-lg me-2" href="/admin/parties">Manage Parties</a>
|
||||
<a class="btn btn-primary btn-lg me-2">Manage Badges</a>
|
||||
</div>
|
||||
</div>
|
||||
<%- include("../base/footer") %>
|
45
server/templates/admin/parties.ejs
Normal file
45
server/templates/admin/parties.ejs
Normal file
|
@ -0,0 +1,45 @@
|
|||
<%- include("../base/header", { title: "Party Management", userId: session.userId, isAdmin: true }) %>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
|
||||
<li class="breadcrumb-item active"><a>Party Management</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>Party Management</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<th>#</th>
|
||||
<th>Party Ref</th>
|
||||
<th>Name</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (const party of parties) { %>
|
||||
<tr>
|
||||
<td><%= party.Id %></td>
|
||||
<td><%= "redacted" %></td>
|
||||
<td><%= party.Name %></td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-sm btn-primary" href="/admin/user?id=<%= party.Id %>">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/admin/userdelete?id=<%= party.Id %>">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<%- include("../base/footer") %>
|
45
server/templates/admin/users.ejs
Normal file
45
server/templates/admin/users.ejs
Normal file
|
@ -0,0 +1,45 @@
|
|||
<%- include("../base/header", { title: "User Management", userId: session.userId, isAdmin: true }) %>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
|
||||
<li class="breadcrumb-item active"><a>User Management</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>User Management</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<th>#</th>
|
||||
<th>Username</th>
|
||||
<th>Permissions</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (const user of users) { %>
|
||||
<tr>
|
||||
<td><%= user.Id %></td>
|
||||
<td><%= user.Username %></td>
|
||||
<td><%= user.UserLevelString %></td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-sm btn-primary" href="/admin/user?id=<%= user.Id %>">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/admin/userdelete?id=<%= user.Id %>">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<%- include("../base/footer") %>
|
|
@ -14,7 +14,7 @@
|
|||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
MultiProbe
|
||||
MultiProbe<%= typeof(isAdmin) === "undefined" ? "" : " Admin" %>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<h3>What would you like to do?</h3>
|
||||
<div class="mt-3 text-nowrap">
|
||||
<div>
|
||||
<a class="btn btn-primary btn-lg me-2 disabled" href="/account/username">Change Username</a>
|
||||
<a class="btn btn-primary btn-lg me-2" href="/account/changeusername">Change Username</a>
|
||||
<a class="btn btn-primary btn-lg me-2 disabled" href="/account/password">Change Password</a>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
|
|
Loading…
Reference in a new issue