diff --git a/controllers/AccountController.ts b/controllers/AccountController.ts index 2f3324f..955ed1c 100644 --- a/controllers/AccountController.ts +++ b/controllers/AccountController.ts @@ -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."; diff --git a/controllers/HomeController.ts b/controllers/HomeController.ts index 0f2962a..2b1149f 100644 --- a/controllers/HomeController.ts +++ b/controllers/HomeController.ts @@ -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(); diff --git a/entities/Domain.ts b/entities/Domain.ts new file mode 100644 index 0000000..ecfe0e1 --- /dev/null +++ b/entities/Domain.ts @@ -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; +} \ No newline at end of file diff --git a/entities/Image.ts b/entities/Media.ts similarity index 84% rename from entities/Image.ts rename to entities/Media.ts index ee4c68e..b28824a 100644 --- a/entities/Image.ts +++ b/entities/Media.ts @@ -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; diff --git a/index.ts b/index.ts index 1e22086..851d13a 100644 --- a/index.ts +++ b/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,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(); +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(); }); diff --git a/objects/Database.ts b/objects/Database.ts index ce00218..aece586 100644 --- a/objects/Database.ts +++ b/objects/Database.ts @@ -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; diff --git a/objects/HashFS.ts b/objects/HashFS.ts index 4280024..0b88053 100644 --- a/objects/HashFS.ts +++ b/objects/HashFS.ts @@ -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(async (resolve, reject) => { + return new Promise(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(async (resolve, reject) => { + public AddFromStream(stream: BusboyFileStream) { + return new Promise(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((resolve, reject) => { - stat(this.getFilePath(hash), (err, _stat) => { + stat(this.GetFilePath(hash), (err, _stat) => { if (err) { if (err.code === "ENOENT") { resolve(false); diff --git a/objects/HashFSFileInformation.ts b/objects/HashFSFileInformation.ts new file mode 100644 index 0000000..c955db5 --- /dev/null +++ b/objects/HashFSFileInformation.ts @@ -0,0 +1,5 @@ +export default class HashFSFileInformation { + public fileHash: string = ""; + public fileSize: number = Number.MIN_VALUE; + public fileExistsAlready: boolean = false; +} \ No newline at end of file diff --git a/repos/DomainRepo.ts b/repos/DomainRepo.ts new file mode 100644 index 0000000..ccac6ee --- /dev/null +++ b/repos/DomainRepo.ts @@ -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(); + + 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; +} \ No newline at end of file diff --git a/repos/ImageRepo.ts b/repos/ImageRepo.ts deleted file mode 100644 index dd5be67..0000000 --- a/repos/ImageRepo.ts +++ /dev/null @@ -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(); - - 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; -} \ No newline at end of file diff --git a/repos/MediaRepo.ts b/repos/MediaRepo.ts new file mode 100644 index 0000000..e9b1217 --- /dev/null +++ b/repos/MediaRepo.ts @@ -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(); + + 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(); + + 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; +} \ No newline at end of file diff --git a/repos/RepoBase.ts b/repos/RepoBase.ts deleted file mode 100644 index ba85361..0000000 --- a/repos/RepoBase.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default class RepoBase { - public static convertNullableDatetimeIntToDate(dateTimeInt?:number) { - return dateTimeInt ? new Date(dateTimeInt) : undefined; - } -} \ No newline at end of file diff --git a/repos/UserRepo.ts b/repos/UserRepo.ts index 6695067..f1efcf4 100644 --- a/repos/UserRepo.ts +++ b/repos/UserRepo.ts @@ -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; } \ No newline at end of file diff --git a/services/MediaService.ts b/services/MediaService.ts new file mode 100644 index 0000000..39aa579 --- /dev/null +++ b/services/MediaService.ts @@ -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; + } + } +} \ No newline at end of file diff --git a/services/UserService.ts b/services/UserService.ts index 6790e3f..ad182b7 100644 --- a/services/UserService.ts +++ b/services/UserService.ts @@ -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; + } + } } \ No newline at end of file diff --git a/views/account/register.ejs b/views/account/register.ejs index 323b43f..b7c0535 100644 --- a/views/account/register.ejs +++ b/views/account/register.ejs @@ -14,7 +14,7 @@ " required autocomplete="new-password" />
- " required autocomplete="new-password" /> + " required /> " required autocomplete="new-password" />
diff --git a/views/base/header.ejs b/views/base/header.ejs index b1418b7..ee5f58e 100644 --- a/views/base/header.ejs +++ b/views/base/header.ejs @@ -29,21 +29,28 @@ -