WIP: Fastify is drunk

This commit is contained in:
Holly Stubbs 2025-01-06 06:53:13 +00:00
parent 2c5e40b36f
commit 24247d938f
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
20 changed files with 472 additions and 134 deletions

View file

@ -46,7 +46,7 @@ export default class AccountController extends Controller {
}
const username = registerViewModel.username.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
if (!await UserService.CreateUser(0, username, registerViewModel.email.trim(), registerViewModel.password)) {
if (!await UserService.CreateUser(1, username, registerViewModel.email.trim(), registerViewModel.password)) {
registerViewModel.password = "";
registerViewModel.password2 = "";
registerViewModel.message = "Sorry! That username is already taken.";

View file

@ -1,10 +1,15 @@
import Config from "../objects/Config";
import HashFS from "../objects/HashFS";
import UserService from "../services/UserService";
import Controller from "./Controller";
import { randomBytes } from "crypto";
export default class HomeController extends Controller {
public Index_Get_AllowAnonymous() {
if (this.session) {
return this.view("dashboard");
}
return this.view();
}
@ -12,16 +17,32 @@ export default class HomeController extends Controller {
const data = await this.req.file();
if (data && data.type === "file") {
let uploadKey: string = "";
let host: string = "";
console.log(this.req.headers);
if ("upload-key" in this.req.headers) {
// @ts-ignore
uploadKey = this.req.headers["upload-key"];
if (uploadKey !== Config.accounts.signup.key) {
return this.unauthorised("Upload key invalid or missing.");
}
} else {
return this.unauthorised("Upload key invalid or missing.");
}
//console.log(uploadKey);
//console.log(data.mimetype);
const hash = await HashFS.GetHashFSInstance("images").AddFromStream(data.file);
if ("host" in this.req.headers) {
// @ts-ignore
host = this.req.headers["host"];
} else {
return this.badRequest("Host header missing?!");
}
const user = await UserService.GetByUploadKey(uploadKey);
if (!user) {
return this.unauthorised("Upload key invalid or missing.");
}
const fileUrl = await UserService.UploadMedia(user.Id, host, data);
if (!fileUrl) {
return this.badRequest("This domain is not registered to your EUS account.");
}
return this.ok(fileUrl);
}
return this.badRequest();

14
entities/Domain.ts Normal file
View file

@ -0,0 +1,14 @@
export default class Domain {
public Id: number = Number.MIN_VALUE;
public UserId: number = Number.MIN_VALUE;
public HasHttps: boolean = false;
public Domain: string = "";
public Active: boolean = false;
public CreatedByUserId = Number.MIN_VALUE;
public CreatedDatetime = new Date(0);
public LastModifiedByUserId?: number;
public LastModifiedDatetime?: Date;
public DeletedByUserId?: number;
public DeletedDatetime?: Date;
public IsDeleted: boolean = false;
}

View file

@ -1,10 +1,10 @@
export default class Image {
export default class Media {
public Id: number = Number.MIN_VALUE;
public UserId: number = Number.MIN_VALUE;
public DomainId: number = Number.MIN_VALUE;
public FileName: string = "";
public ImageTag: string = "";
public ImageType: string = "";
public MediaTag: string = "";
public MediaType: string = "";
public Hash: string = "";
public FileSize: number = Number.MIN_VALUE;
public CreatedByUserId: number = Number.MIN_VALUE;

View file

@ -16,6 +16,10 @@ import { magenta, blue, cyan, green, red } from "dyetty";
import ConsoleUtility from "./utilities/ConsoleUtility";
import HashFS from "./objects/HashFS";
import { existsSync, mkdirSync, rmSync } from "fs";
import FunkyArray from "funky-array";
import UserService from "./services/UserService";
import MediaService from "./services/MediaService";
import Media from "./entities/Media";
Console.customHeader(`EUS server started at ${new Date()}`);
@ -44,29 +48,52 @@ fastify.register(FastifyCookie, {
fastify.register(FastifyStatic, {
root: join(__dirname, "wwwroot"),
preCompressed: true
//prefix: `${Config.ports.web}/static/`
});
fastify.addHook("preValidation", (req, res, done) => {
// @ts-ignore
req.startTime = Date.now();
// * Take usual controller path if this path is registered.
if (Controller.RegisteredPaths.includes(req.url)) {
const hashLookupCache = new FunkyArray<string, Media>();
fastify.addHook("preHandler", (req, res, done) => {
(async () => {
// @ts-ignore
req.logType = cyan("CONTROLLER");
return done();
} else {
// @ts-ignore
req.logType = magenta("STATIC");
}
req.startTime = Date.now();
done();
// * Take usual controller path if this path is registered.
if (Controller.RegisteredPaths.includes(req.url)) {
// @ts-ignore
req.logType = cyan("CONTROLLER");
return done();
} else {
const urlParts = req.url.split("/");
if (urlParts.length === 2 && urlParts[1].length === 16) {
let media = hashLookupCache.get(urlParts[1]) ?? null;
if (!media) {
media = await MediaService.GetByTag(urlParts[1]);
if (media) {
hashLookupCache.set(urlParts[1], media);
}
}
if (media) {
// @ts-ignore
req.logType = cyan("IMAGE");
const fileStore = HashFS.GetHashFSInstance("images");
res.header("content-type", media.MediaType);
res.sendFile(fileStore.GetRelativePath(media.Hash), fileStore.path, { contentType: false });
//return done();
return;
}
}
// @ts-ignore
req.logType = magenta("STATIC");
}
done();
})();
});
fastify.addHook("onSend", (req, res, _payload, done) => {
// @ts-ignore
Console.printInfo(`[ ${req.logType} ] [ ${ConsoleUtility.StatusColor(res.statusCode)} ] [ ${blue(`${Date.now() - req.startTime}ms`)} ] > ${req.url}`);
Console.printInfo(`[ ${req.logType} ] [ ${req.method.toUpperCase()} ] [ ${ConsoleUtility.StatusColor(res.statusCode)} ] [ ${blue(`${Date.now() - req.startTime}ms`)} ] > ${req.url}`);
done();
});

View file

@ -2,7 +2,7 @@ import { blue } from "dyetty";
import { Console } from "hsconsole";
import { createPool, Pool, RowDataPacket } from "mysql2";
export type DBInDataType = string | number | null | undefined;
export type DBInDataType = string | number | Date | null | undefined;
export default class Database {
private connectionPool:Pool;

View file

@ -6,7 +6,8 @@ import { Console } from "hsconsole";
import { yellow } from "dyetty";
import { createHash, randomBytes } from "crypto";
import FunkyArray from "funky-array";
import { Stream } from "stream";
import HashFSFileInformation from "./HashFSFileInformation";
import { BusboyFileStream } from "@fastify/busboy"
export default class HashFS {
public static STARTUP_DIR: string;
@ -21,7 +22,7 @@ export default class HashFS {
return instance;
}
private readonly path: string;
public readonly path: string;
private readonly tempPath: string;
private readonly folder: string;
@ -73,58 +74,85 @@ export default class HashFS {
this.logInfo(`Ready!`);
}
private getFilePath(hash: string) {
public GetFilePath(hash: string) {
return join(this.path, hash[0], `${hash[0]}${hash[1]}`, hash);
}
public GetRelativePath(hash: string) {
return join(hash[0], `${hash[0]}${hash[1]}`, hash);
}
public AddFile(contents: Buffer | string) {
return new Promise<string>(async (resolve, reject) => {
return new Promise<HashFSFileInformation>(async (resolve, reject) => {
const hasher = createHash("sha1");
hasher.setEncoding("hex");
hasher.write(contents);
hasher.end();
const hash: string = hasher.read();
const fileInfo = new HashFSFileInformation();
fileInfo.fileHash = hash;
fileInfo.fileSize = contents.length;
if (await this.FileExists(hash)) {
return resolve(hash);
fileInfo.fileExistsAlready = true;
this.logInfo(`File with hash "${hash}" already exists.`);
return resolve(fileInfo);
}
writeFile(this.getFilePath(hash), contents, (err) => {
writeFile(this.GetFilePath(hash), contents, (err) => {
if (err) {
return reject(err);
}
resolve(hash);
resolve(fileInfo);
})
});
}
public AddFromStream(stream: Stream) {
return new Promise<string>(async (resolve, reject) => {
public AddFromStream(stream: BusboyFileStream) {
return new Promise<HashFSFileInformation>(async (resolve, reject) => {
const hasher = createHash("sha1");
hasher.setEncoding("hex");
const tempFilePath = join(this.tempPath, randomBytes(16).toString("base64url"));
const tempFile = createWriteStream(tempFilePath);
tempFile.on("close", async () => {
const hash: string = hasher.read();
const fileInfo = new HashFSFileInformation();
fileInfo.fileHash = hash;
fileInfo.fileSize = fileSize;
if (await this.FileExists(hash)) {
fileInfo.fileExistsAlready = true;
rm(tempFilePath, err => {
if (err) {
return reject(err);
}
this.logInfo(`File with hash "${hash}" already exists.`);
resolve(fileInfo);
});
} else {
rename(tempFilePath, this.GetFilePath(hash), err => {
if (err) {
return reject(err);
}
this.logInfo(`Stored file as ${hash}`);
resolve(fileInfo);
});
}
});
stream.pipe(tempFile);
stream.pipe(hasher);
tempFile.on("close", () => {
const hash: string = hasher.read();
rename(tempFilePath, this.getFilePath(hash), (err) => {
if (err) {
return reject(err);
}
this.logInfo(`Stored file as ${hash}`);
resolve(hash);
});
});
let fileSize = 0;
stream.on("data", chunk => fileSize += chunk.length);
});
}
public FileExists(hash: string) {
return new Promise<boolean>((resolve, reject) => {
stat(this.getFilePath(hash), (err, _stat) => {
stat(this.GetFilePath(hash), (err, _stat) => {
if (err) {
if (err.code === "ENOENT") {
resolve(false);

View file

@ -0,0 +1,5 @@
export default class HashFSFileInformation {
public fileHash: string = "";
public fileSize: number = Number.MIN_VALUE;
public fileExistsAlready: boolean = false;
}

68
repos/DomainRepo.ts Normal file
View file

@ -0,0 +1,68 @@
import Domain from "../entities/Domain";
import Database from "../objects/Database";
export default class DomainRepo {
public static async SelectAll() {
const dbMedia = await Database.Instance.query("SELECT * FROM Domain WHERE IsDeleted = 0");
const mediaList = new Array<Domain>();
for (const row of dbMedia) {
const media = new Domain();
PopulateDomainFromDB(media, row);
mediaList.push(media);
}
return mediaList;
}
public static async SelectById(id: number) {
const dbMedia = await Database.Instance.query("SELECT * FROM Domain WHERE Id = ? LIMIT 1", [id]);
if (dbMedia == null || dbMedia.length === 0) {
return null;
} else {
const media = new Domain();
PopulateDomainFromDB(media, dbMedia[0]);
return media;
}
}
public static async SelectByDomain(domain: string) {
const dbMedia = await Database.Instance.query("SELECT * FROM Domain WHERE Domain = ? AND IsDeleted = 0 LIMIT 1", [domain]);
if (dbMedia == null || dbMedia.length === 0) {
return null;
} else {
const media = new Domain();
PopulateDomainFromDB(media, dbMedia[0]);
return media;
}
}
public static async InsertUpdate(domain: Domain) {
if (domain.Id === Number.MIN_VALUE) {
domain.Id = (await Database.Instance.query("INSERT Domain (UserId, DomainId, FileName, MediaTag, MediaType, Hash, FileSize, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
domain.UserId, Number(domain.HasHttps), domain.Domain, Number(domain.Active), domain.CreatedByUserId, domain.CreatedDatetime, domain.LastModifiedByUserId ?? null, domain.LastModifiedDatetime ?? null, domain.DeletedByUserId ?? null, domain.DeletedDatetime ?? null, Number(domain.IsDeleted)
]))[0]["Id"];
} else {
await Database.Instance.query(`UPDATE Media SET UserId = ?, HasHttps = ?, Domain = ?, Active = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
domain.UserId, Number(domain.HasHttps), domain.Domain, Number(domain.Active), domain.CreatedByUserId, domain.CreatedDatetime, domain.LastModifiedByUserId ?? null, domain.LastModifiedDatetime ?? null, domain.DeletedByUserId ?? null, domain.DeletedDatetime ?? null, Number(domain.IsDeleted), domain.Id
]);
}
return domain;
}
}
function PopulateDomainFromDB(domain: Domain, dbDomain: any) {
domain.Id = dbDomain.Id;
domain.UserId = dbDomain.UserId;
domain.HasHttps = dbDomain.HasHttps[0] === 1;
domain.Domain = dbDomain.Domain;
domain.Active = dbDomain.Active[0] === 1;
domain.CreatedByUserId = dbDomain.CreatedByUserId;
domain.CreatedDatetime = dbDomain.CreatedDatetime;
domain.LastModifiedByUserId = dbDomain.LastModifiedByUserId;
domain.LastModifiedDatetime = dbDomain.LastModifiedDatetime;
domain.DeletedByUserId = dbDomain.DeletedByUserId;
domain.DeletedDatetime = dbDomain.DeletedDatetime;
domain.IsDeleted = dbDomain.IsDeleted[0] === 1;
}

View file

@ -1,61 +0,0 @@
import Image from "../entities/Image";
import Database from "../objects/Database";
import RepoBase from "./RepoBase";
export default abstract class ImageRepo {
public static async SelectAll() {
const dbImage = await Database.Instance.query("SELECT * FROM Image WHERE IsDeleted = 0");
const images = new Array<Image>();
for (const row of dbImage) {
const image = new Image();
PopulateImageFromDB(image, row);
images.push(image);
}
return images;
}
public static async SelectById(id:number) {
const dbImage = await Database.Instance.query("SELECT * FROM Image WHERE Id = ? LIMIT 1", [id]);
if (dbImage == null || dbImage.length === 0) {
return null;
} else {
const image = new Image();
PopulateImageFromDB(image, dbImage[0]);
return image;
}
}
public static async InsertUpdate(image: Image) {
if (image.Id === Number.MIN_VALUE) {
image.Id = (await Database.Instance.query("INSERT Image (UserId, DomainId, FileName, ImageTag, ImageType, Hash, FileSize, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
image.UserId, image.DomainId, image.FileName, image.ImageTag, image.Hash, image.FileSize, image.CreatedByUserId, image.CreatedDatetime.getTime(), image.LastModifiedByUserId ?? null, image.LastModifiedDatetime?.getTime() ?? null, image.DeletedByUserId ?? null, image.DeletedDatetime?.getTime() ?? null, Number(image.IsDeleted)
]))[0]["Id"];
} else {
await Database.Instance.query(`UPDATE Image SET UserId = ?, DomainId = ?, FileName = ?, ImageTag = ?, ImageType = ?, Hash = ?, FileSize = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
image.UserId, image.DomainId, image.FileName, image.ImageTag, image.Hash, image.FileSize, image.CreatedByUserId, image.CreatedDatetime.getTime(), image.LastModifiedByUserId ?? null, image.LastModifiedDatetime?.getTime() ?? null, image.DeletedByUserId ?? null, image.DeletedDatetime?.getTime() ?? null, Number(image.IsDeleted), image.Id
]);
}
return image;
}
}
function PopulateImageFromDB(image: Image, dbImage: any) {
image.Id = dbImage.Id;
image.UserId = dbImage.UserId;
image.DomainId = dbImage.DomainId;
image.FileName = dbImage.FileName;
image.ImageTag = dbImage.ImageTag;
image.ImageType = dbImage.ImageType;
image.Hash = dbImage.Hash;
image.FileSize = dbImage.FileSize;
image.CreatedByUserId = dbImage.CreatedByUserId;
image.CreatedDatetime = new Date(dbImage.CreatedDatetime);
image.LastModifiedByUserId = dbImage.LastModifiedByUserId;
image.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbImage.LastModifiedDatetime);
image.DeletedByUserId = dbImage.DeletedByUserId;
image.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbImage.DeletedDatetime);
image.IsDeleted = dbImage.IsDeleted[0] === 1;
}

106
repos/MediaRepo.ts Normal file
View file

@ -0,0 +1,106 @@
import Database from "../objects/Database";
import Media from "../entities/Media";
export default abstract class MediaRepo {
public static async SelectAll() {
const dbMedia = await Database.Instance.query("SELECT * FROM Media WHERE IsDeleted = 0");
const mediaList = new Array<Media>();
for (const row of dbMedia) {
const media = new Media();
PopulateMediaFromDB(media, row);
mediaList.push(media);
}
return mediaList;
}
public static async SelectById(id: number) {
const dbMedia = await Database.Instance.query("SELECT * FROM Media WHERE Id = ? LIMIT 1", [id]);
if (dbMedia == null || dbMedia.length === 0) {
return null;
} else {
const media = new Media();
PopulateMediaFromDB(media, dbMedia[0]);
return media;
}
}
public static async SelectByMediaTag(mediaTag: string) {
const dbMedia = await Database.Instance.query("SELECT * FROM Media WHERE MediaTag = ? LIMIT 1", [mediaTag]);
if (dbMedia == null || dbMedia.length === 0) {
return null;
} else {
const media = new Media();
PopulateMediaFromDB(media, dbMedia[0]);
return media;
}
}
public static async SelectByUserHash(currentUserId: number, hash: string) {
const dbMedia = await Database.Instance.query("SELECT * FROM Media WHERE Hash = ? LIMIT 1", [hash]);
if (dbMedia == null || dbMedia.length === 0) {
return null;
} else {
const media = new Media();
PopulateMediaFromDB(media, dbMedia[0]);
return media;
}
}
public static async SelectByHash(hash: string) {
const dbMedia = await Database.Instance.query("SELECT * FROM Media WHERE Hash = ? LIMIT 1", [hash]);
if (dbMedia == null || dbMedia.length === 0) {
return null;
} else {
const media = new Media();
PopulateMediaFromDB(media, dbMedia[0]);
return media;
}
}
public static async SelectRecentMedia(userId: number, amount: number) {
const dbMedia = await Database.Instance.query("SELECT * FROM Media WHERE UserId = ? ORDER BY Id DESC LIMIT ?", [userId, amount]);
const mediaList = new Array<Media>();
for (const row of dbMedia) {
const media = new Media();
PopulateMediaFromDB(media, row);
mediaList.push(media);
}
return mediaList;
}
public static async InsertUpdate(media: Media) {
if (media.Id === Number.MIN_VALUE) {
media.Id = (await Database.Instance.query("INSERT Media (UserId, DomainId, FileName, MediaTag, MediaType, Hash, FileSize, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
media.UserId, media.DomainId, media.FileName, media.MediaTag, media.MediaType, media.Hash, media.FileSize, media.CreatedByUserId, media.CreatedDatetime, media.LastModifiedByUserId ?? null, media.LastModifiedDatetime ?? null, media.DeletedByUserId ?? null, media.DeletedDatetime ?? null, Number(media.IsDeleted)
]))[0]["Id"];
} else {
await Database.Instance.query(`UPDATE Media SET UserId = ?, DomainId = ?, FileName = ?, MediaTag = ?, MediaType = ?, Hash = ?, FileSize = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
media.UserId, media.DomainId, media.FileName, media.MediaTag, media.Hash, media.FileSize, media.CreatedByUserId, media.CreatedDatetime, media.LastModifiedByUserId ?? null, media.LastModifiedDatetime ?? null, media.DeletedByUserId ?? null, media.DeletedDatetime ?? null, Number(media.IsDeleted), media.Id
]);
}
return media;
}
}
function PopulateMediaFromDB(media: Media, dbMedia: any) {
media.Id = dbMedia.Id;
media.UserId = dbMedia.UserId;
media.DomainId = dbMedia.DomainId;
media.FileName = dbMedia.FileName;
media.MediaTag = dbMedia.MediaTag;
media.MediaType = dbMedia.MediaType;
media.Hash = dbMedia.Hash;
media.FileSize = dbMedia.FileSize;
media.CreatedByUserId = dbMedia.CreatedByUserId;
media.CreatedDatetime = dbMedia.CreatedDatetime;
media.LastModifiedByUserId = dbMedia.LastModifiedByUserId;
media.LastModifiedDatetime = dbMedia.LastModifiedDatetime;
media.DeletedByUserId = dbMedia.DeletedByUserId;
media.DeletedDatetime = dbMedia.DeletedDatetime;
media.IsDeleted = dbMedia.IsDeleted[0] === 1;
}

View file

@ -1,5 +0,0 @@
export default class RepoBase {
public static convertNullableDatetimeIntToDate(dateTimeInt?:number) {
return dateTimeInt ? new Date(dateTimeInt) : undefined;
}
}

View file

@ -1,5 +1,4 @@
import Database from "../objects/Database";
import RepoBase from "./RepoBase";
import User from "../entities/User";
export default abstract class UserRepo {
@ -16,7 +15,7 @@ export default abstract class UserRepo {
return users;
}
public static async SelectById(id:number) {
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) {
return null;
@ -27,7 +26,7 @@ export default abstract class UserRepo {
}
}
public static async SelectByUsername(username:string) {
public static async SelectByUsername(username: string) {
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE Username = ? LIMIT 1", [username]);
if (dbUser == null || dbUser.length === 0) {
return null;
@ -38,7 +37,29 @@ export default abstract class UserRepo {
}
}
public static async SelectByEmailAddress(emailAddress:string) {
public static async SelectByApiKey(apiKey: string) {
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE ApiKey = ? LIMIT 1", [apiKey]);
if (dbUser == null || dbUser.length === 0) {
return null;
} else {
const user = new User();
PopulateUserFromDB(user, dbUser[0]);
return user;
}
}
public static async SelectByUploadKey(uploadKey: string) {
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE UploadKey = ? LIMIT 1", [uploadKey]);
if (dbUser == null || dbUser.length === 0) {
return null;
} else {
const user = new User();
PopulateUserFromDB(user, dbUser[0]);
return user;
}
}
public static async SelectByEmailAddress(emailAddress: string) {
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE EmailAddress = ? LIMIT 1", [emailAddress]);
if (dbUser == null || dbUser.length === 0) {
return null;
@ -49,14 +70,14 @@ export default abstract class UserRepo {
}
}
public static async InsertUpdate(user:User) {
public static async InsertUpdate(user: User) {
if (user.Id === Number.MIN_VALUE) {
user.Id = (await Database.Instance.query("INSERT User (UserTypeId, Username, EmailAddress, PasswordHash, PasswordSalt, ApiKey, UploadKey, Verified, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
user.UserType, user.Username, user.EmailAddress, user.PasswordHash, user.PasswordSalt, user.ApiKey, user.UploadKey, Number(user.Verified), 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("INSERT User (UserTypeId, Username, EmailAddress, PasswordHash, PasswordSalt, ApiKey, UploadKey, Verified, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
user.UserType, user.Username, user.EmailAddress, user.PasswordHash, user.PasswordSalt, user.ApiKey, user.UploadKey, Number(user.Verified), user.CreatedByUserId, user.CreatedDatetime, user.LastModifiedByUserId ?? null, user.LastModifiedDatetime ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime ?? null, Number(user.IsDeleted)
]))[0]["Id"];
} else {
await Database.Instance.query(`UPDATE User SET UserTypeId = ?, Username = ?, EmailAddress = ?, PasswordHash = ?, PasswordSalt = ?, ApiKey = ?, UploadKey = ?, Verified = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
user.UserType, user.Username, user.EmailAddress, user.PasswordHash, user.PasswordSalt, user.ApiKey, user.UploadKey, Number(user.Verified), user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id
user.UserType, user.Username, user.EmailAddress, user.PasswordHash, user.PasswordSalt, user.ApiKey, user.UploadKey, Number(user.Verified), user.CreatedByUserId, user.CreatedDatetime, user.LastModifiedByUserId ?? null, user.LastModifiedDatetime ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime ?? null, Number(user.IsDeleted), user.Id
]);
}
@ -64,7 +85,7 @@ export default abstract class UserRepo {
}
}
function PopulateUserFromDB(user:User, dbUser:any) {
function PopulateUserFromDB(user: User, dbUser: any) {
user.Id = dbUser.Id;
user.UserType = dbUser.UserTypeId;
user.Username = dbUser.Username;
@ -75,10 +96,10 @@ function PopulateUserFromDB(user:User, dbUser:any) {
user.UploadKey = dbUser.UploadKey;
user.Verified = dbUser.Verified[0] === 1;
user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
user.CreatedDatetime = dbUser.CreatedDatetime;
user.LastModifiedByUserId = dbUser.LastModifiedByUserId;
user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
user.LastModifiedDatetime = dbUser.LastModifiedDatetime;
user.DeletedByUserId = dbUser.DeletedByUserId;
user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
user.DeletedDatetime = dbUser.DeletedDatetime;
user.IsDeleted = dbUser.IsDeleted[0] === 1;
}

22
services/MediaService.ts Normal file
View file

@ -0,0 +1,22 @@
import { Console } from "hsconsole";
import MediaRepo from "../repos/MediaRepo";
export default abstract class MediaService {
public static async GetByHash(hash: string) {
try {
return await MediaRepo.SelectByHash(hash);
} catch (e) {
Console.printError(`EUS server service error:\n${e}`);
throw e;
}
}
public static async GetByTag(tag: string) {
try {
return await MediaRepo.SelectByMediaTag(tag);
} catch (e) {
Console.printError(`EUS server service error:\n${e}`);
throw e;
}
}
}

View file

@ -3,6 +3,12 @@ import UserRepo from "../repos/UserRepo";
import PasswordUtility from "../utilities/PasswordUtility";
import UserType from "../enums/UserType";
import User from "../entities/User";
import MediaRepo from "../repos/MediaRepo";
import { MultipartFile } from "@fastify/multipart"
import HashFS from "../objects/HashFS";
import Media from "../entities/Media";
import { randomBytes } from "crypto";
import DomainRepo from "../repos/DomainRepo";
export default abstract class UserService {
public static async AuthenticateUser(username:string, password:string) {
@ -63,6 +69,9 @@ export default abstract class UserService {
user.EmailAddress = email;
user.PasswordSalt = PasswordUtility.GenerateSalt();
user.PasswordHash = await PasswordUtility.HashPassword(user.PasswordSalt, password);
user.ApiKey = randomBytes(64).toString("base64url");
user.UploadKey = randomBytes(64).toString("base64url");
user.CreatedByUserId = currentUserId;
user.CreatedDatetime = new Date();
@ -74,4 +83,55 @@ export default abstract class UserService {
throw e;
}
}
public static async GetRecentUploads(currentUserId: number) {
try {
return await MediaRepo.SelectRecentMedia(currentUserId, 10);
} catch (e) {
Console.printError(`EUS server service error:\n${e}`);
throw e;
}
}
public static async GetByUploadKey(uploadKey: string) {
try {
return await UserRepo.SelectByUploadKey(uploadKey);
} catch (e) {
Console.printError(`EUS server service error:\n${e}`);
throw e;
}
}
public static async UploadMedia(currentUserId: number, host: string, data: MultipartFile) {
try {
const fileInfo = await HashFS.GetHashFSInstance("images").AddFromStream(data.file);
const domain = await DomainRepo.SelectByDomain(host);
if (!domain) {
return null;
}
let media = await MediaRepo.SelectByUserHash(currentUserId, fileInfo.fileHash);
if (!media) {
media = new Media();
media.CreatedByUserId = currentUserId;
media.CreatedDatetime = new Date();
media.UserId = currentUserId;
media.DomainId = domain.Id; // TODO: Make this come from the host. Only EUS's domain is supported for now.
media.FileName = data.filename;
media.MediaTag = randomBytes(12).toString("base64url");
media.MediaType = data.mimetype;
media.Hash = fileInfo.fileHash;
media.FileSize = fileInfo.fileSize;
await MediaRepo.InsertUpdate(media);
}
return `${domain.HasHttps ? "https" : "http"}://${domain.Domain}/${media.MediaTag}`;
} catch (e) {
Console.printError(`EUS server service error:\n${e}`);
throw e;
}
}
}

View file

@ -14,7 +14,7 @@
<input class="form-control" name="registerKey" placeholder="Registration Key" value="<%= typeof(registerKey) === "undefined" ? "" : registerKey %>" required autocomplete="new-password" />
</div>
<hr>
<input class="form-control mt-3 mb-2" name="username" placeholder="Username" value="<%= typeof(username) === "undefined" ? "" : username %>" required autocomplete="new-password" />
<input class="form-control mt-3 mb-2" name="username" placeholder="Username" value="<%= typeof(username) === "undefined" ? "" : username %>" required />
<input class="form-control mt-3 mb-2" name="email" type="email" placeholder="Email Address" value="<%= typeof(email) === "undefined" ? "" : email %>" required autocomplete="new-password" />
<hr>
<input class="form-control mb-3" name="password" type="password" placeholder="Password" required autocomplete="new-password" />

View file

@ -29,21 +29,28 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js" integrity="sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<nav class="navbar navbar-expand bg-body-tertiary">
<nav class="navbar navbar-expand">
<div class="container-fluid">
<a class="navbar-brand" href="/">
EUS<%= typeof(isAdmin) === "undefined" ? "" : " Admin" %>
</a>
<a class="navbar-brand" href="/"><img src="/img/EUSIcon32xSlim.webp" alt="EUS"></a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto">
<div class="nav-item">
<!-- <div class="nav-item">
<a class="nav-link" href="/">Home</a>
</div>
</div> -->
</ul>
<ul class="navbar-nav">
<% if (typeof(userId) !== "undefined") { %>
<% if (typeof(session) !== "undefined") { %>
<div class="nav-item float-end">
<a class="nav-link" href="/account/logout">Logout</a>
<!-- <a class="nav-link" href="/account/logout">Logout</a> -->
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Logged in as <%= session.username %></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="/account/pwchange">Change Password</a></li>
<li><a class="dropdown-item" href="/account/2fa">Enable 2FA</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/account/logout">Logout</a></li>
</ul>
</div>
</div>
<% } else { %>
<div class="nav-item float-end">

25
views/home/dashboard.ejs Normal file
View file

@ -0,0 +1,25 @@
<%- include("../base/header", { title: "Home", session }) %>
<div class="row">
<!-- Recent Uploads -->
<div class="col">
<div class="card">
<div class="card-header">
<div class="row">
<div class="col text-start">Recent Uploads</div>
<div class="col text-end"><a aria-label="View All Uploads" href="/imagelist">View All >></a></div>
</div>
</div>
<div class="card-body"></div>
</div>
</div>
<!-- Stats -->
<div class="col">
<div class="card">
<div class="card-header">Stats</div>
<div class="card-body"></div>
</div>
</div>
</div>
<%- include("../base/footer") %>

BIN
wwwroot/img/EUSIcon32x.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB