feat: Add Sliding expiration for user_sessions
The user_session is valid for 24 hours, but the session will be bumped if it has been over an hour since the last update. An hour was set as a deadline, so that we would not update the user_session rows too often.
This commit is contained in:
parent
1d70b3a6d3
commit
43219268e9
13 changed files with 140 additions and 58 deletions
|
|
@ -15,7 +15,9 @@ A simple Go backend with authentication using native `net/http` and PostgreSQL.
|
||||||
|
|
||||||
- Go 1.24+
|
- Go 1.24+
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
- [goose](https://github.com/pressly/goose) for migrations, [air](https://github.com/air-verse/air) for live reload (optional):
|
- [goose](https://github.com/pressly/goose) for migrationns
|
||||||
|
- [sqlc](https://github.com/sqlc-dev/sqlc) for generating typed SQL queries
|
||||||
|
- [air](https://github.com/air-verse/air) for live reload (optional)
|
||||||
```bash
|
```bash
|
||||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||||
go install github.com/air-verse/air@latest
|
go install github.com/air-verse/air@latest
|
||||||
|
|
@ -24,6 +26,7 @@ A simple Go backend with authentication using native `net/http` and PostgreSQL.
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Copy environment variables:
|
1. Copy environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
@ -31,11 +34,13 @@ A simple Go backend with authentication using native `net/http` and PostgreSQL.
|
||||||
2. Update `.env` with your database credentials (both the `DB_*` and `GOOSE_*` values).
|
2. Update `.env` with your database credentials (both the `DB_*` and `GOOSE_*` values).
|
||||||
|
|
||||||
3. Install dependencies:
|
3. Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run database migrations:
|
4. Run database migrations:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/goose.sh up
|
./scripts/goose.sh up
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load .env file
|
|
||||||
if err := godotenv.Load(); err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
log.Println("No .env file found")
|
log.Println("No .env file found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to database
|
|
||||||
db, err := database.Connect()
|
db, err := database.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to connect to database:", err)
|
log.Fatal("Failed to connect to database:", err)
|
||||||
|
|
@ -33,10 +31,8 @@ func main() {
|
||||||
|
|
||||||
handler := handlers.NewHandler(userService, authService)
|
handler := handlers.NewHandler(userService, authService)
|
||||||
|
|
||||||
// Create router
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// Routes
|
|
||||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./ui/static"))))
|
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./ui/static"))))
|
||||||
|
|
||||||
mux.HandleFunc("GET /{$}", handler.HomeHandler)
|
mux.HandleFunc("GET /{$}", handler.HomeHandler)
|
||||||
|
|
@ -48,10 +44,8 @@ func main() {
|
||||||
mux.HandleFunc("POST /api/logout", handler.LogoutHandler)
|
mux.HandleFunc("POST /api/logout", handler.LogoutHandler)
|
||||||
mux.HandleFunc("POST /api/signup", handler.SignUpHandler)
|
mux.HandleFunc("POST /api/signup", handler.SignUpHandler)
|
||||||
|
|
||||||
// Apply middleware (CORS first, then logging)
|
|
||||||
muxMiddleware := handlers.LoggingMiddleware(handlers.CORSMiddleware(mux))
|
muxMiddleware := handlers.LoggingMiddleware(handlers.CORSMiddleware(mux))
|
||||||
|
|
||||||
// Start server
|
|
||||||
port := getEnv("PORT", "8080")
|
port := getEnv("PORT", "8080")
|
||||||
log.Printf("Server starting on port %s", port)
|
log.Printf("Server starting on port %s", port)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ package auth
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sessionExpiresInSeconds = 60 * 60 * 24 // 1 day
|
const sessionExpiresInSeconds = 60 * 60 * 24 // 1 day
|
||||||
|
const activityCheckInterval = 60 * 60 // 1 hour
|
||||||
|
|
||||||
func GenerateSecureRandomString() (string, error) {
|
func GenerateSecureRandomString() (string, error) {
|
||||||
// Human readable alphabet (a-z, 0-9 without l, o, 0, 1 to avoid confusion)
|
// Human readable alphabet (a-z, 0-9 without l, o, 0, 1 to avoid confusion)
|
||||||
|
|
@ -34,6 +36,35 @@ func HashSecret(secret string) []byte {
|
||||||
return hash[:]
|
return hash[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckNeedForBump(lastVerified time.Time) bool {
|
||||||
|
return time.Since(lastVerified).Seconds() >= activityCheckInterval
|
||||||
|
}
|
||||||
|
|
||||||
func CheckExpiration(expirarion time.Time) bool {
|
func CheckExpiration(expirarion time.Time) bool {
|
||||||
return time.Since(expirarion).Seconds() >= sessionExpiresInSeconds
|
return time.Since(expirarion).Seconds() >= sessionExpiresInSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewSessionCookie(token string, deleted bool) *http.Cookie {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: token,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: sessionExpiresInSeconds,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
|
||||||
|
if deleted {
|
||||||
|
cookie = &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: "",
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: -1,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
-- name: SelectSessionById :one
|
-- name: SelectSessionById :one
|
||||||
SELECT id, secret_hash, user_id, created_at
|
SELECT id, secret_hash, user_id, last_verified_at, created_at, updated_at
|
||||||
FROM user_session
|
FROM user_session
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: InsertSession :one
|
-- name: InsertSession :one
|
||||||
INSERT INTO user_session (id, secret_hash, user_id)
|
INSERT INTO user_session (id, secret_hash, user_id)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
RETURNING id, secret_hash, user_id, created_at;
|
RETURNING id, secret_hash, user_id, last_verified_at, created_at, updated_at;
|
||||||
|
|
||||||
|
-- name: UpdateSessionVerifiedAt :one
|
||||||
|
UPDATE user_session SET last_verified_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, secret_hash, user_id, last_verified_at, created_at, updated_at;
|
||||||
|
|
||||||
-- name: DeleteSessionById :exec
|
-- name: DeleteSessionById :exec
|
||||||
DELETE FROM user_session WHERE id = $1;
|
DELETE FROM user_session WHERE id = $1;
|
||||||
|
|
@ -18,8 +18,10 @@ type UserAuth struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSession struct {
|
type UserSession struct {
|
||||||
ID string
|
ID string
|
||||||
SecretHash []byte
|
SecretHash []byte
|
||||||
UserID string
|
UserID string
|
||||||
CreatedAt time.Time
|
LastVerifiedAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func (q *Queries) DeleteSessionById(ctx context.Context, id string) error {
|
||||||
const insertSession = `-- name: InsertSession :one
|
const insertSession = `-- name: InsertSession :one
|
||||||
INSERT INTO user_session (id, secret_hash, user_id)
|
INSERT INTO user_session (id, secret_hash, user_id)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
RETURNING id, secret_hash, user_id, created_at
|
RETURNING id, secret_hash, user_id, last_verified_at, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertSessionParams struct {
|
type InsertSessionParams struct {
|
||||||
|
|
@ -37,13 +37,15 @@ func (q *Queries) InsertSession(ctx context.Context, arg InsertSessionParams) (U
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.SecretHash,
|
&i.SecretHash,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
|
&i.LastVerifiedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectSessionById = `-- name: SelectSessionById :one
|
const selectSessionById = `-- name: SelectSessionById :one
|
||||||
SELECT id, secret_hash, user_id, created_at
|
SELECT id, secret_hash, user_id, last_verified_at, created_at, updated_at
|
||||||
FROM user_session
|
FROM user_session
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -55,7 +57,29 @@ func (q *Queries) SelectSessionById(ctx context.Context, id string) (UserSession
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.SecretHash,
|
&i.SecretHash,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
|
&i.LastVerifiedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSessionVerifiedAt = `-- name: UpdateSessionVerifiedAt :one
|
||||||
|
UPDATE user_session SET last_verified_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, secret_hash, user_id, last_verified_at, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) UpdateSessionVerifiedAt(ctx context.Context, id string) (UserSession, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateSessionVerifiedAt, id)
|
||||||
|
var i UserSession
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SecretHash,
|
||||||
|
&i.UserID,
|
||||||
|
&i.LastVerifiedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"go-backend/internal/auth"
|
||||||
"go-backend/internal/database/sqlc"
|
"go-backend/internal/database/sqlc"
|
||||||
"go-backend/internal/services"
|
"go-backend/internal/services"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -21,7 +22,7 @@ type UserService interface {
|
||||||
|
|
||||||
type AuthService interface {
|
type AuthService interface {
|
||||||
CreateSession(ctx context.Context, userID string) (*services.SessionWithToken, error)
|
CreateSession(ctx context.Context, userID string) (*services.SessionWithToken, error)
|
||||||
ValidateSessionToken(ctx context.Context, token string) (*sqlc.UserSession, error)
|
ValidateSessionToken(ctx context.Context, token string) (*sqlc.UserSession, bool, error)
|
||||||
DeleteSession(ctx context.Context, token string) error
|
DeleteSession(ctx context.Context, token string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
session, bump, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err.Error())
|
log.Print(err.Error())
|
||||||
|
|
@ -58,6 +59,10 @@ func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bump {
|
||||||
|
http.SetCookie(w, auth.NewSessionCookie(cookie.Value, false))
|
||||||
|
}
|
||||||
|
|
||||||
files := []string{
|
files := []string{
|
||||||
"./ui/html/base.tmpl",
|
"./ui/html/base.tmpl",
|
||||||
"./ui/html/pages/index.tmpl",
|
"./ui/html/pages/index.tmpl",
|
||||||
|
|
@ -81,7 +86,7 @@ func (h *Handler) LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
cookie, err := r.Cookie("session")
|
cookie, err := r.Cookie("session")
|
||||||
|
|
||||||
if cookie != nil {
|
if cookie != nil {
|
||||||
session, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
session, bump, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err.Error())
|
log.Print(err.Error())
|
||||||
|
|
@ -89,6 +94,10 @@ func (h *Handler) LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bump {
|
||||||
|
http.SetCookie(w, auth.NewSessionCookie(cookie.Value, false))
|
||||||
|
}
|
||||||
|
|
||||||
if session != nil {
|
if session != nil {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
|
|
@ -118,7 +127,7 @@ func (h *Handler) SignUpPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
cookie, err := r.Cookie("session")
|
cookie, err := r.Cookie("session")
|
||||||
|
|
||||||
if cookie != nil {
|
if cookie != nil {
|
||||||
session, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
session, bump, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err.Error())
|
log.Print(err.Error())
|
||||||
|
|
@ -126,6 +135,10 @@ func (h *Handler) SignUpPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bump {
|
||||||
|
http.SetCookie(w, auth.NewSessionCookie(cookie.Value, false))
|
||||||
|
}
|
||||||
|
|
||||||
if session != nil {
|
if session != nil {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -52,14 +52,7 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie := &http.Cookie{
|
cookie := auth.NewSessionCookie(session.Token, false)
|
||||||
Name: "session",
|
|
||||||
Value: session.Token,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
MaxAge: 86400, // 24 hours
|
|
||||||
Path: "/",
|
|
||||||
}
|
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
w.Header().Set("HX-Redirect", "/")
|
w.Header().Set("HX-Redirect", "/")
|
||||||
|
|
@ -82,16 +75,7 @@ func (h *Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie = &http.Cookie{
|
http.SetCookie(w, auth.NewSessionCookie("", true))
|
||||||
Name: "session",
|
|
||||||
Value: "",
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
MaxAge: -1,
|
|
||||||
Path: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, cookie)
|
|
||||||
|
|
||||||
w.Header().Set("HX-Redirect", "/")
|
w.Header().Set("HX-Redirect", "/")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,7 @@ func (h *Handler) SignUpHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie := &http.Cookie{
|
cookie := auth.NewSessionCookie(session.Token, false)
|
||||||
Name: "session",
|
|
||||||
Value: session.Token,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
MaxAge: 86400, // 24 hours
|
|
||||||
Path: "/",
|
|
||||||
}
|
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
w.Header().Set("HX-Redirect", "/")
|
w.Header().Set("HX-Redirect", "/")
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"go-backend/internal/auth"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -25,16 +26,22 @@ func (h *Handler) UserHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate session
|
// Validate session
|
||||||
session, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
session, bump, err := h.auth.ValidateSessionToken(r.Context(), cookie.Value)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if session == nil {
|
if session == nil {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bump {
|
||||||
|
http.SetCookie(w, auth.NewSessionCookie(cookie.Value, false))
|
||||||
|
}
|
||||||
|
|
||||||
// Get user info
|
// Get user info
|
||||||
user, err := h.users.FindByID(r.Context(), session.UserID)
|
user, err := h.users.FindByID(r.Context(), session.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,10 @@ func (s *AuthService) CreateSession(ctx context.Context, userID string) (*Sessio
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) ValidateSessionToken(ctx context.Context, token string) (*sqlc.UserSession, error) {
|
func (s *AuthService) ValidateSessionToken(ctx context.Context, token string) (*sqlc.UserSession, bool, error) {
|
||||||
tokenParts := strings.Split(token, ".")
|
tokenParts := strings.Split(token, ".")
|
||||||
if len(tokenParts) != 2 {
|
if len(tokenParts) != 2 {
|
||||||
return nil, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := tokenParts[0]
|
sessionID := tokenParts[0]
|
||||||
|
|
@ -71,28 +71,38 @@ func (s *AuthService) ValidateSessionToken(ctx context.Context, token string) (*
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.CheckExpiration(session.CreatedAt) {
|
if auth.CheckExpiration(session.LastVerifiedAt) {
|
||||||
err = s.queries.DeleteSessionById(ctx, sessionID)
|
err = s.queries.DeleteSessionById(ctx, sessionID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenSecretHash := auth.HashSecret(sessionSecret)
|
tokenSecretHash := auth.HashSecret(sessionSecret)
|
||||||
if subtle.ConstantTimeCompare(tokenSecretHash, session.SecretHash) != 1 {
|
if subtle.ConstantTimeCompare(tokenSecretHash, session.SecretHash) != 1 {
|
||||||
return nil, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &session, nil
|
if auth.CheckNeedForBump(session.LastVerifiedAt) {
|
||||||
|
session, err = s.queries.UpdateSessionVerifiedAt(ctx, sessionID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &session, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &session, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) DeleteSession(ctx context.Context, token string) error {
|
func (s *AuthService) DeleteSession(ctx context.Context, token string) error {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ CREATE TABLE IF NOT EXISTS user_session (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
secret_hash BYTEA NOT NULL,
|
secret_hash BYTEA NOT NULL,
|
||||||
user_id TEXT NOT NULL REFERENCES user_auth (id) ON DELETE CASCADE,
|
user_id TEXT NOT NULL REFERENCES user_auth (id) ON DELETE CASCADE,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
last_verified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_session_user_id ON user_session (user_id);
|
CREATE INDEX IF NOT EXISTS idx_user_session_user_id ON user_session (user_id);
|
||||||
|
|
@ -34,6 +36,11 @@ CREATE TRIGGER user_auth_set_updated_at
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION set_updated_at();
|
EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER user_session_set_updated_at
|
||||||
|
BEFORE UPDATE ON user_session
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
|
|
||||||
DROP TRIGGER IF EXISTS user_auth_set_updated_at ON user_auth;
|
DROP TRIGGER IF EXISTS user_auth_set_updated_at ON user_auth;
|
||||||
|
|
|
||||||
7
todo.txt
Normal file
7
todo.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[ ] Add secure TLS to cookies
|
||||||
|
[ ] Add confirm email
|
||||||
|
[ ] Add OTP (Scaleway?)
|
||||||
|
[ ] Add OAuth (Google?)
|
||||||
|
[ ] Add profile - patch user
|
||||||
|
[ ] Add an admin role and admin panel
|
||||||
|
[ ] Add forgotten pw flow
|
||||||
Loading…
Reference in a new issue