mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 17:10:10 +00:00
Harvey Tindall
9c2f27bcdb
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.
695 lines
24 KiB
Go
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(),
|
|
})
|
|
}
|