add session stuff
This commit is contained in:
parent
6e63aba2b4
commit
d61c70da86
@ -17,10 +17,10 @@ import (
|
|||||||
const (
|
const (
|
||||||
// Argon2id parameters recommended in
|
// Argon2id parameters recommended in
|
||||||
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
||||||
time = 3
|
timeCost = 3
|
||||||
memory = 12 << 10
|
memory = 12 << 10
|
||||||
threads = 1
|
threads = 1
|
||||||
hashLen = 48
|
hashLen = 48
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -51,7 +51,7 @@ func Register(ctx context.Context, db Execer, user, pass string) error {
|
|||||||
slog.ErrorContext(ctx, "failed to get salt for new user", "user", user, "err", err.Error())
|
slog.ErrorContext(ctx, "failed to get salt for new user", "user", user, "err", err.Error())
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
h := argon2.IDKey([]byte(pass), salt, time, memory, threads, hashLen)
|
h := argon2.IDKey([]byte(pass), salt, timeCost, memory, threads, hashLen)
|
||||||
id := uuid.New()
|
id := uuid.New()
|
||||||
_, err := db.Exec(ctx, `INSERT INTO shotgun_users(user, pass, salt, id) VALUES (?, ?, ?, ?)`, user, h, salt, id)
|
_, err := db.Exec(ctx, `INSERT INTO shotgun_users(user, pass, salt, id) VALUES (?, ?, ?, ?)`, user, h, salt, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -80,7 +80,7 @@ func Login(ctx context.Context, db RowQuerier, user, pass string) (ID, error) {
|
|||||||
}
|
}
|
||||||
return ID{}, fmt.Errorf("couldn't get user creds: %w", err)
|
return ID{}, fmt.Errorf("couldn't get user creds: %w", err)
|
||||||
}
|
}
|
||||||
h := argon2.IDKey([]byte(pass), salt, time, memory, threads, hashLen)
|
h := argon2.IDKey([]byte(pass), salt, timeCost, memory, threads, hashLen)
|
||||||
if !bytes.Equal(p, h) {
|
if !bytes.Equal(p, h) {
|
||||||
slog.ErrorContext(ctx, "login failed", "user", user)
|
slog.ErrorContext(ctx, "login failed", "user", user)
|
||||||
return ID{}, fmt.Errorf("invalid credentials")
|
return ID{}, fmt.Errorf("invalid credentials")
|
||||||
@ -92,5 +92,5 @@ const initUsers = `CREATE TABLE shotgun_users (
|
|||||||
user TEXT PRIMARY KEY NOT NULL,
|
user TEXT PRIMARY KEY NOT NULL,
|
||||||
pass BLOB NOT NULL,
|
pass BLOB NOT NULL,
|
||||||
salt BLOB NOT NULL,
|
salt BLOB NOT NULL,
|
||||||
id TEXT NOT NULL
|
id TEXT NOT NULL UNIQUE
|
||||||
);`
|
) STRICT;`
|
||||||
|
72
player/session.go
Normal file
72
player/session.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gitlab.com/zephyrtronium/sq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session is a session ID.
|
||||||
|
type Session = uuid.UUID
|
||||||
|
|
||||||
|
// InitSessions initializes an SQLite table relating player IDs to sessions.
|
||||||
|
func InitSessions(ctx context.Context, db Execer) error {
|
||||||
|
_, err := db.Exec(ctx, initSessions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't init sessions table: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartSession creates a new 24-hour session for a player as of now.
|
||||||
|
func StartSession(ctx context.Context, db Execer, p ID, now time.Time) (Session, error) {
|
||||||
|
id := uuid.New()
|
||||||
|
ttl := now.Add(24 * time.Hour).Unix()
|
||||||
|
_, err := db.Exec(ctx, `INSERT OR REPLACE INTO shotgun_session(player, id, ttl) VALUES (?, ?, ?)`, p, id, ttl)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "couldn't start session", "player", p, "error", err.Error())
|
||||||
|
return Session{}, fmt.Errorf("couldn't start session: %w", err)
|
||||||
|
}
|
||||||
|
slog.InfoContext(ctx, "session started", "player", p, "session", id, "now", now)
|
||||||
|
return Session(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromSession gets the player ID associated with a session and updates the
|
||||||
|
// session to last 24 hours past now.
|
||||||
|
func FromSession(ctx context.Context, db RowQuerier, id Session, now time.Time) (ID, error) {
|
||||||
|
ttl := now.Add(24 * time.Hour).Unix()
|
||||||
|
var p ID
|
||||||
|
err := db.QueryRow(ctx, `UPDATE shotgun_session SET ttl = ? WHERE id = ? AND ttl >= ? RETURNING player;`, ttl, id, now.Unix()).Scan(&p)
|
||||||
|
switch err {
|
||||||
|
case nil: // do nothing
|
||||||
|
case sq.ErrNoRows:
|
||||||
|
slog.WarnContext(ctx, "looked for missing session", "session", id, "now", now)
|
||||||
|
return ID{}, fmt.Errorf("no such session")
|
||||||
|
default:
|
||||||
|
slog.ErrorContext(ctx, "couldn't get player from session", "session", id, "now", now, "error", err.Error())
|
||||||
|
return ID{}, fmt.Errorf("couldn't get player from session: %w", err)
|
||||||
|
}
|
||||||
|
slog.InfoContext(ctx, "got player from session", "session", id, "player", p)
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout deletes a session.
|
||||||
|
func Logout(ctx context.Context, db Execer, id Session) error {
|
||||||
|
_, err := db.Exec(ctx, `DELETE FROM shotgun_session WHERE id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "couldn't logout", "session", id, "error", err.Error())
|
||||||
|
return fmt.Errorf("couldn't logout: %w", err)
|
||||||
|
}
|
||||||
|
slog.InfoContext(ctx, "logged out", "session", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSessions = `CREATE TABLE shotgun_session (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
player TEXT NOT NULL UNIQUE,
|
||||||
|
ttl INTEGER
|
||||||
|
) STRICT;`
|
75
player/session_test.go
Normal file
75
player/session_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package player_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gitlab.com/zephyrtronium/sq"
|
||||||
|
|
||||||
|
"git.sunturtle.xyz/studio/shotgun/player"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite" // sqlite driver
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSessions(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := sq.Open("sqlite", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn, err := db.Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := player.InitSessions(ctx, conn); err != nil {
|
||||||
|
t.Fatalf("couldn't init sessions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := player.ID{1}
|
||||||
|
now := time.Unix(0, 0)
|
||||||
|
id, err := player.StartSession(ctx, conn, p, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't start session: %v", err)
|
||||||
|
}
|
||||||
|
if id == uuid.Nil {
|
||||||
|
t.Errorf("got zero session id at start")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := player.FromSession(ctx, conn, id, now.Add(25*time.Hour))
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("no error on expired session")
|
||||||
|
}
|
||||||
|
if u != uuid.Nil {
|
||||||
|
t.Errorf("got nonzero player %v from expired session", u)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err = player.FromSession(ctx, conn, id, now.Add(23*time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't get player from session: %v", err)
|
||||||
|
}
|
||||||
|
if u == uuid.Nil {
|
||||||
|
t.Errorf("got zero player from session")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err = player.FromSession(ctx, conn, id, now.Add(25*time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't get player from extended session: %v", err)
|
||||||
|
}
|
||||||
|
if u == uuid.Nil {
|
||||||
|
t.Errorf("got zero player from extended session")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := player.Logout(ctx, conn, id); err != nil {
|
||||||
|
t.Errorf("couldn't logout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err = player.FromSession(ctx, conn, id, now.Add(25*time.Hour))
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("no error on logged out session")
|
||||||
|
}
|
||||||
|
if u != uuid.Nil {
|
||||||
|
t.Errorf("got nonzero player %v from logged out session", u)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user