WIP: Fastify is drunk
This commit is contained in:
parent
2c5e40b36f
commit
24247d938f
20 changed files with 472 additions and 134 deletions
|
@ -46,7 +46,7 @@ export default class AccountController extends Controller {
|
|||
}
|
||||
|
||||
const username = registerViewModel.username.replaceAll("<", "<").replaceAll(">", ">");
|
||||
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.";
|
||||
|
|
|
@ -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) {
|
||||
} else {
|
||||
return this.unauthorised("Upload key invalid or missing.");
|
||||
}
|
||||
if ("host" in this.req.headers) {
|
||||
// @ts-ignore
|
||||
host = this.req.headers["host"];
|
||||
} else {
|
||||
return this.badRequest("Host header missing?!");
|
||||
}
|
||||
//console.log(uploadKey);
|
||||
//console.log(data.mimetype);
|
||||
const hash = await HashFS.GetHashFSInstance("images").AddFromStream(data.file);
|
||||
|
||||
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
14
entities/Domain.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
33
index.ts
33
index.ts
|
@ -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,10 +48,11 @@ fastify.register(FastifyCookie, {
|
|||
fastify.register(FastifyStatic, {
|
||||
root: join(__dirname, "wwwroot"),
|
||||
preCompressed: true
|
||||
//prefix: `${Config.ports.web}/static/`
|
||||
});
|
||||
|
||||
fastify.addHook("preValidation", (req, res, done) => {
|
||||
const hashLookupCache = new FunkyArray<string, Media>();
|
||||
fastify.addHook("preHandler", (req, res, done) => {
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
req.startTime = Date.now();
|
||||
|
||||
|
@ -57,16 +62,38 @@ fastify.addHook("preValidation", (req, res, done) => {
|
|||
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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
stream.pipe(tempFile);
|
||||
stream.pipe(hasher);
|
||||
tempFile.on("close", () => {
|
||||
tempFile.on("close", async () => {
|
||||
const hash: string = hasher.read();
|
||||
rename(tempFilePath, this.getFilePath(hash), (err) => {
|
||||
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(hash);
|
||||
resolve(fileInfo);
|
||||
});
|
||||
}
|
||||
});
|
||||
stream.pipe(tempFile);
|
||||
stream.pipe(hasher);
|
||||
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);
|
||||
|
|
5
objects/HashFSFileInformation.ts
Normal file
5
objects/HashFSFileInformation.ts
Normal 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
68
repos/DomainRepo.ts
Normal 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;
|
||||
}
|
|
@ -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
106
repos/MediaRepo.ts
Normal 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;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export default class RepoBase {
|
||||
public static convertNullableDatetimeIntToDate(dateTimeInt?:number) {
|
||||
return dateTimeInt ? new Date(dateTimeInt) : undefined;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import Database from "../objects/Database";
|
||||
import RepoBase from "./RepoBase";
|
||||
import User from "../entities/User";
|
||||
|
||||
export default abstract class UserRepo {
|
||||
|
@ -38,6 +37,28 @@ export default abstract class UserRepo {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -51,12 +72,12 @@ export default abstract class UserRepo {
|
|||
|
||||
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
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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
22
services/MediaService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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
25
views/home/dashboard.ejs
Normal 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
BIN
wwwroot/img/EUSIcon32x.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
wwwroot/img/EUSIcon32xSlim.webp
Normal file
BIN
wwwroot/img/EUSIcon32xSlim.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
Loading…
Reference in a new issue