1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

Merge (optional) IP logging

This commit is contained in:
Harvey Tindall 2023-12-24 01:06:07 +00:00 committed by GitHub
commit 0840931fed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 125 additions and 48 deletions

View File

@ -138,6 +138,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
InviteCode: act.InviteCode, InviteCode: act.InviteCode,
Value: act.Value, Value: act.Value,
Time: act.Time.Unix(), Time: act.Time.Unix(),
IP: act.IP,
} }
if act.Type == ActivityDeletion || act.Type == ActivityCreation { if act.Type == ActivityDeletion || act.Type == ActivityCreation {
resp.Activities[i].Username = act.Value resp.Activities[i].Username = act.Value

View File

@ -102,7 +102,7 @@ func (app *appContext) checkInvites() {
InviteCode: data.Code, InviteCode: data.Code,
Value: data.Label, Value: data.Label,
Time: time.Now(), Time: time.Now(),
}) }, nil, false)
} }
} }
@ -161,7 +161,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
InviteCode: code, InviteCode: code,
Value: inv.Label, Value: inv.Label,
Time: time.Now(), Time: time.Now(),
}) }, nil, false)
} else if used { } else if used {
del := false del := false
newInv := inv newInv := inv
@ -174,7 +174,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
InviteCode: code, InviteCode: code,
Value: inv.Label, Value: inv.Label,
Time: time.Now(), Time: time.Now(),
}) }, nil, false)
} else if newInv.RemainingUses != 0 { } else if newInv.RemainingUses != 0 {
// 0 means infinite i guess? // 0 means infinite i guess?
newInv.RemainingUses-- newInv.RemainingUses--
@ -285,7 +285,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
InviteCode: invite.Code, InviteCode: invite.Code,
Value: invite.Label, Value: invite.Label,
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -492,7 +492,7 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
InviteCode: req.Code, InviteCode: req.Code,
Value: inv.Label, Value: inv.Label,
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
app.info.Printf("%s: Invite deleted", req.Code) app.info.Printf("%s: Invite deleted", req.Code)
respondBool(200, true, gc) respondBool(200, true, gc)

View File

@ -573,6 +573,7 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password." // @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
// @Router /matrix/login [post] // @Router /matrix/login [post]
// @Security Bearer
// @tags Other // @tags Other
func (app *appContext) MatrixLogin(gc *gin.Context) { func (app *appContext) MatrixLogin(gc *gin.Context) {
var req MatrixLoginDTO var req MatrixLoginDTO
@ -608,6 +609,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID." // @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
// @Router /users/matrix [post] // @Router /users/matrix [post]
// @Security Bearer
// @tags Other // @tags Other
func (app *appContext) MatrixConnect(gc *gin.Context) { func (app *appContext) MatrixConnect(gc *gin.Context) {
var req MatrixConnectUserDTO var req MatrixConnectUserDTO
@ -639,6 +641,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param username path string true "username to search." // @Param username path string true "username to search."
// @Router /users/discord/{username} [get] // @Router /users/discord/{username} [get]
// @Security Bearer
// @tags Other // @tags Other
func (app *appContext) DiscordGetUsers(gc *gin.Context) { func (app *appContext) DiscordGetUsers(gc *gin.Context) {
name := gc.Param("username") name := gc.Param("username")
@ -665,6 +668,7 @@ func (app *appContext) DiscordGetUsers(gc *gin.Context) {
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID." // @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
// @Router /users/discord [post] // @Router /users/discord [post]
// @Security Bearer
// @tags Other // @tags Other
func (app *appContext) DiscordConnect(gc *gin.Context) { func (app *appContext) DiscordConnect(gc *gin.Context) {
var req DiscordConnectUserDTO var req DiscordConnectUserDTO
@ -688,7 +692,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "discord", Value: "discord",
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
linkExistingOmbiDiscordTelegram(app) linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc) respondBool(200, true, gc)
@ -699,6 +703,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID." // @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
// @Router /users/discord [delete] // @Router /users/discord [delete]
// @Security Bearer
// @Tags Users // @Tags Users
func (app *appContext) UnlinkDiscord(gc *gin.Context) { func (app *appContext) UnlinkDiscord(gc *gin.Context) {
var req forUserDTO var req forUserDTO
@ -717,7 +722,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "discord", Value: "discord",
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -727,6 +732,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID." // @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
// @Router /users/telegram [delete] // @Router /users/telegram [delete]
// @Security Bearer
// @Tags Users // @Tags Users
func (app *appContext) UnlinkTelegram(gc *gin.Context) { func (app *appContext) UnlinkTelegram(gc *gin.Context) {
var req forUserDTO var req forUserDTO
@ -745,7 +751,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "telegram", Value: "telegram",
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -755,6 +761,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID." // @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
// @Router /users/matrix [delete] // @Router /users/matrix [delete]
// @Security Bearer
// @Tags Users // @Tags Users
func (app *appContext) UnlinkMatrix(gc *gin.Context) { func (app *appContext) UnlinkMatrix(gc *gin.Context) {
var req forUserDTO var req forUserDTO
@ -773,7 +780,7 @@ func (app *appContext) UnlinkMatrix(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "matrix", Value: "matrix",
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
respondBool(200, true, gc) respondBool(200, true, gc)
} }

View File

@ -216,7 +216,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "email", Value: "email",
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
ombiUser, code, err := app.getOmbiUser(id) ombiUser, code, err := app.getOmbiUser(id)
@ -378,7 +378,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "discord", Value: "discord",
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -426,7 +426,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "telegram", Value: "telegram",
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -507,7 +507,7 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "matrix", Value: "matrix",
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
delete(app.matrix.tokens, pin) delete(app.matrix.tokens, pin)
respondBool(200, true, gc) respondBool(200, true, gc)
@ -529,7 +529,7 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "discord", Value: "discord",
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -550,7 +550,7 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "telegram", Value: "telegram",
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -571,7 +571,7 @@ func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "matrix", Value: "matrix",
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -701,7 +701,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
SourceType: ActivityUser, SourceType: ActivityUser,
Source: user.ID, Source: user.ID,
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
func() { func() {

View File

@ -55,7 +55,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: user.Name, Value: user.Name,
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
profile := app.storage.GetDefaultProfile() profile := app.storage.GetDefaultProfile()
if req.Profile != "" && req.Profile != "none" { if req.Profile != "" && req.Profile != "none" {
@ -114,7 +114,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
type errorFunc func(gc *gin.Context) type errorFunc func(gc *gin.Context)
// Used on the form & when a users email has been confirmed. // 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) existingUser, _, _ := app.jf.UserByName(req.Username, false)
if existingUser.Name != "" { if existingUser.Name != "" {
f = func(gc *gin.Context) { f = func(gc *gin.Context) {
@ -331,7 +331,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
InviteCode: invite.Code, InviteCode: invite.Code,
Value: user.Name, Value: user.Name,
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
emailStore := EmailAddress{ emailStore := EmailAddress{
Addr: req.Email, Addr: req.Email,
@ -539,7 +539,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
return return
} }
} }
f, success := app.newUser(req, false) f, success := app.newUser(req, false, gc)
if !success { if !success {
f(gc) f(gc)
return return
@ -609,7 +609,7 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
SourceType: ActivityAdmin, SourceType: ActivityAdmin,
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
if sendMail && req.Notify { if sendMail && req.Notify {
if err := app.sendByID(msg, userID); err != nil { if err := app.sendByID(msg, userID); err != nil {
@ -687,7 +687,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: username, Value: username,
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
if sendMail && req.Notify { if sendMail && req.Notify {
if err := app.sendByID(msg, userID); err != nil { if err := app.sendByID(msg, userID); err != nil {
@ -1208,7 +1208,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
Source: gc.GetString("jfId"), Source: gc.GetString("jfId"),
Value: "email", Value: "email",
Time: time.Now(), Time: time.Now(),
}) }, gc, false)
if ombiEnabled { if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id) ombiUser, code, err := app.getOmbiUser(id)

2
api.go
View File

@ -187,7 +187,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
SourceType: ActivityUser, SourceType: ActivityUser,
Source: user.ID, Source: user.ID,
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
prevPassword := req.PIN prevPassword := req.PIN
if isInternal { if isInternal {

39
auth.go
View File

@ -18,6 +18,25 @@ const (
REFRESH_TOKEN_VALIDITY_SEC = 3600 * 24 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 { func (app *appContext) webAuth() gin.HandlerFunc {
return app.authenticate return app.authenticate
} }
@ -133,7 +152,7 @@ type getTokenDTO struct {
Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else. 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) header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
auth, _ := base64.StdEncoding.DecodeString(header[1]) auth, _ := base64.StdEncoding.DecodeString(header[1])
creds := strings.SplitN(string(auth), ":", 2) creds := strings.SplitN(string(auth), ":", 2)
@ -141,7 +160,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, pas
password = creds[1] password = creds[1]
ok = false ok = false
if username == "" || password == "" { if username == "" || password == "" {
app.debug.Println("Auth denied: blank username/password") app.logIpDebug(gc, userpage, "Auth denied: blank username/password")
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
@ -149,17 +168,17 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, pas
return 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 ok = false
user, status, err := app.authJf.Authenticate(username, password) user, status, err := app.authJf.Authenticate(username, password)
if status != 200 || err != nil { if status != 200 || err != nil {
if status == 401 || status == 400 { 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) respond(401, "Unauthorized", gc)
return return
} }
if status == 403 { if status == 403 {
app.info.Println("Auth denied: Jellyfin account disabled") app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled")
respond(403, "yourAccountWasDisabled", gc) respond(403, "yourAccountWasDisabled", gc)
return return
} }
@ -180,8 +199,8 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
// @tags Auth // @tags Auth
// @Security getTokenAuth // @Security getTokenAuth
func (app *appContext) getTokenLogin(gc *gin.Context) { func (app *appContext) getTokenLogin(gc *gin.Context) {
app.info.Println("Token requested (login attempt)") app.logIpInfo(gc, false, "Token requested (login attempt)")
username, password, ok := app.decodeValidateLoginHeader(gc) username, password, ok := app.decodeValidateLoginHeader(gc, false)
if !ok { if !ok {
return return
} }
@ -196,12 +215,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
} }
} }
if !app.jellyfinLogin && !match { 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) respond(401, "Unauthorized", gc)
return return
} }
if !match { if !match {
user, ok := app.validateJellyfinCredentials(username, password, gc) user, ok := app.validateJellyfinCredentials(username, password, gc, false)
if !ok { if !ok {
return return
} }
@ -285,7 +304,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
// @Router /token/refresh [get] // @Router /token/refresh [get]
// @tags Auth // @tags Auth
func (app *appContext) getTokenRefresh(gc *gin.Context) { 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") claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
if !ok { if !ok {
return return

View File

@ -120,6 +120,9 @@ func (app *appContext) loadConfig() error {
app.config.Section("jellyfin").Key("device").SetValue("jfa-go") 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)) 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_userpage").MustBool(false)
// These two settings are pretty much the same // These two settings are pretty much the same
url1 := app.config.Section("invite_emails").Key("url_base").String() url1 := app.config.Section("invite_emails").Key("url_base").String()
url2 := app.config.Section("password_resets").Key("url_base").String() url2 := app.config.Section("password_resets").Key("url_base").String()

View File

@ -297,6 +297,29 @@
"advanced": true "advanced": true
}, },
"settings": { "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": { "tls": {
"name": "TLS/HTTP2", "name": "TLS/HTTP2",
"required": false, "required": false,

View File

@ -46,6 +46,8 @@ var (
SWAGGER *bool SWAGGER *bool
QUIT = false QUIT = false
RUNNING = false RUNNING = false
LOGIP = false // Log admin IPs
LOGIPU = false // Log user IPs
// Used to know how many times to re-broadcast restart signal. // Used to know how many times to re-broadcast restart signal.
RESTARTLISTENERCOUNT = 0 RESTARTLISTENERCOUNT = 0
warning = color.New(color.FgYellow).SprintfFunc() warning = color.New(color.FgYellow).SprintfFunc()

View File

@ -445,6 +445,7 @@ type ActivityDTO struct {
InviteCode string `json:"invite_code"` InviteCode string `json:"invite_code"`
Value string `json:"value"` Value string `json:"value"`
Time int64 `json:"time"` Time int64 `json:"time"`
IP string `json:"ip"`
} }
type GetActivitiesDTO struct { type GetActivitiesDTO struct {

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/logger" "github.com/hrfee/jfa-go/logger"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
@ -55,6 +56,7 @@ type Activity struct {
InviteCode string // Set for ActivityCreation, create/deleteInvite 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. 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 Time time.Time
IP string
} }
type UserExpiry struct { type UserExpiry struct {
@ -563,8 +565,12 @@ func (st *Storage) GetActivityKey(k string) (Activity, bool) {
} }
// SetActivityKey stores value v in key k. // 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 v.ID = k
if gc != nil && ((LOGIPU && user) || (LOGIP && !user)) {
v.IP = gc.ClientIP()
}
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
// fmt.Printf("Failed to set custom content: %v\n", err) // fmt.Printf("Failed to set custom content: %v\n", err)

View File

@ -14,6 +14,7 @@ export interface activity {
time: number; time: number;
username: string; username: string;
source_username: string; source_username: string;
ip: string;
} }
var activityTypeMoods = { var activityTypeMoods = {
@ -43,6 +44,7 @@ export class Activity implements activity, SearchableItem {
private _referrer: HTMLElement; private _referrer: HTMLElement;
private _expiryTypeBadge: HTMLElement; private _expiryTypeBadge: HTMLElement;
private _delete: HTMLElement; private _delete: HTMLElement;
private _ip: HTMLElement;
private _act: activity; private _act: activity;
private _urlBase: string = ((): string => { private _urlBase: string = ((): string => {
let link = window.location.href; 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; } get invite_code(): string { return this._act.invite_code; }
set invite_code(v: string) { set invite_code(v: string) {
this._act.invite_code = v; 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> <span class="activity-expiry-type badge self-start md:self-end mt-1"></span>
</div> </div>
</div> </div>
<div class="flex flex-col md:flex-row justify-between"> <div class="flex flex-row justify-between items-end">
<div> <div class="flex flex-col md:flex-row gap-2">
<span class="content supra mr-2 activity-source-type"></span><span class="activity-source"></span> <div>
</div> <span class="content supra mr-2 activity-source-type"></span><span class="activity-source"></span>
<div> </div>
<span class="content activity-referrer"></span> <span class="content activity-referrer"></span>
<span class="content activity-ip"></span>
</div> </div>
<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> <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._time = this._card.querySelector(".activity-time");
this._sourceType = this._card.querySelector(".activity-source-type"); this._sourceType = this._card.querySelector(".activity-source-type");
this._source = this._card.querySelector(".activity-source"); this._source = this._card.querySelector(".activity-source");
this._ip = this._card.querySelector(".activity-ip");
this._referrer = this._card.querySelector(".activity-referrer"); this._referrer = this._card.querySelector(".activity-referrer");
this._expiryTypeBadge = this._card.querySelector(".activity-expiry-type"); this._expiryTypeBadge = this._card.querySelector(".activity-expiry-type");
this._delete = this._card.querySelector(".activity-delete"); this._delete = this._card.querySelector(".activity-delete");
@ -324,6 +338,7 @@ export class Activity implements activity, SearchableItem {
this.source = act.source; this.source = act.source;
this.value = act.value; this.value = act.value;
this.type = act.type; this.type = act.type;
this.ip = act.ip;
} }
delete = () => _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => { delete = () => _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => {

View File

@ -45,13 +45,13 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
respond(500, "Contact Admin", gc) respond(500, "Contact Admin", gc)
return return
} }
app.info.Println("UserToken requested (login attempt)") app.logIpInfo(gc, true, "UserToken requested (login attempt)")
username, password, ok := app.decodeValidateLoginHeader(gc) username, password, ok := app.decodeValidateLoginHeader(gc, true)
if !ok { if !ok {
return return
} }
user, ok := app.validateJellyfinCredentials(username, password, gc) user, ok := app.validateJellyfinCredentials(username, password, gc, true)
if !ok { if !ok {
return return
} }
@ -86,7 +86,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
return return
} }
app.info.Println("UserToken request (refresh token)") app.logIpInfo(gc, true, "UserToken request (refresh token)")
claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh") claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh")
if !ok { if !ok {
return return

View File

@ -120,7 +120,7 @@ func (app *appContext) checkUsers() {
continue continue
} }
app.storage.SetActivityKey(shortuuid.New(), activity) app.storage.SetActivityKey(shortuuid.New(), activity, nil, false)
app.storage.DeleteUserExpiryKey(expiry.JellyfinID) app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()

View File

@ -365,7 +365,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
SourceType: ActivityUser, SourceType: ActivityUser,
Source: jfUser.ID, Source: jfUser.ID,
Time: time.Now(), Time: time.Now(),
}) }, gc, true)
} }
} }
@ -655,7 +655,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
app.debug.Printf("Invalid key") app.debug.Printf("Invalid key")
return return
} }
f, success := app.newUser(req, true) f, success := app.newUser(req, true, gc)
if !success { if !success {
app.err.Printf("Failed to create new user") 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. // Not meant for us. Calling this will be a mess, but at least it might give us some information.