@@ -0,0 +1,179 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"daili/internal/pw"
|
||||
)
|
||||
|
||||
// User 管理员账户。
|
||||
type User struct {
|
||||
ID int64
|
||||
Username string
|
||||
DisplayName string
|
||||
IsActive bool
|
||||
CreatedAt string
|
||||
LastLoginAt string
|
||||
}
|
||||
|
||||
func scanUser(row interface {
|
||||
Scan(dest ...any) error
|
||||
}) (*User, error) {
|
||||
var u User
|
||||
var active int
|
||||
var last sql.NullString
|
||||
if err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &active, &u.CreatedAt, &last); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
u.IsActive = active == 1
|
||||
u.LastLoginAt = last.String
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
const userCols = `id, username, display_name, is_active, created_at, IFNULL(last_login_at,'')`
|
||||
|
||||
// CountUsers 返回账户数量。
|
||||
func (db *DB) CountUsers() (int, error) {
|
||||
var n int
|
||||
err := db.sql.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// CreateUser 新建账户。
|
||||
func (db *DB) CreateUser(username, displayName, password string) (*User, error) {
|
||||
hash, err := pw.Hash(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := nowStamp()
|
||||
res, err := db.sql.Exec(`INSERT INTO users(username, display_name, password_hash, is_active, created_at)
|
||||
VALUES(?,?,?,1,?)`, username, displayName, hash, now)
|
||||
if err != nil {
|
||||
if isUnique(err) {
|
||||
return nil, ErrDuplicate
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return &User{ID: id, Username: username, DisplayName: displayName, IsActive: true, CreatedAt: now}, nil
|
||||
}
|
||||
|
||||
// Authenticate 校验用户名口令。
|
||||
func (db *DB) Authenticate(username, password string) (*User, error) {
|
||||
var hash string
|
||||
var active int
|
||||
var id int64
|
||||
err := db.sql.QueryRow(`SELECT id, password_hash, is_active FROM users WHERE username=?`, username).
|
||||
Scan(&id, &hash, &active)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if active != 1 || !pw.Verify(password, hash) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
_, _ = db.sql.Exec(`UPDATE users SET last_login_at=? WHERE id=?`, nowStamp(), id)
|
||||
return db.UserByID(id)
|
||||
}
|
||||
|
||||
// UserByID 按 ID 取账户。
|
||||
func (db *DB) UserByID(id int64) (*User, error) {
|
||||
return scanUser(db.sql.QueryRow(`SELECT `+userCols+` FROM users WHERE id=?`, id))
|
||||
}
|
||||
|
||||
// UserByUsername 按用户名取账户。
|
||||
func (db *DB) UserByUsername(name string) (*User, error) {
|
||||
return scanUser(db.sql.QueryRow(`SELECT `+userCols+` FROM users WHERE username=?`, name))
|
||||
}
|
||||
|
||||
// ListUsers 列出账户。
|
||||
func (db *DB) ListUsers() ([]User, error) {
|
||||
rows, err := db.sql.Query(`SELECT ` + userCols + ` FROM users ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []User
|
||||
for rows.Next() {
|
||||
u, err := scanUser(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *u)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// UpdatePassword 修改口令。
|
||||
func (db *DB) UpdatePassword(userID int64, password string) error {
|
||||
hash, err := pw.Hash(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.sql.Exec(`UPDATE users SET password_hash=? WHERE id=?`, hash, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetUserActive 启用/停用账户。
|
||||
func (db *DB) SetUserActive(userID int64, active bool) error {
|
||||
v := 0
|
||||
if active {
|
||||
v = 1
|
||||
}
|
||||
_, err := db.sql.Exec(`UPDATE users SET is_active=? WHERE id=?`, v, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- 会话 ----------
|
||||
|
||||
// CreateSession 新建登录会话。
|
||||
func (db *DB) CreateSession(token string, userID int64, ttl time.Duration, ip string) error {
|
||||
now := time.Now()
|
||||
_, err := db.sql.Exec(`INSERT INTO sessions(token,user_id,created_at,expires_at,ip) VALUES(?,?,?,?,?)`,
|
||||
token, userID, now.Format("2006-01-02 15:04:05"), now.Add(ttl).Format("2006-01-02 15:04:05"), ip)
|
||||
return err
|
||||
}
|
||||
|
||||
// SessionUser 通过令牌取账户,过期则返回 ErrNotFound。
|
||||
func (db *DB) SessionUser(token string) (*User, error) {
|
||||
var userID int64
|
||||
var expires string
|
||||
err := db.sql.QueryRow(`SELECT user_id, expires_at FROM sessions WHERE token=?`, token).Scan(&userID, &expires)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
exp, err := time.ParseInLocation("2006-01-02 15:04:05", expires, time.Local)
|
||||
if err != nil || exp.Before(time.Now()) {
|
||||
_ = db.DeleteSession(token)
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
u, err := db.UserByID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !u.IsActive {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// DeleteSession 删除会话。
|
||||
func (db *DB) DeleteSession(token string) error {
|
||||
_, err := db.sql.Exec(`DELETE FROM sessions WHERE token=?`, token)
|
||||
return err
|
||||
}
|
||||
|
||||
// PurgeSessions 清理过期会话。
|
||||
func (db *DB) PurgeSessions() {
|
||||
_, _ = db.sql.Exec(`DELETE FROM sessions WHERE expires_at < ?`, nowStamp())
|
||||
}
|
||||
Reference in New Issue
Block a user