Compare commits
7 Commits
5b6142dfa6
...
53ee156e67
Author | SHA1 | Date | |
---|---|---|---|
53ee156e67 | |||
07a936acc7 | |||
f892f0da24 | |||
38955ee4e6 | |||
7fa7e87e88 | |||
f085f2e271 | |||
08add259a4 |
1
go.mod
1
go.mod
@ -11,6 +11,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/avct/uasurfer v0.0.0-20250506104815-f2613aa2d406 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -1,3 +1,5 @@
|
|||||||
|
github.com/avct/uasurfer v0.0.0-20250506104815-f2613aa2d406 h1:5/KfwL9TS8yNtUSunutqifcSC8rdX9PNdvbSsw/X/lQ=
|
||||||
|
github.com/avct/uasurfer v0.0.0-20250506104815-f2613aa2d406/go.mod h1:s+GCtuP4kZNxh1WGoqdWI1+PbluBcycrMMWuKQ9e5Nk=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@ -2,11 +2,15 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"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/util"
|
||||||
"gitea.local/admin/hspguard/internal/web"
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
|
"github.com/avct/uasurfer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginParams struct {
|
type LoginParams struct {
|
||||||
@ -47,6 +51,50 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{'{', '}'}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create User Session
|
||||||
|
session, err := h.repo.CreateUserSession(r.Context(), repository.CreateUserSessionParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
SessionType: "user",
|
||||||
|
ExpiresAt: &refresh.ExpiresAt,
|
||||||
|
LastActive: nil,
|
||||||
|
IpAddress: &r.RemoteAddr,
|
||||||
|
UserAgent: &userAgent,
|
||||||
|
AccessTokenID: &access.ID,
|
||||||
|
RefreshTokenID: &refresh.ID,
|
||||||
|
DeviceInfo: serialized,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failedd to create user session after logging in: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("INFO: User session created for '%s': %#v\n", user.Email, session)
|
||||||
|
|
||||||
if err := h.repo.UpdateLastLogin(r.Context(), user.ID); err != nil {
|
if err := h.repo.UpdateLastLogin(r.Context(), user.ID); err != nil {
|
||||||
web.Error(w, "failed to update user's last login", http.StatusInternalServerError)
|
web.Error(w, "failed to update user's last login", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -68,8 +116,8 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if err := encoder.Encode(Response{
|
if err := encoder.Encode(Response{
|
||||||
AccessToken: access,
|
AccessToken: access.Token,
|
||||||
RefreshToken: refresh,
|
RefreshToken: refresh.Token,
|
||||||
FullName: user.FullName,
|
FullName: user.FullName,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Id: user.ID.String(),
|
Id: user.ID.String(),
|
||||||
|
@ -74,8 +74,8 @@ func (h *AuthHandler) refreshToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if err := encoder.Encode(Response{
|
if err := encoder.Encode(Response{
|
||||||
AccessToken: access,
|
AccessToken: access.Token,
|
||||||
RefreshToken: refresh,
|
RefreshToken: refresh.Token,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"gitea.local/admin/hspguard/internal/util"
|
"gitea.local/admin/hspguard/internal/util"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthHandler struct {
|
type AuthHandler struct {
|
||||||
@ -19,7 +20,24 @@ type AuthHandler struct {
|
|||||||
cfg *config.AppConfig
|
cfg *config.AppConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) signTokens(user *repository.User) (string, string, error) {
|
type SignedToken struct {
|
||||||
|
Token string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
ID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignedToken(token string, expiresAt time.Time, jti uuid.UUID) *SignedToken {
|
||||||
|
return &SignedToken{
|
||||||
|
Token: token,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
ID: jti,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) signTokens(user *repository.User) (*SignedToken, *SignedToken, error) {
|
||||||
|
accessExpiresAt := time.Now().Add(15 * time.Minute)
|
||||||
|
accessJTI := uuid.New()
|
||||||
|
|
||||||
accessClaims := types.UserClaims{
|
accessClaims := types.UserClaims{
|
||||||
UserEmail: user.Email,
|
UserEmail: user.Email,
|
||||||
IsAdmin: user.IsAdmin,
|
IsAdmin: user.IsAdmin,
|
||||||
@ -27,15 +45,19 @@ func (h *AuthHandler) signTokens(user *repository.User) (string, string, error)
|
|||||||
Issuer: h.cfg.Uri,
|
Issuer: h.cfg.Uri,
|
||||||
Subject: user.ID.String(),
|
Subject: user.ID.String(),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
ExpiresAt: jwt.NewNumericDate(accessExpiresAt),
|
||||||
|
ID: accessJTI.String(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, err := util.SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey)
|
accessToken, err := util.SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshExpiresAt := time.Now().Add(30 * 24 * time.Hour)
|
||||||
|
refreshJTI := uuid.New()
|
||||||
|
|
||||||
refreshClaims := types.UserClaims{
|
refreshClaims := types.UserClaims{
|
||||||
UserEmail: user.Email,
|
UserEmail: user.Email,
|
||||||
IsAdmin: user.IsAdmin,
|
IsAdmin: user.IsAdmin,
|
||||||
@ -43,16 +65,17 @@ func (h *AuthHandler) signTokens(user *repository.User) (string, string, error)
|
|||||||
Issuer: h.cfg.Uri,
|
Issuer: h.cfg.Uri,
|
||||||
Subject: user.ID.String(),
|
Subject: user.ID.String(),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * 24 * time.Hour)),
|
ExpiresAt: jwt.NewNumericDate(refreshExpiresAt),
|
||||||
|
ID: refreshJTI.String(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshToken, err := util.SignJwtToken(refreshClaims, h.cfg.Jwt.PrivateKey)
|
refreshToken, err := util.SignJwtToken(refreshClaims, h.cfg.Jwt.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return accessToken, refreshToken, nil
|
return NewSignedToken(accessToken, accessExpiresAt, accessJTI), NewSignedToken(refreshToken, refreshExpiresAt, refreshJTI), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthHandler(repo *repository.Queries, cache *cache.Client, cfg *config.AppConfig) *AuthHandler {
|
func NewAuthHandler(repo *repository.Queries, cache *cache.Client, cfg *config.AppConfig) *AuthHandler {
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApiService struct {
|
type ApiService struct {
|
||||||
@ -27,21 +26,21 @@ type ApiService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServiceSession struct {
|
type ServiceSession struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ServiceID uuid.UUID `json:"service_id"`
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
UserID *uuid.UUID `json:"user_id"`
|
UserID *uuid.UUID `json:"user_id"`
|
||||||
IssuedAt time.Time `json:"issued_at"`
|
IssuedAt time.Time `json:"issued_at"`
|
||||||
ExpiresAt *time.Time `json:"expires_at"`
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
LastActive *time.Time `json:"last_active"`
|
LastActive *time.Time `json:"last_active"`
|
||||||
IpAddress pgtype.Text `json:"ip_address"`
|
IpAddress *string `json:"ip_address"`
|
||||||
UserAgent *string `json:"user_agent"`
|
UserAgent *string `json:"user_agent"`
|
||||||
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
||||||
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
RevokedAt *time.Time `json:"revoked_at"`
|
RevokedAt *time.Time `json:"revoked_at"`
|
||||||
Scope *string `json:"scope"`
|
Scope *string `json:"scope"`
|
||||||
Claims []byte `json:"claims"`
|
Claims []byte `json:"claims"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@ -62,17 +61,17 @@ type User struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserSession struct {
|
type UserSession struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
UserID uuid.UUID `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
SessionType string `json:"session_type"`
|
SessionType string `json:"session_type"`
|
||||||
IssuedAt time.Time `json:"issued_at"`
|
IssuedAt time.Time `json:"issued_at"`
|
||||||
ExpiresAt *time.Time `json:"expires_at"`
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
LastActive *time.Time `json:"last_active"`
|
LastActive *time.Time `json:"last_active"`
|
||||||
IpAddress pgtype.Text `json:"ip_address"`
|
IpAddress *string `json:"ip_address"`
|
||||||
UserAgent *string `json:"user_agent"`
|
UserAgent *string `json:"user_agent"`
|
||||||
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
||||||
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
||||||
DeviceInfo []byte `json:"device_info"`
|
DeviceInfo []byte `json:"device_info"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
RevokedAt *time.Time `json:"revoked_at"`
|
RevokedAt *time.Time `json:"revoked_at"`
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const createServiceSession = `-- name: CreateServiceSession :one
|
const createServiceSession = `-- name: CreateServiceSession :one
|
||||||
@ -27,15 +26,15 @@ RETURNING id, service_id, client_id, user_id, issued_at, expires_at, last_active
|
|||||||
`
|
`
|
||||||
|
|
||||||
type CreateServiceSessionParams struct {
|
type CreateServiceSessionParams struct {
|
||||||
ServiceID uuid.UUID `json:"service_id"`
|
ServiceID uuid.UUID `json:"service_id"`
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
UserID *uuid.UUID `json:"user_id"`
|
UserID *uuid.UUID `json:"user_id"`
|
||||||
ExpiresAt *time.Time `json:"expires_at"`
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
LastActive *time.Time `json:"last_active"`
|
LastActive *time.Time `json:"last_active"`
|
||||||
IpAddress pgtype.Text `json:"ip_address"`
|
IpAddress *string `json:"ip_address"`
|
||||||
UserAgent *string `json:"user_agent"`
|
UserAgent *string `json:"user_agent"`
|
||||||
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
||||||
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateServiceSession(ctx context.Context, arg CreateServiceSessionParams) (ServiceSession, error) {
|
func (q *Queries) CreateServiceSession(ctx context.Context, arg CreateServiceSessionParams) (ServiceSession, error) {
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const createUserSession = `-- name: CreateUserSession :one
|
const createUserSession = `-- name: CreateUserSession :one
|
||||||
@ -27,15 +26,15 @@ RETURNING id, user_id, session_type, issued_at, expires_at, last_active, ip_addr
|
|||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserSessionParams struct {
|
type CreateUserSessionParams struct {
|
||||||
UserID uuid.UUID `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
SessionType string `json:"session_type"`
|
SessionType string `json:"session_type"`
|
||||||
ExpiresAt *time.Time `json:"expires_at"`
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
LastActive *time.Time `json:"last_active"`
|
LastActive *time.Time `json:"last_active"`
|
||||||
IpAddress pgtype.Text `json:"ip_address"`
|
IpAddress *string `json:"ip_address"`
|
||||||
UserAgent *string `json:"user_agent"`
|
UserAgent *string `json:"user_agent"`
|
||||||
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
||||||
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
||||||
DeviceInfo []byte `json:"device_info"`
|
DeviceInfo []byte `json:"device_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateUserSession(ctx context.Context, arg CreateUserSessionParams) (UserSession, error) {
|
func (q *Queries) CreateUserSession(ctx context.Context, arg CreateUserSessionParams) (UserSession, error) {
|
||||||
@ -233,3 +232,27 @@ func (q *Queries) UpdateSessionLastActive(ctx context.Context, id uuid.UUID) err
|
|||||||
_, err := q.db.Exec(ctx, updateSessionLastActive, id)
|
_, err := q.db.Exec(ctx, updateSessionLastActive, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateSessionTokens = `-- name: UpdateSessionTokens :exec
|
||||||
|
UPDATE user_sessions
|
||||||
|
SET access_token_id = $2, refresh_token_id = $3, expires_at = $4
|
||||||
|
WHERE id = $1
|
||||||
|
AND is_active = TRUE
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateSessionTokensParams struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
AccessTokenID *uuid.UUID `json:"access_token_id"`
|
||||||
|
RefreshTokenID *uuid.UUID `json:"refresh_token_id"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateSessionTokens(ctx context.Context, arg UpdateSessionTokensParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateSessionTokens,
|
||||||
|
arg.ID,
|
||||||
|
arg.AccessTokenID,
|
||||||
|
arg.RefreshTokenID,
|
||||||
|
arg.ExpiresAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
12
internal/types/device.go
Normal file
12
internal/types/device.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type DeviceInfo struct {
|
||||||
|
DeviceType string `json:"device_type"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
OSVersion string `json:"os_version"`
|
||||||
|
Browser string `json:"browser"`
|
||||||
|
BrowserVersion string `json:"browser_version"`
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
}
|
24
internal/util/location.go
Normal file
24
internal/util/location.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocationResult struct {
|
||||||
|
Country string `json:"country"`
|
||||||
|
Region string `json:"regionName"`
|
||||||
|
City string `json:"city"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLocation(ip string) (LocationResult, error) {
|
||||||
|
var loc LocationResult
|
||||||
|
// Example using ipinfo.io free API
|
||||||
|
resp, err := http.Get("https://ipinfo.io/" + ip + "/json")
|
||||||
|
if err != nil {
|
||||||
|
return loc, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
json.NewDecoder(resp.Body).Decode(&loc)
|
||||||
|
return loc, nil
|
||||||
|
}
|
@ -39,6 +39,12 @@ SET last_active = NOW()
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND is_active = TRUE;
|
AND is_active = TRUE;
|
||||||
|
|
||||||
|
-- name: UpdateSessionTokens :exec
|
||||||
|
UPDATE user_sessions
|
||||||
|
SET access_token_id = $2, refresh_token_id = $3, expires_at = $4
|
||||||
|
WHERE id = $1
|
||||||
|
AND is_active = TRUE;
|
||||||
|
|
||||||
-- name: ListAllSessions :many
|
-- name: ListAllSessions :many
|
||||||
SELECT * FROM user_sessions
|
SELECT * FROM user_sessions
|
||||||
ORDER BY issued_at DESC
|
ORDER BY issued_at DESC
|
||||||
|
25
sqlc.yaml
25
sqlc.yaml
@ -41,20 +41,39 @@ sql:
|
|||||||
# ───── text ──────────────────────────────────────────
|
# ───── text ──────────────────────────────────────────
|
||||||
- db_type: "pg_catalog.text"
|
- db_type: "pg_catalog.text"
|
||||||
go_type: { type: "string" }
|
go_type: { type: "string" }
|
||||||
- db_type: "text" # or just "bool"
|
|
||||||
|
- db_type: "text"
|
||||||
go_type: { type: "string" }
|
go_type: { type: "string" }
|
||||||
|
|
||||||
- db_type: "pg_catalog.text"
|
- db_type: "pg_catalog.text"
|
||||||
nullable: true
|
nullable: true
|
||||||
go_type:
|
go_type:
|
||||||
type: "string"
|
type: "string"
|
||||||
pointer: true # ⇒ *bool for NULLable columns
|
pointer: true
|
||||||
|
|
||||||
- db_type: "text"
|
- db_type: "text"
|
||||||
nullable: true
|
nullable: true
|
||||||
go_type:
|
go_type:
|
||||||
type: "string"
|
type: "string"
|
||||||
pointer: true # ⇒ *bool for NULLable columns
|
pointer: true
|
||||||
|
|
||||||
|
- db_type: "pg_catalog.varchar"
|
||||||
|
go_type: { type: "string" }
|
||||||
|
|
||||||
|
- db_type: "varchar"
|
||||||
|
go_type: { type: "string" }
|
||||||
|
|
||||||
|
- db_type: "pg_catalog.varchar"
|
||||||
|
nullable: true
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
pointer: true
|
||||||
|
|
||||||
|
- db_type: "varchar"
|
||||||
|
nullable: true
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
pointer: true
|
||||||
|
|
||||||
# ───── timestamp (WITHOUT TZ) ────────────────────────
|
# ───── timestamp (WITHOUT TZ) ────────────────────────
|
||||||
- db_type: "pg_catalog.timestamp" # or "timestamp"
|
- db_type: "pg_catalog.timestamp" # or "timestamp"
|
||||||
|
Reference in New Issue
Block a user