mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-26 19:10:10 +00:00
Compare commits
39 Commits
a383292846
...
ec41e63c95
Author | SHA1 | Date | |
---|---|---|---|
|
ec41e63c95 | ||
|
8f2a28e650 | ||
|
8a6102b7b9 | ||
|
0ce5c9923d | ||
|
4073ebe534 | ||
|
387fe082ef | ||
|
ddc36ae897 | ||
|
c62876ff3a | ||
|
2fd71acbb2 | ||
|
4c1d8ed2a1 | ||
7223981280 | |||
47536f3e63 | |||
ac4fecd819 | |||
b75bd4d6c5 | |||
2be7baea4a | |||
d56d45a404 | |||
b50d66d265 | |||
aec0a5349a | |||
20560332ed | |||
202ee0977e | |||
f460bfcfc6 | |||
4f5d12f800 | |||
9092b98b28 | |||
0f72a85724 | |||
0840931fed | |||
00379824df | |||
f823705e40 | |||
269836fc99 | |||
49d8c6f8e4 | |||
278588ca39 | |||
ab05c07469 | |||
|
04c94ba55a | ||
6e205760c3 | |||
82032b98a8 | |||
e8666d5bf2 | |||
d1affe271c | |||
ea109c7b63 | |||
cb5a8c1c23 | |||
|
6ede094034 |
16
.drone.yml
16
.drone.yml
@ -45,9 +45,6 @@ type: docker
|
||||
steps:
|
||||
- name: build-deploy
|
||||
image: appleboy/drone-ssh
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /root/drone_rsa
|
||||
settings:
|
||||
host:
|
||||
from_secret: ssh2_host
|
||||
@ -55,9 +52,8 @@ steps:
|
||||
from_secret: ssh2_username
|
||||
port:
|
||||
from_secret: ssh2_port
|
||||
volumes:
|
||||
- /root/.ssh/docker-build:/root/drone_rsa
|
||||
key_path: /root/drone_rsa
|
||||
key:
|
||||
from_secret: ssh2_key
|
||||
command_timeout: 50m
|
||||
script:
|
||||
- /mnt/buildx/jfa-go/build.sh stable
|
||||
@ -128,9 +124,6 @@ type: docker
|
||||
steps:
|
||||
- name: build-deploy
|
||||
image: appleboy/drone-ssh
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /root/drone_rsa
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
@ -141,11 +134,10 @@ steps:
|
||||
from_secret: ssh2_username
|
||||
port:
|
||||
from_secret: ssh2_port
|
||||
volumes:
|
||||
- /root/.ssh/docker-build:/root/drone_rsa
|
||||
envs:
|
||||
- buildrone_key
|
||||
key_path: /root/drone_rsa
|
||||
key:
|
||||
from_secret: ssh2_key
|
||||
command_timeout: 50m
|
||||
script:
|
||||
- /mnt/buildx/jfa-go/build.sh
|
||||
|
@ -13,7 +13,7 @@
|
||||
Studies mean I can't work on this project a lot outside of breaks, however I hope i'll be able to fit in general support and things like bug fixes into my time. New features and such will likely come in short bursts throughout the year (if they do at all).
|
||||
|
||||
#### Does/Will it still work?
|
||||
jfa-go currently works on Jellyfin 10.8.9, the latest version. I should be able to maintain compatability in the future, unless any big changes occur.
|
||||
jfa-go currently works on Jellyfin 10.8.13, the latest version as of 26/12/23. I should be able to maintain compatability in the future, unless any big changes occur.
|
||||
|
||||
#### Alternatives
|
||||
If you want a bit more of a guarantee of support, I've seen these projects mentioned although haven't tried them myself.
|
||||
|
@ -138,6 +138,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
InviteCode: act.InviteCode,
|
||||
Value: act.Value,
|
||||
Time: act.Time.Unix(),
|
||||
IP: act.IP,
|
||||
}
|
||||
if act.Type == ActivityDeletion || act.Type == ActivityCreation {
|
||||
resp.Activities[i].Username = act.Value
|
||||
|
@ -102,7 +102,7 @@ func (app *appContext) checkInvites() {
|
||||
InviteCode: data.Code,
|
||||
Value: data.Label,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
InviteCode: code,
|
||||
Value: inv.Label,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, nil, false)
|
||||
} else if used {
|
||||
del := false
|
||||
newInv := inv
|
||||
@ -174,7 +174,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
InviteCode: code,
|
||||
Value: inv.Label,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, nil, false)
|
||||
} else if newInv.RemainingUses != 0 {
|
||||
// 0 means infinite i guess?
|
||||
newInv.RemainingUses--
|
||||
@ -285,7 +285,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
InviteCode: invite.Code,
|
||||
Value: invite.Label,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -305,7 +305,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
if inv.IsReferral {
|
||||
continue
|
||||
}
|
||||
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
|
||||
years, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
|
||||
months += years * 12
|
||||
invite := inviteDTO{
|
||||
Code: inv.Code,
|
||||
Months: months,
|
||||
@ -492,7 +493,7 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
InviteCode: req.Code,
|
||||
Value: inv.Label,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
app.info.Printf("%s: Invite deleted", req.Code)
|
||||
respondBool(200, true, gc)
|
||||
|
@ -573,6 +573,7 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
|
||||
// @Router /matrix/login [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
var req MatrixLoginDTO
|
||||
@ -608,6 +609,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
|
||||
// @Router /users/matrix [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
var req MatrixConnectUserDTO
|
||||
@ -639,6 +641,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param username path string true "username to search."
|
||||
// @Router /users/discord/{username} [get]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
|
||||
name := gc.Param("username")
|
||||
@ -665,6 +668,7 @@ func (app *appContext) DiscordGetUsers(gc *gin.Context) {
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
|
||||
// @Router /users/discord [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
var req DiscordConnectUserDTO
|
||||
@ -688,7 +692,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "discord",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
respondBool(200, true, gc)
|
||||
@ -699,6 +703,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
|
||||
// @Router /users/discord [delete]
|
||||
// @Security Bearer
|
||||
// @Tags Users
|
||||
func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
var req forUserDTO
|
||||
@ -717,7 +722,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "discord",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -727,6 +732,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
|
||||
// @Router /users/telegram [delete]
|
||||
// @Security Bearer
|
||||
// @Tags Users
|
||||
func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
var req forUserDTO
|
||||
@ -745,7 +751,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "telegram",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -755,6 +761,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
|
||||
// @Router /users/matrix [delete]
|
||||
// @Security Bearer
|
||||
// @Tags Users
|
||||
func (app *appContext) UnlinkMatrix(gc *gin.Context) {
|
||||
var req forUserDTO
|
||||
@ -773,7 +780,7 @@ func (app *appContext) UnlinkMatrix(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "matrix",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "email",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
ombiUser, code, err := app.getOmbiUser(id)
|
||||
@ -378,7 +378,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "discord",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -426,7 +426,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "telegram",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -507,7 +507,7 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "matrix",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
delete(app.matrix.tokens, pin)
|
||||
respondBool(200, true, gc)
|
||||
@ -529,7 +529,7 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "discord",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -550,7 +550,7 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "telegram",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -571,7 +571,7 @@ func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "matrix",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -701,7 +701,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
SourceType: ActivityUser,
|
||||
Source: user.ID,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
func() {
|
||||
|
16
api-users.go
16
api-users.go
@ -55,7 +55,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: user.Name,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
profile := app.storage.GetDefaultProfile()
|
||||
if req.Profile != "" && req.Profile != "none" {
|
||||
@ -114,7 +114,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
||||
type errorFunc func(gc *gin.Context)
|
||||
|
||||
// Used on the form & when a users email has been confirmed.
|
||||
func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, success bool) {
|
||||
func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) (f errorFunc, success bool) {
|
||||
existingUser, _, _ := app.jf.UserByName(req.Username, false)
|
||||
if existingUser.Name != "" {
|
||||
f = func(gc *gin.Context) {
|
||||
@ -331,7 +331,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
InviteCode: invite.Code,
|
||||
Value: user.Name,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
emailStore := EmailAddress{
|
||||
Addr: req.Email,
|
||||
@ -503,7 +503,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
var req newUserDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: New user attempt", req.Code)
|
||||
if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText) {
|
||||
if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) {
|
||||
app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code)
|
||||
respond(400, "errorCaptcha", gc)
|
||||
return
|
||||
@ -539,7 +539,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
f, success := app.newUser(req, false)
|
||||
f, success := app.newUser(req, false, gc)
|
||||
if !success {
|
||||
f(gc)
|
||||
return
|
||||
@ -609,7 +609,7 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
|
||||
SourceType: ActivityAdmin,
|
||||
Source: gc.GetString("jfId"),
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
if sendMail && req.Notify {
|
||||
if err := app.sendByID(msg, userID); err != nil {
|
||||
@ -687,7 +687,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: username,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
if sendMail && req.Notify {
|
||||
if err := app.sendByID(msg, userID); err != nil {
|
||||
@ -1208,7 +1208,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
Source: gc.GetString("jfId"),
|
||||
Value: "email",
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, false)
|
||||
|
||||
if ombiEnabled {
|
||||
ombiUser, code, err := app.getOmbiUser(id)
|
||||
|
12
api.go
12
api.go
@ -114,6 +114,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
var req ResetPasswordDTO
|
||||
gc.BindJSON(&req)
|
||||
validation := app.validator.validate(req.Password)
|
||||
captcha := app.config.Section("captcha").Key("enabled").MustBool(false)
|
||||
valid := true
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
@ -121,12 +122,18 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
if !valid || req.PIN == "" {
|
||||
// 200 bcs idk what i did in js
|
||||
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
|
||||
gc.JSON(400, validation)
|
||||
return
|
||||
}
|
||||
isInternal := false
|
||||
|
||||
if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) {
|
||||
app.info.Printf("%s: PWR Failed: Captcha Incorrect", req.PIN)
|
||||
respond(400, "errorCaptcha", gc)
|
||||
return
|
||||
}
|
||||
|
||||
var userID, username string
|
||||
if reset, ok := app.internalPWRs[req.PIN]; ok {
|
||||
isInternal = true
|
||||
@ -138,6 +145,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
}
|
||||
userID = reset.ID
|
||||
username = reset.Username
|
||||
|
||||
status, err := app.jf.ResetPasswordAdmin(userID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||
@ -179,7 +187,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
SourceType: ActivityUser,
|
||||
Source: user.ID,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
|
||||
prevPassword := req.PIN
|
||||
if isInternal {
|
||||
|
45
auth.go
45
auth.go
@ -18,6 +18,25 @@ const (
|
||||
REFRESH_TOKEN_VALIDITY_SEC = 3600 * 24
|
||||
)
|
||||
|
||||
func (app *appContext) logIpInfo(gc *gin.Context, user bool, out string) {
|
||||
if (user && LOGIPU) || (!user && LOGIP) {
|
||||
out += fmt.Sprintf(" (ip=%s)", gc.ClientIP())
|
||||
}
|
||||
app.info.Println(out)
|
||||
}
|
||||
func (app *appContext) logIpDebug(gc *gin.Context, user bool, out string) {
|
||||
if (user && LOGIPU) || (!user && LOGIP) {
|
||||
out += fmt.Sprintf(" (ip=%s)", gc.ClientIP())
|
||||
}
|
||||
app.debug.Println(out)
|
||||
}
|
||||
func (app *appContext) logIpErr(gc *gin.Context, user bool, out string) {
|
||||
if (user && LOGIPU) || (!user && LOGIP) {
|
||||
out += fmt.Sprintf(" (ip=%s)", gc.ClientIP())
|
||||
}
|
||||
app.err.Println(out)
|
||||
}
|
||||
|
||||
func (app *appContext) webAuth() gin.HandlerFunc {
|
||||
return app.authenticate
|
||||
}
|
||||
@ -133,7 +152,7 @@ type getTokenDTO struct {
|
||||
Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else.
|
||||
}
|
||||
|
||||
func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, password string, ok bool) {
|
||||
func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool) (username, password string, ok bool) {
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
@ -141,7 +160,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, pas
|
||||
password = creds[1]
|
||||
ok = false
|
||||
if username == "" || password == "" {
|
||||
app.debug.Println("Auth denied: blank username/password")
|
||||
app.logIpDebug(gc, userpage, "Auth denied: blank username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -149,17 +168,17 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, pas
|
||||
return
|
||||
}
|
||||
|
||||
func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context) (user mediabrowser.User, ok bool) {
|
||||
func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context, userpage bool) (user mediabrowser.User, ok bool) {
|
||||
ok = false
|
||||
user, status, err := app.authJf.Authenticate(username, password)
|
||||
if status != 200 || err != nil {
|
||||
if status == 401 || status == 400 {
|
||||
app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
|
||||
app.logIpInfo(gc, userpage, "Auth denied: Invalid username/password (Jellyfin)")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
if status == 403 {
|
||||
app.info.Println("Auth denied: Jellyfin account disabled")
|
||||
app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled")
|
||||
respond(403, "yourAccountWasDisabled", gc)
|
||||
return
|
||||
}
|
||||
@ -180,8 +199,8 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
||||
// @tags Auth
|
||||
// @Security getTokenAuth
|
||||
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
app.info.Println("Token requested (login attempt)")
|
||||
username, password, ok := app.decodeValidateLoginHeader(gc)
|
||||
app.logIpInfo(gc, false, "Token requested (login attempt)")
|
||||
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -196,12 +215,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
if !app.jellyfinLogin && !match {
|
||||
app.info.Println("Auth denied: Invalid username/password")
|
||||
app.logIpInfo(gc, false, "Auth denied: Invalid username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
user, ok := app.validateJellyfinCredentials(username, password, gc)
|
||||
user, ok := app.validateJellyfinCredentials(username, password, gc, false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -233,7 +252,8 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
||||
host := gc.Request.URL.Hostname()
|
||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||
gc.JSON(200, getTokenDTO{token})
|
||||
}
|
||||
|
||||
@ -284,7 +304,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
||||
// @Router /token/refresh [get]
|
||||
// @tags Auth
|
||||
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
app.debug.Println("Token requested (refresh token)")
|
||||
app.logIpInfo(gc, false, "Token requested (refresh token)")
|
||||
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
||||
if !ok {
|
||||
return
|
||||
@ -297,6 +317,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", gc.Request.URL.Hostname(), true, true)
|
||||
host := gc.Request.URL.Hostname()
|
||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||
gc.JSON(200, getTokenDTO{jwt})
|
||||
}
|
||||
|
@ -120,6 +120,9 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||
|
||||
LOGIP = app.config.Section("advanced").Key("log_ips").MustBool(false)
|
||||
LOGIPU = app.config.Section("advanced").Key("log_ips_users").MustBool(false)
|
||||
|
||||
// These two settings are pretty much the same
|
||||
url1 := app.config.Section("invite_emails").Key("url_base").String()
|
||||
url2 := app.config.Section("password_resets").Key("url_base").String()
|
||||
|
@ -297,6 +297,29 @@
|
||||
"advanced": true
|
||||
},
|
||||
"settings": {
|
||||
"log_ips": {
|
||||
"name": "Log IPs accessing Admin Page",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Log IP addresses of admins and admin page requests in console and in activities. See notice below on legality."
|
||||
},
|
||||
"log_ips_users": {
|
||||
"name": "Log IPs accessing User Page",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Log IP addresses of users in console and in activities. See notice below on legality."
|
||||
},
|
||||
"ip_note": {
|
||||
"name": "Logging IPs:",
|
||||
"type": "note",
|
||||
"value": "",
|
||||
"required": "false",
|
||||
"description": "Logging IP addresses through jfa-go may violate GDPR or other privacy regulations, as IPs are linked to account information. Enable at your own risk."
|
||||
},
|
||||
"tls": {
|
||||
"name": "TLS/HTTP2",
|
||||
"required": false,
|
||||
|
150
css/base.css
150
css/base.css
@ -106,48 +106,6 @@ div.card:contains(section.banner.footer) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.al {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ar {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ac {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.align-top {
|
||||
align-items: top;
|
||||
}
|
||||
|
||||
.flex-expand {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-row-group {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -172,23 +130,7 @@ span.sm:not(.heading) {
|
||||
margin: .25rem;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.flex-form {
|
||||
flex: 1;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Who knows for half of these to be honest */
|
||||
@media screen and (max-width: 400px) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
@ -219,69 +161,6 @@ sup.\~critical, .text-critical {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.inv-created-users strong,p {
|
||||
padding-left: 0.5rem;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.inv-created-users.empty strong,p {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inv {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.inv-table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.inv-profilearea {
|
||||
min-width: 20%;
|
||||
}
|
||||
|
||||
.inv-profileselect {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.inv-codearea {
|
||||
max-width: 40%;
|
||||
min-width: 10rem;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inv-empty .inv-codearea {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
|
||||
.invite-link {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-pad {
|
||||
padding: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.elem-pad > * {
|
||||
margin: var(--spacing-4, 1rem);
|
||||
}
|
||||
|
||||
.icon.clickable {
|
||||
padding: 0.5rem 0.6rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
box-sizing: border-box; /* fixes weird length issue with inputs */
|
||||
}
|
||||
@ -300,10 +179,6 @@ sup.\~critical, .text-critical {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
@ -312,14 +187,6 @@ sup.\~critical, .text-critical {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-lp {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.focused {
|
||||
display: block;
|
||||
}
|
||||
@ -416,7 +283,16 @@ table {
|
||||
color: var(--color-content);
|
||||
}
|
||||
|
||||
table.table.manual-pad th, table.table.manual-pad td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.table-p-0 th, table.table-p-0 td {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
p.top {
|
||||
margin-top: 0px;
|
||||
@ -575,7 +451,6 @@ input[type="checkbox" i], [class^="ri-"], [class*=" ri-"], .ri-refresh-line:befo
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.g-recaptcha {
|
||||
overflow: hidden;
|
||||
width: 296px;
|
||||
@ -587,3 +462,8 @@ input[type="checkbox" i], [class^="ri-"], [class*=" ri-"], .ri-refresh-line:befo
|
||||
.g-recaptcha iframe {
|
||||
margin: -2px 0px 0px -4px;
|
||||
}
|
||||
|
||||
.dropdown-manual-toggle {
|
||||
margin-bottom: -0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
.tooltip .content {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
max-width: 10rem;
|
||||
min-width: 6rem;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
@ -13,12 +14,23 @@
|
||||
border-radius: 6px;
|
||||
overflow-wrap: break-word;
|
||||
text-align: center;
|
||||
transition: opacity 100ms;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -1rem;
|
||||
}
|
||||
|
||||
.tooltip.below .content {
|
||||
top: 2.5rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tooltip.darker .content {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.tooltip.right .content {
|
||||
left: 120%;
|
||||
}
|
||||
@ -31,6 +43,10 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.tooltip:hover .content {
|
||||
.tooltip:hover .content,
|
||||
.tooltip:focus .content,
|
||||
.tooltip:focus-within .content
|
||||
{
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
15
daemon.go
15
daemon.go
@ -74,6 +74,17 @@ func (app *appContext) clearTelegram() {
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) clearPWRCaptchas() {
|
||||
app.debug.Println("Housekeeping: Clearing old PWR Captchas")
|
||||
captchas := map[string]Captcha{}
|
||||
for k, capt := range app.pwrCaptchas {
|
||||
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
|
||||
captchas[k] = capt
|
||||
}
|
||||
}
|
||||
app.pwrCaptchas = captchas
|
||||
}
|
||||
|
||||
func (app *appContext) clearActivities() {
|
||||
app.debug.Println("Housekeeping: Cleaning up Activity log...")
|
||||
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
|
||||
@ -136,6 +147,7 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
||||
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
|
||||
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||
|
||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||
@ -153,6 +165,9 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
||||
if clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
||||
}
|
||||
if clearPWR {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
|
||||
}
|
||||
|
||||
return &daemon
|
||||
}
|
||||
|
342
html/admin.html
342
html/admin.html
@ -26,7 +26,7 @@
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
{{ template "login-modal.html" . }}
|
||||
<div id="modal-add-user" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-user" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-add-user" href="">
|
||||
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="add-user-user">
|
||||
<input type="email" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.emailAddress }}">
|
||||
@ -43,31 +43,31 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-about" class="modal">
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-1/3 content card">
|
||||
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 content card">
|
||||
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
||||
<span class="heading"><span class="modal-close">×</span></span>
|
||||
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
|
||||
<p>{{ .strings.commitNoun }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .commit }}</span></p>
|
||||
<p>{{ .strings.buildTime }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .buildTime }}</span></p>
|
||||
<p>{{ .strings.builtBy }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .builtBy }}</span></p>
|
||||
<div class="row col flex">
|
||||
<a class="button ~neutral mr-2 mt-4 mb-4 lang-link" href="https://github.com/hrfee/jfa-go"><i class="ri-github-line mr-2"></i>github</a>
|
||||
<a class="button ~urge mt-4 mb-4 mr-2 lang-link" href="https://wiki.jfa-go.com">wiki/docs</a>
|
||||
<a class="button ~positive mt-4 mb-4 mr-2 lang-link" href="https://weblate.jfa-go.com">translation</a>
|
||||
<div class="dropdown mr-2" tabindex="0">
|
||||
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~info mt-4 mb-4 dropdown-button lang-link">
|
||||
<div class="flex flex-row flex-wrap gap-2 my-2">
|
||||
<a class="button ~neutral lang-link" href="https://github.com/hrfee/jfa-go"><i class="ri-github-line mr-2"></i>github</a>
|
||||
<a class="button ~urge lang-link" href="https://wiki.jfa-go.com">wiki/docs</a>
|
||||
<a class="button ~positive lang-link" href="https://weblate.jfa-go.com">translation</a>
|
||||
<div class="dropdown" tabindex="0">
|
||||
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~info dropdown-button lang-link">
|
||||
<i class="ri-hand-heart-line mr-2"></i>
|
||||
donate
|
||||
<span class="ml-2 chev"></span>
|
||||
</a>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~neutral mb-2 w-100 lang-link">GitHub</a>
|
||||
<a href="https://ko-fi.com/hrfee" target="_blank" class="button ~neutral mb-2 w-100 lang-link">Ko-fi</a>
|
||||
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~neutral mb-2 w-full lang-link">GitHub</a>
|
||||
<a href="https://ko-fi.com/hrfee" target="_blank" class="button ~neutral mb-2 w-full lang-link">Ko-fi</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="button ~urge mt-4 mb-4 @low discord lang-link" href="https://discord.com/invite/MrtvuQmyhP" target="_blank"><i class="ri-discord-line mr-2"></i>discord</a>
|
||||
<a class="button ~urge @low discord lang-link" href="https://discord.com/invite/MrtvuQmyhP" target="_blank"><i class="ri-discord-line mr-2"></i>discord</a>
|
||||
</div>
|
||||
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License. Font "Hanken Grotesk" available under SIL OFL 1.1 License.</a></p>
|
||||
<pre class="font-mono bg-inherit">{{ .license }}</pre>
|
||||
@ -80,15 +80,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-modify-user" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-modify-user" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-modify-user" href="">
|
||||
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
|
||||
<div class="flex flex-row mb-4">
|
||||
<label class="flex-row-group mr-2">
|
||||
<label class="grow mr-2">
|
||||
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
|
||||
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
|
||||
</label>
|
||||
<label class="flex-row-group ml-2">
|
||||
<label class="grow ml-2">
|
||||
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
|
||||
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
|
||||
</label>
|
||||
@ -111,15 +111,15 @@
|
||||
</div>
|
||||
{{ if .referralsEnabled }}
|
||||
<div id="modal-enable-referrals-user" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-enable-referrals-user" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-enable-referrals-user" href="">
|
||||
<span class="heading"><span id="header-enable-referrals-user"></span> <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.enableReferralsDescription }}</p>
|
||||
<div class="flex flex-row mb-4">
|
||||
<label class="flex-row-group mr-2">
|
||||
<label class="grow mr-2">
|
||||
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-profile" checked>
|
||||
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
|
||||
</label>
|
||||
<label class="flex-row-group ml-2">
|
||||
<label class="grow ml-2">
|
||||
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-invite">
|
||||
<span class="button ~neutral @low supra full-width center">{{ .strings.invite }}</span>
|
||||
</label>
|
||||
@ -142,7 +142,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-enable-referrals-profile" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-enable-referrals-profile" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-enable-referrals-profile" href="">
|
||||
<span class="heading"><span id="header-enable-referrals-profile">{{ .strings.enableReferrals }}</span> <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.enableReferralsProfileDescription }}</p>
|
||||
<label class="supra" for="enable-referrals-profile-invites">{{ .strings.invite }}</label>
|
||||
@ -162,7 +162,7 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
<div id="modal-delete-user" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-delete-user" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-delete-user" href="">
|
||||
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-8">
|
||||
<label class="switch mb-4">
|
||||
@ -178,7 +178,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-extend-expiry" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-extend-expiry" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-extend-expiry" href="">
|
||||
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-8">
|
||||
<aside class="aside sm ~urge dark:~d_info mb-2 @low row unfocused" id="extend-expiry-date"></aside>
|
||||
@ -242,7 +242,7 @@
|
||||
<div id="modal-announce" class="modal">
|
||||
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-announce" href="">
|
||||
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
||||
<div class="row">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<div class="col card ~neutral @low">
|
||||
<div id="announce-details">
|
||||
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
||||
@ -259,7 +259,7 @@
|
||||
<input type="text" class="input ~neutral @low mb-2 mt-4">
|
||||
<p class="support">{{ .strings.templateEnterName }}</p>
|
||||
</label>
|
||||
<div class="row flex-expand">
|
||||
<div class="flex flex-row justify-between">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low center supra submit">{{ .strings.send }}</span>
|
||||
@ -275,10 +275,10 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-customize" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
|
||||
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.customizeMessagesDescription }}</p>
|
||||
<div class="table-responsive">
|
||||
<div class="">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -319,7 +319,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-restart" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~critical @low">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~critical @low">
|
||||
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.settingsRestartRequiredDescription }}</p>
|
||||
<div class="float-right">
|
||||
@ -329,7 +329,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-backups" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3">
|
||||
<span class="heading">{{ .strings.backups }} <span class="modal-close">×</span></span>
|
||||
<div class="content my-4">
|
||||
{{ .strings.backupsDescription }}
|
||||
@ -345,7 +345,7 @@
|
||||
<input id="backups-file" name="backups-file" type="file" hidden>
|
||||
<button class="button ~neutral @low mr-2 mb-2" id="settings-backups-sort-direction">{{ .strings.sortDirection }}</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto text-xs md:text-sm">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -360,30 +360,30 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-backed-up" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<span class="heading">{{ .strings.backupCreated }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4" id="settings-backed-up-location"></p>
|
||||
<p class="content my-4">{{ .strings.backupCanDownload }}</p>
|
||||
<div>
|
||||
<button class="button flex w-100 ~info @low mb-2"><span class="flex items-center" id="settings-backed-up-download">{{ .strings.download }}</span></button>
|
||||
<button class="button flex w-full ~info @low mb-2"><span class="flex items-center" id="settings-backed-up-download">{{ .strings.download }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-refresh" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<span class="heading">{{ .strings.settingsApplied }}</span>
|
||||
<p class="content">{{ .strings.settingsRefreshPage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-send-pwr" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<span class="heading">{{ .strings.sendPWR }}</span>
|
||||
<p class="content my-2" id="send-pwr-note"></p>
|
||||
<span class="button ~urge @low mt-2" id="send-pwr-link">{{ .strings.copy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-ombi-profile" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-ombi-defaults" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-ombi-defaults" href="">
|
||||
<span class="heading">{{ .strings.ombiProfile }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.ombiUserDefaultsDescription }}</p>
|
||||
<div class="select ~neutral @low mb-4">
|
||||
@ -396,7 +396,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-user-profiles" class="modal">
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card">
|
||||
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 content card">
|
||||
<span class="heading">{{ .strings.userProfiles }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.userProfilesDescription }}</p>
|
||||
<div class="table-responsive">
|
||||
@ -422,7 +422,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-add-profile" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-profile" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-add-profile" href="">
|
||||
<span class="heading">{{ .strings.addProfile }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.addProfileDescription }}</p>
|
||||
<label>
|
||||
@ -445,7 +445,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-update" class="modal">
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card">
|
||||
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 content card">
|
||||
<span class="heading">{{ .strings.updates }} <span class="modal-close">×</span></span>
|
||||
<p class="content">
|
||||
<h2 class="mt-2">
|
||||
@ -461,7 +461,7 @@
|
||||
</div>
|
||||
{{ if .telegramEnabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
|
||||
<p class="content mb-4">{{ .strings.sendPIN }}</p>
|
||||
<h1 class="ac" id="telegram-pin"></h1>
|
||||
@ -479,7 +479,7 @@
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<div id="modal-discord" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4"><span id="discord-header"></span><span class="modal-close">×</span></span>
|
||||
<p class="content mb-4" id="discord-description"></p>
|
||||
<div class="row">
|
||||
@ -490,7 +490,7 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
<div id="modal-matrix" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-matrix" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-matrix" href="">
|
||||
<span class="heading">{{ .strings.linkMatrix }}</span>
|
||||
<p class="content my-4">{{ .strings.linkMatrixDescription }}</p>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
|
||||
@ -503,7 +503,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<div class="top-4 left-4 absolute">
|
||||
<div class="top-4 left-4 absolute flex flex-row gap-2">
|
||||
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
@ -532,8 +532,8 @@
|
||||
{{ end }}
|
||||
<div class="page-container">
|
||||
<div class="mb-4">
|
||||
<header class="flex flex-wrap items-center justify-between">
|
||||
<div>
|
||||
<header>
|
||||
<div class="flex flex-row overflow-x-scroll items-center">
|
||||
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.invites }}</span>
|
||||
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.accounts }}</span>
|
||||
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.activity }}</span>
|
||||
@ -547,55 +547,55 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-invites">
|
||||
<div class="card @low invites dark:~d_neutral mb-4">
|
||||
<div class="card @low invites dark:~d_neutral mb-4 overflow-visible">
|
||||
<span class="heading">{{ .strings.invites }}</span>
|
||||
<div id="invites"></div>
|
||||
<div id="invites" class="mt-2"></div>
|
||||
</div>
|
||||
<div class="card @low dark:~d_neutral">
|
||||
<span class="heading">{{ .strings.create }}</span>
|
||||
<div class="flex flex-col md:flex-row gap-3" id="create-inv">
|
||||
<div class="card ~neutral @low col">
|
||||
<div class="row mb-2">
|
||||
<label class="col mr-2">
|
||||
<div class="flex flex-col md:flex-row gap-3 mt-2" id="create-inv">
|
||||
<div class="card ~neutral @low flex flex-col gap-2 w-1/2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<label class="w-1/2">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
||||
<span class="button ~neutral @high supra full-width center">{{ .strings.inviteDuration }}</span>
|
||||
</label>
|
||||
<label class="col ml-2">
|
||||
<label class="w-1/2">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
|
||||
<span class="button ~neutral @low supra full-width center">{{ .strings.userExpiry }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="inv-duration">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div id="inv-duration" class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="create-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="create-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="create-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="create-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="create-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="create-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="create-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
@ -603,44 +603,46 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="user-expiry" class="unfocused">
|
||||
<p class="support mb-2">{{ .strings.userExpiryDescription }}</p>
|
||||
<div class="mb-2">
|
||||
<label for="create-user-expiry-enabled" class="button ~neutral @low">
|
||||
<input type="checkbox" id="create-user-expiry-enabled" aria-label="User duration enabled">
|
||||
<span class="ml-2">{{ .strings.enabled }} </span>
|
||||
</label>
|
||||
<div id="user-expiry" class="unfocused flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<p class="support">{{ .strings.userExpiryDescription }}</p>
|
||||
<div>
|
||||
<label for="create-user-expiry-enabled" class="button ~neutral @low">
|
||||
<input type="checkbox" id="create-user-expiry-enabled" aria-label="User duration enabled">
|
||||
<span class="ml-2">{{ .strings.enabled }} </span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="user-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="user-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="user-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="user-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="user-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="user-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="grow flex flex-col gap-4">
|
||||
<label class="label supra" for="user-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="user-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
@ -648,77 +650,91 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
|
||||
<input type="text" id="create-label" class="input ~neutral @low mb-2 mt-4">
|
||||
<input type="text" id="create-label" class="input ~neutral @low">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-user-label"> {{ .strings.userLabel }}</label>
|
||||
<p class="support">{{ .strings.userLabelDescription }}</p>
|
||||
<input type="text" id="create-user-label" class="input ~neutral @low mb-2 mt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label class="label supra" for="create-user-label"> {{ .strings.userLabel }}</label>
|
||||
<p class="support">{{ .strings.userLabelDescription }}</p>
|
||||
</div>
|
||||
<input type="text" id="create-user-label" class="input ~neutral @low">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral @low col">
|
||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||
<div class="flex-expand mb-2 mt-4">
|
||||
<input type="number" min="0" id="create-uses" class="input ~neutral @low mr-2" value=1>
|
||||
<label for="create-inf-uses" class="button ~neutral @low" title="Set uses to infinite">
|
||||
<span>∞</span>
|
||||
<input type="checkbox" class="unfocused" id="create-inf-uses" aria-label="Set uses to infinite">
|
||||
</label>
|
||||
</div>
|
||||
<p class="support unfocused my-2" id="create-inf-uses-warning"><span class="badge ~critical">{{ .strings.warning }}</span> {{ .strings.inviteInfiniteUsesWarning }}</p>
|
||||
<label class="label supra">{{ .strings.profile }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="create-profile">
|
||||
</select>
|
||||
</div>
|
||||
<div id="create-send-to-container">
|
||||
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
||||
<div class="flex-expand mb-2 mt-4">
|
||||
{{ if .discordEnabled }}
|
||||
<input type="text" id="create-send-to" class="input ~neutral @low mr-2" placeholder="example@example.com | user#1234">
|
||||
<span id="create-send-to-search" class="button ~neutral @low mr-2">
|
||||
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
|
||||
</span>
|
||||
{{ else }}
|
||||
<input type="email" id="create-send-to" class="input ~neutral @low mr-2" placeholder="example@example.com">
|
||||
{{ end }}
|
||||
<label for="create-send-to-enabled" class="button ~neutral @low">
|
||||
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
||||
</label>
|
||||
<div class="card ~neutral @low flex flex-col justify-between gap-2 w-1/2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||
<div class="flex flex-row gap-2">
|
||||
<input type="number" min="0" id="create-uses" class="input ~neutral @low" value=1>
|
||||
<label for="create-inf-uses" class="button ~neutral @low" title="Set uses to infinite">
|
||||
<span>∞</span>
|
||||
<input type="checkbox" class="unfocused" id="create-inf-uses" aria-label="Set uses to infinite">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="support unfocused" id="create-inf-uses-warning"><span class="badge ~critical">{{ .strings.warning }}</span> {{ .strings.inviteInfiniteUsesWarning }}</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="label supra">{{ .strings.profile }}</label>
|
||||
<div class="select ~neutral @low">
|
||||
<select id="create-profile">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="create-send-to-container" class="flex flex-col gap-4">
|
||||
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
||||
<div class="flex flex-row gap-2">
|
||||
{{ if .discordEnabled }}
|
||||
<input type="text" id="create-send-to" class="input ~neutral @low" placeholder="example@example.com | user#1234">
|
||||
<span id="create-send-to-search" class="button ~neutral @low">
|
||||
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
|
||||
</span>
|
||||
{{ else }}
|
||||
<input type="email" id="create-send-to" class="input ~neutral @low" placeholder="example@example.com">
|
||||
{{ end }}
|
||||
<label for="create-send-to-enabled" class="button ~neutral @low">
|
||||
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="button ~urge @low supra full-width center lg" id="create-submit">{{ .strings.create }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low supra full-width center lg" id="create-submit">{{ .strings.create }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-accounts" class="unfocused">
|
||||
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
||||
<div class="flex-expand align-middle">
|
||||
<span class="text-3xl font-bold mr-4">{{ .strings.accounts }}</span>
|
||||
<div id="accounts-filter-dropdown" class="dropdown z-10" tabindex="0">
|
||||
<span class="h-100 button ~neutral @low center" id="accounts-filter-button">{{ .strings.filters }}</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low mt-2" id="accounts-filter-list">
|
||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||
</div>
|
||||
<div id="accounts-filter-dropdown" class="dropdown manual z-10 w-full">
|
||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||
<span class="text-3xl font-bold mr-4">{{ .strings.accounts }}</span>
|
||||
<span class="dropdown-manual-toggle"><button class="h-full button ~neutral @low center" id="accounts-filter-button" tabindex="0">{{ .strings.filters }}</button></span>
|
||||
</div>
|
||||
<div class="flex flex-row align-middle w-full">
|
||||
<input type="search" class="field ~neutral @low input search mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
|
||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-display max-w-full">
|
||||
<div class="card ~neutral @low mt-2 overflow-x-scroll" id="accounts-filter-list">
|
||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
|
||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||
</div>
|
||||
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
|
||||
<div class="row -mx-2 mb-2">
|
||||
<button type="button" class="button ~neutral @low center mx-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
||||
<span id="accounts-filter-area"></span>
|
||||
</div>
|
||||
<div class="supra py-1 sm">{{ .strings.actions }}</div>
|
||||
<div class="row -mx-2">
|
||||
<span class="col button ~neutral @low center max-w-[20%]" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||
<div id="accounts-announce-dropdown" class="col dropdown pb-0i max-w-[20%]" tabindex="0">
|
||||
<span class="w-100 button ~info @low center" id="accounts-announce">{{ .strings.announce }}</span>
|
||||
<div class="supra pt-1 pb-2 sm">{{ .strings.actions }}</div>
|
||||
<div class="flex flex-row flex-wrap gap-3 mb-4">
|
||||
<span class="button ~neutral @low center " id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||
<div id="accounts-announce-dropdown" class="dropdown pb-0i " tabindex="0">
|
||||
<span class="w-full button ~info @low center items-baseline" id="accounts-announce">{{ .strings.announce }}</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<span class="supra sm">{{ .strings.templates }}</span>
|
||||
@ -726,12 +742,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="col button ~urge @low center max-w-[20%]" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
<span class="button ~urge @low center " id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
{{ if .referralsEnabled }}
|
||||
<span class="col button ~urge @low center max-w-[20%]" id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
|
||||
<span class="button ~urge @low center " id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
|
||||
{{ end }}
|
||||
<div id="accounts-expiry-dropdown" class="col dropdown pb-0i max-w-[20%]" tabindex="0">
|
||||
<span class="w-100 button ~positive @low center" id="accounts-expiry-dropdown-button">{{ .strings.expiry }} <i class="ri-arrow-down-s-line ml-2"></i></span>
|
||||
<div id="accounts-expiry-dropdown" class="dropdown pb-0i " tabindex="0">
|
||||
<span class="w-full button ~positive @low center items-baseline" id="accounts-expiry-dropdown-button">{{ .strings.expiry }} <i class="ri-arrow-down-s-line ml-2"></i></span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<span class="button ~warning full-width @low center" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
||||
@ -739,16 +755,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accounts-disable-enable-dropdown" class="col dropdown manual pb-0i max-w-[20%]" tabindex="0">
|
||||
<span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>
|
||||
<div id="accounts-disable-enable-dropdown" class="dropdown manual pb-0i " tabindex="0">
|
||||
<span class="w-full button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<span class="button ~urge full-width accounts-announce-template-button" id="accounts-enable-expiry">{{ .strings.setExpiry }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="col button ~info @low center unfocused max-w-[20%]" id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
|
||||
<span class="col button ~critical @low center max-w-[20%]" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
<span class="button ~info @low center unfocused " id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
|
||||
<span class="button ~critical @low center " id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
</div>
|
||||
<div class="card @low accounts-header table-responsive mt-2">
|
||||
<table class="table text-base leading-4">
|
||||
@ -791,27 +807,33 @@
|
||||
</div>
|
||||
<div id="tab-activity" class="unfocused">
|
||||
<div class="card @low dark:~d_neutral activity mb-4 overflow-visible">
|
||||
<div class="flex-expand align-middle">
|
||||
<span class="text-3xl font-bold mr-4">{{ .strings.activity }}</span>
|
||||
<div id="activity-filter-dropdown" class="dropdown z-10" tabindex="0">
|
||||
<span class="h-100 button ~neutral @low center" id="activity-filter-button">{{ .strings.filters }}</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low mt-2" id="activity-filter-list">
|
||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||
<div id="activity-filter-dropdown" class="dropdown manual z-10 w-full" tabindex="0">
|
||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||
<span class="text-3xl font-bold mr-4">{{ .strings.activity }}</span>
|
||||
<div class="flex flex-row align-middle">
|
||||
<span class="dropdown-manual-toggle"><button class="h-full button ~neutral @low center" id="activity-filter-button">{{ .strings.filters }}</button></span>
|
||||
<button class="button ~neutral @low ml-2" id="activity-sort-direction">{{ .strings.sortDirection }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row align-middle w-full">
|
||||
<input type="search" class="field ~neutral @low input search mr-2" id="activity-search" placeholder="{{ .strings.search }}">
|
||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none activity-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||
<button class="button ~info @low ml-2" id="activity-refresh" aria-label="{{ .strings.refresh }}" disabled><i class="ri-refresh-line"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-display max-w-full">
|
||||
<div class="card ~neutral @low mt-2 overflow-x-scroll" id="activity-filter-list">
|
||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="button ~neutral @low ml-2" id="activity-sort-direction">{{ .strings.sortDirection }}</button>
|
||||
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="activity-search" placeholder="{{ .strings.search }}">
|
||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none activity-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||
<button class="button ~info @low ml-2" id="activity-refresh" aria-label="{{ .strings.refresh }}" disabled><i class="ri-refresh-line"></i></button>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between py-2">
|
||||
<div class="flex flex-row justify-between pt-3 pb-2">
|
||||
<div class="supra sm hidden" id="activity-search-options-header">{{ .strings.searchOptions }}</div>
|
||||
<div class="supra sm">
|
||||
<span id="activity-total-records" class="mx-2"></span>
|
||||
<span id="activity-loaded-records" class="mx-2"></span>
|
||||
<span id="activity-shown-records" class="mx-2"></span>
|
||||
<div class="supra sm flex flex-row gap-2">
|
||||
<span id="activity-total-records"></span>
|
||||
<span id="activity-loaded-records"></span>
|
||||
<span id="activity-shown-records"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row -mx-2 mb-2">
|
||||
@ -842,24 +864,24 @@
|
||||
</div>
|
||||
<div id="tab-settings" class="unfocused">
|
||||
<div class="card @low dark:~d_neutral settings overflow">
|
||||
<div class="flex-expand">
|
||||
<div class="flex-row">
|
||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||
<span class="heading">{{ .strings.settings }}</span>
|
||||
<label for="settings-advanced-enabled" class="button ~neutral @low ml-2 my-2">
|
||||
<label for="settings-advanced-enabled" class="button ~neutral @low ml-2">
|
||||
<input type="checkbox" id="settings-advanced-enabled" aria-label="Advanced settings enabled">
|
||||
<span class="ml-2">{{ .strings.advancedSettings }} </span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<span class="button ~neutral @low my-1" id="settings-logs">{{ .strings.logs }}</span>
|
||||
<span class="button ~info @low my-1" id="settings-backups">{{ .strings.backups }}</span>
|
||||
<span class="button ~neutral @low my-1" id="settings-restart">{{ .strings.settingsRestart }}</span>
|
||||
<span class="button ~urge @low unfocused my-1" id="settings-save">{{ .strings.settingsSave }}</span>
|
||||
<div class="flex flex-row justify-start md:justify-end gap-2 w-full">
|
||||
<span class="button ~neutral @low" id="settings-logs">{{ .strings.logs }}</span>
|
||||
<span class="button ~info @low" id="settings-backups">{{ .strings.backups }}</span>
|
||||
<span class="button ~neutral @low" id="settings-restart">{{ .strings.settingsRestart }}</span>
|
||||
<span class="button ~urge @low unfocused" id="settings-save">{{ .strings.settingsSave }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<div class="card @low dark:~d_neutral col" id="settings-sidebar">
|
||||
<div class="flex-expand">
|
||||
<div class="flex flex-row justify-between">
|
||||
<input type="search" class="field ~neutral @low input settings-section-button justify-between mb-2" id="settings-search" placeholder="{{ .strings.search }}">
|
||||
<button class="button ~neutral @low center -ml-10 rounded-s-none mb-2 settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<a class="button ~critical mb-4" target="_blank" href="https://github.com/hrfee/jfa-go/issues/new/choose">Create an Issue</a>
|
||||
</section>
|
||||
<section class="section ~neutral @low">
|
||||
<div class="flex-expand">
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="subheading">Full Log</span>
|
||||
<span class="button ~urge ml-4" id="copy-log">Copy</span>
|
||||
</div>
|
||||
|
@ -34,8 +34,12 @@
|
||||
</script>
|
||||
{{ if .passwordReset }}
|
||||
<script src="js/pwr.js" type="module"></script>
|
||||
<script>
|
||||
window.pwrPIN = "{{ .pwrPIN }}";
|
||||
</script>
|
||||
{{ else }}
|
||||
<script src="js/form.js" type="module"></script>
|
||||
{{ end }}
|
||||
{{ if .reCAPTCHA }}
|
||||
<script>
|
||||
var reCAPTCHACallback = () => {
|
||||
@ -49,4 +53,3 @@
|
||||
<script src="https://www.google.com/recaptcha/api.js?onload=reCAPTCHACallback&render=explicit" async defer></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
@ -1,21 +1,21 @@
|
||||
<div id="modal-login" class="modal">
|
||||
<div class="my-[10%] row items-stretch relative mx-auto w-[40%] lg:w-[60%]">
|
||||
<div class="my-[10%] row items-stretch relative mx-auto w-11/12 sm:w-4/5 lg:w-1/2">
|
||||
{{ if index . "LoginMessageEnabled" }}
|
||||
{{ if .LoginMessageEnabled }}
|
||||
<div class="card mx-2 flex-initial w-[100%] xl:w-[35%] mb-4 xl:mb-0 dark:~d_neutral @low content">
|
||||
<div class="card mx-2 flex-initial w-[100%] lg:w-[35%] mb-4 lg:mb-0 dark:~d_neutral @low content">
|
||||
{{ .LoginMessageContent }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if index . "userPageEnabled" }}
|
||||
{{ if and .userPageEnabled .showUserPageLink }}
|
||||
<div class="card mx-2 flex-initial w-[100%] xl:w-[35%] mb-4 xl:mb-0 dark:~d_neutral @low content">
|
||||
<div class="card mx-2 flex-initial w-[100%] lg:w-[35%] mb-4 lg:mb-0 dark:~d_neutral @low content">
|
||||
<span class="heading row">{{ .strings.loginNotAdmin }}</span>
|
||||
<a class="button ~info h-12 w-100" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||
<a class="button ~info h-12 w-full" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<form class="card mx-2 flex-auto form-login w-[100%] xl:w-[55%] mb-0" href="">
|
||||
<form class="card mx-2 form-login w-[100%] lg:w-[55%] mb-0" href="">
|
||||
<span class="heading">{{ .strings.login }}</span>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
|
||||
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
|
||||
|
@ -35,7 +35,7 @@
|
||||
<aside class="aside ~warning">
|
||||
{{ .strings.changeYourPassword }}
|
||||
</aside>
|
||||
<span class="button ~urge @low w-100 text-center text-xl p-1 mt-4" id="pin" title="{{ .strings.copy }}">{{ .pin }}</span>
|
||||
<span class="button ~urge @low w-full text-center text-xl p-1 mt-4" id="pin" title="{{ .strings.copy }}">{{ .pin }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<i class="content">{{ .contactMessage }}</i>
|
||||
|
@ -30,7 +30,7 @@
|
||||
<div class="row col flex center">
|
||||
<p class="content my-2">{{ .lang.StartPage.pressStart }}</p>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="support">{{ .lang.StartPage.httpsNotice }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.StartPage.start }}</span>
|
||||
</section>
|
||||
@ -59,7 +59,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</section>
|
||||
@ -150,7 +150,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</section>
|
||||
@ -189,7 +189,7 @@
|
||||
<span class="support mb-2 mt-1">{{ .lang.Login.emailNotice }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</section>
|
||||
@ -235,7 +235,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low" id="jellyfin-test-connection">{{ .lang.JellyfinEmby.testConnection }}</span>
|
||||
@ -258,7 +258,7 @@
|
||||
<input type="text" class="input ~neutral @low mt-4" id="ombi-api_key">
|
||||
<p class="support mb-2 mt-1">{{ .lang.Ombi.apiKeyNotice }}</p>
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
@ -273,7 +273,7 @@
|
||||
<input type="checkbox" class="mr-2" id="userpage-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<p class="support mb-1 mt-1">{{ .lang.UserPage.requiredSettings }}</p>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
@ -370,7 +370,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
@ -392,7 +392,7 @@
|
||||
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="welcome_email-subject" placeholder="{{ .emailLang.WelcomeEmail.title }}">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
@ -413,7 +413,7 @@
|
||||
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="invite_emails-subject" placeholder="{{ .emailLang.InviteEmail.title }}">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
@ -450,7 +450,7 @@
|
||||
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="password_resets-subject" placeholder="{{ .emailLang.PasswordReset.title }}">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
@ -483,7 +483,7 @@
|
||||
<span class="mt-4">{{ .lang.PasswordValidation.special }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-special" value="0">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
@ -513,7 +513,7 @@
|
||||
<input type="text" class="input ~neutral @low mt-4" id="email-message">
|
||||
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.emailMessageNotice }}</p>
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
|
@ -109,10 +109,10 @@
|
||||
<div class="card @low dark:~d_neutral mb-4" id="card-user">
|
||||
<span class="heading mb-2"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="columns-1 sm:columns-2 gap-4" id="user-cardlist">
|
||||
{{ if index . "PageMessageEnabled" }}
|
||||
{{ if .PageMessageEnabled }}
|
||||
<div class="card @low dark:~d_neutral content" id="card-message">
|
||||
<div class="card @low dark:~d_neutral content break-words" id="card-message">
|
||||
{{ .PageMessageContent }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@ -162,7 +162,7 @@
|
||||
<div class="card @low dark:~d_neutral unfocused" id="card-referrals">
|
||||
<span class="heading mb-2">{{ .strings.referrals }}</span>
|
||||
<aside class="aside ~neutral my-4 col user-referrals-description"></aside>
|
||||
<div class="row flex-expand">
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<div class="user-referrals-info"></div>
|
||||
<div class="grid my-2">
|
||||
<button type="button" class="user-referrals-button button ~info dark:~d_info @low" title="Copy">{{ .strings.copyReferral }}<i class="ri-file-copy-line ml-2"></i></button>
|
||||
|
@ -32,12 +32,12 @@
|
||||
"before": "قبل",
|
||||
"user": "مستخدم",
|
||||
"userExpiry": "انتهاء صلاحية المستخدم",
|
||||
"userExpiryDescription": "",
|
||||
"userExpiryDescription": "بعد وقت محدد من تسجيل مستخدم جديد, jfa-go سوف يمسح\\يلغي تفعيل الحساب. بامكانك تغيير هذا السلوك في الاعدادات.",
|
||||
"aboutProgram": "حول",
|
||||
"version": "إصدار",
|
||||
"commitNoun": "تعديل",
|
||||
"commitNoun": "فرض",
|
||||
"newUser": "مستخدم جديد",
|
||||
"profile": "ملف",
|
||||
"profile": "حساب تعريفي",
|
||||
"unknown": "غير معروف",
|
||||
"label": "وسم",
|
||||
"logs": "السجلات",
|
||||
@ -63,19 +63,19 @@
|
||||
"markdownSupported": "",
|
||||
"modifySettings": "",
|
||||
"modifySettingsDescription": "",
|
||||
"applyHomescreenLayout": "",
|
||||
"sendDeleteNotificationEmail": "",
|
||||
"sendDeleteNotifiationExample": "",
|
||||
"settingsRestart": "",
|
||||
"settingsRestarting": "",
|
||||
"settingsRestartRequired": "",
|
||||
"settingsRestartRequiredDescription": "",
|
||||
"settingsApplyRestartLater": "",
|
||||
"settingsApplyRestartNow": "",
|
||||
"settingsApplied": "",
|
||||
"settingsRefreshPage": "",
|
||||
"settingsRequiredOrRestartMessage": "",
|
||||
"settingsSave": "",
|
||||
"applyHomescreenLayout": "تطبيق ترتيب الصفحه الرئيسيه",
|
||||
"sendDeleteNotificationEmail": "ارسال رساله اشعار",
|
||||
"sendDeleteNotifiationExample": "تم حذف حسابك.",
|
||||
"settingsRestart": "اعاده تشغيل",
|
||||
"settingsRestarting": "اعاده التشغيل…",
|
||||
"settingsRestartRequired": "يجب اعاده التشغيل",
|
||||
"settingsRestartRequiredDescription": "يجب اعاده التشغيل لتطبيق بعض الاعدادات التي تم تغييرها. اعاده التشغيل الان ام لاحقا؟",
|
||||
"settingsApplyRestartLater": "تطبيق الاعدادات, اعاده التشغيل لاحقا",
|
||||
"settingsApplyRestartNow": "تطبيق الاعدادات و اعاده التشغيل",
|
||||
"settingsApplied": "تم تطبيق الاعدادات.",
|
||||
"settingsRefreshPage": "اعد انعاش الصفحه بعد بضع ثوان.",
|
||||
"settingsRequiredOrRestartMessage": "ملاحظه: {n} تشير الى حقل اجباري, {n} تشير ان التغييرات تحتاج لاعاده التشغيل.",
|
||||
"settingsSave": "حفظ",
|
||||
"ombiProfile": "",
|
||||
"ombiUserDefaultsDescription": "",
|
||||
"userProfiles": "",
|
||||
@ -117,7 +117,15 @@
|
||||
"userPageLogin": "",
|
||||
"userPagePage": "",
|
||||
"buildTime": "",
|
||||
"builtBy": ""
|
||||
"builtBy": "",
|
||||
"activity": "الانشطه",
|
||||
"userLabel": "وسم المستخدم",
|
||||
"userLabelDescription": "الوسام للمستخدمين المفعلين من هذه الدعوه.",
|
||||
"enableReferrals": "تفعيل الاحالات",
|
||||
"disableReferrals": "ابطال الاحالات",
|
||||
"invite": "دعوه",
|
||||
"enableReferralsProfileDescription": "تمكين المستخدمين من هذا الحساب التعريفي للاحالات الخاصه, لارسالها للعائله\\الاصدقاء. انشاء دعوه بالاعدادات المطلوبه, ثم اختارها هنا. كل احاله سوف تكون مبنيه على اعدادات هذه الدعوه. بامكانك مسح الدعوه عند لانتهاء.",
|
||||
"enableReferralsDescription": "تمكين المستخدمين لاستعمال احالات خاصه مثل الدعوه, لارسالها للعائله\\للاصدقاء. ممكن اصدارها من قوالب الاحالات في الحساب التعريفي, او من دعوه مفعله."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "",
|
||||
|
@ -115,7 +115,8 @@
|
||||
"after": "nach",
|
||||
"before": "vor",
|
||||
"unlink": "Account trennen",
|
||||
"sortingBy": "Sortieren nach"
|
||||
"sortingBy": "Sortieren nach",
|
||||
"activity": "Aktivität"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
|
||||
|
@ -150,7 +150,8 @@
|
||||
"accountDisabled": "Account disabled: {user}",
|
||||
"accountReEnabled": "Account re-enabled: {user}",
|
||||
"accountExpired": "Account expired: {user}",
|
||||
"accountWillExpire": "Account will expire on {date}",
|
||||
"accountWillExpire": "Account will expire on {date}.",
|
||||
"expirationBasedOn": "Given date based on 1st user.",
|
||||
"userDeleted": "User was deleted.",
|
||||
"userDisabled": "User was disabled",
|
||||
"inviteCreated": "Invite created: {invite}",
|
||||
|
@ -130,7 +130,53 @@
|
||||
"searchOptions": "Zoekopties",
|
||||
"matchText": "Tekstovereenkomst",
|
||||
"jellyfinID": "Jellyfin ID",
|
||||
"userPagePage": "Gebruikerspagina: Pagina"
|
||||
"userPagePage": "Gebruikerspagina: Pagina",
|
||||
"activity": "Activiteit",
|
||||
"deleted": "Verwijderd",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"keepSearching": "Blijf zoeken",
|
||||
"keepSearchingDescription": "Alleen momenteel ingeladen activiteiten zijn doorzocht. Klik hieronder om alle activiteiten te doorzoeken.",
|
||||
"sortDirection": "Sorteerrichting",
|
||||
"referrer": "Verwijzer",
|
||||
"accountLinked": "{contactMethod} gekoppeld: {user}",
|
||||
"accountUnlinked": "{contactMethod} verwijderd: {user}",
|
||||
"accountResetPassword": "{user} heeft hun wachtwoord gereset",
|
||||
"accountChangedPassword": "{user} heeft hun wachtwoord gewijzigd",
|
||||
"accountDisabled": "Account uitgeschakeld: {user}",
|
||||
"accountDeleted": "Account verwijderd: {user}",
|
||||
"accountCreated": "Account aangemaakt: {user}",
|
||||
"accountReEnabled": "Account opnieuw ingeschakeld: {user}",
|
||||
"accountExpired": "Account verlopen: {user}",
|
||||
"userDeleted": "Gebruiker is verwijderd.",
|
||||
"userDisabled": "Gebruiker is uitgeschakeld",
|
||||
"inviteCreated": "Uitnodiging aangemaakt: {invite}",
|
||||
"inviteDeleted": "Uitnodiging verwijderd: {invite}",
|
||||
"inviteExpired": "Uitnodiging verlopen: {invite}",
|
||||
"fromInvite": "Via uitnodiging",
|
||||
"byAdmin": "Door beheerder",
|
||||
"byUser": "Door gebruiker",
|
||||
"byJfaGo": "Door jfa-go",
|
||||
"activityID": "Activiteit ID",
|
||||
"title": "Titel",
|
||||
"usersMentioned": "Genoemde gebruiker",
|
||||
"actor": "Uitvoerder",
|
||||
"actorDescription": "Wat deze actie veroorzaakt heeft. \"gebruiker\"/\"beheerder\"/\"daemon\" of een gebruikersnaam.",
|
||||
"accountCreationFilter": "Aanmaken van account",
|
||||
"accountDeletionFilter": "Verwijderen van account",
|
||||
"accountDisabledFilter": "Account uitgeschakeld",
|
||||
"accountEnabledFilter": "Account ingeschakeld",
|
||||
"contactLinkedFilter": "Contact gekoppeld",
|
||||
"contactUnlinkedFilter": "Contact ontkoppeld",
|
||||
"passwordChangeFilter": "Wachtwoord gewijzigd",
|
||||
"passwordResetFilter": "Wachtwoord gereset",
|
||||
"inviteCreatedFilter": "Uitnodiging aangemaakt",
|
||||
"inviteDeletedFilter": "Uitnodiging verwijderd/verlopen",
|
||||
"loadMore": "Laad meer",
|
||||
"loadAll": "Laad alles",
|
||||
"noMoreResults": "Niet meer resultaten.",
|
||||
"totalRecords": "{n} documenten totaal",
|
||||
"loadedRecords": "{n} geladen",
|
||||
"shownRecords": "{n} getoond"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
|
||||
@ -170,7 +216,11 @@
|
||||
"setOmbiProfile": "Opgeslagen ombi-profiel.",
|
||||
"errorSetOmbiProfile": "Opslaan van ombi-profiel mislukt.",
|
||||
"errorNoReferralTemplate": "Profiel bevat geen verwijzingssjabloon, voeg er een toe bij instellingen.",
|
||||
"referralsEnabled": "Verwijzingen actief."
|
||||
"referralsEnabled": "Verwijzingen actief.",
|
||||
"activityDeleted": "Activiteit verwijderd.",
|
||||
"errorInviteNoLongerExists": "Uitnodiging bestaat niet meer.",
|
||||
"errorInviteNotFound": "Uitnodiging niet gevonden.",
|
||||
"errorLoadActivities": "Laden van activiteiten mislukt."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
|
@ -69,7 +69,7 @@
|
||||
"ombiUserDefaults": "Ombi 用户默认值",
|
||||
"ombiUserDefaultsDescription": "创建并配置 Ombi 用户,然后在下面选择它。它的设置/权限将被存储并应用于由 jfa-go 创建的新 Ombi 用户。",
|
||||
"userProfiles": "用户档案",
|
||||
"userProfilesDescription": "配置文件在用户创建帐户时应用于用户。配置文件包括库访问权限和主屏幕布局。",
|
||||
"userProfilesDescription": "个人资料在用户创建帐户时应用于他们。个人资料包括库访问权限和主屏幕布局。",
|
||||
"userProfilesIsDefault": "默认",
|
||||
"userProfilesLibraries": "库",
|
||||
"addProfile": "添加档案",
|
||||
@ -117,7 +117,66 @@
|
||||
"before": "之前",
|
||||
"unlink": "取消关联帐户",
|
||||
"sortingBy": "排序方式",
|
||||
"userPageLogin": "用户页面:登录"
|
||||
"userPageLogin": "用户页面:登录",
|
||||
"activity": "活动",
|
||||
"userLabelDescription": "标签应用于使用此邀请创建的用户。",
|
||||
"disabled": "禁用",
|
||||
"keepSearchingDescription": "只有当前加载的活动被搜索了。如果您想搜索所有活动,请点击下方。",
|
||||
"enableReferralsDescription": "为用户提供一个个人的推荐链接,类似于邀请,以便他们发送给朋友和家人。可以从个人资料中的推荐模板获取,或从现有的邀请中获取。",
|
||||
"userDeleted": "用户已被删除。",
|
||||
"inviteCreated": "邀请已创建:{invite}",
|
||||
"usersMentioned": "用户提到的",
|
||||
"actorDescription": "引起这个操作的事物。可以是“用户”、“管理员”、“守护程序”或用户名。",
|
||||
"loginNotAdmin": "不是管理员?",
|
||||
"invite": "邀请",
|
||||
"noResultsFound": "没有发现任何结果",
|
||||
"settingsHiddenDependency": "匹配设置被隐藏,因为它们取决于另一个设置的值:",
|
||||
"settingsDependsOn": "{setting}:依赖于 {dependency}",
|
||||
"settingsAdvancedMode": "{setting}:必须启用高级设置",
|
||||
"settingsMaybeUnderAdvanced": "提示:通过启用高级设置,您可能会找到您正在寻找的内容。",
|
||||
"userLabel": "用户标签",
|
||||
"deleted": "删除",
|
||||
"keepSearching": "继续搜索",
|
||||
"enableReferrals": "启用推荐",
|
||||
"disableReferrals": "禁用推荐",
|
||||
"enableReferralsProfileDescription": "为使用该个人资料创建的用户提供一个类似邀请的个人推荐链接,以便他们发送给朋友和家人。创建一个具有所需设置的邀请,然后在此处进行选择。然后,每个推荐都将基于这个邀请。完成后,您可以删除邀请。",
|
||||
"sortDirection": "排序方向",
|
||||
"referrer": "推荐人",
|
||||
"accountLinked": "{contactMethod} 已关联:{user}",
|
||||
"accountUnlinked": "{contactMethod} 已移除:{user}",
|
||||
"accountResetPassword": "{user} 重置了他们的密码",
|
||||
"accountChangedPassword": "{user} 更改了他们的密码",
|
||||
"accountCreated": "账户已创建:{user}",
|
||||
"accountDeleted": "账户已删除:{user}",
|
||||
"accountDisabled": "账户已禁用:{user}",
|
||||
"accountReEnabled": "账户已重新启用:{user}",
|
||||
"accountExpired": "账户已过期:{user}",
|
||||
"userDisabled": "用户已被禁用",
|
||||
"inviteDeleted": "邀请已删除:{invite}",
|
||||
"inviteExpired": "邀请已过期:{invite}",
|
||||
"fromInvite": "来自邀请",
|
||||
"byAdmin": "由管理员发起的",
|
||||
"byUser": "由用户发起的",
|
||||
"byJfaGo": "由jfa-go发起的",
|
||||
"activityID": "活动ID",
|
||||
"title": "标题",
|
||||
"actor": "角色",
|
||||
"accountCreationFilter": "账户创建",
|
||||
"accountDeletionFilter": "账户删除",
|
||||
"accountDisabledFilter": "账户禁用",
|
||||
"accountEnabledFilter": "账户启用",
|
||||
"contactLinkedFilter": "联系方式已关联",
|
||||
"contactUnlinkedFilter": "联系方式未关联",
|
||||
"passwordChangeFilter": "密码已更改",
|
||||
"passwordResetFilter": "密码重置",
|
||||
"inviteCreatedFilter": "邀请已创建",
|
||||
"inviteDeletedFilter": "邀请已删除/过期",
|
||||
"loadMore": "加载更多",
|
||||
"loadAll": "加载全部",
|
||||
"noMoreResults": "没有更多结果了。",
|
||||
"totalRecords": "{n} 总记录数",
|
||||
"loadedRecords": "已加载{n}",
|
||||
"shownRecords": "已显示{n}"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "更改了 {n} 的电子邮件地址。",
|
||||
@ -155,7 +214,13 @@
|
||||
"updateAvailable": "有新更新可用,请检查设置。",
|
||||
"noUpdatesAvailable": "没有可用的更新。",
|
||||
"setOmbiProfile": "保存ombi配置文件。",
|
||||
"errorSetOmbiProfile": "无法保存ombi配置文件。"
|
||||
"errorSetOmbiProfile": "无法保存ombi配置文件。",
|
||||
"activityDeleted": "活动已删除。",
|
||||
"errorNoReferralTemplate": "个人资料不包含推荐模板,请在设置中添加一个。",
|
||||
"referralsEnabled": "已启用推荐。",
|
||||
"errorInviteNoLongerExists": "邀请已不存在。",
|
||||
"errorInviteNotFound": "未找到邀请。",
|
||||
"errorLoadActivities": "无法加载活动。"
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
@ -213,6 +278,10 @@
|
||||
"setExpiry": {
|
||||
"plural": "为{n}用户设置到期时间",
|
||||
"singular": "为{n}用户设置到期时间"
|
||||
},
|
||||
"enableReferralsFor": {
|
||||
"singular": "为{n}用户启用推荐功能",
|
||||
"plural": "为{n}个用户启用推荐功能"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
"logout": "登出",
|
||||
"admin": "管理员",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"disabled": "禁用",
|
||||
"reEnable": "重新启用",
|
||||
"disable": "禁用",
|
||||
"expiry": "到期",
|
||||
@ -40,7 +40,8 @@
|
||||
"contactMethods": "联系方式",
|
||||
"accountStatus": "帐户状态",
|
||||
"notSet": "未设置",
|
||||
"myAccount": "我的帐户"
|
||||
"myAccount": "我的帐户",
|
||||
"referrals": "推荐"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "用户名/密码留空。",
|
||||
|
@ -34,7 +34,10 @@
|
||||
"welcomeUser": "欢迎,{user}!",
|
||||
"editContactMethod": "修改联系方式",
|
||||
"joinTheServer": "加入服务器:",
|
||||
"customMessagePlaceholderHeader": "自定义此卡"
|
||||
"customMessagePlaceholderHeader": "自定义此卡",
|
||||
"referralsDescription": "使用此链接邀请朋友和家人加入Jellyfin。如果链接过期,请回到这里获取一个新的。",
|
||||
"copyReferral": "复制链接",
|
||||
"invitedBy": "您是由用户{user}邀请的。"
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "用户已经存在。",
|
||||
|
@ -22,7 +22,8 @@
|
||||
"errorNotAdmin": "Gebruiker heeft geen beheersrechten.",
|
||||
"errorConnectionRefused": "Verbinding geweigerd.",
|
||||
"errorUnknown": "Onbekende fout, bekijk de logs.",
|
||||
"error": "Fout"
|
||||
"error": "Fout",
|
||||
"errorProxy": "Proxy-instellingen onjuist."
|
||||
},
|
||||
"startPage": {
|
||||
"welcome": "Welkom!",
|
||||
@ -156,5 +157,11 @@
|
||||
"title": "Gebruikerspagina",
|
||||
"customizeMessages": "Gebruik de bewerken knop naast \"Gebruikerspagina\" in de instellingen om dit later in te stellen.",
|
||||
"requiredSettings": "Inloggen bij jfa-go via Jellyfin moet ingesteld zijn. Controleer dat \"reset wachtwoord via link\" later wordt gekozen voor zelfservice wachtwoord-resets."
|
||||
},
|
||||
"proxy": {
|
||||
"title": "Proxy",
|
||||
"description": "Laat jfa-go alle verbindingen via een HTTP/SOCKS5 proxy maken. De verbinding met Jellyfin wordt hierdoorheen getest.",
|
||||
"protocol": "Protocol",
|
||||
"address": "Adres (inclusief poort)"
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"back": "上一步",
|
||||
"optional": "可选的",
|
||||
"serverType": "服务类型",
|
||||
"disabled": "已禁用",
|
||||
"disabled": "禁用",
|
||||
"enabled": "已启用",
|
||||
"port": "端口",
|
||||
"message": "信息",
|
||||
@ -17,12 +17,13 @@
|
||||
"URL": "链接",
|
||||
"apiKey": "API 密钥",
|
||||
"errorInvalidUserPass": "无效的用户名/密码。",
|
||||
"errorNotAdmin": "此用户不允许管理服务。",
|
||||
"errorNotAdmin": "用户没有权限管理服务器。",
|
||||
"errorUserDisabled": "此永固可能已被禁用。",
|
||||
"error404": "404,请检查内部URL。",
|
||||
"errorConnectionRefused": "连接被拒绝。",
|
||||
"error": "错误",
|
||||
"errorUnknown": "未知错误,请检查应用程序日志。"
|
||||
"errorUnknown": "未知错误,请检查应用程序日志。",
|
||||
"errorProxy": "代理配置无效。"
|
||||
},
|
||||
"startPage": {
|
||||
"welcome": "欢迎!",
|
||||
@ -70,7 +71,8 @@
|
||||
"adminOnly": "仅允许管理员账户(推荐)",
|
||||
"emailNotice": "您的电子邮件地址可以用来接收通知。",
|
||||
"allowAllDescription": "不建议,您应该允许单个用户在设置后登录。",
|
||||
"allowAll": "允许所有Jellyfin用户登录"
|
||||
"allowAll": "允许所有Jellyfin用户登录",
|
||||
"authorizeManualUserPageNotice": "使用此选项将禁用“用户页面”功能。"
|
||||
},
|
||||
"jellyfinEmby": {
|
||||
"title": "Jellyfin/Emby",
|
||||
@ -126,7 +128,8 @@
|
||||
"resetLinksNotice": "如果启用了 Ombi 集成,请使用它与 Ombi 同步 Jellyfin 密码重置。",
|
||||
"resetLinksLanguage": "默认重置链接语言",
|
||||
"setPassword": "通过链接设置密码",
|
||||
"setPasswordNotice": "启用此功能意味着用户无需在重置后通过 PIN 更改其密码。并将强制执行密码验证。"
|
||||
"setPasswordNotice": "启用此功能意味着用户无需在重置后通过 PIN 更改其密码。并将强制执行密码验证。",
|
||||
"resetLinksRequiredForUserPage": "对于用户页面上的自助密码重置,这是必需的。"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "密码验证",
|
||||
@ -148,5 +151,17 @@
|
||||
"successMessageNotice": "在用户创建账户时显示。",
|
||||
"emailMessage": "电子邮件",
|
||||
"emailMessageNotice": "显示在电子邮件的底部。"
|
||||
},
|
||||
"proxy": {
|
||||
"description": "让jfa-go通过HTTP/SOCKS5代理进行所有连接。连接到Jellyfin将通过此代理进行测试。",
|
||||
"title": "代理",
|
||||
"protocol": "协议",
|
||||
"address": "地址(包括端口)"
|
||||
},
|
||||
"userPage": {
|
||||
"description": "用户页面(显示为“我的帐户”)允许用户访问有关他们帐户的信息,如其联系方式和账户过期日期。他们还可以更改密码、启动密码重置,并在无需询问您的情况下链接/更改联系方式。此外,用户还可以在登录前后看到自定义的Markdown消息。",
|
||||
"title": "用户页面",
|
||||
"customizeMessages": "在设置中,单击“用户页面”旁边的编辑按钮以稍后进行设置。",
|
||||
"requiredSettings": "通过Jellyfin登录到jfa-go必须设置。确保稍后选择“通过链接重置密码”以进行自助密码重置。"
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@
|
||||
"discordStartMessage": "您好!\n请输入 `/pin <PIN码>`以验证您的账户。",
|
||||
"languageMessageDiscord": "提示:使用 `/lang <语言>` 设置语言。",
|
||||
"languageSet": "语言改成 {language}。",
|
||||
"discordDMs": "请检查您的DM找回答。"
|
||||
"discordDMs": "请检查您的DM找回答。",
|
||||
"sentInvite": "已发送邀请。",
|
||||
"sentInviteFailure": "发送邀请失败,请检查日志。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
main.go
14
main.go
@ -46,6 +46,8 @@ var (
|
||||
SWAGGER *bool
|
||||
QUIT = false
|
||||
RUNNING = false
|
||||
LOGIP = false // Log admin IPs
|
||||
LOGIPU = false // Log user IPs
|
||||
// Used to know how many times to re-broadcast restart signal.
|
||||
RESTARTLISTENERCOUNT = 0
|
||||
warning = color.New(color.FgYellow).SprintfFunc()
|
||||
@ -120,6 +122,7 @@ type appContext struct {
|
||||
proxyTransport *http.Transport
|
||||
proxyConfig easyproxy.ProxyConfig
|
||||
internalPWRs map[string]InternalPWR
|
||||
pwrCaptchas map[string]Captcha
|
||||
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
|
||||
confirmationKeysLock sync.Mutex
|
||||
}
|
||||
@ -554,7 +557,16 @@ func start(asDaemon, firstCall bool) {
|
||||
cert := app.config.Section("advanced").Key("tls_cert").MustString("")
|
||||
key := app.config.Section("advanced").Key("tls_key").MustString("")
|
||||
if err := SRV.ListenAndServeTLS(cert, key); err != nil {
|
||||
app.err.Printf("Failure serving: %s", err)
|
||||
filesToCheck := []string{cert, key}
|
||||
fileNames := []string{"Certificate", "Key"}
|
||||
for i, v := range filesToCheck {
|
||||
_, err := os.Stat(v)
|
||||
if err != nil {
|
||||
app.err.Printf("SSL/TLS %s: %v\n", fileNames[i], err)
|
||||
}
|
||||
}
|
||||
|
||||
app.err.Fatalf("Failure serving with SSL/TLS: %s", err)
|
||||
}
|
||||
} else {
|
||||
if err := SRV.ListenAndServe(); err != nil {
|
||||
|
@ -332,8 +332,9 @@ type MatrixLoginDTO struct {
|
||||
}
|
||||
|
||||
type ResetPasswordDTO struct {
|
||||
PIN string `json:"pin"`
|
||||
Password string `json:"password"`
|
||||
PIN string `json:"pin"`
|
||||
Password string `json:"password"`
|
||||
CaptchaText string `json:"captcha_text"`
|
||||
}
|
||||
|
||||
type AdminPasswordResetDTO struct {
|
||||
@ -444,6 +445,7 @@ type ActivityDTO struct {
|
||||
InviteCode string `json:"invite_code"`
|
||||
Value string `json:"value"`
|
||||
Time int64 `json:"time"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
type GetActivitiesDTO struct {
|
||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -4816,9 +4816,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@ -10364,9 +10364,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
|
||||
},
|
||||
"send": {
|
||||
"version": "0.18.0",
|
||||
|
@ -7,11 +7,11 @@ all:
|
||||
cp -r ts tempts
|
||||
../scripts/dark-variant.sh tempts
|
||||
npx esbuild --target=es6 --bundle tempts/main.ts --outfile=out/main.js --minify
|
||||
npx esbuild --bundle base.css --outfile=out/bundle.css --external:remixicon.css --external:modal.css --minify
|
||||
npx esbuild --bundle base.css --outfile=out/bundle.css --external:remixicon.css --external:modal.css --external:../fonts/hanken* --minify
|
||||
npx tailwindcss -c tailwind.config.js -i out/bundle.css -o out/bundle.css
|
||||
cd out && npx uncss index.html --stylesheets remixicon.css > _remixicon.css; cd ..
|
||||
mv out/_remixicon.css out/remixicon.css
|
||||
cp ../static/* out/
|
||||
cp -r ../static/* out/
|
||||
node inject.js
|
||||
|
||||
debug:
|
||||
@ -22,10 +22,10 @@ debug:
|
||||
-rm -r tempts
|
||||
cp -r ts tempts
|
||||
../scripts/dark-variant.sh tempts
|
||||
npx esbuild --bundle base.css --outfile=out/bundle.css --external:remixicon.css --minify
|
||||
npx esbuild --bundle base.css --outfile=out/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
|
||||
npx esbuild --target=es6 --bundle ts/main.ts --sourcemap --outfile=out/main.js --minify
|
||||
npx tailwindcss -c tailwind.config.js -i out/bundle.css -o out/bundle.css
|
||||
cp ../static/* out/
|
||||
cp -r ../static/* out/
|
||||
|
||||
monitor:
|
||||
npx live-server --watch=out --open=out/index.html &
|
||||
|
@ -21,7 +21,8 @@
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card">
|
||||
<span class="heading"> Debian/Ubuntu (apt)</span>
|
||||
<div class="mt-1">
|
||||
<pre style="margin: 0; line-height: 125%">curl https://apt.hrfee.dev/hrfee.pubkey.gpg | sudo apt-key add -
|
||||
<pre style="margin: 0; line-height: 125%">sudo apt-get update && sudo apt-get install curl apt-transport-https gnupg
|
||||
curl https://apt.hrfee.dev/hrfee.pubkey.gpg | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/apt.hrfee.dev.gpg
|
||||
|
||||
echo <span style="color: #aa5500">"deb https://apt.hrfee.dev trusty<span id="deb-unstable" class="unfocused">-unstable</span> main"</span> | sudo tee /etc/apt/sources.list.d/hrfee.list
|
||||
|
||||
@ -129,7 +130,7 @@ sudo apt-get install jfa-go-tray
|
||||
<a class="button ~info mr-2 mb-2 lang-link" target="_blank" href="https://aur.archlinux.org/packages/jfa-go-git">arch (aur git)</a>
|
||||
</div>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||
<a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE" class="support">© 2023 Harvey Tindall</a>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/logger"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
@ -55,6 +56,7 @@ type Activity struct {
|
||||
InviteCode string // Set for ActivityCreation, create/deleteInvite
|
||||
Value string // Used for ActivityContactLinked where it's "email/discord/telegram/matrix", Create/DeleteInvite, where it's the label, and Creation/Deletion, where it's the Username.
|
||||
Time time.Time
|
||||
IP string
|
||||
}
|
||||
|
||||
type UserExpiry struct {
|
||||
@ -563,8 +565,12 @@ func (st *Storage) GetActivityKey(k string) (Activity, bool) {
|
||||
}
|
||||
|
||||
// SetActivityKey stores value v in key k.
|
||||
func (st *Storage) SetActivityKey(k string, v Activity) {
|
||||
// If the IP should be logged, pass "gc", and whether or not the action is of a user
|
||||
func (st *Storage) SetActivityKey(k string, v Activity, gc *gin.Context, user bool) {
|
||||
v.ID = k
|
||||
if gc != nil && ((LOGIPU && user) || (LOGIP && !user)) {
|
||||
v.IP = gc.ClientIP()
|
||||
}
|
||||
err := st.db.Upsert(k, v)
|
||||
if err != nil {
|
||||
// fmt.Printf("Failed to set custom content: %v\n", err)
|
||||
|
@ -7,7 +7,7 @@ import { accountsList } from "./modules/accounts.js";
|
||||
import { settingsList } from "./modules/settings.js";
|
||||
import { activityList } from "./modules/activity.js";
|
||||
import { ProfileEditor } from "./modules/profiles.js";
|
||||
import { _get, _post, notificationBox, whichAnimationEvent } from "./modules/common.js";
|
||||
import { _get, _post, notificationBox, whichAnimationEvent, bindManualDropdowns } from "./modules/common.js";
|
||||
import { Updater } from "./modules/update.js";
|
||||
import { Login } from "./modules/login.js";
|
||||
|
||||
@ -216,6 +216,8 @@ login.onLogin = () => {
|
||||
}
|
||||
}
|
||||
|
||||
bindManualDropdowns();
|
||||
|
||||
login.bindLogout(document.getElementById("logout-button"));
|
||||
|
||||
login.login("", "");
|
||||
|
72
ts/form.ts
72
ts/form.ts
@ -4,6 +4,7 @@ import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
|
||||
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
|
||||
import { Captcha, GreCAPTCHA } from "./modules/captcha.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
invalidPassword: string;
|
||||
@ -172,35 +173,7 @@ if (!window.usernameEnabled) { usernameField.parentElement.remove(); usernameFie
|
||||
const passwordField = document.getElementById("create-password") as HTMLInputElement;
|
||||
const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement;
|
||||
|
||||
let captchaVerified = false;
|
||||
let captchaID = "";
|
||||
let captchaInput = document.getElementById("captcha-input") as HTMLInputElement;
|
||||
const captchaCheckbox = document.getElementById("captcha-success") as HTMLSpanElement;
|
||||
let prevCaptcha = "";
|
||||
|
||||
let baseValidator = (oncomplete: (valid: boolean) => void): void => {
|
||||
if (window.captcha && !window.reCAPTCHA && (captchaInput.value != prevCaptcha)) {
|
||||
prevCaptcha = captchaInput.value;
|
||||
_post("/captcha/verify/" + window.code + "/" + captchaID + "/" + captchaInput.value, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 204) {
|
||||
captchaCheckbox.innerHTML = `<i class="ri-check-line"></i>`;
|
||||
captchaCheckbox.classList.add("~positive");
|
||||
captchaCheckbox.classList.remove("~critical");
|
||||
captchaVerified = true;
|
||||
} else {
|
||||
captchaCheckbox.innerHTML = `<i class="ri-close-line"></i>`;
|
||||
captchaCheckbox.classList.add("~critical");
|
||||
captchaCheckbox.classList.remove("~positive");
|
||||
captchaVerified = false;
|
||||
}
|
||||
_baseValidator(oncomplete, captchaVerified);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_baseValidator(oncomplete, captchaVerified);
|
||||
}
|
||||
}
|
||||
let captcha = new Captcha(window.code, window.captcha, window.reCAPTCHA, false);
|
||||
|
||||
function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
|
||||
if (window.emailRequired) {
|
||||
@ -228,20 +201,9 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool
|
||||
oncomplete(true);
|
||||
}
|
||||
|
||||
interface GreCAPTCHA {
|
||||
render: (container: HTMLDivElement, parameters: {
|
||||
sitekey?: string,
|
||||
theme?: string,
|
||||
size?: string,
|
||||
tabindex?: number,
|
||||
"callback"?: () => void,
|
||||
"expired-callback"?: () => void,
|
||||
"error-callback"?: () => void
|
||||
}) => void;
|
||||
getResponse: (opt_widget_id?: HTMLDivElement) => string;
|
||||
}
|
||||
let baseValidator = captcha.baseValidatorWrapper(_baseValidator);
|
||||
|
||||
declare var grecaptcha: GreCAPTCHA
|
||||
declare var grecaptcha: GreCAPTCHA;
|
||||
|
||||
let validatorConf: ValidatorConf = {
|
||||
passwordField: passwordField,
|
||||
@ -273,29 +235,15 @@ interface sendDTO {
|
||||
captcha_text?: string;
|
||||
}
|
||||
|
||||
const genCaptcha = () => {
|
||||
_get("/captcha/gen/"+window.code, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200) {
|
||||
captchaID = req.response["id"];
|
||||
document.getElementById("captcha-img").innerHTML = `
|
||||
<img class="w-100" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf("/invite"))}/captcha/img/${window.code}/${captchaID}"></img>
|
||||
`;
|
||||
captchaInput.value = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (window.captcha && !window.reCAPTCHA) {
|
||||
genCaptcha();
|
||||
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = genCaptcha;
|
||||
captchaInput.onkeyup = validator.validate;
|
||||
captcha.generate();
|
||||
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = captcha.generate;
|
||||
captcha.input.onkeyup = validator.validate;
|
||||
}
|
||||
|
||||
const create = (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
if (window.captcha && !window.reCAPTCHA && !captchaVerified) {
|
||||
if (window.captcha && !window.reCAPTCHA && !captcha.verified) {
|
||||
|
||||
}
|
||||
addLoader(submitSpan);
|
||||
@ -330,8 +278,8 @@ const create = (event: SubmitEvent) => {
|
||||
if (window.reCAPTCHA) {
|
||||
send.captcha_text = grecaptcha.getResponse();
|
||||
} else {
|
||||
send.captcha_id = captchaID;
|
||||
send.captcha_text = captchaInput.value;
|
||||
send.captcha_id = captcha.captchaID;
|
||||
send.captcha_text = captcha.input.value;
|
||||
}
|
||||
}
|
||||
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||
|
@ -188,7 +188,7 @@ class user implements User, SearchableItem {
|
||||
if (!telegram && !discord && !matrix && !email) return;
|
||||
let innerHTML = `
|
||||
<i class="icon ri-settings-2-line ml-2 dropdown-button"></i>
|
||||
<div class="dropdown over-top manual">
|
||||
<div class="dropdown manual">
|
||||
<div class="dropdown-display lg">
|
||||
<div class="card ~neutral @low">
|
||||
<div class="supra sm mb-2">${window.lang.strings("contactThrough")}</div>
|
||||
@ -218,13 +218,13 @@ class user implements User, SearchableItem {
|
||||
</div>
|
||||
<div class="supra sm mb-2 accounts-unlink-header">${window.lang.strings("unlink")}:</div>
|
||||
<div class="accounts-unlink-telegram">
|
||||
<button class="button ~critical mb-2 w-100">Telegram</button>
|
||||
<button class="button ~critical mb-2 w-full">Telegram</button>
|
||||
</div>
|
||||
<div class="accounts-unlink-discord">
|
||||
<button class="button ~critical mb-2 w-100">Discord</button>
|
||||
<button class="button ~critical mb-2 w-full">Discord</button>
|
||||
</div>
|
||||
<div class="accounts-unlink-matrix">
|
||||
<button class="button ~critical mb-2 w-100">Matrix</button>
|
||||
<button class="button ~critical mb-2 w-full">Matrix</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1271,7 +1271,7 @@ export class accountsList {
|
||||
dList.textContent = '';
|
||||
for (let name of list) {
|
||||
const el = document.createElement("div") as HTMLDivElement;
|
||||
el.classList.add("flex-expand", "ellipsis", "mt-2");
|
||||
el.classList.add("flex", "flex-row", "justify-between", "truncate", "mt-2");
|
||||
el.innerHTML = `
|
||||
<span class="button ~neutral sm full-width accounts-announce-template-button">${name}</span><span class="button ~critical fr ml-4 accounts-announce-template-delete">×</span>
|
||||
`;
|
||||
@ -1634,6 +1634,7 @@ export class accountsList {
|
||||
_displayExpiryDate = () => {
|
||||
let date: Date;
|
||||
let invalid = false;
|
||||
let users = this._collectUsers();
|
||||
if (this._usingExtendExpiryTextInput) {
|
||||
date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date;
|
||||
invalid = "invalid" in (date as any);
|
||||
@ -1645,7 +1646,7 @@ export class accountsList {
|
||||
document.getElementById("extend-expiry-minutes") as HTMLSelectElement
|
||||
];
|
||||
invalid = fields[0].value == "0" && fields[1].value == "0" && fields[2].value == "0" && fields[3].value == "0";
|
||||
let id = this._collectUsers().length == 1 ? this._collectUsers()[0] : "";
|
||||
let id = users.length > 0 ? users[0] : "";
|
||||
if (!id) invalid = true;
|
||||
else {
|
||||
date = new Date(this._users[id].expiry*1000);
|
||||
@ -1665,7 +1666,12 @@ export class accountsList {
|
||||
} else {
|
||||
submit.disabled = false;
|
||||
submitSpan.classList.remove("opacity-60");
|
||||
this._extendExpiryDate.textContent = window.lang.strings("accountWillExpire").replace("{date}", toDateString(date));
|
||||
this._extendExpiryDate.innerHTML = `
|
||||
<div class="flex flex-col">
|
||||
<span>${window.lang.strings("accountWillExpire").replace("{date}", toDateString(date))}</span>
|
||||
${users.length > 1 ? "<span>"+window.lang.strings("expirationBasedOn")+"</span>" : ""}
|
||||
</div>
|
||||
`;
|
||||
this._extendExpiryDate.classList.remove("unfocused");
|
||||
}
|
||||
}
|
||||
@ -1740,6 +1746,9 @@ export class accountsList {
|
||||
}
|
||||
}
|
||||
this._extendExpiryTextInput.value = "";
|
||||
this._usingExtendExpiryTextInput = false;
|
||||
this._extendExpiryDate.classList.add("unfocused");
|
||||
this._displayExpiryDate();
|
||||
window.modals.extendExpiry.show();
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ export interface activity {
|
||||
time: number;
|
||||
username: string;
|
||||
source_username: string;
|
||||
ip: string;
|
||||
}
|
||||
|
||||
var activityTypeMoods = {
|
||||
@ -43,6 +44,7 @@ export class Activity implements activity, SearchableItem {
|
||||
private _referrer: HTMLElement;
|
||||
private _expiryTypeBadge: HTMLElement;
|
||||
private _delete: HTMLElement;
|
||||
private _ip: HTMLElement;
|
||||
private _act: activity;
|
||||
private _urlBase: string = ((): string => {
|
||||
let link = window.location.href;
|
||||
@ -205,6 +207,16 @@ export class Activity implements activity, SearchableItem {
|
||||
}
|
||||
}
|
||||
|
||||
get ip(): string { return this._act.ip; }
|
||||
set ip(v: string) {
|
||||
this._act.ip = v;
|
||||
if (v) {
|
||||
this._ip.innerHTML = `<span class="supra mr-2">IP</span><span class="font-mono bg-inherit">${v}</span>`;
|
||||
} else {
|
||||
this._ip.textContent = ``;
|
||||
}
|
||||
}
|
||||
|
||||
get invite_code(): string { return this._act.invite_code; }
|
||||
set invite_code(v: string) {
|
||||
this._act.invite_code = v;
|
||||
@ -260,12 +272,13 @@ export class Activity implements activity, SearchableItem {
|
||||
<span class="activity-expiry-type badge self-start md:self-end mt-1"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row justify-between">
|
||||
<div>
|
||||
<span class="content supra mr-2 activity-source-type"></span><span class="activity-source"></span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-row justify-between items-end">
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<div>
|
||||
<span class="content supra mr-2 activity-source-type"></span><span class="activity-source"></span>
|
||||
</div>
|
||||
<span class="content activity-referrer"></span>
|
||||
<span class="content activity-ip"></span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="button @low hover:~critical rounded-full px-1 py-px activity-delete" aria-label="${window.lang.strings("delete")}"><i class="ri-close-line"></i></button>
|
||||
@ -277,6 +290,7 @@ export class Activity implements activity, SearchableItem {
|
||||
this._time = this._card.querySelector(".activity-time");
|
||||
this._sourceType = this._card.querySelector(".activity-source-type");
|
||||
this._source = this._card.querySelector(".activity-source");
|
||||
this._ip = this._card.querySelector(".activity-ip");
|
||||
this._referrer = this._card.querySelector(".activity-referrer");
|
||||
this._expiryTypeBadge = this._card.querySelector(".activity-expiry-type");
|
||||
this._delete = this._card.querySelector(".activity-delete");
|
||||
@ -324,6 +338,7 @@ export class Activity implements activity, SearchableItem {
|
||||
this.source = act.source;
|
||||
this.value = act.value;
|
||||
this.type = act.type;
|
||||
this.ip = act.ip;
|
||||
}
|
||||
|
||||
delete = () => _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => {
|
||||
|
81
ts/modules/captcha.ts
Normal file
81
ts/modules/captcha.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { _get, _post } from "./common.js";
|
||||
|
||||
export class Captcha {
|
||||
isPWR = false;
|
||||
enabled = true;
|
||||
verified = false;
|
||||
captchaID = "";
|
||||
input = document.getElementById("captcha-input") as HTMLInputElement;
|
||||
checkbox = document.getElementById("captcha-success") as HTMLSpanElement;
|
||||
previous = "";
|
||||
reCAPTCHA = false;
|
||||
code = "";
|
||||
|
||||
get value(): string { return this.input.value; }
|
||||
|
||||
hasChanged = (): boolean => { return this.value != this.previous; }
|
||||
|
||||
baseValidatorWrapper = (_baseValidator: (oncomplete: (valid: boolean) => void, captchaValid: boolean) => void) => {
|
||||
return (oncomplete: (valid: boolean) => void): void => {
|
||||
if (this.enabled && !this.reCAPTCHA && this.hasChanged()) {
|
||||
this.previous = this.value;
|
||||
this.verify(() => {
|
||||
_baseValidator(oncomplete, this.verified);
|
||||
});
|
||||
} else {
|
||||
_baseValidator(oncomplete, this.verified);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
verify = (callback: () => void) => _post("/captcha/verify/" + this.code + "/" + this.captchaID + "/" + this.input.value + (this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 204) {
|
||||
this.checkbox.innerHTML = `<i class="ri-check-line"></i>`;
|
||||
this.checkbox.classList.add("~positive");
|
||||
this.checkbox.classList.remove("~critical");
|
||||
this.verified = true;
|
||||
} else {
|
||||
this.checkbox.innerHTML = `<i class="ri-close-line"></i>`;
|
||||
this.checkbox.classList.add("~critical");
|
||||
this.checkbox.classList.remove("~positive");
|
||||
this.verified = false;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
generate = () => _get("/captcha/gen/"+this.code+(this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200) {
|
||||
this.captchaID = this.isPWR ? this.code : req.response["id"];
|
||||
// the Math.random() appearance below is used for PWRs, since they don't have a unique captchaID. The parameter is ignored by the server, but tells the browser to reload the image.
|
||||
document.getElementById("captcha-img").innerHTML = `
|
||||
<img class="w-full" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf("/invite"))}/captcha/img/${this.code}/${this.isPWR ? Math.random() : this.captchaID}${this.isPWR ? "?pwr=true" : ""}"></img>
|
||||
`;
|
||||
this.input.value = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
constructor(code: string, enabled: boolean, reCAPTCHA: boolean, isPWR: boolean) {
|
||||
this.code = code;
|
||||
this.enabled = enabled;
|
||||
this.reCAPTCHA = reCAPTCHA;
|
||||
this.isPWR = isPWR;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GreCAPTCHA {
|
||||
render: (container: HTMLDivElement, parameters: {
|
||||
sitekey?: string,
|
||||
theme?: string,
|
||||
size?: string,
|
||||
tabindex?: number,
|
||||
"callback"?: () => void,
|
||||
"expired-callback"?: () => void,
|
||||
"error-callback"?: () => void
|
||||
}) => void;
|
||||
getResponse: (opt_widget_id?: HTMLDivElement) => string;
|
||||
}
|
||||
|
@ -263,3 +263,31 @@ export function insertText(textarea: HTMLTextAreaElement, text: string) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export function bindManualDropdowns() {
|
||||
const buttons = Array.from(document.getElementsByClassName("dropdown-manual-toggle") as HTMLCollectionOf<HTMLSpanElement>);
|
||||
for (let button of buttons) {
|
||||
const parent = button.closest(".dropdown.manual");
|
||||
const display = parent.querySelector(".dropdown-display");
|
||||
const mousein = () => parent.classList.add("selected");
|
||||
const mouseout = () => parent.classList.remove("selected");
|
||||
button.addEventListener("mouseover", mousein);
|
||||
button.addEventListener("mouseout", mouseout);
|
||||
display.addEventListener("mouseover", mousein);
|
||||
display.addEventListener("mouseout", mouseout);
|
||||
button.onclick = () => {
|
||||
parent.classList.add("selected");
|
||||
document.addEventListener("click", outerClickListener);
|
||||
button.removeEventListener("mouseout", mouseout);
|
||||
display.removeEventListener("mouseout", mouseout);
|
||||
};
|
||||
const outerClickListener = (event: Event) => {
|
||||
if (!(event.target instanceof HTMLElement && (display.contains(event.target) || button.contains(event.target)))) {
|
||||
parent.classList.remove("selected");
|
||||
document.removeEventListener("click", outerClickListener);
|
||||
button.addEventListener("mouseout", mouseout);
|
||||
display.addEventListener("mouseout", mouseout);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ class DOMInvite implements Invite {
|
||||
get expiresIn(): string { return this._expiresIn }
|
||||
set expiresIn(expiry: string) {
|
||||
this._expiresIn = expiry;
|
||||
this._infoArea.querySelector("span.inv-duration").textContent = expiry;
|
||||
this._codeArea.querySelector("span.inv-duration").textContent = expiry;
|
||||
}
|
||||
|
||||
private _userExpiry: string;
|
||||
@ -110,15 +110,15 @@ class DOMInvite implements Invite {
|
||||
const chip = container.querySelector("span.inv-email-chip");
|
||||
const tooltip = container.querySelector("span.content") as HTMLSpanElement;
|
||||
if (address == "") {
|
||||
container.classList.remove("mr-4");
|
||||
icon.classList.remove("ri-mail-line");
|
||||
icon.classList.remove("ri-mail-close-line");
|
||||
chip.classList.remove("~neutral");
|
||||
chip.classList.remove("~critical");
|
||||
chip.classList.remove("chip");
|
||||
chip.classList.remove("button");
|
||||
chip.parentElement.classList.remove("h-full");
|
||||
} else {
|
||||
container.classList.add("mr-4");
|
||||
chip.classList.add("chip");
|
||||
chip.classList.add("button");
|
||||
chip.parentElement.classList.add("h-full");
|
||||
if (address.includes("Failed")) {
|
||||
icon.classList.remove("ri-mail-line");
|
||||
icon.classList.add("ri-mail-close-line");
|
||||
@ -146,10 +146,11 @@ class DOMInvite implements Invite {
|
||||
}
|
||||
this._right.classList.remove("empty");
|
||||
let innerHTML = `
|
||||
<table class="table inv-table">
|
||||
<table class="table inv-table table-p-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${window.lang.strings("name")}</th>
|
||||
<th class="w-2"></th>
|
||||
<th>${window.lang.strings("date")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -159,6 +160,7 @@ class DOMInvite implements Invite {
|
||||
innerHTML += `
|
||||
<tr>
|
||||
<td>${username}</td>
|
||||
<td class="w-2"></td>
|
||||
<td>${toDateString(new Date(uB[username] * 1000))}</td>
|
||||
</tr>
|
||||
`;
|
||||
@ -266,18 +268,21 @@ class DOMInvite implements Invite {
|
||||
constructor(invite: Invite) {
|
||||
// first create the invite structure, then use our setter methods to fill in the data.
|
||||
this._container = document.createElement('div') as HTMLDivElement;
|
||||
this._container.classList.add("inv");
|
||||
this._container.classList.add("inv", "overflow-visible");
|
||||
|
||||
this._header = document.createElement('div') as HTMLDivElement;
|
||||
this._container.appendChild(this._header);
|
||||
this._header.classList.add("card", "dark:~d_neutral", "@low", "inv-header", "elem-pad", "no-pad", "flex-expand", "row", "mt-2", "overflow-y");
|
||||
this._header.classList.add("card", "dark:~d_neutral", "@low", "inv-header", "flex", "flex-row", "justify-between", "mt-2", "overflow-visible", "gap-2");
|
||||
|
||||
this._codeArea = document.createElement('div') as HTMLDivElement;
|
||||
this._header.appendChild(this._codeArea);
|
||||
this._codeArea.classList.add("inv-codearea");
|
||||
this._codeArea.classList.add("flex", "flex-row", "flex-wrap", "justify-between", "w-full", "items-baseline", "gap-2", "truncate");
|
||||
this._codeArea.innerHTML = `
|
||||
<a class="invite-link text-black dark:text-white font-mono bg-inherit mr-4" href=""></a>
|
||||
<span class="button ~info @low" title="${window.lang.strings("copy")}"><i class="ri-file-copy-line"></i></span>
|
||||
<div class="flex items-baseline gap-x-4 gap-y-2 truncate">
|
||||
<a class="invite-link text-black dark:text-white font-mono bg-inherit truncate" href=""></a>
|
||||
<span class="button ~info @low" title="${window.lang.strings("copy")}"><i class="ri-file-copy-line"></i></span>
|
||||
</div>
|
||||
<span class="inv-duration"></span>
|
||||
`;
|
||||
const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
|
||||
copyButton.onclick = () => {
|
||||
@ -297,16 +302,15 @@ class DOMInvite implements Invite {
|
||||
|
||||
this._infoArea = document.createElement('div') as HTMLDivElement;
|
||||
this._header.appendChild(this._infoArea);
|
||||
this._infoArea.classList.add("inv-infoarea");
|
||||
this._infoArea.classList.add("inv-infoarea", "flex", "flex-row", "items-baseline", "gap-2");
|
||||
this._infoArea.innerHTML = `
|
||||
<div class="tooltip left">
|
||||
<span class="inv-email-chip"><i></i></span>
|
||||
<span class="content sm"></span>
|
||||
<div class="tooltip below darker" tabindex="0">
|
||||
<span class="inv-email-chip h-full"><i></i></span>
|
||||
<span class="content sm p-1"></span>
|
||||
</div>
|
||||
<span class="inv-duration mr-4"></span>
|
||||
<span class="button ~critical @low inv-delete">${window.lang.strings("delete")}</span>
|
||||
<span class="button ~critical @low inv-delete h-full">${window.lang.strings("delete")}</span>
|
||||
<label>
|
||||
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
|
||||
<i class="icon px-2.5 py-2 ri-arrow-down-s-line not-rotated"></i>
|
||||
<input class="inv-toggle-details unfocused" type="checkbox">
|
||||
</label>
|
||||
`;
|
||||
@ -315,25 +319,30 @@ class DOMInvite implements Invite {
|
||||
|
||||
const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
|
||||
toggle.onchange = () => { this.expanded = !this.expanded; };
|
||||
this._header.onclick = (event: Event) => {
|
||||
if (event.target == this._header) {
|
||||
const toggleDetails = (event: Event) => {
|
||||
if (event.target == this._header || event.target == this._codeArea || event.target == this._infoArea) {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
};
|
||||
this._header.onclick = toggleDetails;
|
||||
|
||||
|
||||
this._details = document.createElement('div') as HTMLDivElement;
|
||||
this._container.appendChild(this._details);
|
||||
this._details.classList.add("card", "~neutral", "@low", "mt-2", "no-pad", "inv-details");
|
||||
this._details.classList.add("card", "~neutral", "@low", "mt-2", "inv-details");
|
||||
const detailsInner = document.createElement('div') as HTMLDivElement;
|
||||
this._details.appendChild(detailsInner);
|
||||
detailsInner.classList.add("inv-row", "flex-expand", "row", "elem-pad", "align-top");
|
||||
detailsInner.classList.add("inv-row", "flex", "flex-row", "flex-wrap", "justify-between", "gap-4");
|
||||
|
||||
this._left = document.createElement('div') as HTMLDivElement;
|
||||
this._left.classList.add("flex", "flex-row", "flex-wrap", "gap-4", "min-w-full", "sm:min-w-fit", "whitespace-nowrap");
|
||||
detailsInner.appendChild(this._left);
|
||||
this._left.classList.add("inv-profilearea");
|
||||
const leftLeft = document.createElement("div") as HTMLDivElement;
|
||||
this._left.appendChild(leftLeft);
|
||||
leftLeft.classList.add("inv-profilearea", "min-w-full", "sm:min-w-fit");
|
||||
let innerHTML = `
|
||||
<p class="supra mb-2 top">${window.lang.strings("profile")}</p>
|
||||
<div class="select ~neutral @low inv-profileselect inline-block mb-2">
|
||||
<div class="select ~neutral @low inv-profileselect min-w-full inline-block mb-2">
|
||||
<select>
|
||||
<option value="noProfile" selected>${window.lang.strings("inviteNoProfile")}</option>
|
||||
</select>
|
||||
@ -352,7 +361,7 @@ class DOMInvite implements Invite {
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
this._left.innerHTML = innerHTML;
|
||||
leftLeft.innerHTML = innerHTML;
|
||||
(this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile;
|
||||
|
||||
if (window.notificationsEnabled) {
|
||||
@ -364,20 +373,21 @@ class DOMInvite implements Invite {
|
||||
}
|
||||
|
||||
this._middle = document.createElement('div') as HTMLDivElement;
|
||||
detailsInner.appendChild(this._middle);
|
||||
this._middle.classList.add("block");
|
||||
this._left.appendChild(this._middle);
|
||||
this._middle.classList.add("flex", "flex-col", "justify-between");
|
||||
this._middle.innerHTML = `
|
||||
<p class="supra mb-4 top">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p>
|
||||
<p class="supra mb-4">${window.lang.strings("inviteRemainingUses")} <strong class="inv-remaining"></strong></p>
|
||||
<p class="supra mb-4"><span class="user-expiry"></span> <strong class="user-expiry-time"></strong></p>
|
||||
<p class="mb-4 flex items-center"><span class="user-label-label supra mr-2"></span> <span class="user-label chip ~blue unfocused"></span></p>
|
||||
<p class="supra top">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p>
|
||||
<p class="supra">${window.lang.strings("inviteRemainingUses")} <strong class="inv-remaining"></strong></p>
|
||||
<p class="supra"><span class="user-expiry"></span> <strong class="user-expiry-time"></strong></p>
|
||||
<p class="flex items-center"><span class="user-label-label supra mr-2"></span> <span class="user-label chip ~blue unfocused"></span></p>
|
||||
`;
|
||||
|
||||
this._right = document.createElement('div') as HTMLDivElement;
|
||||
detailsInner.appendChild(this._right);
|
||||
this._right.classList.add("card", "~neutral", "@low", "inv-created-users");
|
||||
this._right.innerHTML = `<strong class="supra table-header">${window.lang.strings("inviteUsersCreated")}</strong>`;
|
||||
this._right.classList.add("card", "~neutral", "@low", "inv-created-users", "min-w-full", "sm:min-w-fit", "whitespace-nowrap");
|
||||
this._right.innerHTML = `<span class="supra table-header">${window.lang.strings("inviteUsersCreated")}</span>`;
|
||||
this._userTable = document.createElement('div') as HTMLDivElement;
|
||||
this._userTable.classList.add("text-sm", "mt-1", );
|
||||
this._right.appendChild(this._userTable);
|
||||
|
||||
|
||||
@ -470,8 +480,8 @@ export class inviteList implements inviteList {
|
||||
this._list.classList.add("empty");
|
||||
this._list.innerHTML = `
|
||||
<div class="inv inv-empty">
|
||||
<div class="card dark:~d_neutral @low inv-header flex-expand mt-2">
|
||||
<div class="inv-codearea">
|
||||
<div class="card dark:~d_neutral @low inv-header mt-2">
|
||||
<div class="justify-start">
|
||||
<span class="text-black dark:text-white font-mono bg-inherit">${window.lang.strings("inviteNoInvites")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -83,7 +83,7 @@ export const loadLangSelector = (page: string) => {
|
||||
let innerHTML = '';
|
||||
for (let code in req.response) {
|
||||
queryString.set("lang", code);
|
||||
innerHTML += `<a href="?${queryString.toString()}" class="button w-100 al justify-start ~neutral mb-2 lang-link">${req.response[code]}</a>`;
|
||||
innerHTML += `<a href="?${queryString.toString()}" class="button w-full text-left justify-start ~neutral mb-2 lang-link">${req.response[code]}</a>`;
|
||||
queryString.delete("lang");
|
||||
}
|
||||
list.innerHTML = innerHTML;
|
||||
|
@ -86,7 +86,7 @@ class profile implements Profile {
|
||||
<td><span class="button @low profile-referrals"></span></td>
|
||||
`;
|
||||
innerHTML += `
|
||||
<td class="profile-from ellipsis"></td>
|
||||
<td class="profile-from truncate"></td>
|
||||
<td class="profile-libraries"></td>
|
||||
<td><span class="button ~critical @low">${window.lang.strings("delete")}</span></td>
|
||||
`;
|
||||
|
@ -311,7 +311,7 @@ export class Search {
|
||||
}
|
||||
|
||||
const container = document.createElement("span") as HTMLSpanElement;
|
||||
container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2");
|
||||
container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2", "align-bottom");
|
||||
container.innerHTML = `
|
||||
<div class="flex flex-col mr-2">
|
||||
<span>${query.name}</span>
|
||||
|
@ -22,13 +22,18 @@ export class Tabs implements Tabs {
|
||||
|
||||
switch = (tabID: string, noRun: boolean = false, keepURL: boolean = false) => {
|
||||
this._current = tabID;
|
||||
let baseOffset = -1;
|
||||
for (let t of this.tabs) {
|
||||
if (baseOffset == -1) baseOffset = t.buttonEl.offsetLeft;
|
||||
if (t.tabID == tabID) {
|
||||
t.buttonEl.classList.add("active", "~urge");
|
||||
if (t.preFunc && !noRun) { t.preFunc(); }
|
||||
t.tabEl.classList.remove("unfocused");
|
||||
if (t.postFunc && !noRun) { t.postFunc(); }
|
||||
document.dispatchEvent(new CustomEvent("tab-change", { detail: keepURL ? "" : tabID }));
|
||||
// t.buttonEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' })
|
||||
|
||||
t.buttonEl.parentElement.scrollTo(t.buttonEl.offsetLeft-baseOffset, 0);
|
||||
} else {
|
||||
t.buttonEl.classList.remove("active");
|
||||
t.buttonEl.classList.remove("~urge");
|
||||
|
52
ts/pwr.ts
52
ts/pwr.ts
@ -2,6 +2,7 @@ import { Modal } from "./modules/modal.js";
|
||||
import { Validator, ValidatorConf } from "./modules/validator.js";
|
||||
import { _post, addLoader, removeLoader } from "./modules/common.js";
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
import { Captcha, GreCAPTCHA } from "./modules/captcha.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
invalidPassword: string;
|
||||
@ -28,6 +29,10 @@ interface formWindow extends Window {
|
||||
userExpiryHours: number;
|
||||
userExpiryMinutes: number;
|
||||
userExpiryMessage: string;
|
||||
captcha: boolean;
|
||||
reCAPTCHA: boolean;
|
||||
reCAPTCHASiteKey: string;
|
||||
pwrPIN: string;
|
||||
}
|
||||
|
||||
loadLangSelector("pwr");
|
||||
@ -42,11 +47,26 @@ const rePasswordField = document.getElementById("create-reenter-password") as HT
|
||||
|
||||
window.successModal = new Modal(document.getElementById("modal-success"), true);
|
||||
|
||||
function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
|
||||
if (window.captcha && !window.reCAPTCHA && !captchaValid) {
|
||||
oncomplete(false);
|
||||
return;
|
||||
}
|
||||
oncomplete(true);
|
||||
}
|
||||
|
||||
let captcha = new Captcha(window.pwrPIN, window.captcha, window.reCAPTCHA, true);
|
||||
|
||||
declare var grecaptcha: GreCAPTCHA;
|
||||
|
||||
let baseValidator = captcha.baseValidatorWrapper(_baseValidator);
|
||||
|
||||
let validatorConf: ValidatorConf = {
|
||||
passwordField: passwordField,
|
||||
rePasswordField: rePasswordField,
|
||||
submitInput: submitInput,
|
||||
submitButton: submitSpan
|
||||
submitButton: submitSpan,
|
||||
validatorFunc: baseValidator
|
||||
};
|
||||
|
||||
var validator = new Validator(validatorConf);
|
||||
@ -55,6 +75,13 @@ var requirements = validator.requirements;
|
||||
interface sendDTO {
|
||||
pin: string;
|
||||
password: string;
|
||||
captcha_text?: string;
|
||||
}
|
||||
|
||||
if (window.captcha && !window.reCAPTCHA) {
|
||||
captcha.generate();
|
||||
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = captcha.generate;
|
||||
captcha.input.onkeyup = validator.validate;
|
||||
}
|
||||
|
||||
form.onsubmit = (event: Event) => {
|
||||
@ -65,12 +92,31 @@ form.onsubmit = (event: Event) => {
|
||||
pin: params.get("pin"),
|
||||
password: passwordField.value
|
||||
};
|
||||
if (window.captcha) {
|
||||
if (window.reCAPTCHA) {
|
||||
send.captcha_text = grecaptcha.getResponse();
|
||||
} else {
|
||||
send.captcha_text = captcha.input.value;
|
||||
}
|
||||
}
|
||||
_post("/reset", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
removeLoader(submitSpan);
|
||||
if (req.status == 400) {
|
||||
for (let type in req.response) {
|
||||
if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; }
|
||||
if (req.response["error"] as string) {
|
||||
const old = submitSpan.textContent;
|
||||
submitSpan.textContent = window.messages[req.response["error"]];
|
||||
submitSpan.classList.add("~critical");
|
||||
submitSpan.classList.remove("~urge");
|
||||
setTimeout(() => {
|
||||
submitSpan.classList.add("~urge");
|
||||
submitSpan.classList.remove("~critical");
|
||||
submitSpan.textContent = old;
|
||||
}, 2000);
|
||||
} else {
|
||||
for (let type in req.response) {
|
||||
if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; }
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if (req.status != 200) {
|
||||
|
80
ts/user.ts
80
ts/user.ts
@ -144,9 +144,9 @@ class ContactMethods {
|
||||
|
||||
append = (name: string, details: MyDetailsContactMethod, icon: string, addEditFunc?: (add: boolean) => void, required?: boolean) => {
|
||||
const row = document.createElement("div");
|
||||
row.classList.add("flex", "flex-expand", "my-2", "flex-nowrap");
|
||||
row.classList.add("flex", "flex-row", "justify-between", "my-2", "flex-nowrap");
|
||||
let innerHTML = `
|
||||
<div class="flex items-baseline flex-nowrap ellipsis">
|
||||
<div class="flex items-baseline flex-nowrap truncate">
|
||||
<span class="shield ~urge" alt="${name}">
|
||||
<span class="icon">
|
||||
${icon}
|
||||
@ -637,10 +637,10 @@ document.addEventListener("details-reload", () => {
|
||||
|
||||
if (typeof(messageCard) != "undefined" && messageCard != null) {
|
||||
messageCard.innerHTML = messageCard.innerHTML.replace(new RegExp("{username}", "g"), details.username);
|
||||
setBestRowSpan(messageCard, false);
|
||||
// setBestRowSpan(messageCard, false);
|
||||
// contactCard.querySelector(".content").classList.add("h-100");
|
||||
} else if (!statusCard.classList.contains("unfocused")) {
|
||||
setBestRowSpan(passwordCard, true);
|
||||
// setBestRowSpan(passwordCard, true);
|
||||
}
|
||||
|
||||
if (window.referralsEnabled) {
|
||||
@ -649,15 +649,69 @@ document.addEventListener("details-reload", () => {
|
||||
if (req.readyState != 4 || req.status != 200) return;
|
||||
const referral: MyReferral = req.response as MyReferral;
|
||||
referralCard.update(referral);
|
||||
setCardOrder(messageCard);
|
||||
});
|
||||
} else {
|
||||
referralCard.hide();
|
||||
setCardOrder(messageCard);
|
||||
}
|
||||
} else {
|
||||
setCardOrder(messageCard);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const setCardOrder = (messageCard: HTMLElement) => {
|
||||
const cards = document.getElementById("user-cardlist");
|
||||
const children = Array.from(cards.children);
|
||||
const idxs = [...Array(cards.childElementCount).keys()]
|
||||
// The message card is the first element and should always be so, so remove it from the list.
|
||||
const hasMessageCard = !(typeof(messageCard) == "undefined" || messageCard == null);
|
||||
if (hasMessageCard) idxs.shift();
|
||||
const perms = generatePermutations(idxs);
|
||||
let minHeight = 999999;
|
||||
let minHeightPerm: [number[], number[]];
|
||||
for (let perm of perms) {
|
||||
let leftHeight = 0;
|
||||
for (let idx of perm[0]) {
|
||||
leftHeight += (cards.children[idx] as HTMLElement).offsetHeight;
|
||||
}
|
||||
if (hasMessageCard) leftHeight += (cards.children[0] as HTMLElement).offsetHeight;
|
||||
let rightHeight = 0;
|
||||
for (let idx of perm[1]) {
|
||||
rightHeight += (cards.children[idx] as HTMLElement).offsetHeight;
|
||||
}
|
||||
let height = Math.max(leftHeight, rightHeight);
|
||||
// console.log("got height", leftHeight, rightHeight, height, "for", perm);
|
||||
if (height < minHeight) {
|
||||
minHeight = height;
|
||||
minHeightPerm = perm;
|
||||
}
|
||||
}
|
||||
|
||||
const gapDiv = () => {
|
||||
const g = document.createElement("div");
|
||||
g.classList.add("my-4");
|
||||
return g;
|
||||
};
|
||||
|
||||
let addValue = hasMessageCard ? 1 : 0;
|
||||
// if (hasMessageCard) cards.appendChild(children[0]);
|
||||
if (hasMessageCard) cards.appendChild(gapDiv());
|
||||
for (let side of minHeightPerm) {
|
||||
for (let i = 0; i < side.length; i++) {
|
||||
// (cards.children[side[i]] as HTMLElement).style.order = (i+addValue).toString();
|
||||
children[side[i]].remove();
|
||||
cards.appendChild(children[side[i]]);
|
||||
cards.appendChild(gapDiv());
|
||||
}
|
||||
// addValue += side.length;
|
||||
}
|
||||
|
||||
console.log("Shortest order:", minHeightPerm);
|
||||
};
|
||||
|
||||
const login = new Login(window.modals.login as Modal, "/my/", "opaque");
|
||||
login.onLogin = () => {
|
||||
console.log("Logged in.");
|
||||
@ -699,6 +753,24 @@ const computeRealHeight = (el: HTMLElement): number => {
|
||||
return total;
|
||||
}
|
||||
|
||||
const generatePermutations = (xs: number[]): [number[], number[]][] => {
|
||||
const l = xs.length;
|
||||
let out: [number[], number[]][] = [];
|
||||
for (let i = 0; i < (l << 1); i++) {
|
||||
let incl = [];
|
||||
let excl = [];
|
||||
for (let j = 0; j < l; j++) {
|
||||
if (i & (1 << j)) {
|
||||
incl.push(xs[j]);
|
||||
} else {
|
||||
excl.push(xs[j]);
|
||||
}
|
||||
}
|
||||
out.push([incl, excl]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
login.bindLogout(document.getElementById("logout-button"));
|
||||
|
||||
login.login("", "");
|
||||
|
@ -45,13 +45,13 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
||||
respond(500, "Contact Admin", gc)
|
||||
return
|
||||
}
|
||||
app.info.Println("UserToken requested (login attempt)")
|
||||
username, password, ok := app.decodeValidateLoginHeader(gc)
|
||||
app.logIpInfo(gc, true, "UserToken requested (login attempt)")
|
||||
username, password, ok := app.decodeValidateLoginHeader(gc, true)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := app.validateJellyfinCredentials(username, password, gc)
|
||||
user, ok := app.validateJellyfinCredentials(username, password, gc, true)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -86,7 +86,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
app.info.Println("UserToken request (refresh token)")
|
||||
app.logIpInfo(gc, true, "UserToken request (refresh token)")
|
||||
claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh")
|
||||
if !ok {
|
||||
return
|
||||
|
@ -120,7 +120,7 @@ func (app *appContext) checkUsers() {
|
||||
continue
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), activity)
|
||||
app.storage.SetActivityKey(shortuuid.New(), activity, nil, false)
|
||||
|
||||
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
|
122
views.go
122
views.go
@ -296,6 +296,10 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
data["telegramEnabled"] = false
|
||||
data["discordEnabled"] = false
|
||||
data["matrixEnabled"] = false
|
||||
data["captcha"] = app.config.Section("captcha").Key("enabled").MustBool(false)
|
||||
data["reCAPTCHA"] = app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||
data["reCAPTCHASiteKey"] = app.config.Section("captcha").Key("recaptcha_site_key").MustString("")
|
||||
data["pwrPIN"] = pin
|
||||
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
||||
return
|
||||
}
|
||||
@ -361,7 +365,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
SourceType: ActivityUser,
|
||||
Source: jfUser.ID,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}, gc, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,20 +397,28 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
// @Router /captcha/img/{code}/{captchaID} [get]
|
||||
func (app *appContext) GetCaptcha(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
isPWR := gc.Query("pwr") == "true"
|
||||
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 inv Invite
|
||||
var capt Captcha
|
||||
ok = true
|
||||
if inv.Captchas != nil {
|
||||
capt, ok = inv.Captchas[captchaID]
|
||||
ok := true
|
||||
if !isPWR {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
if inv.Captchas != nil {
|
||||
capt, ok = inv.Captchas[captchaID]
|
||||
} else {
|
||||
ok = false
|
||||
}
|
||||
} else {
|
||||
capt, ok = app.pwrCaptchas[code]
|
||||
}
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
@ -425,7 +437,13 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
|
||||
// @tags Users
|
||||
func (app *appContext) GenCaptcha(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
inv, ok := app.storage.GetInvitesKey(code)
|
||||
isPWR := gc.Query("pwr") == "true"
|
||||
var inv Invite
|
||||
ok := true
|
||||
if !isPWR {
|
||||
inv, ok = app.storage.GetInvitesKey(code)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
@ -440,7 +458,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if inv.Captchas == nil {
|
||||
if !isPWR && inv.Captchas == nil {
|
||||
inv.Captchas = map[string]Captcha{}
|
||||
}
|
||||
captchaID := genAuthToken()
|
||||
@ -450,26 +468,43 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
inv.Captchas[captchaID] = Captcha{
|
||||
Answer: capt.Text,
|
||||
Image: buf.Bytes(),
|
||||
Generated: time.Now(),
|
||||
if isPWR {
|
||||
if app.pwrCaptchas == nil {
|
||||
app.pwrCaptchas = map[string]Captcha{}
|
||||
}
|
||||
app.pwrCaptchas[code] = Captcha{
|
||||
Answer: capt.Text,
|
||||
Image: buf.Bytes(),
|
||||
Generated: time.Now(),
|
||||
}
|
||||
} else {
|
||||
inv.Captchas[captchaID] = Captcha{
|
||||
Answer: capt.Text,
|
||||
Image: buf.Bytes(),
|
||||
Generated: time.Now(),
|
||||
}
|
||||
app.storage.SetInvitesKey(code, inv)
|
||||
}
|
||||
app.storage.SetInvitesKey(code, inv)
|
||||
gc.JSON(200, genCaptchaDTO{captchaID})
|
||||
return
|
||||
}
|
||||
|
||||
func (app *appContext) verifyCaptcha(code, id, text string) bool {
|
||||
func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) 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
|
||||
var c Captcha
|
||||
ok := true
|
||||
if !isPWR {
|
||||
inv, ok := app.storage.GetInvitesKey(code)
|
||||
if !ok || (!isPWR && inv.Captchas == nil) {
|
||||
app.debug.Printf("Couldn't find invite \"%s\"", code)
|
||||
return false
|
||||
}
|
||||
c, ok = inv.Captchas[id]
|
||||
} else {
|
||||
c, ok = app.pwrCaptchas[code]
|
||||
}
|
||||
c, ok := inv.Captchas[id]
|
||||
if !ok {
|
||||
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
|
||||
return false
|
||||
@ -529,21 +564,30 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool {
|
||||
// @Router /captcha/verify/{code}/{captchaID}/{text} [get]
|
||||
func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
isPWR := gc.Query("pwr") == "true"
|
||||
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 inv Invite
|
||||
var capt Captcha
|
||||
if inv.Captchas != nil {
|
||||
capt, ok = inv.Captchas[captchaID]
|
||||
var ok bool
|
||||
if !isPWR {
|
||||
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 inv.Captchas != nil {
|
||||
capt, ok = inv.Captchas[captchaID]
|
||||
} else {
|
||||
ok = false
|
||||
}
|
||||
} else {
|
||||
capt, ok = app.pwrCaptchas[code]
|
||||
}
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
@ -611,7 +655,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
app.debug.Printf("Invalid key")
|
||||
return
|
||||
}
|
||||
f, success := app.newUser(req, true)
|
||||
f, success := app.newUser(req, true, gc)
|
||||
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.
|
||||
|
Loading…
Reference in New Issue
Block a user