mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-06-28 12:27:47 +02:00
Harvey Tindall
99875b9176
When signing up, the user is given a pin code which they send to a telegram bot. This provides user verification, but more importantly allows the bot to message the user, as the Telegram API requires the user to interact with the bot before it can do the opposite. The bot should recognize the correct language, but a /lang command is also provided to change it. The verification process is pretty much functional but ui is still broken, and it isn't properly integrated yet.
303 lines
9.7 KiB
Go
303 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"io/fs"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dgrijalva/jwt-go"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
var css = []string{"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, admin bool) {
|
|
if pusher := gc.Writer.Pusher(); pusher != nil {
|
|
app.debug.Println("Using HTTP2 Server push")
|
|
if admin {
|
|
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"}
|
|
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
|
|
)
|
|
|
|
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:
|
|
if _, ok := app.storage.lang.Form[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:
|
|
if _, ok := app.storage.lang.Form[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, true)
|
|
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)
|
|
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)
|
|
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
|
|
"urlBase": app.getURLBase(gc),
|
|
"cssClass": app.cssClass,
|
|
"contactMessage": "",
|
|
"email_enabled": emailEnabled,
|
|
"notifications": notificationsEnabled,
|
|
"version": version,
|
|
"commit": commit,
|
|
"ombiEnabled": ombiEnabled,
|
|
"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,
|
|
})
|
|
}
|
|
|
|
func (app *appContext) ResetPassword(gc *gin.Context) {
|
|
pin := gc.Query("pin")
|
|
if pin == "" {
|
|
app.NoRouteHandler(gc)
|
|
return
|
|
}
|
|
app.pushResources(gc, false)
|
|
lang := app.getLang(gc, PWRPage, app.storage.lang.chosenPWRLang)
|
|
data := gin.H{
|
|
"urlBase": app.getURLBase(gc),
|
|
"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),
|
|
}
|
|
resp, status, err := app.jf.ResetPassword(pin)
|
|
if status == 200 && err == nil && resp.Success {
|
|
data["success"] = true
|
|
data["pin"] = pin
|
|
} else {
|
|
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
|
}
|
|
defer gcHTML(gc, http.StatusOK, "password-reset.html", data)
|
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
jfUser, status, err := app.jf.UserByName(resp.UsersReset[0], false)
|
|
if status != 200 || err != nil {
|
|
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", resp.UsersReset[0], 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", resp.UsersReset[0], 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"])
|
|
}
|
|
}
|
|
|
|
func (app *appContext) InviteProxy(gc *gin.Context) {
|
|
app.pushResources(gc, false)
|
|
code := gc.Param("invCode")
|
|
lang := app.getLang(gc, FormPage, app.storage.lang.chosenFormLang)
|
|
/* 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.invites[code]
|
|
if !ok {
|
|
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
|
"cssClass": app.cssClass,
|
|
"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) {
|
|
validKey := false
|
|
keyIndex := -1
|
|
for i, k := range inv.Keys {
|
|
if k == key {
|
|
validKey = true
|
|
keyIndex = i
|
|
break
|
|
}
|
|
}
|
|
fail := func() {
|
|
gcHTML(gc, 404, "404.html", gin.H{
|
|
"cssClass": app.cssClass,
|
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
|
})
|
|
}
|
|
if !validKey {
|
|
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)
|
|
expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
|
|
if err != nil {
|
|
fail()
|
|
app.err.Printf("Failed to parse key expiry: %s", err)
|
|
return
|
|
}
|
|
expiry := time.Unix(expiryUnix, 0)
|
|
if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
|
fail()
|
|
app.debug.Printf("Invalid key")
|
|
return
|
|
}
|
|
req := newUserDTO{
|
|
Email: claims["email"].(string),
|
|
Username: claims["username"].(string),
|
|
Password: claims["password"].(string),
|
|
Code: claims["invite"].(string),
|
|
}
|
|
_, success := app.newUser(req, true)
|
|
if !success {
|
|
fail()
|
|
return
|
|
}
|
|
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
|
|
"strings": app.storage.lang.Form[lang].Strings,
|
|
"successMessage": app.config.Section("ui").Key("success_message").String(),
|
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
|
"jfLink": app.config.Section("jellyfin").Key("public_server").String(),
|
|
})
|
|
inv, ok := app.storage.invites[code]
|
|
if ok {
|
|
l := len(inv.Keys)
|
|
inv.Keys[l-1], inv.Keys[keyIndex] = inv.Keys[keyIndex], inv.Keys[l-1]
|
|
app.storage.invites[code] = inv
|
|
}
|
|
return
|
|
}
|
|
email := app.storage.invites[code].Email
|
|
if strings.Contains(email, "Failed") {
|
|
email = ""
|
|
}
|
|
data := gin.H{
|
|
"urlBase": app.getURLBase(gc),
|
|
"cssClass": app.cssClass,
|
|
"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("jellyfin").Key("public_server").String(),
|
|
"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.Form[lang].Strings,
|
|
"validationStrings": app.storage.lang.Form[lang].validationStringsJSON,
|
|
"notifications": app.storage.lang.Form[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.Form[lang].Strings.get("yourAccountIsValidUntil"),
|
|
"langName": lang,
|
|
"telegramEnabled": app.config.Section("telegram").Key("enabled").MustBool(false),
|
|
}
|
|
if data["telegramEnabled"].(bool) {
|
|
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)
|
|
}
|
|
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
|
}
|
|
|
|
func (app *appContext) NoRouteHandler(gc *gin.Context) {
|
|
app.pushResources(gc, false)
|
|
gcHTML(gc, 404, "404.html", gin.H{
|
|
"cssClass": app.cssClass,
|
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
|
})
|
|
}
|