refactor: moved all data access/storage into repositories
also various refactorings in resource editing (*/[id]/+page.server.ts) pages
This commit is contained in:
parent
5e3fb972e8
commit
d2e064d40f
@ -1,4 +1,4 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { initRepos, users } from '$lib/server/db';
|
||||
import { Guard } from '$lib/server/guard';
|
||||
import { getUserFromSession } from '$lib/server/sessions';
|
||||
import type { Handle, ServerInit } from '@sveltejs/kit';
|
||||
@ -7,20 +7,17 @@ import { writeFileSync } from 'fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const init: ServerInit = async () => {
|
||||
const anyUser = db.data.users[0];
|
||||
await initRepos();
|
||||
|
||||
if (!anyUser) {
|
||||
if (!(await users.any())) {
|
||||
const pass = nanoid();
|
||||
|
||||
await db.update(({ users }) => {
|
||||
users.push({
|
||||
id: nanoid(),
|
||||
name: 'admin',
|
||||
password: bcrypt.hashSync(pass, 10),
|
||||
admin: true,
|
||||
groups: [],
|
||||
devices: [],
|
||||
});
|
||||
await users.create({
|
||||
name: 'admin',
|
||||
password: bcrypt.hashSync(pass, 10),
|
||||
admin: true,
|
||||
groups: [],
|
||||
devices: [],
|
||||
});
|
||||
|
||||
console.log(`default admin password: ${pass}`);
|
||||
@ -33,7 +30,7 @@ export const init: ServerInit = async () => {
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const { cookies, locals } = event;
|
||||
|
||||
locals.guard = new Guard(getUserFromSession(cookies.get('session')));
|
||||
locals.guard = new Guard(await getUserFromSession(cookies.get('session')));
|
||||
|
||||
return await resolve(event);
|
||||
};
|
||||
|
||||
6
src/lib/server/commonResponses.ts
Normal file
6
src/lib/server/commonResponses.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { ZodError } from 'zod';
|
||||
|
||||
export const FORBIDDEN = fail(403, { error: 'You do not have permission to do that.' });
|
||||
export const SUCCESS = { success: true };
|
||||
export const PARSE_ERROR = (err: ZodError) => fail(400, { error: err.errors[0].message });
|
||||
@ -1,42 +1,16 @@
|
||||
import { JSONFilePreset } from 'lowdb/node';
|
||||
import type { Device } from './types/device';
|
||||
import type { Group } from './types/group';
|
||||
import type { User } from './types/user';
|
||||
import type { IDeviceRepo } from './repos/deviceRepo';
|
||||
import type { IGroupRepo } from './repos/groupRepo';
|
||||
import { LowStorage } from './repos/lowdb/_init';
|
||||
import type { IUserRepo } from './repos/userRepo';
|
||||
|
||||
type AppData = {
|
||||
users: User[];
|
||||
groups: Group[];
|
||||
devices: Device[];
|
||||
};
|
||||
export let users: IUserRepo;
|
||||
export let groups: IGroupRepo;
|
||||
export let devices: IDeviceRepo;
|
||||
|
||||
export const db = await JSONFilePreset<AppData>('./data/db.json', {
|
||||
users: [],
|
||||
groups: [],
|
||||
devices: [],
|
||||
});
|
||||
export async function initRepos() {
|
||||
const repos = await LowStorage.init();
|
||||
|
||||
export function getUsersDevices(userId: string): Device[] {
|
||||
const user = db.data.users.find((u) => u.id === userId)!;
|
||||
|
||||
if (user.admin) return db.data.devices;
|
||||
|
||||
const userDevices = db.data.devices.filter((d) => user.devices.includes(d.id));
|
||||
const userGroups = db.data.groups.filter((g) => user.groups.includes(g.id));
|
||||
const groupDevices = userGroups.flatMap((g) =>
|
||||
db.data.devices.filter((d) => g.devices.includes(d.id)),
|
||||
);
|
||||
|
||||
// concat and dedupe
|
||||
return [...new Set([...userDevices, ...groupDevices])];
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function assignDefaults<T>(defaults: Partial<T>, obj: any, options = { mutate: false }): T {
|
||||
for (let k of Object.keys(obj)) {
|
||||
if (obj[k] === undefined) {
|
||||
delete obj[k];
|
||||
}
|
||||
}
|
||||
|
||||
return options.mutate ? Object.assign(defaults, obj) : Object.assign({}, defaults, obj);
|
||||
users = repos.users;
|
||||
groups = repos.groups;
|
||||
devices = repos.devices;
|
||||
}
|
||||
|
||||
10
src/lib/server/db/repos/deviceRepo.ts
Normal file
10
src/lib/server/db/repos/deviceRepo.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { New, Updated } from '../types';
|
||||
import type { Device, DeviceError } from '../types/device';
|
||||
|
||||
export interface IDeviceRepo {
|
||||
getAll(): Promise<Device[]>;
|
||||
getById(id: string): Promise<Device | undefined>;
|
||||
create(group: New<Device>): Promise<DeviceError | undefined>;
|
||||
update(group: Updated<Device>): Promise<DeviceError | undefined>;
|
||||
delete(groupId: string): Promise<DeviceError | undefined>;
|
||||
}
|
||||
11
src/lib/server/db/repos/groupRepo.ts
Normal file
11
src/lib/server/db/repos/groupRepo.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { New, Updated } from '../types';
|
||||
import type { Group, GroupError } from '../types/group';
|
||||
|
||||
export interface IGroupRepo {
|
||||
getAll(): Promise<Group[]>;
|
||||
getById(id: string): Promise<Group | undefined>;
|
||||
create(group: New<Group>): Promise<GroupError | undefined>;
|
||||
update(group: Updated<Group>): Promise<GroupError | undefined>;
|
||||
delete(groupId: string): Promise<GroupError | undefined>;
|
||||
countUsers(groupId: string): Promise<number>;
|
||||
}
|
||||
29
src/lib/server/db/repos/lowdb/_init.ts
Normal file
29
src/lib/server/db/repos/lowdb/_init.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { JSONFilePreset } from 'lowdb/node';
|
||||
import type { Device } from '../../types/device';
|
||||
import type { Group } from '../../types/group';
|
||||
import type { User } from '../../types/user';
|
||||
import { LowUserRepo } from './userRepo';
|
||||
import { LowGroupRepo } from './groupRepo';
|
||||
import { LowDeviceRepo } from './deviceRepo';
|
||||
|
||||
const db = await JSONFilePreset<LowStorage.LowData>('./data/db.json', {
|
||||
users: [],
|
||||
groups: [],
|
||||
devices: [],
|
||||
});
|
||||
|
||||
export namespace LowStorage {
|
||||
export type LowData = {
|
||||
users: User[];
|
||||
groups: Group[];
|
||||
devices: Device[];
|
||||
};
|
||||
|
||||
export async function init() {
|
||||
return {
|
||||
users: new LowUserRepo(db),
|
||||
groups: new LowGroupRepo(db),
|
||||
devices: new LowDeviceRepo(db),
|
||||
};
|
||||
}
|
||||
}
|
||||
76
src/lib/server/db/repos/lowdb/deviceRepo.ts
Normal file
76
src/lib/server/db/repos/lowdb/deviceRepo.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { IDeviceRepo } from '../deviceRepo';
|
||||
import type { Low } from 'lowdb';
|
||||
import { type New, type Updated } from '../../types';
|
||||
import { type Device, type DeviceError, DeviceErrors } from '../../types/device';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { LowStorage } from './_init';
|
||||
|
||||
export class LowDeviceRepo implements IDeviceRepo {
|
||||
private readonly db;
|
||||
|
||||
constructor(db: Low<LowStorage.LowData>) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getAll(): Promise<Device[]> {
|
||||
return this.db.data.devices;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<Device | undefined> {
|
||||
return this.db.data.devices.find((d) => d.id === id);
|
||||
}
|
||||
|
||||
async create(device: New<Device>): Promise<DeviceError | undefined> {
|
||||
if (this.db.data.devices.find((d) => d.name === device.name)) {
|
||||
return DeviceErrors.DUPLICATE_NAME;
|
||||
}
|
||||
|
||||
await this.db.update(({ devices }) => {
|
||||
devices.push({ id: nanoid(), ...device });
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async update(device: Updated<Device>): Promise<DeviceError | undefined> {
|
||||
if (this.db.data.devices.find((d) => d.name === device.name && d.id !== device.id)) {
|
||||
return DeviceErrors.DUPLICATE_NAME;
|
||||
}
|
||||
|
||||
let found = false;
|
||||
await this.db.update(({ devices }) => {
|
||||
const existingDevice = devices.find((d) => d.id === device.id);
|
||||
if (!existingDevice) return;
|
||||
|
||||
Object.assign(existingDevice, device);
|
||||
found = true;
|
||||
});
|
||||
|
||||
return found ? undefined : DeviceErrors.NOT_FOUND;
|
||||
}
|
||||
|
||||
async delete(deviceId: string): Promise<DeviceError | undefined> {
|
||||
let found = false;
|
||||
|
||||
this.db.data.devices = this.db.data.devices.filter((d) => {
|
||||
if (d.id === deviceId) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
this.db.data.users.forEach((u) => {
|
||||
u.devices = u.devices.filter((d) => d !== deviceId);
|
||||
});
|
||||
this.db.data.groups.forEach((g) => {
|
||||
g.devices = g.devices.filter((d) => d !== deviceId);
|
||||
});
|
||||
|
||||
await this.db.write();
|
||||
}
|
||||
|
||||
return found ? undefined : DeviceErrors.NOT_FOUND;
|
||||
}
|
||||
}
|
||||
86
src/lib/server/db/repos/lowdb/groupRepo.ts
Normal file
86
src/lib/server/db/repos/lowdb/groupRepo.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import type { IGroupRepo } from '../groupRepo';
|
||||
import type { Low } from 'lowdb';
|
||||
import { type New, type Updated } from '../../types';
|
||||
import { type Group, type GroupError, GroupErrors } from '../../types/group';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { LowStorage } from './_init';
|
||||
|
||||
export class LowGroupRepo implements IGroupRepo {
|
||||
private readonly db;
|
||||
|
||||
constructor(db: Low<LowStorage.LowData>) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getAll(): Promise<Group[]> {
|
||||
return this.db.data.groups;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<Group | undefined> {
|
||||
return this.db.data.groups.find((g) => g.id === id);
|
||||
}
|
||||
|
||||
async create(group: New<Group>): Promise<GroupError | undefined> {
|
||||
if (this.db.data.groups.find((g) => g.name === group.name)) {
|
||||
return GroupErrors.DUPLICATE_NAME;
|
||||
}
|
||||
|
||||
for (const deviceId of group.devices) {
|
||||
if (!this.db.data.devices.some((d) => d.id === deviceId)) {
|
||||
return GroupErrors.UNKNOWN_DEVICE;
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.update(({ groups }) => {
|
||||
groups.push({ id: nanoid(), ...group });
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async update(group: Updated<Group>): Promise<GroupError | undefined> {
|
||||
if (this.db.data.groups.find((g) => g.name === group.name && g.id !== group.id)) {
|
||||
return GroupErrors.DUPLICATE_NAME;
|
||||
}
|
||||
|
||||
for (const deviceId of group.devices ?? []) {
|
||||
if (!this.db.data.devices.some((d) => d.id === deviceId)) {
|
||||
return GroupErrors.UNKNOWN_DEVICE;
|
||||
}
|
||||
}
|
||||
|
||||
let found = false;
|
||||
await this.db.update(({ groups }) => {
|
||||
const existingGroup = groups.find((g) => g.id === group.id);
|
||||
if (!existingGroup) return;
|
||||
|
||||
Object.assign(existingGroup, group);
|
||||
found = true;
|
||||
});
|
||||
|
||||
return found ? undefined : GroupErrors.NOT_FOUND;
|
||||
}
|
||||
|
||||
async delete(groupId: string): Promise<GroupError | undefined> {
|
||||
let found = false;
|
||||
|
||||
this.db.data.groups = this.db.data.groups.filter((g) => {
|
||||
if (g.id === groupId) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
this.db.data.users.forEach((u) => (u.groups = u.groups.filter((gid) => gid != groupId)));
|
||||
await this.db.write();
|
||||
}
|
||||
|
||||
return found ? undefined : GroupErrors.NOT_FOUND;
|
||||
}
|
||||
|
||||
async countUsers(groupId: string): Promise<number> {
|
||||
return this.db.data.users.filter((u) => u.groups.includes(groupId)).length;
|
||||
}
|
||||
}
|
||||
113
src/lib/server/db/repos/lowdb/userRepo.ts
Normal file
113
src/lib/server/db/repos/lowdb/userRepo.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { Low } from 'lowdb';
|
||||
import { type New, type Updated } from '../../types';
|
||||
import { UserErrors, type User } from '../../types/user';
|
||||
import type { IUserRepo } from '../userRepo';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { LowStorage } from './_init';
|
||||
|
||||
export class LowUserRepo implements IUserRepo {
|
||||
private readonly db;
|
||||
|
||||
constructor(db: Low<LowStorage.LowData>) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async any() {
|
||||
return this.db.data.users[0] != undefined;
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
return this.db.data.users;
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return this.db.data.users.find((u) => u.id == id);
|
||||
}
|
||||
|
||||
async getByName(name: string) {
|
||||
return this.db.data.users.find((u) => u.name == name);
|
||||
}
|
||||
|
||||
async create(user: New<User>) {
|
||||
if (!this.db.data.users.find((u) => u.name == user.name)) {
|
||||
return UserErrors.DUPLICATE_NAME;
|
||||
}
|
||||
|
||||
for (let id of user.devices) {
|
||||
if (!this.db.data.devices.some((d) => d.id == id)) {
|
||||
return UserErrors.UNKNOWN_DEVICE;
|
||||
}
|
||||
}
|
||||
|
||||
for (let id of user.groups) {
|
||||
if (!this.db.data.groups.some((g) => g.id == id)) {
|
||||
return UserErrors.UNKNOWN_GROUP;
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.update(({ users }) => {
|
||||
users.push({ id: nanoid(), ...user });
|
||||
});
|
||||
}
|
||||
|
||||
async update(user: Updated<User>) {
|
||||
if (this.db.data.users.find((u) => u.name == user.name && u.id != user.id)) {
|
||||
return UserErrors.DUPLICATE_NAME;
|
||||
}
|
||||
|
||||
for (let id of user.devices ?? []) {
|
||||
if (!this.db.data.devices.some((d) => d.id == id)) {
|
||||
return UserErrors.UNKNOWN_DEVICE;
|
||||
}
|
||||
}
|
||||
|
||||
for (let id of user.groups ?? []) {
|
||||
if (!this.db.data.groups.some((g) => g.id == id)) {
|
||||
return UserErrors.UNKNOWN_GROUP;
|
||||
}
|
||||
}
|
||||
|
||||
let found = false;
|
||||
await this.db.update(({ users }) => {
|
||||
let existing = users.find((u) => u.id == user.id);
|
||||
if (!existing) return;
|
||||
|
||||
Object.assign(existing, user);
|
||||
found = true;
|
||||
});
|
||||
|
||||
return found ? undefined : UserErrors.NOT_FOUND;
|
||||
}
|
||||
|
||||
async fetchDevices(userId: string) {
|
||||
const user = this.db.data.users.find((u) => u.id === userId)!;
|
||||
|
||||
if (user.admin) return this.db.data.devices;
|
||||
|
||||
const userDevices = this.db.data.devices.filter((d) => user.devices.includes(d.id));
|
||||
const userGroups = this.db.data.groups.filter((g) => user.groups.includes(g.id));
|
||||
const groupDevices = userGroups.flatMap((g) =>
|
||||
this.db.data.devices.filter((d) => g.devices.includes(d.id)),
|
||||
);
|
||||
|
||||
// concat and dedupe
|
||||
return [...new Set([...userDevices, ...groupDevices])];
|
||||
}
|
||||
|
||||
async delete(userId: string) {
|
||||
let found = false;
|
||||
|
||||
this.db.data.users = this.db.data.users.filter((u) => {
|
||||
if (u.id == userId) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await this.db.write();
|
||||
|
||||
return found ? undefined : UserErrors.NOT_FOUND;
|
||||
}
|
||||
}
|
||||
14
src/lib/server/db/repos/userRepo.ts
Normal file
14
src/lib/server/db/repos/userRepo.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { New, Updated } from '../types';
|
||||
import type { Device } from '../types/device';
|
||||
import type { User, UserError } from '../types/user';
|
||||
|
||||
export interface IUserRepo {
|
||||
any(): Promise<boolean>;
|
||||
getAll(): Promise<User[]>;
|
||||
getById(id: string): Promise<User | undefined>;
|
||||
getByName(name: string): Promise<User | undefined>;
|
||||
fetchDevices(userId: string): Promise<Device[]>;
|
||||
create(user: New<User>): Promise<UserError | undefined>;
|
||||
update(user: Updated<User>): Promise<UserError | undefined>;
|
||||
delete(userId: string): Promise<UserError | undefined>;
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import { AppError } from '.';
|
||||
|
||||
export type Device = {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -6,3 +8,16 @@ export type Device = {
|
||||
port: number;
|
||||
packets: number;
|
||||
};
|
||||
|
||||
export class DeviceError extends AppError {
|
||||
constructor(message: string, status: number) {
|
||||
super(message, status);
|
||||
this.name = 'DeviceError';
|
||||
Object.setPrototypeOf(this, DeviceError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace DeviceErrors {
|
||||
export const NOT_FOUND = new DeviceError('Group not found.', 404);
|
||||
export const DUPLICATE_NAME = new DeviceError('Device with this name already exists.', 409);
|
||||
}
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
import { AppError } from '.';
|
||||
|
||||
export type Group = {
|
||||
id: string;
|
||||
name: string;
|
||||
devices: string[];
|
||||
};
|
||||
|
||||
export class GroupError extends AppError {
|
||||
constructor(message: string, status: number) {
|
||||
super(message, status);
|
||||
this.name = 'GroupError';
|
||||
Object.setPrototypeOf(this, GroupError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace GroupErrors {
|
||||
export const NOT_FOUND = new GroupError('Group not found.', 404);
|
||||
export const DUPLICATE_NAME = new GroupError('Group with this name already exists.', 409);
|
||||
export const UNKNOWN_DEVICE = new GroupError(
|
||||
'One or more of the selected devices do not exist.',
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
38
src/lib/server/db/types/index.ts
Normal file
38
src/lib/server/db/types/index.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
|
||||
/** @deprecated unused for now - keeping around just in case but should be removed someday */
|
||||
export class Result<TOk, TErr extends Error> {
|
||||
public readonly err;
|
||||
private readonly res;
|
||||
|
||||
constructor(res?: TOk, err?: TErr) {
|
||||
this.res = res;
|
||||
this.err = err;
|
||||
}
|
||||
|
||||
public unwrap(): TOk {
|
||||
if (this.err) {
|
||||
console.error('Tried to unwrap a Result that contained an error');
|
||||
throw this.err;
|
||||
}
|
||||
|
||||
return this.res!;
|
||||
}
|
||||
}
|
||||
|
||||
export class AppError extends Error {
|
||||
public readonly status: number;
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = 'AppError';
|
||||
this.status = status;
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
}
|
||||
|
||||
public toFail() {
|
||||
return fail(this.status, { error: this.message });
|
||||
}
|
||||
}
|
||||
|
||||
export type New<T> = Omit<T, 'id'>;
|
||||
export type Updated<T> = Partial<T> & { id: string };
|
||||
@ -1,3 +1,5 @@
|
||||
import { AppError } from '.';
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -14,3 +16,24 @@ export function toPublicUser(user: User): PublicUser {
|
||||
delete clonedUser.password;
|
||||
return clonedUser as PublicUser;
|
||||
}
|
||||
|
||||
export class UserError extends AppError {
|
||||
constructor(message: string, status: number) {
|
||||
super(message, status);
|
||||
this.name = 'UserError';
|
||||
Object.setPrototypeOf(this, UserError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UserErrors {
|
||||
export const NOT_FOUND = new UserError('User not found.', 404);
|
||||
export const DUPLICATE_NAME = new UserError('User with this username already exists.', 409);
|
||||
export const UNKNOWN_GROUP = new UserError(
|
||||
'One or more of the selected groups do not exist.',
|
||||
400,
|
||||
);
|
||||
export const UNKNOWN_DEVICE = new UserError(
|
||||
'One or more of the selected devices do not exist.',
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { db } from './db';
|
||||
import { users } from './db';
|
||||
|
||||
type SessionData = {
|
||||
userId: string;
|
||||
@ -15,13 +15,13 @@ export function createSession(data: SessionData) {
|
||||
return token;
|
||||
}
|
||||
|
||||
export function getUserFromSession(sessionId?: string) {
|
||||
export async function getUserFromSession(sessionId?: string) {
|
||||
if (!sessionId) return undefined;
|
||||
|
||||
const data = sessions.get(sessionId);
|
||||
if (!data) return undefined;
|
||||
|
||||
return db.data.users.find((u) => u.id === data.userId);
|
||||
return await users.getById(data.userId);
|
||||
}
|
||||
|
||||
export function deleteSession(sessionId?: string) {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { getUsersDevices } from '$lib/server/db';
|
||||
import { users } from '$lib/server/db';
|
||||
import type { ServerLoad } from '@sveltejs/kit';
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard } }) => {
|
||||
const user = guard.requiresAuth().orRedirects().getUser();
|
||||
return {
|
||||
devices: getUsersDevices(user.id),
|
||||
devices: await users.fetchDevices(user.id),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { db, getUsersDevices } from '$lib/server/db/index.js';
|
||||
import { FORBIDDEN, PARSE_ERROR, SUCCESS } from '$lib/server/commonResponses';
|
||||
import { devices, users } from '$lib/server/db/index.js';
|
||||
import { fail, redirect, type Actions, type ServerLoad } from '@sveltejs/kit';
|
||||
import { nanoid } from 'nanoid';
|
||||
import validator from 'validator';
|
||||
import { wake } from 'wake_on_lan';
|
||||
import { z } from 'zod';
|
||||
@ -8,7 +8,7 @@ import { z } from 'zod';
|
||||
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
const device = db.data.devices.find((d) => d.id === params.id);
|
||||
const device = await devices.getById(params.id ?? '');
|
||||
|
||||
if (!device && params.id !== 'new') {
|
||||
redirect(302, '/dash/devices');
|
||||
@ -21,7 +21,7 @@ export const load: ServerLoad = async ({ locals: { guard }, params }) => {
|
||||
|
||||
export const actions = {
|
||||
update: async ({ request, params, locals: { guard } }) => {
|
||||
if (guard.requiresAdmin().isFailed()) return fail(403);
|
||||
if (guard.requiresAdmin().isFailed()) return FORBIDDEN;
|
||||
|
||||
const form = await request.formData();
|
||||
|
||||
@ -55,54 +55,29 @@ export const actions = {
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return fail(400, { error: parsed.error.errors[0].message });
|
||||
return PARSE_ERROR(parsed.error);
|
||||
}
|
||||
|
||||
if (params.id === 'new') {
|
||||
await db.update(({ devices }) => {
|
||||
devices.push({ id: nanoid(), ...parsed.data });
|
||||
});
|
||||
const err =
|
||||
params.id === 'new'
|
||||
? await devices.create(parsed.data)
|
||||
: await devices.update({ id: params.id ?? '', ...parsed.data });
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
let found = false;
|
||||
|
||||
await db.update(({ devices }) => {
|
||||
let dev = devices.find((d) => d.id === params.id);
|
||||
if (!dev) return;
|
||||
|
||||
Object.assign(dev, parsed.data);
|
||||
found = true;
|
||||
});
|
||||
|
||||
return found ? { success: true } : fail(404, { error: 'Device not found.' });
|
||||
return err ? err.toFail() : SUCCESS;
|
||||
},
|
||||
|
||||
delete: async ({ locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) {
|
||||
return fail(403);
|
||||
}
|
||||
if (guard.requiresAdmin().isFailed()) return FORBIDDEN;
|
||||
|
||||
db.data.devices = db.data.devices.filter((d) => d.id !== params.id);
|
||||
db.data.users.forEach((u) => {
|
||||
u.devices = u.devices.filter((d) => d !== params.id);
|
||||
});
|
||||
db.data.groups.forEach((g) => {
|
||||
g.devices = g.devices.filter((d) => d !== params.id);
|
||||
});
|
||||
db.write();
|
||||
|
||||
return { success: true };
|
||||
const err = await devices.delete(params.id ?? '');
|
||||
return err ? err.toFail() : SUCCESS;
|
||||
},
|
||||
|
||||
wake: async ({ params, locals: { guard } }) => {
|
||||
guard = guard.requiresAuth();
|
||||
if (guard.isFailed()) return FORBIDDEN;
|
||||
|
||||
if (guard.isFailed()) {
|
||||
console.log('Failed guard');
|
||||
return fail(403);
|
||||
}
|
||||
|
||||
const device = getUsersDevices(guard.getUser().id).find((d) => d.id === params.id);
|
||||
const device = (await users.fetchDevices(guard.getUser().id)).find((d) => d.id === params.id);
|
||||
|
||||
if (!device) {
|
||||
return fail(404);
|
||||
@ -115,5 +90,7 @@ export const actions = {
|
||||
port: device.port,
|
||||
num_packets: device.packets,
|
||||
});
|
||||
|
||||
return SUCCESS;
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { groups } from '$lib/server/db';
|
||||
import { type ServerLoad } from '@sveltejs/kit';
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard } }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
const allGroups = await groups.getAll();
|
||||
let userCounts: { [key: string]: number } = {};
|
||||
|
||||
for (let g of allGroups) {
|
||||
userCounts[g.id] = await groups.countUsers(g.id);
|
||||
}
|
||||
|
||||
return {
|
||||
groups: db.data.groups,
|
||||
userCounts: db.data.groups.reduce(
|
||||
(acc, group) => {
|
||||
acc[group.id] = db.data.users.filter((u) => u.groups.includes(group.id)).length;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
groups: allGroups,
|
||||
userCounts,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { fail, redirect, type Actions, type ServerLoad } from '@sveltejs/kit';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { FORBIDDEN, PARSE_ERROR, SUCCESS } from '$lib/server/commonResponses';
|
||||
import { devices, groups } from '$lib/server/db';
|
||||
import { redirect, type Actions, type ServerLoad } from '@sveltejs/kit';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
const group = db.data.groups.find((g) => g.id === params.id);
|
||||
const group = await groups.getById(params.id ?? '');
|
||||
|
||||
if (!group && params.id !== 'new') {
|
||||
redirect(302, '/dash/groups');
|
||||
}
|
||||
|
||||
return {
|
||||
devices: db.data.devices,
|
||||
devices: await devices.getAll(),
|
||||
group,
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
update: async ({ request, locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) return fail(403);
|
||||
if (guard.requiresAdmin().isFailed()) return FORBIDDEN;
|
||||
|
||||
const form = await request.formData();
|
||||
|
||||
@ -29,12 +29,7 @@ export const actions = {
|
||||
.string({ message: 'Name is required.' })
|
||||
.min(3, { message: 'Name must be at least 3 characters.' })
|
||||
.max(24, { message: 'Name must be at most 24 characters.' }),
|
||||
|
||||
devices: z.array(
|
||||
z.string().refine((v) => db.data.devices.find((d) => d.id === v), {
|
||||
message: 'Invalid device ID.',
|
||||
}),
|
||||
),
|
||||
devices: z.array(z.string()),
|
||||
});
|
||||
|
||||
const parsed = schema.safeParse({
|
||||
@ -43,41 +38,21 @@ export const actions = {
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return fail(400, { error: parsed.error.errors[0].message });
|
||||
return PARSE_ERROR(parsed.error);
|
||||
}
|
||||
|
||||
if (params.id === 'new') {
|
||||
await db.update(({ groups }) => {
|
||||
groups.push({ id: nanoid(), ...parsed.data });
|
||||
});
|
||||
const err =
|
||||
params.id === 'new'
|
||||
? await groups.create({ ...parsed.data })
|
||||
: await groups.update({ id: params.id ?? '', ...parsed.data });
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
let found = false;
|
||||
|
||||
await db.update(({ groups }) => {
|
||||
let group = groups.find((g) => g.id === params.id);
|
||||
if (!group) return;
|
||||
|
||||
Object.assign(group, parsed.data);
|
||||
found = true;
|
||||
});
|
||||
|
||||
return found ? { success: true } : fail(400, { error: 'Group not found.' });
|
||||
return err ? err.toFail() : SUCCESS;
|
||||
},
|
||||
|
||||
delete: async ({ locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) {
|
||||
return fail(403);
|
||||
}
|
||||
if (guard.requiresAdmin().isFailed()) return FORBIDDEN;
|
||||
|
||||
db.data.groups = db.data.groups.filter((g) => g.id !== params.id);
|
||||
db.data.users.forEach((u) => {
|
||||
u.groups = u.groups.filter((g) => g !== params.id);
|
||||
});
|
||||
db.write();
|
||||
|
||||
redirect(302, '/dashboard/groups');
|
||||
const err = await groups.delete(params.id ?? '');
|
||||
return err ? err.toFail() : SUCCESS;
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { users } from '$lib/server/db';
|
||||
import { toPublicUser, type User } from '$lib/server/db/types/user';
|
||||
import type { ServerLoad } from '@sveltejs/kit';
|
||||
|
||||
@ -6,6 +6,6 @@ export const load: ServerLoad = async ({ locals: { guard } }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
return {
|
||||
users: db.data.users.map((u) => toPublicUser(u)),
|
||||
users: (await users.getAll()).map((u) => toPublicUser(u)),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,29 +1,30 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { FORBIDDEN, PARSE_ERROR, SUCCESS } from '$lib/server/commonResponses';
|
||||
import { devices, groups, users } from '$lib/server/db';
|
||||
import type { Updated } from '$lib/server/db/types';
|
||||
import { toPublicUser, type User } from '$lib/server/db/types/user.js';
|
||||
import { fail, redirect, type Actions, type ServerLoad } from '@sveltejs/kit';
|
||||
import { redirect, type Actions, type ServerLoad } from '@sveltejs/kit';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
let user = db.data.users.find((u) => u.id === params.id);
|
||||
let user = await users.getById(params.id ?? '');
|
||||
|
||||
if (!user && params.id !== 'new') {
|
||||
redirect(302, '/dashboard/users');
|
||||
redirect(302, '/dash/users');
|
||||
}
|
||||
|
||||
return {
|
||||
user: toPublicUser(user!),
|
||||
groups: db.data.groups,
|
||||
devices: db.data.devices,
|
||||
groups: await groups.getAll(),
|
||||
devices: await devices.getAll(),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
update: async ({ request, locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) return fail(403);
|
||||
if (guard.requiresAdmin().isFailed()) return FORBIDDEN;
|
||||
|
||||
const form = await request.formData();
|
||||
|
||||
@ -35,23 +36,16 @@ export const actions = {
|
||||
admin: z.boolean(),
|
||||
password: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((v) => (params.id === 'new' ? v && v.length > 0 : true), {
|
||||
.refine((v) => (params.id === 'new' ? v.length > 0 : true), {
|
||||
// if /new, password must be present
|
||||
message: 'Password is required at user creation.',
|
||||
})
|
||||
.refine((v) => !v || v.length >= 8, {
|
||||
.refine((v) => (v.length > 0 ? v.length >= 8 : true), {
|
||||
// if password present, must be at least 8 chars
|
||||
message: 'Password must be at least 8 characters.',
|
||||
}),
|
||||
groups: z.array(
|
||||
z.string().refine((v) => db.data.groups.find((g) => g.id === v), {
|
||||
message: 'Invalid group ID.',
|
||||
}),
|
||||
),
|
||||
devices: z.array(
|
||||
z.string().refine((v) => db.data.devices.find((d) => d.id === v), {
|
||||
message: 'Invalid device ID.',
|
||||
}),
|
||||
),
|
||||
groups: z.array(z.string()),
|
||||
devices: z.array(z.string()),
|
||||
});
|
||||
|
||||
const parsed = schema.safeParse({
|
||||
@ -63,49 +57,35 @@ export const actions = {
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return fail(400, { error: parsed.error.errors[0].message });
|
||||
return PARSE_ERROR(parsed.error);
|
||||
}
|
||||
|
||||
if (params.id === 'new') {
|
||||
await db.update(({ users }) => {
|
||||
users.push({
|
||||
id: nanoid(),
|
||||
name: parsed.data.name,
|
||||
admin: parsed.data.admin,
|
||||
groups: parsed.data.groups,
|
||||
devices: parsed.data.devices,
|
||||
password: bcrypt.hashSync(parsed.data.password!, 10),
|
||||
});
|
||||
const err = await users.create({
|
||||
...parsed.data,
|
||||
password: bcrypt.hashSync(parsed.data.password, 10),
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
return err ? err.toFail() : SUCCESS;
|
||||
}
|
||||
|
||||
let found = false;
|
||||
await db.update(({ users }) => {
|
||||
let user = users.find((u) => u.id === params.id);
|
||||
if (!user) return;
|
||||
let updatedUser: Updated<User> = { id: params.id ?? '', ...parsed.data };
|
||||
|
||||
user.name = parsed.data.name;
|
||||
user.admin = parsed.data.admin;
|
||||
user.groups = parsed.data.groups;
|
||||
user.devices = parsed.data.devices;
|
||||
if (parsed.data.password.length > 0) {
|
||||
updatedUser.password = bcrypt.hashSync(parsed.data.password, 10);
|
||||
} else {
|
||||
// avoid saving empty passwpord
|
||||
delete updatedUser.password;
|
||||
}
|
||||
|
||||
if (parsed.data.password && parsed.data.password.length > 0) {
|
||||
user.password = bcrypt.hashSync(parsed.data.password, 10);
|
||||
}
|
||||
|
||||
found = true;
|
||||
});
|
||||
|
||||
return found ? { success: true } : fail(404, { error: 'User not found.' });
|
||||
const err = await users.update(updatedUser);
|
||||
return err ? err.toFail() : SUCCESS;
|
||||
},
|
||||
delete: async ({ locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) {
|
||||
return fail(403);
|
||||
}
|
||||
|
||||
db.data.users = db.data.users.filter((u) => u.id !== params.id);
|
||||
db.write();
|
||||
delete: async ({ locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) return FORBIDDEN;
|
||||
|
||||
const err = await users.delete(params.id ?? '');
|
||||
return err ? err.toFail() : SUCCESS;
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<p class="text-white">{form?.error}</p>
|
||||
<EditPage createOnly={!data.user}>
|
||||
<InputText
|
||||
name="name"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { createSession, getUserFromSession } from '$lib/server/sessions';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { users } from '$lib/server/db';
|
||||
import { createSession } from '$lib/server/sessions';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import type { Actions } from './$types';
|
||||
import { dev } from '$app/environment';
|
||||
@ -16,17 +16,17 @@ export const actions = {
|
||||
const password = data.get('password')?.toString();
|
||||
|
||||
if (!username || !password) {
|
||||
return {
|
||||
return fail(400, {
|
||||
error: 'MISSING_CREDENTIALS',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const user = db.data.users.find((u) => u.name === username);
|
||||
const user = await users.getByName(username);
|
||||
|
||||
if (!user || !bcrypt.compareSync(password, user.password)) {
|
||||
return {
|
||||
return fail(403, {
|
||||
error: 'INVALID_CREDENTIALS',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
cookies.set(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user