diff --git a/api.go b/api.go index 75a013c..6a4e38f 100644 --- a/api.go +++ b/api.go @@ -490,6 +490,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc if lang, ok := app.telegram.languages[tgToken.ChatID]; ok { tgUser.Lang = lang } + if app.storage.telegram == nil { + app.storage.telegram = map[string]TelegramUser{} + } app.storage.telegram[user.ID] = tgUser err := app.storage.storeTelegramUsers() if err != nil { @@ -1474,6 +1477,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { // @Param lang query string false "Language for email titles." // @Success 200 {object} emailListDTO // @Router /config/emails [get] +// @Security Bearer // @tags Configuration func (app *appContext) GetCustomEmails(gc *gin.Context) { lang := gc.Query("lang") @@ -1502,6 +1506,7 @@ func (app *appContext) GetCustomEmails(gc *gin.Context) { // @Failure 500 {object} boolResponse // @Param id path string true "ID of email" // @Router /config/emails/{id} [post] +// @Security Bearer // @tags Configuration func (app *appContext) SetCustomEmail(gc *gin.Context) { var req customEmail @@ -1561,6 +1566,7 @@ func (app *appContext) SetCustomEmail(gc *gin.Context) { // @Param enable/disable path string true "enable/disable" // @Param id path string true "ID of email" // @Router /config/emails/{id}/state/{enable/disable} [post] +// @Security Bearer // @tags Configuration func (app *appContext) SetCustomEmailState(gc *gin.Context) { id := gc.Param("id") @@ -1610,6 +1616,7 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) { // @Failure 500 {object} boolResponse // @Param id path string true "ID of email" // @Router /config/emails/{id} [get] +// @Security Bearer // @tags Configuration func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { lang := app.storage.lang.chosenEmailLang @@ -1798,6 +1805,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { // @Produce json // @Success 200 {object} checkUpdateDTO // @Router /config/update [get] +// @Security Bearer // @tags Configuration func (app *appContext) CheckUpdate(gc *gin.Context) { if !app.newUpdate { @@ -1812,6 +1820,7 @@ func (app *appContext) CheckUpdate(gc *gin.Context) { // @Success 400 {object} stringResponse // @Success 500 {object} boolResponse // @Router /config/update [post] +// @Security Bearer // @tags Configuration func (app *appContext) ApplyUpdate(gc *gin.Context) { if !app.update.CanUpdate { @@ -1837,6 +1846,7 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) { // @Success 200 {object} boolResponse // @Failure 500 {object} stringResponse // @Router /logout [post] +// @Security Bearer // @tags Other func (app *appContext) Logout(gc *gin.Context) { cookie, err := gc.Cookie("refresh") @@ -1910,7 +1920,94 @@ func (app *appContext) ServeLang(gc *gin.Context) { respondBool(400, false, gc) } -// @Summary Returns true/false on whether or not a telegram PIN was verified. +// @Summary Returns a new Telegram verification PIN, and the bot username. +// @Produce json +// @Success 200 {object} telegramPinDTO +// @Router /telegram/pin [get] +// @Security Bearer +// @tags Other +func (app *appContext) TelegramGetPin(gc *gin.Context) { + gc.JSON(200, telegramPinDTO{ + Token: app.telegram.NewAuthToken(), + Username: app.telegram.username, + }) +} + +// @Summary Link a Jellyfin & Telegram user together via a verification PIN. +// @Produce json +// @Param telegramSetDTO body telegramSetDTO true "Token and user's Jellyfin ID." +// @Success 200 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Router /users/telegram [post] +// @Security Bearer +// @tags Other +func (app *appContext) TelegramAddUser(gc *gin.Context) { + var req telegramSetDTO + gc.BindJSON(&req) + if req.Token == "" || req.ID == "" { + respondBool(400, false, gc) + return + } + tokenIndex := -1 + for i, v := range app.telegram.verifiedTokens { + if v.Token == req.Token { + tokenIndex = i + break + } + } + if tokenIndex == -1 { + respondBool(500, false, gc) + return + } + tgToken := app.telegram.verifiedTokens[tokenIndex] + tgUser := TelegramUser{ + ChatID: tgToken.ChatID, + Username: tgToken.Username, + Contact: true, + } + if lang, ok := app.telegram.languages[tgToken.ChatID]; ok { + tgUser.Lang = lang + } + if app.storage.telegram == nil { + app.storage.telegram = map[string]TelegramUser{} + } + app.storage.telegram[req.ID] = tgUser + err := app.storage.storeTelegramUsers() + if err != nil { + app.err.Printf("Failed to store Telegram users: %v", err) + } else { + app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1] + app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1] + } + respondBool(200, true, gc) +} + +// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth. +// @Produce json +// @Success 200 {object} boolResponse +// @Param pin path string true "PIN code to check" +// @Router /telegram/verified/{pin} [get] +// @Security Bearer +// @tags Other +func (app *appContext) TelegramVerified(gc *gin.Context) { + pin := gc.Param("pin") + tokenIndex := -1 + for i, v := range app.telegram.verifiedTokens { + if v.Token == pin { + tokenIndex = i + break + } + } + // if tokenIndex != -1 { + // length := len(app.telegram.verifiedTokens) + // app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1] + // app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1] + // } + respondBool(200, tokenIndex != -1, gc) +} + +// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code. // @Produce json // @Success 200 {object} boolResponse // @Success 401 {object} boolResponse @@ -1918,7 +2015,7 @@ func (app *appContext) ServeLang(gc *gin.Context) { // @Param invCode path string true "invite Code" // @Router /invite/{invCode}/telegram/verified/{pin} [get] // @tags Other -func (app *appContext) TelegramVerified(gc *gin.Context) { +func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) { code := gc.Param("invCode") if _, ok := app.storage.invites[code]; !ok { respondBool(401, false, gc) @@ -1942,6 +2039,7 @@ func (app *appContext) TelegramVerified(gc *gin.Context) { // @Summary Restarts the program. No response means success. // @Router /restart [post] +// @Security Bearer // @tags Other func (app *appContext) restart(gc *gin.Context) { app.info.Println("Restarting...") diff --git a/css/base.css b/css/base.css index 95f1b4b..976062b 100644 --- a/css/base.css +++ b/css/base.css @@ -39,6 +39,11 @@ } } +.chip.btn:hover:not([disabled]):not(.textarea), +.chip.btn:focus:not([disabled]):not(.textarea) { + filter: brightness(var(--button-filter-brightness,95%)); +} + .banner { margin: calc(-1 * var(--spacing-4,1rem)); } diff --git a/html/admin.html b/html/admin.html index 955e1be..a074ec3 100644 --- a/html/admin.html +++ b/html/admin.html @@ -309,6 +309,24 @@ {{ .strings.update }} + {{ if .telegram_enabled }} + + {{ end }}
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index dcd9d6a..4082881 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -92,7 +92,8 @@ "inviteExpiresInTime": "Expires in {n}", "notifyEvent": "Notify on:", "notifyInviteExpiry": "On expiry", - "notifyUserCreation": "On user creation" + "notifyUserCreation": "On user creation", + "sendPIN": "Ask the user to send the PIN below to the bot." }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", @@ -104,6 +105,7 @@ "setOmbiDefaults": "Stored ombi defaults.", "updateApplied": "Update applied, please restart.", "updateAppliedRefresh": "Update applied, please refresh.", + "telegramVerified": "Telegram account verified.", "errorConnection": "Couldn't connect to jfa-go.", "error401Unauthorized": "Unauthorized. Try refreshing the page.", "errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.", diff --git a/lang/common/en-us.json b/lang/common/en-us.json index 8dd076a..9d77a89 100644 --- a/lang/common/en-us.json +++ b/lang/common/en-us.json @@ -14,6 +14,9 @@ "copied": "Copied", "time24h": "24h Time", "time12h": "12h Time", + "linkTelegram": "Link Telegram", + "contactEmail": "Contact through Email", + "contactTelegram": "Contact through Telegram", "theme": "Theme" } } diff --git a/lang/form/en-us.json b/lang/form/en-us.json index 36b1089..391d672 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -18,10 +18,7 @@ "confirmationRequired": "Email confirmation required", "confirmationRequiredMessage": "Please check your email inbox to verify your address.", "yourAccountIsValidUntil": "Your account will be valid until {date}.", - "linkTelegram": "Link Telegram", - "sendPIN": "Send the PIN below to the bot, then come back here to link your account.", - "contactEmail": "Contact through Email", - "contactTelegram": "Contact through Telegram" + "sendPIN": "Send the PIN below to the bot, then come back here to link your account." }, "notifications": { "errorUserExists": "User already exists.", diff --git a/models.go b/models.go index 8bb844f..e7adc17 100644 --- a/models.go +++ b/models.go @@ -237,3 +237,13 @@ type checkUpdateDTO struct { New bool `json:"new"` // Whether or not there's a new update. Update Update `json:"update"` } + +type telegramPinDTO struct { + Token string `json:"token" example:"A1-B2-3C"` + Username string `json:"username"` +} + +type telegramSetDTO struct { + Token string `json:"token" example:"A1-B2-3C"` + ID string `json:"id"` // Jellyfin ID of user. +} diff --git a/router.go b/router.go index 63892ad..2d17e26 100644 --- a/router.go +++ b/router.go @@ -119,7 +119,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) { router.Use(static.Serve(p+"/invite/", app.webFS)) router.GET(p+"/invite/:invCode", app.InviteProxy) if app.config.Section("telegram").Key("enabled").MustBool(false) { - router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerified) + router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite) } } if *SWAGGER { @@ -158,6 +158,11 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.GET(p+"/config", app.GetConfig) api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/restart", app.restart) + if app.config.Section("telegram").Key("enabled").MustBool(false) { + api.GET(p+"/telegram/pin", app.TelegramGetPin) + api.GET(p+"/telegram/verified/:pin", app.TelegramVerified) + api.POST(p+"/users/telegram", app.TelegramAddUser) + } if app.config.Section("ombi").Key("enabled").MustBool(false) { api.GET(p+"/ombi/users", app.OmbiUsers) api.POST(p+"/ombi/defaults", app.SetOmbiDefaults) diff --git a/ts/admin.ts b/ts/admin.ts index a7516bb..6c14be7 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -62,6 +62,10 @@ window.availableProfiles = window.availableProfiles || []; window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry")); window.modals.updateInfo = new Modal(document.getElementById("modal-update")); + + if (window.telegramEnabled) { + window.modals.telegram = new Modal(document.getElementById("modal-telegram")); + } })(); var inviteCreator = new createInvite(); diff --git a/ts/form.ts b/ts/form.ts index 17c84a1..0cd1611 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -53,7 +53,10 @@ if (window.telegramEnabled) { toggleLoader(waiting); window.telegramModal.show(); let modalClosed = false; - window.telegramModal.onclose = () => { modalClosed = true; } + window.telegramModal.onclose = () => { + modalClosed = true; + toggleLoader(waiting); + } const checkVerified = () => _get("/invite/" + window.code + "/telegram/verified/" + window.telegramPIN, null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 401) { @@ -63,7 +66,6 @@ if (window.telegramEnabled) { } else if (req.status == 200) { if (req.response["success"] as boolean) { telegramVerified = true; - toggleLoader(waiting); waiting.classList.add("~positive"); waiting.classList.remove("~info"); window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]); diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 60bef54..4201895 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -1,4 +1,4 @@ -import { _get, _post, _delete, toggleLoader, toDateString } from "../modules/common.js"; +import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateString } from "../modules/common.js"; import { templateEmail } from "../modules/settings.js"; import { Marked } from "@ts-stack/markdown"; import { stripMarkdown } from "../modules/stripmd.js"; @@ -14,6 +14,11 @@ interface User { telegram: string; } +interface getPinResponse { + token: string; + username: string; +} + class user implements User { private _row: HTMLTableRowElement; private _check: HTMLInputElement; @@ -80,7 +85,8 @@ class user implements User { if (!window.telegramEnabled) return; this._telegramUsername = u; if (u == "") { - this._telegram.textContent = ""; + this._telegram.innerHTML = `Add`; + (this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram; } else { this._telegram.innerHTML = `@${u}`; } @@ -193,6 +199,50 @@ class user implements User { }); } + private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => { + if (req.readyState == 4 && req.status == 200) { + const pin = document.getElementById("telegram-pin"); + const link = document.getElementById("telegram-link") as HTMLAnchorElement; + const username = document.getElementById("telegram-username") as HTMLSpanElement; + const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement; + let resp = req.response as getPinResponse; + pin.textContent = resp.token; + link.href = "https://t.me/" + resp.username; + username.textContent = resp.username; + addLoader(waiting); + let modalClosed = false; + window.modals.telegram.onclose = () => { + modalClosed = true; + removeLoader(waiting); + } + let send = { + token: resp.token, + id: this.id + }; + const checkVerified = () => _post("/users/telegram", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 && req.response["success"] as boolean) { + removeLoader(waiting); + waiting.classList.add("~positive"); + waiting.classList.remove("~info"); + window.notifications.customSuccess("telegramVerified", window.lang.notif("telegramVerified")); + setTimeout(() => { + window.modals.telegram.close(); + waiting.classList.add("~info"); + waiting.classList.remove("~positive"); + }, 2000); + document.dispatchEvent(new CustomEvent("accounts-reload")); + } else if (!modalClosed) { + setTimeout(checkVerified, 1500); + } + } + }, true); + window.modals.telegram.show(); + checkVerified(); + } + }); + + update = (user: User) => { this.id = user.id; this.name = user.name; @@ -723,6 +773,7 @@ export class accountsList { this._selectAll.onchange = () => { this.selectAll = this._selectAll.checked; }; + document.addEventListener("accounts-reload", this.reload); document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); }); document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); }); this._addUserButton.onclick = window.modals.addUser.toggle; diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 4142f42..f761a4c 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -179,3 +179,22 @@ export function toggleLoader(el: HTMLElement, small: boolean = true) { el.appendChild(dot); } } + +export function addLoader(el: HTMLElement, small: boolean = true) { + if (!el.classList.contains("loader")) { + el.classList.add("loader"); + if (small) { el.classList.add("loader-sm"); } + const dot = document.createElement("span") as HTMLSpanElement; + dot.classList.add("dot") + el.appendChild(dot); + } +} + +export function removeLoader(el: HTMLElement, small: boolean = true) { + if (el.classList.contains("loader")) { + el.classList.remove("loader"); + el.classList.remove("loader-sm"); + const dot = el.querySelector("span.dot"); + if (dot) { dot.remove(); } + } +} diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 70a260f..7e0bf3e 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -4,6 +4,8 @@ declare interface Modal { show: () => void; close: (event?: Event) => void; toggle: () => void; + onopen: (f: () => void) => void; + onclose: (f: () => void) => void; } interface ArrayConstructor { @@ -98,6 +100,7 @@ declare interface Modals { customizeEmails: Modal; extendExpiry: Modal; updateInfo: Modal; + telegram: Modal; } interface Invite {