1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-06-28 20:37:46 +02:00
jfa-go/views.go
Harvey Tindall 9c2f27bcdb
referrals: 1/2 generation routes, display route, partial frontend
route for generation/enabling of referral for user(s) done? the frontend
is mostly done, but functionality is not there yet. Route for finding
and displaying referral to user is done. Also the config option for
referral is there, in user page settings.
2023-06-28 16:05:24 +01:00

695 lines
24 KiB
Go

package main
import (
"encoding/json"
"html/template"
"io"
"io/fs"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/gomarkdown/markdown"
"github.com/hrfee/mediabrowser"
"github.com/steambap/captcha"
)
var cssVersion string
var css = []string{cssVersion + "bundle.css", "remixicon.css"}
var cssHeader string
func (app *appContext) loadCSSHeader() string {
l := len(css)
h := ""
for i, f := range css {
h += "<" + app.URLBase + "/css/" + f + ">; rel=preload; as=style"
if l > 1 && i != (l-1) {
h += ", "
}
}
return h
}
func (app *appContext) getURLBase(gc *gin.Context) string {
if strings.HasPrefix(gc.Request.URL.String(), app.URLBase) {
return app.URLBase
}
return ""
}
func gcHTML(gc *gin.Context, code int, file string, templ gin.H) {
gc.Header("Cache-Control", "no-cache")
gc.HTML(code, file, templ)
}
func (app *appContext) pushResources(gc *gin.Context, page Page) {
var toPush []string
switch page {
case AdminPage:
toPush = []string{"/js/admin.js", "/js/theme.js", "/js/lang.js", "/js/modal.js", "/js/tabs.js", "/js/invites.js", "/js/accounts.js", "/js/settings.js", "/js/profiles.js", "/js/common.js"}
break
case UserPage:
toPush = []string{"/js/user.js", "/js/theme.js", "/js/lang.js", "/js/modal.js", "/js/common.js"}
break
default:
toPush = []string{}
}
if pusher := gc.Writer.Pusher(); pusher != nil {
app.debug.Println("Using HTTP2 Server push")
for _, f := range toPush {
if err := pusher.Push(app.URLBase+f, nil); err != nil {
app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err)
}
}
}
gc.Header("Link", cssHeader)
}
type Page int
const (
AdminPage Page = iota + 1
FormPage
PWRPage
UserPage
OtherPage
)
func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string {
lang := gc.Query("lang")
cookie, err := gc.Cookie("lang")
if lang != "" {
switch page {
case AdminPage:
if _, ok := app.storage.lang.Admin[lang]; ok {
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
return lang
}
case FormPage, UserPage:
if _, ok := app.storage.lang.User[lang]; ok {
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
return lang
}
case PWRPage:
if _, ok := app.storage.lang.PasswordReset[lang]; ok {
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
return lang
}
}
}
if cookie != "" && err == nil {
switch page {
case AdminPage:
if _, ok := app.storage.lang.Admin[cookie]; ok {
return cookie
}
case FormPage, UserPage:
if _, ok := app.storage.lang.User[cookie]; ok {
return cookie
}
case PWRPage:
if _, ok := app.storage.lang.PasswordReset[cookie]; ok {
return cookie
}
}
}
return chosen
}
func (app *appContext) AdminPage(gc *gin.Context) {
app.pushResources(gc, AdminPage)
lang := app.getLang(gc, AdminPage, app.storage.lang.chosenAdminLang)
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)
jfAdminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
var license string
l, err := fs.ReadFile(localFS, "LICENSE")
if err != nil {
app.debug.Printf("Failed to load LICENSE: %s", err)
license = ""
}
license = string(l)
fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt"))
if err != nil {
app.debug.Printf("Failed to load OFL.txt: %s", err)
}
license += "---Hanken Grotesk---\n\n"
license += string(fontLicense)
if builtBy == "" {
builtBy = "???"
}
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": "",
"emailEnabled": emailEnabled,
"telegramEnabled": telegramEnabled,
"discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled,
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled,
"version": version,
"commit": commit,
"buildTime": buildTime,
"builtBy": builtBy,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.Admin[lang].Strings,
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
"language": app.storage.lang.Admin[lang].JSON,
"langName": lang,
"license": license,
"jellyfinLogin": app.jellyfinLogin,
"jfAdminOnly": jfAdminOnly,
"jfAllowAll": jfAllowAll,
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false),
})
}
func (app *appContext) MyUserPage(gc *gin.Context) {
app.pushResources(gc, UserPage)
lang := app.getLang(gc, UserPage, app.storage.lang.chosenUserLang)
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)
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,
"ombiEnabled": ombiEnabled,
"pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false),
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.User[lang].Strings,
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
"language": app.storage.lang.User[lang].JSON,
"langName": lang,
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
"requirements": app.validator.getCriteria(),
}
if telegramEnabled {
data["telegramUsername"] = 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": `<span class="text-black dark:text-white font-mono">/` + app.config.Section("discord").Key("start_command").MustString("start") + `</span>`,
"server_channel": app.discord.serverChannelName,
}))
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
pageMessagesExist := map[string]bool{}
pageMessages := map[string]CustomContent{}
pageMessages["Login"], pageMessagesExist["Login"] = app.storage.GetCustomContentKey("UserLogin")
pageMessages["Page"], pageMessagesExist["Page"] = app.storage.GetCustomContentKey("UserPage")
for name, msg := range pageMessages {
if !pageMessagesExist[name] {
continue
}
data[name+"MessageEnabled"] = msg.Enabled
if !msg.Enabled {
continue
}
// We don't template here, since the username is only known after login.
data[name+"MessageContent"] = template.HTML(markdown.ToHTML([]byte(msg.Content), nil, markdownRenderer))
}
gcHTML(gc, http.StatusOK, "user.html", data)
}
func (app *appContext) ResetPassword(gc *gin.Context) {
isBot := strings.Contains(gc.Request.Header.Get("User-Agent"), "Bot")
setPassword := app.config.Section("password_resets").Key("set_password").MustBool(false)
pin := gc.Query("pin")
if pin == "" {
app.NoRouteHandler(gc)
return
}
app.pushResources(gc, PWRPage)
lang := app.getLang(gc, PWRPage, app.storage.lang.chosenPWRLang)
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"strings": app.storage.lang.PasswordReset[lang].Strings,
"success": false,
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
}
pwr, isInternal := app.internalPWRs[pin]
if isInternal && setPassword {
data["helpMessage"] = app.config.Section("ui").Key("help_message").String()
data["successMessage"] = app.config.Section("ui").Key("success_message").String()
data["jfLink"] = app.config.Section("ui").Key("redirect_url").String()
data["redirectToJellyfin"] = app.config.Section("ui").Key("auto_redirect").MustBool(false)
data["validate"] = app.config.Section("password_validation").Key("enabled").MustBool(false)
data["requirements"] = app.validator.getCriteria()
data["strings"] = app.storage.lang.PasswordReset[lang].Strings
data["validationStrings"] = app.storage.lang.User[lang].validationStringsJSON
data["notifications"] = app.storage.lang.User[lang].notificationsJSON
data["langName"] = lang
data["passwordReset"] = true
data["telegramEnabled"] = false
data["discordEnabled"] = false
data["matrixEnabled"] = false
gcHTML(gc, http.StatusOK, "form-loader.html", data)
return
}
defer gcHTML(gc, http.StatusOK, "password-reset.html", data)
// If it's a bot, pretend to be a success so the preview is nice.
if isBot {
app.debug.Println("PWR: Ignoring magic link visit from bot")
data["success"] = true
data["pin"] = "NO-BO-TS"
return
}
// if reset, ok := app.internalPWRs[pin]; ok {
// status, err := app.jf.ResetPasswordAdmin(reset.ID)
// if !(status == 200 || status == 204) || err != nil {
// app.err.Printf("Password Reset failed (%d): %v", status, err)
// return
// }
// status, err = app.jf.SetPassword(reset.ID, "", pin)
// if !(status == 200 || status == 204) || err != nil {
// app.err.Printf("Password Reset failed (%d): %v", status, err)
// return
// }
// data["success"] = true
// data["pin"] = pin
// }
var resp mediabrowser.PasswordResetResponse
var status int
var err error
var username string
if !isInternal {
resp, status, err = app.jf.ResetPassword(pin)
} else if time.Now().After(pwr.Expiry) {
app.debug.Printf("Ignoring PWR request due to expired internal PIN: %s", pin)
app.NoRouteHandler(gc)
return
} else {
status, err = app.jf.ResetPasswordAdmin(pwr.ID)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Password Reset failed (%d): %v", status, err)
} else {
status, err = app.jf.SetPassword(pwr.ID, "", pin)
}
username = pwr.Username
}
if (status == 200 || status == 204) && err == nil && (isInternal || resp.Success) {
data["success"] = true
data["pin"] = pin
if !isInternal {
username = resp.UsersReset[0]
}
} else {
app.err.Printf("Password Reset failed (%d): %v", status, err)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
jfUser, status, err := app.jf.UserByName(username, false)
if status != 200 || err != nil {
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
return
}
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
if status != 200 || err != nil {
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", username, status, err)
return
}
ombiUser["password"] = pin
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
return
}
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
}
}
// @Summary returns the captcha image corresponding to the given ID.
// @Param code path string true "invite code"
// @Param captchaID path string true "captcha ID"
// @Tags Other
// @Router /captcha/img/{code}/{captchaID} [get]
func (app *appContext) GetCaptcha(gc *gin.Context) {
code := gc.Param("invCode")
captchaID := gc.Param("captchaID")
inv, ok := app.storage.GetInvitesKey(code)
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
var capt *captcha.Data
if inv.Captchas != nil {
capt = inv.Captchas[captchaID]
}
if capt == nil {
respondBool(400, false, gc)
return
}
if err := capt.WriteImage(gc.Writer); err != nil {
app.err.Printf("Failed to write CAPTCHA image: %v", err)
respondBool(500, false, gc)
return
}
gc.Status(200)
return
}
// @Summary Generates a new captcha and returns it's ID. This can then be included in a request to /captcha/img/{id} to get an image.
// @Produce json
// @Param code path string true "invite code"
// @Success 200 {object} genCaptchaDTO
// @Router /captcha/gen/{code} [get]
// @Security Bearer
// @tags Users
func (app *appContext) GenCaptcha(gc *gin.Context) {
code := gc.Param("invCode")
inv, ok := app.storage.GetInvitesKey(code)
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
capt, err := captcha.New(300, 100)
if err != nil {
app.err.Printf("Failed to generate captcha: %v", err)
respondBool(500, false, gc)
return
}
if inv.Captchas == nil {
inv.Captchas = map[string]*captcha.Data{}
}
captchaID := genAuthToken()
inv.Captchas[captchaID] = capt
app.storage.SetInvitesKey(code, inv)
gc.JSON(200, genCaptchaDTO{captchaID})
return
}
func (app *appContext) verifyCaptcha(code, id, text string) bool {
reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false)
if !reCAPTCHA {
// internal CAPTCHA
inv, ok := app.storage.GetInvitesKey(code)
if !ok || inv.Captchas == nil {
app.debug.Printf("Couldn't find invite \"%s\"", code)
return false
}
c, ok := inv.Captchas[id]
if !ok {
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
return false
}
return strings.ToLower(c.Text) == strings.ToLower(text)
}
// reCAPTCHA
msg := ReCaptchaRequestDTO{
Secret: app.config.Section("captcha").Key("recaptcha_secret_key").MustString(""),
Response: text,
}
// Why doesn't this endpoint accept JSON???
urlencode := url.Values{}
urlencode.Set("secret", msg.Secret)
urlencode.Set("response", msg.Response)
req, _ := http.NewRequest("POST", "https://www.google.com/recaptcha/api/siteverify", strings.NewReader(urlencode.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
app.err.Printf("Failed to read reCAPTCHA status (%d): %+v\n", resp.Status, err)
return false
}
defer resp.Body.Close()
var data ReCaptchaResponseDTO
body, err := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &data)
if err != nil {
app.err.Printf("Failed to unmarshal reCAPTCHA response: %+v\n", err)
return false
}
hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("")
if strings.ToLower(data.Hostname) != strings.ToLower(hostname) && data.Hostname != "" {
app.debug.Printf("Invalidating reCAPTCHA request: Hostnames didn't match (Wanted \"%s\", got \"%s\"\n", hostname, data.Hostname)
return false
}
if len(data.ErrorCodes) > 0 {
app.err.Printf("reCAPTCHA returned errors: %+v\n", data.ErrorCodes)
return false
}
return data.Success
}
// @Summary returns 204 if the given Captcha contents is correct for the corresponding captcha ID and invite code.
// @Param code path string true "invite code"
// @Param captchaID path string true "captcha ID"
// @Param text path string true "Captcha text"
// @Success 204
// @Tags Other
// @Router /captcha/verify/{code}/{captchaID}/{text} [get]
func (app *appContext) VerifyCaptcha(gc *gin.Context) {
code := gc.Param("invCode")
captchaID := gc.Param("captchaID")
text := gc.Param("text")
inv, ok := app.storage.GetInvitesKey(code)
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
return
}
var capt *captcha.Data
if inv.Captchas != nil {
capt = inv.Captchas[captchaID]
}
if capt == nil {
respondBool(400, false, gc)
return
}
if strings.ToLower(capt.Text) != strings.ToLower(text) {
respondBool(400, false, gc)
return
}
respondBool(204, true, gc)
return
}
func (app *appContext) InviteProxy(gc *gin.Context) {
app.pushResources(gc, FormPage)
code := gc.Param("invCode")
lang := app.getLang(gc, FormPage, app.storage.lang.chosenUserLang)
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
// if app.checkInvite(code, false, "") {
inv, ok := app.storage.GetInvitesKey(code)
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
return
}
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
var req newUserDTO
if app.ConfirmationKeys == nil {
fail()
return
}
invKeys, ok := app.ConfirmationKeys[code]
if !ok {
fail()
return
}
req, ok = invKeys[key]
if !ok {
fail()
return
}
token, err := jwt.Parse(key, checkToken)
if err != nil {
fail()
app.err.Printf("Failed to parse key: %s", err)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
fail()
app.debug.Printf("Invalid key")
return
}
f, success := app.newUser(req, true)
if !success {
app.err.Printf("Failed to create new user")
// Not meant for us. Calling this will be a mess, but at least it might give us some information.
f(gc)
fail()
return
}
jfLink := app.config.Section("ui").Key("redirect_url").String()
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
gc.Redirect(301, jfLink)
} else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"strings": app.storage.lang.User[lang].Strings,
"successMessage": app.config.Section("ui").Key("success_message").String(),
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"jfLink": jfLink,
})
}
delete(invKeys, key)
app.confirmationKeysLock.Lock()
app.ConfirmationKeys[code] = invKeys
app.confirmationKeysLock.Unlock()
return
}
email := ""
if invite, ok := app.storage.GetInvitesKey(code); ok {
email = invite.SendTo
}
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
email = ""
}
telegram := telegramEnabled && app.config.Section("telegram").Key("show_on_reg").MustBool(true)
discord := discordEnabled && app.config.Section("discord").Key("show_on_reg").MustBool(true)
matrix := matrixEnabled && app.config.Section("matrix").Key("show_on_reg").MustBool(true)
userPageAddress := app.config.Section("invite_emails").Key("url_base").String()
if userPageAddress == "" {
userPageAddress = app.config.Section("password_resets").Key("url_base").String()
}
userPageAddress += "/my/account"
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"helpMessage": app.config.Section("ui").Key("help_message").String(),
"successMessage": app.config.Section("ui").Key("success_message").String(),
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
"redirectToJellyfin": app.config.Section("ui").Key("auto_redirect").MustBool(false),
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
"requirements": app.validator.getCriteria(),
"email": email,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.User[lang].Strings,
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
"notifications": app.storage.lang.User[lang].notificationsJSON,
"code": code,
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
"userExpiry": inv.UserExpiry,
"userExpiryMonths": inv.UserMonths,
"userExpiryDays": inv.UserDays,
"userExpiryHours": inv.UserHours,
"userExpiryMinutes": inv.UserMinutes,
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"passwordReset": false,
"telegramEnabled": telegram,
"discordEnabled": discord,
"matrixEnabled": matrix,
"emailRequired": app.config.Section("email").Key("required").MustBool(false),
"captcha": app.config.Section("captcha").Key("enabled").MustBool(false),
"reCAPTCHA": app.config.Section("captcha").Key("recaptcha").MustBool(false),
"reCAPTCHASiteKey": app.config.Section("captcha").Key("recaptcha_site_key").MustString(""),
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"userPageAddress": userPageAddress,
}
if telegram {
data["telegramPIN"] = app.telegram.NewAuthToken()
data["telegramUsername"] = app.telegram.username
data["telegramURL"] = app.telegram.link
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
}
if matrix {
data["matrixRequired"] = app.config.Section("matrix").Key("required").MustBool(false)
data["matrixUser"] = app.matrix.userID
}
if discord {
data["discordPIN"] = app.discord.NewAuthToken()
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": `<span class="text-black dark:text-white font-mono">/` + app.config.Section("discord").Key("start_command").MustString("start") + `</span>`,
"server_channel": app.discord.serverChannelName,
}))
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
// if discordEnabled {
// pin := ""
// for _, token := range app.discord.tokens {
// if
gcHTML(gc, http.StatusOK, "form-loader.html", data)
}
func (app *appContext) NoRouteHandler(gc *gin.Context) {
app.pushResources(gc, OtherPage)
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}