From cf7983ca11645bd91948b9619cfacccedb1aaebe Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 18 Jun 2023 21:38:12 +0100 Subject: [PATCH] userpage: add/edit discord works identically to on the form, would like to eventually factor out the discord/telegram/matrix verif stuff so it can be shared between the two pages though. --- api-userpage.go | 78 +++++++++++++++++++++++++++++++++++++++- html/user.html | 57 +++++++++++++++++++++++++++--- lang/form/en-us.json | 2 +- models.go | 4 +++ router.go | 3 ++ ts/form.ts | 7 +--- ts/modules/common.ts | 5 +++ ts/user.ts | 84 +++++++++++++++++++++++++++++++++++++++++--- views.go | 25 +++++++++++-- 9 files changed, 246 insertions(+), 19 deletions(-) diff --git a/api-userpage.go b/api-userpage.go index dbfd0d5..c771b73 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -207,7 +207,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) { // @Failure 500 {object} stringResponse // @Router /my/email [post] // @Security Bearer -// @tags Users +// @tags User Page func (app *appContext) ModifyMyEmail(gc *gin.Context) { var req ModifyMyEmailDTO gc.BindJSON(&req) @@ -258,3 +258,79 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) { app.confirmMyAction(gc, key) return } + +// @Summary Returns a 10-minute, one-use Discord server invite +// @Produce json +// @Success 200 {object} DiscordInviteDTO +// @Failure 400 {object} boolResponse +// @Failure 401 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Param invCode path string true "invite Code" +// @Router /my/discord/invite [get] +// @tags User Page +func (app *appContext) MyDiscordServerInvite(gc *gin.Context) { + if app.discord.inviteChannelName == "" { + respondBool(400, false, gc) + return + } + invURL, iconURL := app.discord.NewTempInvite(10*60, 1) + if invURL == "" { + respondBool(500, false, gc) + return + } + gc.JSON(200, DiscordInviteDTO{invURL, iconURL}) +} + +// @Summary Returns a linking PIN for discord/telegram +// @Produce json +// @Success 200 {object} GetMyPINDTO +// @Failure 400 {object} stringResponse +// Param service path string true "discord/telegram" +// @Router /my/pin/{service} [get] +// @tags User Page +func (app *appContext) GetMyPIN(gc *gin.Context) { + service := gc.Param("service") + resp := GetMyPINDTO{} + switch service { + case "discord": + resp.PIN = app.discord.NewAuthToken() + break + case "telegram": + resp.PIN = app.telegram.NewAuthToken() + break + default: + respond(400, "invalid service", gc) + return + } + gc.JSON(200, resp) +} + +// @Summary Returns true/false on whether or not your discord PIN was verified, and assigns the discord user to you. +// @Produce json +// @Success 200 {object} boolResponse +// @Failure 401 {object} boolResponse +// @Param pin path string true "PIN code to check" +// @Router /my/discord/verified/{pin} [get] +// @tags User Page +func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) { + pin := gc.Param("pin") + dcUser, ok := app.discord.verifiedTokens[pin] + if !ok { + respondBool(200, false, gc) + return + } + if app.config.Section("discord").Key("require_unique").MustBool(false) { + for _, u := range app.storage.discord { + if app.discord.verifiedTokens[pin].ID == u.ID { + delete(app.discord.verifiedTokens, pin) + respondBool(400, false, gc) + return + } + } + } + dc := app.storage.discord + dc[gc.GetString("jfId")] = dcUser + app.storage.discord = dc + app.storage.storeDiscordUsers() + respondBool(200, true, gc) +} diff --git a/html/user.html b/html/user.html index 39789cd..3e61003 100644 --- a/html/user.html +++ b/html/user.html @@ -4,14 +4,22 @@ {{ template "header.html" . }} {{ .lang.Strings.pageTitle }} @@ -33,6 +41,47 @@ + + +
diff --git a/lang/form/en-us.json b/lang/form/en-us.json index 43b2928..715d0d3 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -60,4 +60,4 @@ "plural": "Must have at least {n} special characters" } } -} \ No newline at end of file +} diff --git a/models.go b/models.go index 084d010..9f3aef9 100644 --- a/models.go +++ b/models.go @@ -402,3 +402,7 @@ const ( UserEmailChange ConfirmationTarget = iota NoOp ) + +type GetMyPINDTO struct { + PIN string `json:"pin"` +} diff --git a/router.go b/router.go index 27e2281..80bec1a 100644 --- a/router.go +++ b/router.go @@ -231,6 +231,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) { user.POST(p+"/contact", app.SetMyContactMethods) user.POST(p+"/logout", app.LogoutUser) user.POST(p+"/email", app.ModifyMyEmail) + user.GET(p+"/discord/invite", app.MyDiscordServerInvite) + user.GET(p+"/pin/:service", app.GetMyPIN) + user.GET(p+"/discord/verified/:pin", app.MyDiscordVerifiedInvite) } } } diff --git a/ts/form.ts b/ts/form.ts index 31cdf0d..b3dbc25 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -1,6 +1,6 @@ import { Modal } from "./modules/modal.js"; import { notificationBox, whichAnimationEvent } from "./modules/common.js"; -import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js"; +import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString, DiscordInvite } from "./modules/common.js"; import { loadLangSelector } from "./modules/lang.js"; import { initValidator } from "./modules/validator.js"; @@ -91,11 +91,6 @@ if (window.telegramEnabled) { }; } -interface DiscordInvite { - invite: string; - icon: string; -} - var discordVerified = false; if (window.discordEnabled) { window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired); diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 4687cc8..6ae320d 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -221,3 +221,8 @@ export function insertText(textarea: HTMLTextAreaElement, text: string) { textarea.focus(); } } + +export interface DiscordInvite { + invite: string; + icon: string; +} diff --git a/ts/user.ts b/ts/user.ts index a611c15..ac34517 100644 --- a/ts/user.ts +++ b/ts/user.ts @@ -1,12 +1,20 @@ import { ThemeManager } from "./modules/theme.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { Modal } from "./modules/modal.js"; -import { _get, _post, notificationBox, whichAnimationEvent, toDateString, toggleLoader } from "./modules/common.js"; +import { _get, _post, notificationBox, whichAnimationEvent, toDateString, toggleLoader, DiscordInvite } from "./modules/common.js"; import { Login } from "./modules/login.js"; interface userWindow extends Window { jellyfinID: string; username: string; + emailRequired: boolean; + discordRequired: boolean; + telegramRequired: boolean; + matrixRequired: boolean; + discordServerName: string; + discordInviteLink: boolean; + matrixUserID: string; + discordSendPINMessage: string; } declare var window: userWindow; @@ -26,6 +34,9 @@ window.modals = {} as Modals; (() => { window.modals.login = new Modal(document.getElementById("modal-login"), true); window.modals.email = new Modal(document.getElementById("modal-email"), false); + window.modals.discord = new Modal(document.getElementById("modal-discord"), false); + window.modals.telegram = new Modal(document.getElementById("modal-telegram"), false); + window.modals.matrix = new Modal(document.getElementById("modal-matrix"), false); })(); window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); @@ -87,8 +98,8 @@ class ContactMethods { ${(details.value == "") ? window.lang.strings("notSet") : details.value}
- `; @@ -239,7 +250,6 @@ var expiryCard = new ExpiryCard(statusCard); var contactMethodList = new ContactMethods(contactCard); const addEditEmail = (add: boolean): void => { - console.log("call"); const heading = window.modals.email.modal.querySelector(".heading"); heading.innerHTML = (add ? window.lang.strings("addContactMethod") : window.lang.strings("editContactMethod")) + `×`; const input = document.getElementById("modal-email-input") as HTMLInputElement; @@ -268,6 +278,70 @@ const addEditEmail = (add: boolean): void => { window.modals.email.show(); } +let discordModalClosed = false; +let discordPIN = ""; +const addEditDiscord = (add: boolean): void => { + if (window.discordInviteLink) { + _get("/my/discord/invite", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) return; + const inv = req.response as DiscordInvite; + const link = document.getElementById("discord-invite") as HTMLAnchorElement; + link.href = inv.invite; + link.target = "_blank"; + link.innerHTML = `${window.discordServerName}`; + } + }); + } + + _get("/my/pin/discord", null, (req: XMLHttpRequest) => { + if (req.readyState == 4 && req.status == 200) { + discordPIN = req.response["pin"]; + window.modals.discord.modal.querySelector(".pin").textContent = discordPIN; + } + }); + + const waiting = document.getElementById("discord-waiting") as HTMLSpanElement; + toggleLoader(waiting); + window.modals.discord.show(); + discordModalClosed = false; + window.modals.discord.onclose = () => { + discordModalClosed = true; + toggleLoader(waiting); + } + const checkVerified = () => { + if (discordPIN == "") { + setTimeout(checkVerified, 1500); + } + if (discordModalClosed) return; + _get("/my/discord/verified/" + discordPIN, null, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + if (req.status == 401) { + window.modals.discord.close(); + window.notifications.customError("invalidCodeError", window.lang.notif("errorInvalidCode")); + return; + } else if (req.status == 400) { + window.modals.discord.close(); + window.notifications.customError("accountLinkedError", window.lang.notif("errorAccountLinked")); + } else if (req.status == 200) { + if (req.response["success"] as boolean) { + waiting.classList.add("~positive"); + waiting.classList.remove("~info"); + window.notifications.customPositive("discordVerified", "", window.lang.notif("verified")); + setTimeout(() => { + window.modals.discord.close; + window.location.reload(); + }, 2000); + } else if (!discordModalClosed) { + setTimeout(checkVerified, 1500); + } + } + }); + }; + + checkVerified(); +}; + document.addEventListener("details-reload", () => { _get("/my/details", null, (req: XMLHttpRequest) => { if (req.readyState == 4) { @@ -294,7 +368,7 @@ document.addEventListener("details-reload", () => { const contactMethods: { name: string, icon: string, f: (add: boolean) => void }[] = [ {name: "email", icon: ``, f: addEditEmail}, - {name: "discord", icon: ``, f: null}, + {name: "discord", icon: ``, f: addEditDiscord}, {name: "telegram", icon: ``, f: null}, {name: "matrix", icon: `[m]`, f: null} ]; diff --git a/views.go b/views.go index a158766..ebb855b 100644 --- a/views.go +++ b/views.go @@ -165,12 +165,13 @@ func (app *appContext) MyUserPage(gc *gin.Context) { emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) - gcHTML(gc, http.StatusOK, "user.html", gin.H{ + data := gin.H{ "urlBase": app.getURLBase(gc), "cssClass": app.cssClass, "cssVersion": cssVersion, "contactMessage": app.config.Section("ui").Key("contact_message").String(), "emailEnabled": emailEnabled, + "emailRequired": app.config.Section("email").Key("required").MustBool(false), "telegramEnabled": telegramEnabled, "discordEnabled": discordEnabled, "matrixEnabled": matrixEnabled, @@ -182,7 +183,27 @@ func (app *appContext) MyUserPage(gc *gin.Context) { "validationStrings": app.storage.lang.User[lang].ValidationStrings, "language": app.storage.lang.User[lang].JSON, "langName": lang, - }) + } + if telegramEnabled { + data["telegramUser"] = app.telegram.username + data["telegramURL"] = app.telegram.link + data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false) + } + if matrixEnabled { + data["matrixRequired"] = app.config.Section("matrix").Key("required").MustBool(false) + data["matrixUser"] = app.matrix.userID + } + if discordEnabled { + data["discordUsername"] = app.discord.username + data["discordRequired"] = app.config.Section("discord").Key("required").MustBool(false) + data["discordSendPINMessage"] = template.HTML(app.storage.lang.User[lang].Strings.template("sendPINDiscord", tmpl{ + "command": `/` + app.config.Section("discord").Key("start_command").MustString("start") + ``, + "server_channel": app.discord.serverChannelName, + })) + data["discordServerName"] = app.discord.serverName + data["discordInviteLink"] = app.discord.inviteChannelName != "" + } + gcHTML(gc, http.StatusOK, "user.html", data) } func (app *appContext) ResetPassword(gc *gin.Context) {