From 04c94ba55aa88cc29d552c2d5a5aef3649f811d7 Mon Sep 17 00:00:00 2001 From: kimboslice99 <94807745+kimboslice99@users.noreply.github.com> Date: Sat, 23 Dec 2023 13:09:49 -0500 Subject: [PATCH 1/3] Log IPs --- auth.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/auth.go b/auth.go index 18230c9..20bb18c 100644 --- a/auth.go +++ b/auth.go @@ -134,6 +134,7 @@ type getTokenDTO struct { } func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, password string, ok bool) { + ip := strings.TrimSpace(gc.Request.Header.Get("X-Real-IP")) header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) auth, _ := base64.StdEncoding.DecodeString(header[1]) creds := strings.SplitN(string(auth), ":", 2) @@ -141,7 +142,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.debug.Print("Auth denied: blank username/password ip=", ip, "\n") respond(401, "Unauthorized", gc) return } @@ -150,16 +151,17 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, pas } func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context) (user mediabrowser.User, ok bool) { + ip := strings.TrimSpace(gc.Request.Header.Get("X-Real-IP")) 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.info.Print("Auth denied: Invalid username/password (Jellyfin) ip=", ip, "\n") respond(401, "Unauthorized", gc) return } if status == 403 { - app.info.Println("Auth denied: Jellyfin account disabled") + app.info.Print("Auth denied: Jellyfin account disabled ip=", ip, "\n") respond(403, "yourAccountWasDisabled", gc) return } @@ -180,6 +182,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc // @tags Auth // @Security getTokenAuth func (app *appContext) getTokenLogin(gc *gin.Context) { + ip := strings.TrimSpace(gc.Request.Header.Get("X-Real-IP")) app.info.Println("Token requested (login attempt)") username, password, ok := app.decodeValidateLoginHeader(gc) if !ok { @@ -196,7 +199,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { } } if !app.jellyfinLogin && !match { - app.info.Println("Auth denied: Invalid username/password") + app.info.Print("Auth denied: Invalid username/password ip=", ip, "\n") respond(401, "Unauthorized", gc) return } From 269836fc991a746ebd7b16bede8f8c1e042461e1 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 23 Dec 2023 20:54:55 +0000 Subject: [PATCH 2/3] ips: add advanced settings for ip logging --- auth.go | 41 ++++++++++++++++++++++++++++++----------- config.go | 3 +++ config/config-base.json | 23 +++++++++++++++++++++++ main.go | 2 ++ user-auth.go | 4 ++-- 5 files changed, 60 insertions(+), 13 deletions(-) diff --git a/auth.go b/auth.go index 20bb18c..64635c1 100644 --- a/auth.go +++ b/auth.go @@ -18,6 +18,28 @@ const ( REFRESH_TOKEN_VALIDITY_SEC = 3600 * 24 ) +func (app *appContext) logIpInfo(gc *gin.Context, user bool, out string) { + app.info.Printf(out) + if (user && LOGIPU) || (!user && LOGIP) { + app.info.Printf(" (ip=%s)", strings.TrimSpace(gc.Request.Header.Get("X-Real-IP"))) + } + app.info.Print("\n") +} +func (app *appContext) logIpDebug(gc *gin.Context, user bool, out string) { + app.debug.Printf(out) + if (user && LOGIPU) || (!user && LOGIP) { + app.debug.Printf(" (ip=%s)", strings.TrimSpace(gc.Request.Header.Get("X-Real-IP"))) + } + app.debug.Print("\n") +} +func (app *appContext) logIpErr(gc *gin.Context, user bool, out string) { + app.err.Printf(out) + if (user && LOGIPU) || (!user && LOGIP) { + app.err.Printf(" (ip=%s)", strings.TrimSpace(gc.Request.Header.Get("X-Real-IP"))) + } + app.err.Print("\n") +} + func (app *appContext) webAuth() gin.HandlerFunc { return app.authenticate } @@ -133,8 +155,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) { - ip := strings.TrimSpace(gc.Request.Header.Get("X-Real-IP")) +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) @@ -142,7 +163,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, pas password = creds[1] ok = false if username == "" || password == "" { - app.debug.Print("Auth denied: blank username/password ip=", ip, "\n") + app.logIpDebug(gc, userpage, "Auth denied: blank username/password") respond(401, "Unauthorized", gc) return } @@ -150,18 +171,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) { - ip := strings.TrimSpace(gc.Request.Header.Get("X-Real-IP")) +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.Print("Auth denied: Invalid username/password (Jellyfin) ip=", ip, "\n") + app.logIpInfo(gc, userpage, "Auth denied: Invalid username/password (Jellyfin)") respond(401, "Unauthorized", gc) return } if status == 403 { - app.info.Print("Auth denied: Jellyfin account disabled ip=", ip, "\n") + app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled") respond(403, "yourAccountWasDisabled", gc) return } @@ -182,9 +202,8 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc // @tags Auth // @Security getTokenAuth func (app *appContext) getTokenLogin(gc *gin.Context) { - ip := strings.TrimSpace(gc.Request.Header.Get("X-Real-IP")) app.info.Println("Token requested (login attempt)") - username, password, ok := app.decodeValidateLoginHeader(gc) + username, password, ok := app.decodeValidateLoginHeader(gc, false) if !ok { return } @@ -199,12 +218,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { } } if !app.jellyfinLogin && !match { - app.info.Print("Auth denied: Invalid username/password ip=", ip, "\n") + 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 } diff --git a/config.go b/config.go index 91ea6bd..4b1d835 100644 --- a/config.go +++ b/config.go @@ -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_userpage").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() diff --git a/config/config-base.json b/config/config-base.json index 5bcb500..1f19920 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -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 in console and in activities. See notice below on legality." + }, + "log_ips_userpage": { + "name": "Log IPs accessing User Page", + "required": false, + "requires_restart": true, + "type": "bool", + "value": false, + "description": "Log IP addresses 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, diff --git a/main.go b/main.go index 52c96c8..949d91a 100644 --- a/main.go +++ b/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() diff --git a/user-auth.go b/user-auth.go index 53c9ae0..518d6c6 100644 --- a/user-auth.go +++ b/user-auth.go @@ -46,12 +46,12 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) { return } app.info.Println("UserToken requested (login attempt)") - username, password, ok := app.decodeValidateLoginHeader(gc) + 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 } From f823705e40f8e821fc71baee5f7d528f2cd4f2b7 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 23 Dec 2023 21:47:41 +0000 Subject: [PATCH 3/3] ips: log on activities, show on card --- api-activities.go | 1 + api-invites.go | 10 +++++----- api-messages.go | 15 +++++++++++---- api-userpage.go | 16 ++++++++-------- api-users.go | 14 +++++++------- api.go | 2 +- auth.go | 19 ++++++++----------- config/config-base.json | 6 +++--- models.go | 1 + storage.go | 8 +++++++- ts/modules/activity.ts | 25 ++++++++++++++++++++----- user-auth.go | 4 ++-- userdaemon.go | 2 +- views.go | 4 ++-- 14 files changed, 77 insertions(+), 50 deletions(-) diff --git a/api-activities.go b/api-activities.go index 929a6f5..17e24ad 100644 --- a/api-activities.go +++ b/api-activities.go @@ -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 diff --git a/api-invites.go b/api-invites.go index eb6e810..d17b8e3 100644 --- a/api-invites.go +++ b/api-invites.go @@ -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) } @@ -492,7 +492,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) diff --git a/api-messages.go b/api-messages.go index fcb14b9..452dc57 100644 --- a/api-messages.go +++ b/api-messages.go @@ -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) } diff --git a/api-userpage.go b/api-userpage.go index ea7adb7..ee7eb33 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -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() { diff --git a/api-users.go b/api-users.go index 138c287..b13b2f2 100644 --- a/api-users.go +++ b/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, @@ -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) diff --git a/api.go b/api.go index 1b926e0..c1fea0a 100644 --- a/api.go +++ b/api.go @@ -179,7 +179,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { SourceType: ActivityUser, Source: user.ID, Time: time.Now(), - }) + }, gc, true) prevPassword := req.PIN if isInternal { diff --git a/auth.go b/auth.go index 64635c1..877d7ab 100644 --- a/auth.go +++ b/auth.go @@ -19,25 +19,22 @@ const ( ) func (app *appContext) logIpInfo(gc *gin.Context, user bool, out string) { - app.info.Printf(out) if (user && LOGIPU) || (!user && LOGIP) { - app.info.Printf(" (ip=%s)", strings.TrimSpace(gc.Request.Header.Get("X-Real-IP"))) + out += fmt.Sprintf(" (ip=%s)", gc.ClientIP()) } - app.info.Print("\n") + app.info.Println(out) } func (app *appContext) logIpDebug(gc *gin.Context, user bool, out string) { - app.debug.Printf(out) if (user && LOGIPU) || (!user && LOGIP) { - app.debug.Printf(" (ip=%s)", strings.TrimSpace(gc.Request.Header.Get("X-Real-IP"))) + out += fmt.Sprintf(" (ip=%s)", gc.ClientIP()) } - app.debug.Print("\n") + app.debug.Println(out) } func (app *appContext) logIpErr(gc *gin.Context, user bool, out string) { - app.err.Printf(out) if (user && LOGIPU) || (!user && LOGIP) { - app.err.Printf(" (ip=%s)", strings.TrimSpace(gc.Request.Header.Get("X-Real-IP"))) + out += fmt.Sprintf(" (ip=%s)", gc.ClientIP()) } - app.err.Print("\n") + app.err.Println(out) } func (app *appContext) webAuth() gin.HandlerFunc { @@ -202,7 +199,7 @@ 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)") + app.logIpInfo(gc, false, "Token requested (login attempt)") username, password, ok := app.decodeValidateLoginHeader(gc, false) if !ok { return @@ -307,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 diff --git a/config/config-base.json b/config/config-base.json index 1f19920..e09ff14 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -303,15 +303,15 @@ "requires_restart": true, "type": "bool", "value": false, - "description": "Log IP addresses in console and in activities. See notice below on legality." + "description": "Log IP addresses of admins and admin page requests in console and in activities. See notice below on legality." }, - "log_ips_userpage": { + "log_ips_users": { "name": "Log IPs accessing User Page", "required": false, "requires_restart": true, "type": "bool", "value": false, - "description": "Log IP addresses in console and in activities. See notice below on legality." + "description": "Log IP addresses of users in console and in activities. See notice below on legality." }, "ip_note": { "name": "Logging IPs:", diff --git a/models.go b/models.go index ba46f7b..b7f5f7b 100644 --- a/models.go +++ b/models.go @@ -444,6 +444,7 @@ type ActivityDTO struct { InviteCode string `json:"invite_code"` Value string `json:"value"` Time int64 `json:"time"` + IP string `json:"ip"` } type GetActivitiesDTO struct { diff --git a/storage.go b/storage.go index 19067ba..1bc6b01 100644 --- a/storage.go +++ b/storage.go @@ -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) diff --git a/ts/modules/activity.ts b/ts/modules/activity.ts index 6bf2f2f..d5bf3d3 100644 --- a/ts/modules/activity.ts +++ b/ts/modules/activity.ts @@ -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 = `IP${v}`; + } 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 { -
-
- -
-
+
+
+
+ +
+
@@ -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) => { diff --git a/user-auth.go b/user-auth.go index 518d6c6..d347f08 100644 --- a/user-auth.go +++ b/user-auth.go @@ -45,7 +45,7 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) { respond(500, "Contact Admin", gc) return } - app.info.Println("UserToken requested (login attempt)") + app.logIpInfo(gc, true, "UserToken requested (login attempt)") username, password, ok := app.decodeValidateLoginHeader(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 diff --git a/userdaemon.go b/userdaemon.go index af3b860..abe8212 100644 --- a/userdaemon.go +++ b/userdaemon.go @@ -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() diff --git a/views.go b/views.go index 51ccca8..ff1131d 100644 --- a/views.go +++ b/views.go @@ -361,7 +361,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) { SourceType: ActivityUser, Source: jfUser.ID, Time: time.Now(), - }) + }, gc, true) } } @@ -611,7 +611,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.