check / check (push) Waiting to run
The server binary carried exported names that only tests call, including raw SQL access and a way to weaken the password hash. Delete testing.go, GetDB and SetBcryptCost from the shipped code, so passwords always hash at bcrypt.DefaultCost. Move GetDB, GetOrCreateChannel, JoinChannel and HasChannelInvite into export_test.go. Delete GetClientHostInfo, whose remaining check GetLatestClientForSession covers. Tests in other packages now build the database with db.New and run raw SQL through dbtest.Exec. Handler tests that need a password only to log in store one hashed at bcrypt.MinCost. Tests of PASS itself keep the real cost. Model: opus-5-5
113 lines
2.2 KiB
Go
113 lines
2.2 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
var errNoPassword = errors.New(
|
|
"account has no password set",
|
|
)
|
|
|
|
// SetPassword sets a bcrypt-hashed password on a session,
|
|
// enabling multi-client login via POST /api/v1/login.
|
|
func (database *Database) SetPassword(
|
|
ctx context.Context,
|
|
sessionID int64,
|
|
password string,
|
|
) error {
|
|
hash, err := bcrypt.GenerateFromPassword(
|
|
[]byte(password), bcrypt.DefaultCost,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("hash password: %w", err)
|
|
}
|
|
|
|
_, err = database.conn.ExecContext(ctx,
|
|
"UPDATE sessions SET password_hash = ? WHERE id = ?",
|
|
string(hash), sessionID)
|
|
if err != nil {
|
|
return fmt.Errorf("set password: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoginUser verifies a nick/password and creates a new
|
|
// client token.
|
|
func (database *Database) LoginUser(
|
|
ctx context.Context,
|
|
nick, password, remoteIP, hostname string,
|
|
) (int64, int64, string, error) {
|
|
var (
|
|
sessionID int64
|
|
passwordHash string
|
|
)
|
|
|
|
err := database.conn.QueryRowContext(
|
|
ctx,
|
|
`SELECT id, password_hash
|
|
FROM sessions WHERE nick = ?`,
|
|
nick,
|
|
).Scan(&sessionID, &passwordHash)
|
|
if err != nil {
|
|
return 0, 0, "", fmt.Errorf(
|
|
"get session for login: %w", err,
|
|
)
|
|
}
|
|
|
|
if passwordHash == "" {
|
|
return 0, 0, "", fmt.Errorf(
|
|
"login: %w", errNoPassword,
|
|
)
|
|
}
|
|
|
|
err = bcrypt.CompareHashAndPassword(
|
|
[]byte(passwordHash), []byte(password),
|
|
)
|
|
if err != nil {
|
|
return 0, 0, "", fmt.Errorf(
|
|
"verify password: %w", err,
|
|
)
|
|
}
|
|
|
|
clientUUID := uuid.New().String()
|
|
|
|
token, err := generateToken()
|
|
if err != nil {
|
|
return 0, 0, "", err
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
tokenHash := hashToken(token)
|
|
|
|
res, err := database.conn.ExecContext(ctx,
|
|
`INSERT INTO clients
|
|
(uuid, session_id, token, ip, hostname,
|
|
created_at, last_seen)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
clientUUID, sessionID, tokenHash,
|
|
remoteIP, hostname, now, now)
|
|
if err != nil {
|
|
return 0, 0, "", fmt.Errorf(
|
|
"create login client: %w", err,
|
|
)
|
|
}
|
|
|
|
clientID, _ := res.LastInsertId()
|
|
|
|
_, _ = database.conn.ExecContext(
|
|
ctx,
|
|
"UPDATE sessions SET last_seen = ? WHERE id = ?",
|
|
now, sessionID,
|
|
)
|
|
|
|
return sessionID, clientID, token, nil
|
|
}
|