diff --git a/controllers/AccountController.ts b/controllers/AccountController.ts index 8334737..0c6849d 100644 --- a/controllers/AccountController.ts +++ b/controllers/AccountController.ts @@ -9,6 +9,8 @@ import UserService from "../services/UserService"; import ArrayUtility from "../utilities/ArrayUtility"; import Controller from "./Controller"; import type DomainsViewModel from "../models/account/DomainsViewModel"; +import type MediaViewModel from "../models/account/MediaViewModel"; +import type MediaGetParameters from "../models/account/MediaGetParameters"; export default class AccountController extends Controller { public async Login_Get_AllowAnonymous() { @@ -114,8 +116,20 @@ export default class AccountController extends Controller { return this.view(domainsViewModel); } - public async Media_Get() { - return this.view(); + public async Media_Get(mediaGetParameters: MediaGetParameters) { + let pageNumber = parseInt(mediaGetParameters.pageNumber); + if (isNaN(pageNumber)) { + pageNumber = 0; + } + + const mediaViewModel: MediaViewModel = { + media: await UserService.GetMediaListPaged(this.session.userId, pageNumber), + domains: ArrayUtility.ToIdKeyedDict(await DomainService.LoadDomains()), + + pageNumber + }; + + return this.view(mediaViewModel); } public async API_Get() { @@ -132,6 +146,27 @@ export default class AccountController extends Controller { return this.view(apiViewModel); } + public async Information_Get() { + const user = await UserService.GetUser(this.session.userId); + if (!user) { + return this.forbidden(); + } + + return this.view(); + } + + public async NewAPIKey_Get() { + await UserService.ResetAPIKey(this.session.userId); + + return this.redirectToAction("api"); + } + + public async NewUploadKey_Get() { + await UserService.ResetUploadKey(this.session.userId); + + return this.redirectToAction("api"); + } + public async Logout_Get_AllowAnonymous() { Session.Clear(this.req.cookies, this.res); diff --git a/entities/listitems/MediaListItem.ts b/entities/listitems/MediaListItem.ts new file mode 100644 index 0000000..d101727 --- /dev/null +++ b/entities/listitems/MediaListItem.ts @@ -0,0 +1,10 @@ +export default class MediaListItem { + public Id: number = Number.MIN_VALUE; + public FileName: string = ""; + public MediaTag: string = ""; + public MediaType: string = ""; + public DomainId: number = Number.MIN_VALUE; + public DomainName: string = ""; + public DomainHasHttps: boolean = false; + public CreatedDatetime: Date = new Date(0); +} \ No newline at end of file diff --git a/index.ts b/index.ts index d9a3bb1..f399ea3 100644 --- a/index.ts +++ b/index.ts @@ -81,7 +81,7 @@ fastify.addHook("preHandler", (req, res, done) => { req.startTime = Date.now(); // * Take usual controller path if this path is registered. - if (Controller.RegisteredPaths.includes(req.url)) { + if (Controller.RegisteredPaths.includes(req.url.split("?")[0])) { // @ts-ignore req.logType = cyan("CONTROLLER"); HeaderUtility.AddBakedHeaders(res); diff --git a/models/account/MediaGetParameters.ts b/models/account/MediaGetParameters.ts new file mode 100644 index 0000000..d8ad0d2 --- /dev/null +++ b/models/account/MediaGetParameters.ts @@ -0,0 +1,3 @@ +export default interface MediaGetParameters { + pageNumber: string +} \ No newline at end of file diff --git a/models/account/MediaViewModel.ts b/models/account/MediaViewModel.ts new file mode 100644 index 0000000..f33e456 --- /dev/null +++ b/models/account/MediaViewModel.ts @@ -0,0 +1,10 @@ +import type { Domain } from "domain"; +import type MediaListItem from "../../entities/listitems/MediaListItem"; +import type Paged from "../../objects/Paged"; + +export default interface MediaViewModel { + media: Paged, + domains: { [key: string]: Domain }, + + pageNumber: number +} \ No newline at end of file diff --git a/objects/Paged.ts b/objects/Paged.ts new file mode 100644 index 0000000..45cc281 --- /dev/null +++ b/objects/Paged.ts @@ -0,0 +1,13 @@ +export default class Paged { + public Data: Array; + public readonly TotalRecords: number; + public readonly PageSize: number; + public readonly PageCount: number; + + public constructor(totalRecords: number, pageSize: number) { + this.Data = new Array(); + this.TotalRecords = totalRecords; + this.PageSize = pageSize; + this.PageCount = Math.max(Math.ceil(totalRecords / pageSize), 1); + } +} \ No newline at end of file diff --git a/repos/MediaRepo.ts b/repos/MediaRepo.ts index 0431e03..0f53c2c 100644 --- a/repos/MediaRepo.ts +++ b/repos/MediaRepo.ts @@ -1,6 +1,8 @@ import Database from "../objects/Database"; import Media from "../entities/Media"; import MediaCount from "../entities/MediaCount"; +import Paged from "../objects/Paged"; +import MediaListItem from "../entities/listitems/MediaListItem"; export default abstract class MediaRepo { public static async SelectAll() { @@ -82,7 +84,7 @@ export default abstract class MediaRepo { public static async SelectTotalMediaByUserId(userId: number) { const dbCount = await Database.Instance.query('SELECT COUNT(Id) FROM Media WHERE IsDeleted = 0 AND UserId = ?', [ userId ]); - return dbCount[0]["COUNT(Id)"]; + return dbCount[0]["COUNT(Id)"] ?? 0; } public static async SelectMediaTypeCountsByUserId(userId: number) { @@ -98,6 +100,20 @@ export default abstract class MediaRepo { return mediaCountList; } + public static async SelectUserMediaListPaged(userId: number, pageNumber: number, pageSize: number) { + const totalRecords = (await Database.Instance.query("SELECT COUNT(Id) FROM Media WHERE IsDeleted = 0 AND UserId = ?", [ userId ]))[0]["COUNT(Id)"] ?? 0; + const mediaListPaged = new Paged(totalRecords, pageSize); + const pageRecords = await Database.Instance.query('SELECT Media.Id, FileName, MediaTag, MediaType, Domain.Id AS "DomainId", Domain.Domain AS "DomainName", Domain.HasHttps AS "DomainHasHttps", Media.CreatedDatetime FROM Media JOIN Domain ON Domain.Id = DomainId WHERE Media.IsDeleted = 0 AND Media.UserId = ? ORDER BY Media.CreatedDatetime DESC LIMIT ? OFFSET ?', [ userId, pageSize, pageNumber * pageSize ]); + + for (const row of pageRecords) { + const mediaListItem = new MediaListItem(); + PopulateMediaListItemFromDB(mediaListItem, row); + mediaListPaged.Data.push(mediaListItem); + } + + return mediaListPaged; + } + 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;", [ @@ -113,6 +129,17 @@ export default abstract class MediaRepo { } } +function PopulateMediaListItemFromDB(mediaListItem: MediaListItem, dbMediaListItem: any) { + mediaListItem.Id = dbMediaListItem.Id; + mediaListItem.FileName = dbMediaListItem.FileName; + mediaListItem.MediaTag = dbMediaListItem.MediaTag; + mediaListItem.MediaType = dbMediaListItem.MediaType; + mediaListItem.DomainId = dbMediaListItem.DomainId; + mediaListItem.DomainName = dbMediaListItem.DomainName; + mediaListItem.DomainHasHttps = dbMediaListItem.DomainHasHttps[0] === 1; + mediaListItem.CreatedDatetime = dbMediaListItem.CreatedDatetime; +} + function PopulateMediaFromDB(media: Media, dbMedia: any) { media.Id = dbMedia.Id; media.UserId = dbMedia.UserId; diff --git a/services/UserService.ts b/services/UserService.ts index 62c123c..ccadbfe 100644 --- a/services/UserService.ts +++ b/services/UserService.ts @@ -170,4 +170,51 @@ export default abstract class UserService { throw e; } } + + public static async GetMediaListPaged(currentUserId: number, pageNumber: number) { + try { + return await MediaRepo.SelectUserMediaListPaged(currentUserId, pageNumber, 50); + } catch (e) { + Console.printError(`EUS server service error:\n${e}`); + throw e; + } + } + + public static async ResetAPIKey(currentUserId: number) { + try { + const user = await UserRepo.SelectById(currentUserId); + if (!user) { + return null; + } + + user.ApiKey = randomBytes(64).toString("base64url"); + + user.LastModifiedByUserId = currentUserId; + user.LastModifiedDatetime = new Date(); + + return await UserRepo.InsertUpdate(user); + } catch (e) { + Console.printError(`EUS server service error:\n${e}`); + throw e; + } + } + + public static async ResetUploadKey(currentUserId: number) { + try { + const user = await UserRepo.SelectById(currentUserId); + if (!user) { + return null; + } + + user.UploadKey = randomBytes(64).toString("base64url"); + + user.LastModifiedByUserId = currentUserId; + user.LastModifiedDatetime = new Date(); + + return await UserRepo.InsertUpdate(user); + } catch (e) { + Console.printError(`EUS server service error:\n${e}`); + throw e; + } + } } \ No newline at end of file diff --git a/views/account/api.ejs b/views/account/api.ejs index 7cae18d..0fe43fe 100644 --- a/views/account/api.ejs +++ b/views/account/api.ejs @@ -34,9 +34,7 @@ Hide - Request New API Key -
- Only do this if you really need to, existing applications using this API Key will cease to function until it is replaced. + Request New API Key

Upload Key

@@ -51,7 +49,7 @@ Hide
- Request New Upload Key + Request New Upload Key diff --git a/views/account/dashboard.ejs b/views/account/dashboard.ejs index ee10e5c..c679375 100644 --- a/views/account/dashboard.ejs +++ b/views/account/dashboard.ejs @@ -48,9 +48,11 @@
- Account Information + API + <% if (session.userType === UserType.Admin) { %> Domains + <% } %> Media
@@ -59,6 +61,9 @@
Stats
+ <% if (mediaCount === 0) { %> +

No stats to show.

+ <% } else { %>

Total Media: <%= FormattingUtility.NumberHumanReadable(mediaCount) %>

Media By Type:

    @@ -67,6 +72,7 @@ <% } %>

Total size of Media: <%= FormattingUtility.NumberAsFileSize(mediaSize) %>

+ <% } %>
diff --git a/views/account/information.ejs b/views/account/information.ejs new file mode 100644 index 0000000..93b3087 --- /dev/null +++ b/views/account/information.ejs @@ -0,0 +1,34 @@ +<%- include("../base/header", { title: "API Information", session }) %> + +
+
+ +
+
+ +
+
+
+
+
+
Account Information
+
+
+
+ +
+
+
+
+ + + +<%- include("../base/footer") %> diff --git a/views/account/media.ejs b/views/account/media.ejs index fabe160..b88ad15 100644 --- a/views/account/media.ejs +++ b/views/account/media.ejs @@ -19,11 +19,53 @@
Your Media
+
Media: <%= FormattingUtility.NumberHumanReadable(media.TotalRecords) %>
-
+
- +
+
+
+
+
File Name
+
Media Tag
+
Upload Date/Time
+
 
+
+
+
+ <% for (const mediaItem of media.Data) { %> +
+
+
+
+
+ <% if (mediaItem.MediaType.startsWith("image/")) { %> + ://<%= domains[mediaItem.DomainId].Domain %>/<%= mediaItem.MediaTag %>" height="30" width="50"> + <% } else { %> + + <% } %> +
+ +
+
+
File Name: <%= mediaItem.FileName %>
+
Media Tag: <%= mediaItem.MediaTag %>
+
+ Upload Date/Time: + <%= mediaItem.CreatedDatetime.toString().split(" ").slice(1, 5).join(" ") %>
+ <%= mediaItem.CreatedDatetime.toString().split(" ").slice(-4, -3).join(" ") %> +
+ +
+
+ <% } %> + <%- include("../base/paging", { pageCount: media.PageCount, pageNumber: pageNumber }) %>
diff --git a/views/base/footer.ejs b/views/base/footer.ejs index 794bf3a..5f74d44 100644 --- a/views/base/footer.ejs +++ b/views/base/footer.ejs @@ -14,6 +14,23 @@ form.classList.add('was-validated'); }, false); }); + + const allButtons = document.querySelectorAll("a, button, input"); + Array.from(allButtons).forEach(button => { + if ("confirm" in button.dataset) { + button.addEventListener("click", e => { + const result = confirm(button.dataset["confirm"].replaceAll("\\n", "\n")); + if (result) { + return true; + } else { + e.preventDefault(); + return false; + } + + return false; + }); + } + }); })(); window.cookieconsent.initialise({ diff --git a/views/base/header.ejs b/views/base/header.ejs index 9068d76..2ff9e17 100644 --- a/views/base/header.ejs +++ b/views/base/header.ejs @@ -46,9 +46,9 @@ diff --git a/views/base/paging.ejs b/views/base/paging.ejs new file mode 100644 index 0000000..13f151b --- /dev/null +++ b/views/base/paging.ejs @@ -0,0 +1,35 @@ + \ No newline at end of file