add session stuff

This commit is contained in:
Branden J Brown 2024-01-31 22:29:31 -06:00
parent 6e63aba2b4
commit d61c70da86
3 changed files with 155 additions and 8 deletions

View File

@ -17,10 +17,10 @@ import (
const (
// Argon2id parameters recommended in
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
time = 3
memory = 12 << 10
threads = 1
hashLen = 48
timeCost = 3
memory = 12 << 10
threads = 1
hashLen = 48
)
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())
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()
_, err := db.Exec(ctx, `INSERT INTO shotgun_users(user, pass, salt, id) VALUES (?, ?, ?, ?)`, user, h, salt, id)
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)
}
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) {
slog.ErrorContext(ctx, "login failed", "user", user)
return ID{}, fmt.Errorf("invalid credentials")
@ -92,5 +92,5 @@ const initUsers = `CREATE TABLE shotgun_users (
user TEXT PRIMARY KEY NOT NULL,
pass BLOB NOT NULL,
salt BLOB NOT NULL,
id TEXT NOT NULL
);`
id TEXT NOT NULL UNIQUE
) STRICT;`

72
player/session.go Normal file
View 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
View 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)
}
}