This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authsqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gamertan.com/web/auth"
|
||||
"modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var _ auth.OwnProfileRepository = (*Store)(nil)
|
||||
|
||||
const ownProfileQuery = `SELECT u.id,u.username,u.email,u.display_name,u.profile_revision
|
||||
FROM gwf_users u JOIN gwf_auth_sessions s ON s.user_id=u.id
|
||||
WHERE s.token_hash=? AND s.expires_at>? AND u.status='active'
|
||||
AND u.registration_pending=0 AND u.password_change_required=0`
|
||||
|
||||
func scanOwnProfile(row interface{ Scan(...any) error }) (auth.OwnProfile, error) {
|
||||
var profile auth.OwnProfile
|
||||
err := row.Scan(&profile.UserID, &profile.Username, &profile.Email, &profile.DisplayName, &profile.Revision)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return auth.OwnProfile{}, auth.ErrProfileAccess
|
||||
}
|
||||
return profile, err
|
||||
}
|
||||
|
||||
func (store *Store) OwnProfile(ctx context.Context, session [32]byte, now time.Time) (auth.OwnProfile, error) {
|
||||
if zeroDigest(session) || now.IsZero() {
|
||||
return auth.OwnProfile{}, auth.ErrProfileAccess
|
||||
}
|
||||
return scanOwnProfile(store.db.QueryRowContext(ctx, ownProfileQuery, session[:], now.Unix()))
|
||||
}
|
||||
|
||||
func (store *Store) UpdateOwnProfile(ctx context.Context, change auth.ProfileEdit, audit auth.AuditEvent) (auth.OwnProfile, error) {
|
||||
value, err := auth.NormalizeProfileValue(change.Field, change.Value)
|
||||
if err != nil || !opaqueID(change.UserID) || zeroDigest(change.SessionDigest) || change.ExpectedRevision < 1 || change.ExpectedRevision == math.MaxInt64 ||
|
||||
!validAuditEvent(audit) || audit.ActorUserID != change.UserID || audit.ResourceType != "user" || audit.ResourceID != change.UserID || audit.Action != "auth.profile."+change.Field ||
|
||||
change.ExpectedPasswordHash != "" && (change.Field != "username" || len(change.ExpectedPasswordHash) > 1024) {
|
||||
return auth.OwnProfile{}, auth.ErrProfileInput
|
||||
}
|
||||
tx, err := store.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
// This first statement takes the writer lock and tests the real current
|
||||
// session/account/revision together. No read-before-write lock upgrade race.
|
||||
set := `display_name=?`
|
||||
args := []any{value}
|
||||
if change.Field == "username" {
|
||||
set = `username=?,username_normalized=?`
|
||||
args = append(args, normalize(value))
|
||||
}
|
||||
args = append(args, audit.CreatedAt.Unix(), change.UserID, change.ExpectedRevision, change.SessionDigest[:], audit.CreatedAt.Unix(), change.ExpectedPasswordHash, change.ExpectedPasswordHash)
|
||||
result, err := tx.ExecContext(ctx, `UPDATE gwf_users SET `+set+`,profile_revision=profile_revision+1,updated_at=MAX(updated_at,?)
|
||||
WHERE id=? AND profile_revision=? AND status='active' AND registration_pending=0 AND password_change_required=0
|
||||
AND EXISTS (SELECT 1 FROM gwf_auth_sessions WHERE user_id=gwf_users.id AND token_hash=? AND expires_at>?)
|
||||
AND (?='' OR EXISTS (SELECT 1 FROM gwf_password_credentials WHERE user_id=gwf_users.id AND password_hash=?))`, args...)
|
||||
if err != nil {
|
||||
var constraint *sqlite.Error
|
||||
if errors.As(err, &constraint) && constraint.Code() == 2067 {
|
||||
return auth.OwnProfile{}, auth.ErrUsernameUnavailable
|
||||
}
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
changed, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
if changed != 1 {
|
||||
profile, err := scanOwnProfile(tx.QueryRowContext(ctx, ownProfileQuery, change.SessionDigest[:], audit.CreatedAt.Unix()))
|
||||
if err != nil {
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
if profile.UserID != change.UserID {
|
||||
return auth.OwnProfile{}, auth.ErrProfileAccess
|
||||
}
|
||||
return auth.OwnProfile{}, auth.ErrProfileConflict
|
||||
}
|
||||
if change.Field == "username" {
|
||||
if _, err = tx.ExecContext(ctx, `DELETE FROM gwf_auth_sessions WHERE user_id=? AND token_hash<>?`, change.UserID, change.SessionDigest[:]); err != nil {
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
}
|
||||
if err = appendAudit(ctx, tx, audit); err != nil {
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
profile, err := scanOwnProfile(tx.QueryRowContext(ctx, ownProfileQuery, change.SessionDigest[:], audit.CreatedAt.Unix()))
|
||||
if err != nil {
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return auth.OwnProfile{}, err
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
Reference in New Issue
Block a user