Compare commits

...

5 Commits

Author SHA1 Message Date
8d38a86f86 feat: get client ip util 2025-06-11 20:35:38 +02:00
e0c095c24d feat: create/update session when refreshing 2025-06-11 20:34:56 +02:00
4c318b15cd feat: refactor login 2025-06-11 20:33:49 +02:00
5ea6bc4251 feat: build device info util 2025-06-11 20:33:34 +02:00
1cbe908489 fix: use printf 2025-06-11 18:49:39 +02:00
4 changed files with 112 additions and 33 deletions

View File

@ -2,15 +2,12 @@ package auth
import (
"encoding/json"
"fmt"
"log"
"net/http"
"gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/types"
"gitea.local/admin/hspguard/internal/util"
"gitea.local/admin/hspguard/internal/web"
"github.com/avct/uasurfer"
)
type LoginParams struct {
@ -36,12 +33,14 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
user, err := h.repo.FindUserEmail(r.Context(), params.Email)
if err != nil {
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
log.Printf("DEBUG: No user found with '%s' email: %v\n", params.Email, err)
web.Error(w, "email or/and password are incorrect", http.StatusBadRequest)
return
}
if !util.VerifyPassword(params.Password, user.PasswordHash) {
web.Error(w, "username or/and password are incorrect", http.StatusBadRequest)
log.Printf("DEBUG: Incorrect password '%s' for '%s' email: %v\n", params.Password, params.Email, err)
web.Error(w, "email or/and password are incorrect", http.StatusBadRequest)
return
}
@ -53,29 +52,8 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
userAgent := r.UserAgent()
var deviceInfo types.DeviceInfo
parsed := uasurfer.Parse(userAgent)
deviceInfo.Browser = parsed.Browser.Name.StringTrimPrefix()
deviceInfo.BrowserVersion = fmt.Sprintf("%d.%d.%d", parsed.Browser.Version.Major, parsed.Browser.Version.Minor, parsed.Browser.Version.Patch)
deviceInfo.DeviceName = fmt.Sprintf("%s %s", parsed.OS.Platform.StringTrimPrefix(), parsed.OS.Name.StringTrimPrefix())
deviceInfo.DeviceType = parsed.DeviceType.StringTrimPrefix()
deviceInfo.OS = parsed.OS.Platform.StringTrimPrefix()
deviceInfo.OSVersion = fmt.Sprintf("%d.%d.%d", parsed.OS.Version.Major, parsed.OS.Version.Minor, parsed.OS.Version.Patch)
deviceInfo.UserAgent = userAgent
if location, err := util.GetLocation(r.RemoteAddr); err != nil {
log.Println("WARN: Failed to get location from ip (%s): %v\n", r.RemoteAddr, err)
} else {
deviceInfo.Location = fmt.Sprintf("%s, %s, %s", location.Country, location.Region, location.City)
}
serialized, err := json.Marshal(deviceInfo)
if err != nil {
log.Println("ERR: Failed to serialize device info: %v\n", err)
serialized = []byte{'{', '}'}
}
ipAddr := util.GetClientIP(r)
deviceInfo := util.BuildDeviceInfo(userAgent, ipAddr)
// Create User Session
session, err := h.repo.CreateUserSession(r.Context(), repository.CreateUserSessionParams{
@ -83,17 +61,17 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
SessionType: "user",
ExpiresAt: &refresh.ExpiresAt,
LastActive: nil,
IpAddress: &r.RemoteAddr,
IpAddress: &ipAddr,
UserAgent: &userAgent,
AccessTokenID: &access.ID,
RefreshTokenID: &refresh.ID,
DeviceInfo: serialized,
DeviceInfo: deviceInfo,
})
if err != nil {
log.Println("ERR: Failedd to create user session after logging in: %v\n", err)
log.Printf("ERR: Failed to create user session after logging in: %v\n", err)
}
log.Println("INFO: User session created for '%s': %#v\n", user.Email, session)
log.Printf("INFO: User session created for '%s' with '%s' id\n", user.Email, session.ID.String())
if err := h.repo.UpdateLastLogin(r.Context(), user.ID); err != nil {
web.Error(w, "failed to update user's last login", http.StatusInternalServerError)

View File

@ -3,10 +3,12 @@ package auth
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/types"
"gitea.local/admin/hspguard/internal/util"
"gitea.local/admin/hspguard/internal/web"
@ -64,6 +66,44 @@ func (h *AuthHandler) refreshToken(w http.ResponseWriter, r *http.Request) {
return
}
jti, err := uuid.Parse(userClaims.ID)
if session, err := h.repo.GetUserSessionByRefreshJTI(r.Context(), &jti); err != nil {
log.Printf("WARN: No existing user session found for user with '%s' email (jti: '%s'): %v\n", user.Email, userClaims.ID, err)
userAgent := r.UserAgent()
ipAddr := util.GetClientIP(r)
deviceInfo := util.BuildDeviceInfo(userAgent, ipAddr)
// Create User Session
session, err := h.repo.CreateUserSession(r.Context(), repository.CreateUserSessionParams{
UserID: user.ID,
SessionType: "user",
ExpiresAt: &refresh.ExpiresAt,
LastActive: nil,
IpAddress: &ipAddr,
UserAgent: &userAgent,
AccessTokenID: &access.ID,
RefreshTokenID: &refresh.ID,
DeviceInfo: deviceInfo,
})
if err != nil {
log.Printf("ERR: Failed to create user session after logging in: %v\n", err)
}
log.Printf("INFO: User session created for '%s' with '%s' id\n", user.Email, session.ID.String())
} else {
err := h.repo.UpdateSessionTokens(r.Context(), repository.UpdateSessionTokensParams{
ID: session.ID,
AccessTokenID: &access.ID,
RefreshTokenID: &refresh.ID,
ExpiresAt: &refresh.ExpiresAt,
})
if err != nil {
log.Printf("ERR: Failed to update user session with '%s' id: %v\n", session.ID.String(), err)
}
}
type Response struct {
AccessToken string `json:"access"`
RefreshToken string `json:"refresh"`

View File

@ -2,7 +2,10 @@ package util
import (
"encoding/json"
"log"
"net"
"net/http"
"strings"
)
type LocationResult struct {
@ -14,7 +17,7 @@ type LocationResult struct {
func GetLocation(ip string) (LocationResult, error) {
var loc LocationResult
// Example using ipinfo.io free API
resp, err := http.Get("https://ipinfo.io/" + ip + "/json")
resp, err := http.Get("http://ip-api.com/json/" + ip + "?fields=25")
if err != nil {
return loc, err
}
@ -22,3 +25,22 @@ func GetLocation(ip string) (LocationResult, error) {
json.NewDecoder(resp.Body).Decode(&loc)
return loc, nil
}
func GetClientIP(r *http.Request) string {
// This header will be set by ngrok to the original client IP
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
log.Printf("DEBUG: Getting IP from X-Forwarded-For: %s\n", xff)
// X-Forwarded-For: client, proxy1, proxy2, ...
ips := strings.Split(xff, ",")
if len(ips) > 0 {
return strings.TrimSpace(ips[0])
}
}
// Fallback to RemoteAddr (not the real client IP, but just in case)
host, _, err := net.SplitHostPort(r.RemoteAddr)
log.Printf("DEBUG: Falling to request remote addr: %s (%s)\n", host, r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}

39
internal/util/session.go Normal file
View File

@ -0,0 +1,39 @@
package util
import (
"encoding/json"
"fmt"
"log"
"gitea.local/admin/hspguard/internal/types"
"github.com/avct/uasurfer"
)
func BuildDeviceInfo(userAgent string, remoteAddr string) []byte {
var deviceInfo types.DeviceInfo
parsed := uasurfer.Parse(userAgent)
deviceInfo.Browser = parsed.Browser.Name.StringTrimPrefix()
deviceInfo.BrowserVersion = fmt.Sprintf("%d.%d.%d", parsed.Browser.Version.Major, parsed.Browser.Version.Minor, parsed.Browser.Version.Patch)
deviceInfo.DeviceName = fmt.Sprintf("%s %s", parsed.OS.Platform.StringTrimPrefix(), parsed.OS.Name.StringTrimPrefix())
deviceInfo.DeviceType = parsed.DeviceType.StringTrimPrefix()
deviceInfo.OS = parsed.OS.Platform.StringTrimPrefix()
deviceInfo.OSVersion = fmt.Sprintf("%d.%d.%d", parsed.OS.Version.Major, parsed.OS.Version.Minor, parsed.OS.Version.Patch)
deviceInfo.UserAgent = userAgent
if location, err := GetLocation(remoteAddr); err != nil {
log.Printf("WARN: Failed to get location from ip (%s): %v\n", remoteAddr, err)
} else {
log.Printf("DEBUG: Response from IP fetcher: %#v\n", location)
deviceInfo.Location = fmt.Sprintf("%s, %s, %s", location.Country, location.Region, location.City)
}
serialized, err := json.Marshal(deviceInfo)
if err != nil {
log.Printf("ERR: Failed to serialize device info: %v\n", err)
serialized = []byte{'{', '}'}
}
return serialized
}