mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
4 Commits
e71d492495
...
e5f79c60ae
Author | SHA1 | Date | |
---|---|---|---|
e5f79c60ae | |||
8307d3da90 | |||
6bad293f74 | |||
b2771e6cc5 |
@ -585,7 +585,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
|||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tempConfig, _ := ini.Load(app.configPath)
|
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||||
matrix := tempConfig.Section("matrix")
|
matrix := tempConfig.Section("matrix")
|
||||||
matrix.Key("enabled").SetValue("true")
|
matrix.Key("enabled").SetValue("true")
|
||||||
matrix.Key("homeserver").SetValue(req.Homeserver)
|
matrix.Key("homeserver").SetValue(req.Homeserver)
|
||||||
|
114
api-users.go
114
api-users.go
@ -42,7 +42,7 @@ func (app *appContext) NewUserFromAdmin(gc *gin.Context) {
|
|||||||
profile = p
|
profile = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nu := app.NewUserPostVerification(NewUserParams{
|
nu /*wg*/, _ := app.NewUserPostVerification(NewUserParams{
|
||||||
Req: req,
|
Req: req,
|
||||||
SourceType: ActivityAdmin,
|
SourceType: ActivityAdmin,
|
||||||
Source: gc.GetString("jfId"),
|
Source: gc.GetString("jfId"),
|
||||||
@ -59,6 +59,8 @@ func (app *appContext) NewUserFromAdmin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
respondUser(nu.Status, nu.Created, welcomeMessageSentIfNecessary, nu.Message, gc)
|
respondUser(nu.Status, nu.Created, welcomeMessageSentIfNecessary, nu.Message, gc)
|
||||||
|
// These don't need to complete anytime soon
|
||||||
|
// wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Creates a new Jellyfin user via invite code
|
// @Summary Creates a new Jellyfin user via invite code
|
||||||
@ -205,8 +207,7 @@ func (app *appContext) NewUserFromInvite(gc *gin.Context) {
|
|||||||
profile = &p
|
profile = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Use NewUserPostVerification
|
nu /*wg*/, _ := app.NewUserPostVerification(NewUserParams{
|
||||||
nu := app.NewUserPostVerification(NewUserParams{
|
|
||||||
Req: req,
|
Req: req,
|
||||||
SourceType: sourceType,
|
SourceType: sourceType,
|
||||||
Source: source,
|
Source: source,
|
||||||
@ -341,7 +342,10 @@ func (app *appContext) NewUserFromInvite(gc *gin.Context) {
|
|||||||
code = 400
|
code = 400
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gc.JSON(code, validation)
|
gc.JSON(code, validation)
|
||||||
|
// These don't need to complete anytime soon
|
||||||
|
// wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Enable/Disable a list of users, optionally notifying them why.
|
// @Summary Enable/Disable a list of users, optionally notifying them why.
|
||||||
@ -822,6 +826,60 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
|
|||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *appContext) userSummary(jfUser mediabrowser.User) respUser {
|
||||||
|
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||||
|
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
||||||
|
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
|
||||||
|
user := respUser{
|
||||||
|
ID: jfUser.ID,
|
||||||
|
Name: jfUser.Name,
|
||||||
|
Admin: jfUser.Policy.IsAdministrator,
|
||||||
|
Disabled: jfUser.Policy.IsDisabled,
|
||||||
|
ReferralsEnabled: false,
|
||||||
|
}
|
||||||
|
if !jfUser.LastActivityDate.IsZero() {
|
||||||
|
user.LastActive = jfUser.LastActivityDate.Unix()
|
||||||
|
}
|
||||||
|
if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok {
|
||||||
|
user.Email = email.Addr
|
||||||
|
user.NotifyThroughEmail = email.Contact
|
||||||
|
user.Label = email.Label
|
||||||
|
user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll)
|
||||||
|
}
|
||||||
|
expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID)
|
||||||
|
if ok {
|
||||||
|
user.Expiry = expiry.Expiry.Unix()
|
||||||
|
}
|
||||||
|
if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
|
||||||
|
user.Telegram = tgUser.Username
|
||||||
|
user.NotifyThroughTelegram = tgUser.Contact
|
||||||
|
}
|
||||||
|
if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok {
|
||||||
|
user.Matrix = mxUser.UserID
|
||||||
|
user.NotifyThroughMatrix = mxUser.Contact
|
||||||
|
}
|
||||||
|
if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok {
|
||||||
|
user.Discord = RenderDiscordUsername(dcUser)
|
||||||
|
// user.Discord = dcUser.Username + "#" + dcUser.Discriminator
|
||||||
|
user.DiscordID = dcUser.ID
|
||||||
|
user.NotifyThroughDiscord = dcUser.Contact
|
||||||
|
}
|
||||||
|
// FIXME: Send referral data
|
||||||
|
referrerInv := Invite{}
|
||||||
|
if referralsEnabled {
|
||||||
|
// 1. Directly attached invite.
|
||||||
|
err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID))
|
||||||
|
if err == nil {
|
||||||
|
user.ReferralsEnabled = true
|
||||||
|
// 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database.
|
||||||
|
} else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" {
|
||||||
|
user.ReferralsEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// @Summary Get a list of Jellyfin users.
|
// @Summary Get a list of Jellyfin users.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} getUsersDTO
|
// @Success 200 {object} getUsersDTO
|
||||||
@ -838,57 +896,9 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
|
||||||
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
|
||||||
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
|
|
||||||
i := 0
|
i := 0
|
||||||
for _, jfUser := range users {
|
for _, jfUser := range users {
|
||||||
user := respUser{
|
user := app.userSummary(jfUser)
|
||||||
ID: jfUser.ID,
|
|
||||||
Name: jfUser.Name,
|
|
||||||
Admin: jfUser.Policy.IsAdministrator,
|
|
||||||
Disabled: jfUser.Policy.IsDisabled,
|
|
||||||
ReferralsEnabled: false,
|
|
||||||
}
|
|
||||||
if !jfUser.LastActivityDate.IsZero() {
|
|
||||||
user.LastActive = jfUser.LastActivityDate.Unix()
|
|
||||||
}
|
|
||||||
if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok {
|
|
||||||
user.Email = email.Addr
|
|
||||||
user.NotifyThroughEmail = email.Contact
|
|
||||||
user.Label = email.Label
|
|
||||||
user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll)
|
|
||||||
}
|
|
||||||
expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID)
|
|
||||||
if ok {
|
|
||||||
user.Expiry = expiry.Expiry.Unix()
|
|
||||||
}
|
|
||||||
if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
|
|
||||||
user.Telegram = tgUser.Username
|
|
||||||
user.NotifyThroughTelegram = tgUser.Contact
|
|
||||||
}
|
|
||||||
if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok {
|
|
||||||
user.Matrix = mxUser.UserID
|
|
||||||
user.NotifyThroughMatrix = mxUser.Contact
|
|
||||||
}
|
|
||||||
if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok {
|
|
||||||
user.Discord = RenderDiscordUsername(dcUser)
|
|
||||||
// user.Discord = dcUser.Username + "#" + dcUser.Discriminator
|
|
||||||
user.DiscordID = dcUser.ID
|
|
||||||
user.NotifyThroughDiscord = dcUser.Contact
|
|
||||||
}
|
|
||||||
// FIXME: Send referral data
|
|
||||||
referrerInv := Invite{}
|
|
||||||
if referralsEnabled {
|
|
||||||
// 1. Directly attached invite.
|
|
||||||
err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID))
|
|
||||||
if err == nil {
|
|
||||||
user.ReferralsEnabled = true
|
|
||||||
// 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database.
|
|
||||||
} else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" {
|
|
||||||
user.ReferralsEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.UserList[i] = user
|
resp.UserList[i] = user
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
17
api.go
17
api.go
@ -290,6 +290,8 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
|||||||
val := app.config.Section(sectName).Key(settingName)
|
val := app.config.Section(sectName).Key(settingName)
|
||||||
s := resp.Sections[sectName].Settings[settingName]
|
s := resp.Sections[sectName].Settings[settingName]
|
||||||
switch setting.Type {
|
switch setting.Type {
|
||||||
|
case "list":
|
||||||
|
s.Value = val.StringsWithShadows("|")
|
||||||
case "text", "email", "select", "password", "note":
|
case "text", "email", "select", "password", "note":
|
||||||
s.Value = val.MustString("")
|
s.Value = val.MustString("")
|
||||||
case "number":
|
case "number":
|
||||||
@ -327,7 +329,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
|||||||
|
|
||||||
// @Summary Modify app config.
|
// @Summary Modify app config.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
|
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings (lists split with | delimiter)."
|
||||||
// @Success 200 {object} boolResponse
|
// @Success 200 {object} boolResponse
|
||||||
// @Failure 500 {object} stringResponse
|
// @Failure 500 {object} stringResponse
|
||||||
// @Router /config [post]
|
// @Router /config [post]
|
||||||
@ -337,7 +339,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|||||||
var req configDTO
|
var req configDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
// Load a new config, as we set various default values in app.config that shouldn't be stored.
|
// Load a new config, as we set various default values in app.config that shouldn't be stored.
|
||||||
tempConfig, _ := ini.Load(app.configPath)
|
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||||
for section, settings := range req {
|
for section, settings := range req {
|
||||||
if section != "restart-program" {
|
if section != "restart-program" {
|
||||||
_, err := tempConfig.GetSection(section)
|
_, err := tempConfig.GetSection(section)
|
||||||
@ -350,6 +352,17 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
if (section == "discord" || section == "matrix") && setting == "language" {
|
if (section == "discord" || section == "matrix") && setting == "language" {
|
||||||
tempConfig.Section("telegram").Key("language").SetValue(value.(string))
|
tempConfig.Section("telegram").Key("language").SetValue(value.(string))
|
||||||
|
} else if app.configBase.Sections[section].Settings[setting].Type == "list" {
|
||||||
|
splitValues := strings.Split(value.(string), "|")
|
||||||
|
// Delete the key first to get rid of any shadow values
|
||||||
|
tempConfig.Section(section).DeleteKey(setting)
|
||||||
|
for i, v := range splitValues {
|
||||||
|
if i == 0 {
|
||||||
|
tempConfig.Section(section).Key(setting).SetValue(v)
|
||||||
|
} else {
|
||||||
|
tempConfig.Section(section).Key(setting).AddShadow(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if value.(string) != app.config.Section(section).Key(setting).MustString("") {
|
} else if value.(string) != app.config.Section(section).Key(setting).MustString("") {
|
||||||
tempConfig.Section(section).Key(setting).SetValue(value.(string))
|
tempConfig.Section(section).Key(setting).SetValue(value.(string))
|
||||||
}
|
}
|
||||||
|
7
auth.go
7
auth.go
@ -248,7 +248,9 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
host := gc.Request.URL.Hostname()
|
// host := gc.Request.URL.Hostname()
|
||||||
|
host := app.ExternalDomain
|
||||||
|
|
||||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||||
gc.JSON(200, getTokenDTO{token})
|
gc.JSON(200, getTokenDTO{token})
|
||||||
}
|
}
|
||||||
@ -307,7 +309,8 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
|||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
host := gc.Request.URL.Hostname()
|
// host := gc.Request.URL.Hostname()
|
||||||
|
host := app.ExternalDomain
|
||||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||||
gc.JSON(200, getTokenDTO{jwt})
|
gc.JSON(200, getTokenDTO{jwt})
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
)
|
)
|
||||||
@ -77,3 +83,68 @@ type ConfigurableTransport interface {
|
|||||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||||
SetTransport(t *http.Transport)
|
SetTransport(t *http.Transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stripped down-ish version of rough http request function used in most of the API clients.
|
||||||
|
func Req(httpClient *http.Client, timeoutHandler TimeoutHandler, mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
|
||||||
|
var params []byte
|
||||||
|
if data != nil {
|
||||||
|
params, _ = json.Marshal(data)
|
||||||
|
}
|
||||||
|
if qp := queryParams.Encode(); qp != "" {
|
||||||
|
uri += "?" + qp
|
||||||
|
}
|
||||||
|
var req *http.Request
|
||||||
|
if data != nil {
|
||||||
|
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
|
||||||
|
} else {
|
||||||
|
req, _ = http.NewRequest(mode, uri, nil)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
for name, value := range headers {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if resp == nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
err = GenericErr(resp.StatusCode, err)
|
||||||
|
if timeoutHandler != nil {
|
||||||
|
defer timeoutHandler()
|
||||||
|
}
|
||||||
|
var responseText string
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if response || err != nil {
|
||||||
|
responseText, err = decodeResp(resp)
|
||||||
|
if err != nil {
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
var msg any
|
||||||
|
err = json.Unmarshal([]byte(responseText), &msg)
|
||||||
|
if err != nil {
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
if msg != nil {
|
||||||
|
err = fmt.Errorf("got %d: %+v", resp.StatusCode, msg)
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeResp(resp *http.Response) (string, error) {
|
||||||
|
var out io.Reader
|
||||||
|
switch resp.Header.Get("Content-Encoding") {
|
||||||
|
case "gzip":
|
||||||
|
out, _ = gzip.NewReader(resp.Body)
|
||||||
|
default:
|
||||||
|
out = resp.Body
|
||||||
|
}
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
_, err := io.Copy(buf, out)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
14
config.go
14
config.go
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -35,7 +36,7 @@ func (app *appContext) MustSetValue(section, key, val string) {
|
|||||||
|
|
||||||
func (app *appContext) loadConfig() error {
|
func (app *appContext) loadConfig() error {
|
||||||
var err error
|
var err error
|
||||||
app.config, err = ini.Load(app.configPath)
|
app.config, err = ini.ShadowLoad(app.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -60,10 +61,17 @@ func (app *appContext) loadConfig() error {
|
|||||||
if app.URLBase == "/invite" || app.URLBase == "/accounts" || app.URLBase == "/settings" || app.URLBase == "/activity" {
|
if app.URLBase == "/invite" || app.URLBase == "/accounts" || app.URLBase == "/settings" || app.URLBase == "/activity" {
|
||||||
app.err.Printf(lm.BadURLBase, app.URLBase)
|
app.err.Printf(lm.BadURLBase, app.URLBase)
|
||||||
}
|
}
|
||||||
app.ExternalHost = strings.TrimSuffix(strings.TrimSuffix(app.config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/")
|
app.ExternalURI = strings.TrimSuffix(strings.TrimSuffix(app.config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/")
|
||||||
if !strings.HasSuffix(app.ExternalHost, app.URLBase) {
|
if !strings.HasSuffix(app.ExternalURI, app.URLBase) {
|
||||||
app.err.Println(lm.NoURLSuffix)
|
app.err.Println(lm.NoURLSuffix)
|
||||||
}
|
}
|
||||||
|
if app.ExternalURI == "" {
|
||||||
|
app.err.Println(lm.NoExternalHost + lm.LoginWontSave)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(app.ExternalURI)
|
||||||
|
if err == nil {
|
||||||
|
app.ExternalDomain = u.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
|
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
|
||||||
|
|
||||||
|
@ -2017,6 +2017,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"webhooks": {
|
||||||
|
"order": [],
|
||||||
|
"meta": {
|
||||||
|
"name": "Webhooks",
|
||||||
|
"description": "jfa-go will send a POST request to these URLs when an event occurs, with relevant information. Request information is logged when debug logging is enabled.",
|
||||||
|
"wiki_link": "https://wiki.jfa-go.com/docs/webhooks/"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"created": {
|
||||||
|
"name": "User Created",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"type": "list",
|
||||||
|
"value": "",
|
||||||
|
"description": "URLs to hit when an account is created through jfa-go. Sends a `respUser` object."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"order": [],
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
|
4
email.go
4
email.go
@ -325,7 +325,7 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message := app.config.Section("messages").Key("message").String()
|
message := app.config.Section("messages").Key("message").String()
|
||||||
inviteLink := app.ExternalHost
|
inviteLink := app.ExternalURI
|
||||||
if code == "" { // Personal email change
|
if code == "" { // Personal email change
|
||||||
inviteLink = fmt.Sprintf("%s/my/confirm/%s", inviteLink, url.PathEscape(key))
|
inviteLink = fmt.Sprintf("%s/my/confirm/%s", inviteLink, url.PathEscape(key))
|
||||||
} else { // Invite email confirmation
|
} else { // Invite email confirmation
|
||||||
@ -393,7 +393,7 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext
|
|||||||
expiry := invite.ValidTill
|
expiry := invite.ValidTill
|
||||||
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||||
message := app.config.Section("messages").Key("message").String()
|
message := app.config.Section("messages").Key("message").String()
|
||||||
inviteLink := fmt.Sprintf("%s/invite/%s", app.ExternalHost, code)
|
inviteLink := fmt.Sprintf("%s/invite/%s", app.ExternalURI, code)
|
||||||
template := map[string]interface{}{
|
template := map[string]interface{}{
|
||||||
"hello": emailer.lang.InviteEmail.get("hello"),
|
"hello": emailer.lang.InviteEmail.get("hello"),
|
||||||
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
|
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="light">
|
<html lang="en" class="light">
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||||
|
@ -210,6 +210,7 @@ const (
|
|||||||
NoURLSuffix = `Warning: Given "jfa_url"/"External jfa-go URL" value does not include "url_base" value!`
|
NoURLSuffix = `Warning: Given "jfa_url"/"External jfa-go URL" value does not include "url_base" value!`
|
||||||
BadURLBase = `Warning: Given URL Base "%s" may conflict with the applications subpaths.`
|
BadURLBase = `Warning: Given URL Base "%s" may conflict with the applications subpaths.`
|
||||||
NoExternalHost = `No "External jfa-go URL" provided, set one in Settings > General.`
|
NoExternalHost = `No "External jfa-go URL" provided, set one in Settings > General.`
|
||||||
|
LoginWontSave = ` Your login won't save until you do.`
|
||||||
|
|
||||||
// discord.go
|
// discord.go
|
||||||
StartDaemon = "Started %s daemon"
|
StartDaemon = "Started %s daemon"
|
||||||
@ -306,6 +307,9 @@ const (
|
|||||||
CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\""
|
CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\""
|
||||||
FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v"
|
FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v"
|
||||||
InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")"
|
InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")"
|
||||||
|
|
||||||
|
// webhooks.go
|
||||||
|
WebhookRequest = "Webhook request send to \"%s\" (%d): %v"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
72
main.go
72
main.go
@ -101,36 +101,37 @@ type appContext struct {
|
|||||||
adminUsers []User
|
adminUsers []User
|
||||||
invalidTokens []string
|
invalidTokens []string
|
||||||
// Keeping jf name because I can't think of a better one
|
// Keeping jf name because I can't think of a better one
|
||||||
jf *mediabrowser.MediaBrowser
|
jf *mediabrowser.MediaBrowser
|
||||||
authJf *mediabrowser.MediaBrowser
|
authJf *mediabrowser.MediaBrowser
|
||||||
ombi *OmbiWrapper
|
ombi *OmbiWrapper
|
||||||
js *JellyseerrWrapper
|
js *JellyseerrWrapper
|
||||||
thirdPartyServices []ThirdPartyService
|
thirdPartyServices []ThirdPartyService
|
||||||
datePattern string
|
datePattern string
|
||||||
timePattern string
|
timePattern string
|
||||||
storage Storage
|
storage Storage
|
||||||
validator Validator
|
validator Validator
|
||||||
email *Emailer
|
email *Emailer
|
||||||
telegram *TelegramDaemon
|
telegram *TelegramDaemon
|
||||||
discord *DiscordDaemon
|
discord *DiscordDaemon
|
||||||
matrix *MatrixDaemon
|
matrix *MatrixDaemon
|
||||||
contactMethods []ContactMethodLinker
|
contactMethods []ContactMethodLinker
|
||||||
info, debug, err *logger.Logger
|
info, debug, err *logger.Logger
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
version string
|
version string
|
||||||
URLBase, ExternalHost string
|
URLBase, ExternalURI, ExternalDomain string
|
||||||
updater *Updater
|
updater *Updater
|
||||||
newUpdate bool // Whether whatever's in update is new.
|
webhooks *WebhookSender
|
||||||
tag Tag
|
newUpdate bool // Whether whatever's in update is new.
|
||||||
update Update
|
tag Tag
|
||||||
proxyEnabled bool
|
update Update
|
||||||
proxyTransport *http.Transport
|
proxyEnabled bool
|
||||||
proxyConfig easyproxy.ProxyConfig
|
proxyTransport *http.Transport
|
||||||
internalPWRs map[string]InternalPWR
|
proxyConfig easyproxy.ProxyConfig
|
||||||
pwrCaptchas map[string]Captcha
|
internalPWRs map[string]InternalPWR
|
||||||
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
|
pwrCaptchas map[string]Captcha
|
||||||
confirmationKeysLock sync.Mutex
|
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
|
||||||
|
confirmationKeysLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSecret(length int) (string, error) {
|
func generateSecret(length int) (string, error) {
|
||||||
@ -232,7 +233,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.err.Fatalf(lm.FailedCopyConfig, app.configPath, err)
|
app.err.Fatalf(lm.FailedCopyConfig, app.configPath, err)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.CopyConfig, app.configPath)
|
app.info.Printf(lm.CopyConfig, app.configPath)
|
||||||
tempConfig, _ := ini.Load(app.configPath)
|
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||||
tempConfig.Section("").Key("first_run").SetValue("true")
|
tempConfig.Section("").Key("first_run").SetValue("true")
|
||||||
tempConfig.SaveTo(app.configPath)
|
tempConfig.SaveTo(app.configPath)
|
||||||
}
|
}
|
||||||
@ -556,6 +557,13 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-consequential if we don't need it
|
||||||
|
app.webhooks = NewWebhookSender(
|
||||||
|
common.NewTimeoutHandler("Webhook", "?", true),
|
||||||
|
app.debug,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Updater proxy set in config.go, don't worry!
|
||||||
if app.proxyEnabled {
|
if app.proxyEnabled {
|
||||||
app.jf.SetTransport(app.proxyTransport)
|
app.jf.SetTransport(app.proxyTransport)
|
||||||
for _, c := range app.thirdPartyServices {
|
for _, c := range app.thirdPartyServices {
|
||||||
@ -682,7 +690,7 @@ func flagPassed(name string) (found bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @title jfa-go internal API
|
// @title jfa-go internal API
|
||||||
// @version 0.5.1
|
// @version 0.5.2
|
||||||
// @description API for the jfa-go frontend
|
// @description API for the jfa-go frontend
|
||||||
// @contact.name Harvey Tindall
|
// @contact.name Harvey Tindall
|
||||||
// @contact.email hrfee@hrfee.dev
|
// @contact.email hrfee@hrfee.dev
|
||||||
|
@ -60,7 +60,7 @@ func migrateBootstrap(app *appContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func migrateEmailConfig(app *appContext) {
|
func migrateEmailConfig(app *appContext) {
|
||||||
tempConfig, _ := ini.Load(app.configPath)
|
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||||
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
|
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
|
||||||
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -111,7 +111,7 @@ func migrateEmailStorage(app *appContext) error {
|
|||||||
return fmt.Errorf("email address was type %T, not string: \"%+v\"\n", addr, addr)
|
return fmt.Errorf("email address was type %T, not string: \"%+v\"\n", addr, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config, err := ini.Load(app.configPath)
|
config, err := ini.ShadowLoad(app.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -467,7 +467,7 @@ func intialiseCustomContent(app *appContext) {
|
|||||||
|
|
||||||
// Migrate poorly-named and duplicate "url_base" settings to the single "external jfa-go URL" setting.
|
// Migrate poorly-named and duplicate "url_base" settings to the single "external jfa-go URL" setting.
|
||||||
func migrateExternalURL(app *appContext) {
|
func migrateExternalURL(app *appContext) {
|
||||||
tempConfig, _ := ini.Load(app.configPath)
|
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||||
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf("Failed to backup config: %v", err)
|
app.err.Fatalf("Failed to backup config: %v", err)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type stringResponse struct {
|
type stringResponse struct {
|
||||||
Response string `json:"response" example:"message"`
|
Response string `json:"response" example:"message"`
|
||||||
|
1999
package-lock.json
generated
1999
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -27,7 +27,7 @@
|
|||||||
"inline-source": "^8.0.2",
|
"inline-source": "^8.0.2",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mjml": "^4.14.1",
|
"mjml": "^4.15.3",
|
||||||
"nightwind": "^1.1.13",
|
"nightwind": "^1.1.13",
|
||||||
"perl-regex": "^1.0.4",
|
"perl-regex": "^1.0.4",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
|
@ -30,7 +30,7 @@ func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
|||||||
|
|
||||||
// GenResetLink generates and returns a password reset link.
|
// GenResetLink generates and returns a password reset link.
|
||||||
func (app *appContext) GenResetLink(pin string) (string, error) {
|
func (app *appContext) GenResetLink(pin string) (string, error) {
|
||||||
url := app.ExternalHost
|
url := app.ExternalURI
|
||||||
var pinLink string
|
var pinLink string
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return pinLink, errors.New(lm.NoExternalHost)
|
return pinLink, errors.New(lm.NoExternalHost)
|
||||||
|
@ -29,7 +29,7 @@ interface Setting {
|
|||||||
requires_restart: boolean;
|
requires_restart: boolean;
|
||||||
advanced?: boolean;
|
advanced?: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
value: string | boolean | number;
|
value: string | boolean | number | string[];
|
||||||
depends_true?: string;
|
depends_true?: string;
|
||||||
depends_false?: string;
|
depends_false?: string;
|
||||||
wiki_link?: string;
|
wiki_link?: string;
|
||||||
@ -37,6 +37,11 @@ interface Setting {
|
|||||||
|
|
||||||
asElement: () => HTMLElement;
|
asElement: () => HTMLElement;
|
||||||
update: (s: Setting) => void;
|
update: (s: Setting) => void;
|
||||||
|
|
||||||
|
hide: () => void;
|
||||||
|
show: () => void;
|
||||||
|
|
||||||
|
valueAsString: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitDependant = (section: string, dep: string): string[] => {
|
const splitDependant = (section: string, dep: string): string[] => {
|
||||||
@ -49,17 +54,22 @@ const splitDependant = (section: string, dep: string): string[] => {
|
|||||||
|
|
||||||
class DOMInput {
|
class DOMInput {
|
||||||
protected _input: HTMLInputElement;
|
protected _input: HTMLInputElement;
|
||||||
private _container: HTMLDivElement;
|
protected _container: HTMLDivElement;
|
||||||
private _tooltip: HTMLDivElement;
|
protected _tooltip: HTMLDivElement;
|
||||||
private _required: HTMLSpanElement;
|
protected _required: HTMLSpanElement;
|
||||||
private _restart: HTMLSpanElement;
|
protected _restart: HTMLSpanElement;
|
||||||
private _advanced: boolean;
|
protected _advanced: boolean;
|
||||||
|
protected _section: string;
|
||||||
|
protected _name: string;
|
||||||
|
|
||||||
|
hide = () => this._input.parentElement.classList.add("unfocused");
|
||||||
|
show = () => this._input.parentElement.classList.remove("unfocused");
|
||||||
|
|
||||||
private _advancedListener = (event: settingsBoolEvent) => {
|
private _advancedListener = (event: settingsBoolEvent) => {
|
||||||
if (!Boolean(event.detail)) {
|
if (!Boolean(event.detail)) {
|
||||||
this._input.parentElement.classList.add("unfocused");
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._input.parentElement.classList.remove("unfocused");
|
this.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,10 +119,23 @@ class DOMInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(inputType: string, setting: Setting, section: string, name: string) {
|
valueAsString = (): string => { return ""+this.value; };
|
||||||
|
|
||||||
|
onValueChange = () => {
|
||||||
|
const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": this.valueAsString() })
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(inputType: string, setting: Setting, section: string, name: string, customInput?: string) {
|
||||||
|
this._section = section;
|
||||||
|
this._name = name;
|
||||||
this._container = document.createElement("div");
|
this._container = document.createElement("div");
|
||||||
this._container.classList.add("setting");
|
this._container.classList.add("setting");
|
||||||
this._container.setAttribute("data-name", name)
|
this._container.setAttribute("data-name", name)
|
||||||
|
const defaultInput = `
|
||||||
|
<input type="${inputType}" class="input setting-input ~neutral @low mt-2 mb-2">
|
||||||
|
`;
|
||||||
this._container.innerHTML = `
|
this._container.innerHTML = `
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
||||||
@ -120,31 +143,26 @@ class DOMInput {
|
|||||||
<i class="icon ri-information-line"></i>
|
<i class="icon ri-information-line"></i>
|
||||||
<span class="content sm"></span>
|
<span class="content sm"></span>
|
||||||
</div>
|
</div>
|
||||||
<input type="${inputType}" class="input ~neutral @low mt-2 mb-2">
|
${customInput ? customInput : defaultInput}
|
||||||
</label>
|
</label>
|
||||||
`;
|
`;
|
||||||
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
|
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
|
||||||
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
|
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
|
||||||
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
||||||
this._input = this._container.querySelector("input[type=" + inputType + "]") as HTMLInputElement;
|
this._input = this._container.querySelector(".setting-input") as HTMLInputElement;
|
||||||
if (setting.depends_false || setting.depends_true) {
|
if (setting.depends_false || setting.depends_true) {
|
||||||
let dependant = splitDependant(section, setting.depends_true || setting.depends_false);
|
let dependant = splitDependant(section, setting.depends_true || setting.depends_false);
|
||||||
let state = true;
|
let state = true;
|
||||||
if (setting.depends_false) { state = false; }
|
if (setting.depends_false) { state = false; }
|
||||||
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
||||||
if (Boolean(event.detail) !== state) {
|
if (Boolean(event.detail) !== state) {
|
||||||
this._input.parentElement.classList.add("unfocused");
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._input.parentElement.classList.remove("unfocused");
|
this.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const onValueChange = () => {
|
this._input.onchange = this.onValueChange;
|
||||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
|
||||||
};
|
|
||||||
this._input.onchange = onValueChange;
|
|
||||||
this.update(setting);
|
this.update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +191,85 @@ class DOMText extends DOMInput implements SText {
|
|||||||
set value(v: string) { this._input.value = v; }
|
set value(v: string) { this._input.value = v; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SList extends Setting {
|
||||||
|
value: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class DOMList extends DOMInput implements SList {
|
||||||
|
protected _inputs: HTMLDivElement;
|
||||||
|
type: string = "list";
|
||||||
|
|
||||||
|
valueAsString = (): string => { return this.value.join("|"); };
|
||||||
|
|
||||||
|
get value(): string[] {
|
||||||
|
let values = [];
|
||||||
|
const inputs = this._input.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||||
|
for (let i in inputs) {
|
||||||
|
if (inputs[i].value) values.push(inputs[i].value);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
set value(v: string[]) {
|
||||||
|
this._input.textContent = ``;
|
||||||
|
for (let val of v) {
|
||||||
|
let input = this.inputRow(val);
|
||||||
|
this._input.appendChild(input);
|
||||||
|
}
|
||||||
|
const addDummy = () => {
|
||||||
|
const dummyRow = this.inputRow();
|
||||||
|
const input = dummyRow.querySelector("input") as HTMLInputElement;
|
||||||
|
input.placeholder = window.lang.strings("add");
|
||||||
|
const onDummyChange = () => {
|
||||||
|
if (!(input.value)) return;
|
||||||
|
addDummy();
|
||||||
|
input.removeEventListener("change", onDummyChange);
|
||||||
|
input.placeholder = ``;
|
||||||
|
}
|
||||||
|
input.addEventListener("change", onDummyChange);
|
||||||
|
this._input.appendChild(dummyRow);
|
||||||
|
};
|
||||||
|
addDummy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private inputRow(v: string = ""): HTMLDivElement {
|
||||||
|
let container = document.createElement("div") as HTMLDivElement;
|
||||||
|
container.classList.add("flex", "flex-row", "justify-between");
|
||||||
|
container.innerHTML = `
|
||||||
|
<input type="text" class="input ~neutral @low">
|
||||||
|
<button class="button ~neutral @low center -ml-10 rounded-s-none aria-label="${window.lang.strings("delete")}" title="${window.lang.strings("delete")}">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
const input = container.querySelector("input") as HTMLInputElement;
|
||||||
|
input.value = v;
|
||||||
|
input.onchange = this.onValueChange;
|
||||||
|
const removeRow = container.querySelector("button") as HTMLButtonElement;
|
||||||
|
removeRow.onclick = () => {
|
||||||
|
if (!(container.nextElementSibling)) return;
|
||||||
|
container.remove();
|
||||||
|
this.onValueChange();
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
update = (s: Setting) => {
|
||||||
|
this.name = s.name;
|
||||||
|
this.description = s.description;
|
||||||
|
this.required = s.required;
|
||||||
|
this.requires_restart = s.requires_restart;
|
||||||
|
this.value = s.value as string[];
|
||||||
|
this.advanced = s.advanced;
|
||||||
|
}
|
||||||
|
|
||||||
|
asElement = (): HTMLDivElement => { return this._container; }
|
||||||
|
|
||||||
|
constructor(setting: Setting, section: string, name: string) {
|
||||||
|
super("list", setting, section, name,
|
||||||
|
`<div class="setting-input flex flex-col gap-2 mt-2 mb-2"></div>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface SPassword extends Setting {
|
interface SPassword extends Setting {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@ -214,12 +311,15 @@ class DOMBool implements SBool {
|
|||||||
private _restart: HTMLSpanElement;
|
private _restart: HTMLSpanElement;
|
||||||
type: string = "bool";
|
type: string = "bool";
|
||||||
private _advanced: boolean;
|
private _advanced: boolean;
|
||||||
|
|
||||||
|
hide = () => this._input.parentElement.classList.add("unfocused");
|
||||||
|
show = () => this._input.parentElement.classList.remove("unfocused");
|
||||||
|
|
||||||
private _advancedListener = (event: settingsBoolEvent) => {
|
private _advancedListener = (event: settingsBoolEvent) => {
|
||||||
if (!Boolean(event.detail)) {
|
if (!Boolean(event.detail)) {
|
||||||
this._input.parentElement.classList.add("unfocused");
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._input.parentElement.classList.remove("unfocused");
|
this.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,8 +368,12 @@ class DOMBool implements SBool {
|
|||||||
this._restart.textContent = "";
|
this._restart.textContent = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
valueAsString = (): string => { return ""+this.value; };
|
||||||
|
|
||||||
get value(): boolean { return this._input.checked; }
|
get value(): boolean { return this._input.checked; }
|
||||||
set value(state: boolean) { this._input.checked = state; }
|
set value(state: boolean) { this._input.checked = state; }
|
||||||
|
|
||||||
constructor(setting: SBool, section: string, name: string) {
|
constructor(setting: SBool, section: string, name: string) {
|
||||||
this._container = document.createElement("div");
|
this._container = document.createElement("div");
|
||||||
this._container.classList.add("setting");
|
this._container.classList.add("setting");
|
||||||
@ -289,7 +393,7 @@ class DOMBool implements SBool {
|
|||||||
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
||||||
this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement;
|
this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||||
const onValueChange = () => {
|
const onValueChange = () => {
|
||||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.valueAsString() })
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
};
|
};
|
||||||
this._input.onchange = () => {
|
this._input.onchange = () => {
|
||||||
@ -304,9 +408,9 @@ class DOMBool implements SBool {
|
|||||||
if (setting.depends_false) { state = false; }
|
if (setting.depends_false) { state = false; }
|
||||||
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
||||||
if (Boolean(event.detail) !== state) {
|
if (Boolean(event.detail) !== state) {
|
||||||
this._input.parentElement.classList.add("unfocused");
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._input.parentElement.classList.remove("unfocused");
|
this.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -338,11 +442,14 @@ class DOMSelect implements SSelect {
|
|||||||
type: string = "bool";
|
type: string = "bool";
|
||||||
private _advanced: boolean;
|
private _advanced: boolean;
|
||||||
|
|
||||||
|
hide = () => this._container.classList.add("unfocused");
|
||||||
|
show = () => this._container.classList.remove("unfocused");
|
||||||
|
|
||||||
private _advancedListener = (event: settingsBoolEvent) => {
|
private _advancedListener = (event: settingsBoolEvent) => {
|
||||||
if (!Boolean(event.detail)) {
|
if (!Boolean(event.detail)) {
|
||||||
this._container.classList.add("unfocused");
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._container.classList.remove("unfocused");
|
this.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,6 +498,9 @@ class DOMSelect implements SSelect {
|
|||||||
this._restart.textContent = "";
|
this._restart.textContent = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
valueAsString = (): string => { return ""+this.value; };
|
||||||
|
|
||||||
get value(): string { return this._select.value; }
|
get value(): string { return this._select.value; }
|
||||||
set value(v: string) { this._select.value = v; }
|
set value(v: string) { this._select.value = v; }
|
||||||
|
|
||||||
@ -431,14 +541,14 @@ class DOMSelect implements SSelect {
|
|||||||
if (setting.depends_false) { state = false; }
|
if (setting.depends_false) { state = false; }
|
||||||
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
||||||
if (Boolean(event.detail) !== state) {
|
if (Boolean(event.detail) !== state) {
|
||||||
this._container.classList.add("unfocused");
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._container.classList.remove("unfocused");
|
this.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const onValueChange = () => {
|
const onValueChange = () => {
|
||||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.valueAsString() })
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||||
};
|
};
|
||||||
@ -478,6 +588,9 @@ class DOMNote implements SNote {
|
|||||||
type: string = "note";
|
type: string = "note";
|
||||||
private _style: string;
|
private _style: string;
|
||||||
|
|
||||||
|
hide = () => this._container.classList.add("unfocused");
|
||||||
|
show = () => this._container.classList.remove("unfocused");
|
||||||
|
|
||||||
get name(): string { return this._name.textContent; }
|
get name(): string { return this._name.textContent; }
|
||||||
set name(n: string) { this._name.textContent = n; }
|
set name(n: string) { this._name.textContent = n; }
|
||||||
|
|
||||||
@ -486,6 +599,8 @@ class DOMNote implements SNote {
|
|||||||
this._description.innerHTML = d;
|
this._description.innerHTML = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
valueAsString = (): string => { return ""; };
|
||||||
|
|
||||||
get value(): string { return ""; }
|
get value(): string { return ""; }
|
||||||
set value(v: string) { return; }
|
set value(v: string) { return; }
|
||||||
|
|
||||||
@ -521,9 +636,9 @@ class DOMNote implements SNote {
|
|||||||
if (setting.depends_false) { state = false; }
|
if (setting.depends_false) { state = false; }
|
||||||
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
||||||
if (Boolean(event.detail) !== state) {
|
if (Boolean(event.detail) !== state) {
|
||||||
this._container.classList.add("unfocused");
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._container.classList.remove("unfocused");
|
this.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -603,12 +718,15 @@ class sectionPanel {
|
|||||||
case "note":
|
case "note":
|
||||||
setting = new DOMNote(setting as SNote, this._sectionName);
|
setting = new DOMNote(setting as SNote, this._sectionName);
|
||||||
break;
|
break;
|
||||||
|
case "list":
|
||||||
|
setting = new DOMList(setting as SList, this._sectionName, name);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (setting.type != "note") {
|
if (setting.type != "note") {
|
||||||
this.values[name] = ""+setting.value;
|
this.values[name] = ""+setting.value;
|
||||||
document.addEventListener(`settings-${this._sectionName}-${name}`, (event: CustomEvent) => {
|
document.addEventListener(`settings-${this._sectionName}-${name}`, (event: CustomEvent) => {
|
||||||
// const oldValue = this.values[name];
|
// const oldValue = this.values[name];
|
||||||
this.values[name] = ""+event.detail;
|
this.values[name] = event.detail;
|
||||||
document.dispatchEvent(new CustomEvent("settings-section-changed"));
|
document.dispatchEvent(new CustomEvent("settings-section-changed"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -64,11 +64,13 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// host := gc.Request.URL.Hostname()
|
||||||
|
host := app.ExternalDomain
|
||||||
uri := "/my"
|
uri := "/my"
|
||||||
if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) {
|
if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) {
|
||||||
uri = "/accounts/my"
|
uri = "/accounts/my"
|
||||||
}
|
}
|
||||||
gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, uri, gc.Request.URL.Hostname(), true, true)
|
gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, uri, host, true, true)
|
||||||
gc.JSON(200, getTokenDTO{token})
|
gc.JSON(200, getTokenDTO{token})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +103,8 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/my", gc.Request.URL.Hostname(), true, true)
|
// host := gc.Request.URL.Hostname()
|
||||||
|
host := app.ExternalDomain
|
||||||
|
gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/my", host, true, true)
|
||||||
gc.JSON(200, getTokenDTO{jwt})
|
gc.JSON(200, getTokenDTO{jwt})
|
||||||
}
|
}
|
||||||
|
18
users.go
18
users.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -49,7 +50,8 @@ type NewUserData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
|
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
|
||||||
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData) {
|
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData, pendingTasks *sync.WaitGroup) {
|
||||||
|
pendingTasks = &sync.WaitGroup{}
|
||||||
// Some helper functions which will behave as our app.info/error/debug
|
// Some helper functions which will behave as our app.info/error/debug
|
||||||
deferLogInfo := func(s string, args ...any) {
|
deferLogInfo := func(s string, args ...any) {
|
||||||
out.Log = func() {
|
out.Log = func() {
|
||||||
@ -124,7 +126,19 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Welcome email is sent by each user of this method separately..
|
webhookURIs := app.config.Section("webhooks").Key("created").StringsWithShadows("|")
|
||||||
|
if len(webhookURIs) != 0 {
|
||||||
|
summary := app.userSummary(out.User)
|
||||||
|
for _, uri := range webhookURIs {
|
||||||
|
go func() {
|
||||||
|
pendingTasks.Add(1)
|
||||||
|
app.webhooks.Send(uri, summary)
|
||||||
|
pendingTasks.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Welcome email is sent by each user of this method separately.
|
||||||
|
|
||||||
out.Status = 200
|
out.Status = 200
|
||||||
out.Success = true
|
out.Success = true
|
||||||
|
7
views.go
7
views.go
@ -666,7 +666,7 @@ func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lan
|
|||||||
profile = &p
|
profile = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
nu := app.NewUserPostVerification(NewUserParams{
|
nu /*wg*/, _ := app.NewUserPostVerification(NewUserParams{
|
||||||
Req: req,
|
Req: req,
|
||||||
SourceType: sourceType,
|
SourceType: sourceType,
|
||||||
Source: source,
|
Source: source,
|
||||||
@ -707,6 +707,9 @@ func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lan
|
|||||||
delete(invKeys, key)
|
delete(invKeys, key)
|
||||||
app.ConfirmationKeys[invite.Code] = invKeys
|
app.ConfirmationKeys[invite.Code] = invKeys
|
||||||
app.confirmationKeysLock.Unlock()
|
app.confirmationKeysLock.Unlock()
|
||||||
|
|
||||||
|
// These don't need to complete anytime soon
|
||||||
|
// wg.Wait()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,7 +743,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
discord := discordEnabled && app.config.Section("discord").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)
|
matrix := matrixEnabled && app.config.Section("matrix").Key("show_on_reg").MustBool(true)
|
||||||
|
|
||||||
userPageAddress := fmt.Sprintf("%s/my/account", app.ExternalHost)
|
userPageAddress := fmt.Sprintf("%s/my/account", app.ExternalURI)
|
||||||
|
|
||||||
fromUser := ""
|
fromUser := ""
|
||||||
if invite.ReferrerJellyfinID != "" {
|
if invite.ReferrerJellyfinID != "" {
|
||||||
|
38
webhooks.go
Normal file
38
webhooks.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/common"
|
||||||
|
"github.com/hrfee/jfa-go/logger"
|
||||||
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookSender struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
timeoutHandler common.TimeoutHandler
|
||||||
|
log *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||||
|
func (ws *WebhookSender) SetTransport(t *http.Transport) {
|
||||||
|
ws.httpClient.Transport = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookSender(timeoutHandler common.TimeoutHandler, log *logger.Logger) *WebhookSender {
|
||||||
|
return &WebhookSender{
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
timeoutHandler: timeoutHandler,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebhookSender) Send(uri string, payload any) (int, error) {
|
||||||
|
_, status, err := common.Req(ws.httpClient, ws.timeoutHandler, http.MethodPost, uri, payload, url.Values{}, nil, true)
|
||||||
|
ws.log.Printf(lm.WebhookRequest, uri, status, err)
|
||||||
|
return status, err
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user