feat: store accounts and per-device sessions for members

This commit is contained in:
DiheChen committed 2026-11-08 21:46:22 +08:00
1 parent 026b7e7f1a
commit 1277b3ac85
17 files changed
+813 -73

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
-- +goose Up
-- 一个会话对应一个成员 token,同一个成员可以在多台设备上各有一个会话。
CREATE TABLE member_sessions (
token_hash bytea PRIMARY KEY,
member_id uuid NOT NULL REFERENCES members ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON member_sessions (member_id);
-- bot 成员只通过 API Key 使用 HTTP 接口,不需要会话。
INSERT INTO member_sessions (token_hash, member_id, created_at)
SELECT m.token_hash, m.id, m.created_at FROM members m
WHERE NOT EXISTS (SELECT FROM rooms r WHERE r.bot_member_id = m.id);
ALTER TABLE members DROP COLUMN token_hash;
-- 设置了用户名的成员是已注册的账号,否则是游客。用户名只能由小写字母、数字、下划线和连字符组成,所以按原文唯一即可。
-- 邮箱属于账号,在所有设备上一致;游客没有账号,不替匿名身份保存个人信息,邮箱只存在游客自己的浏览器里。
ALTER TABLE members
ADD COLUMN username text UNIQUE,
ADD COLUMN password_hash text,
ADD CHECK (password_hash IS NULL OR username IS NOT NULL),
ADD COLUMN email text,
ADD CHECK (email IS NULL OR username IS NOT NULL);
+33 -4
View File
@@ -1,14 +1,43 @@
-- name: CreateMember :one
INSERT INTO members (token_hash, name) VALUES (@token_hash, @name)
INSERT INTO members (name) VALUES (@name)
RETURNING *;
-- name: CreateSession :exec
INSERT INTO member_sessions (token_hash, member_id) VALUES (@token_hash, @member_id);
-- name: MemberByToken :one
SELECT * FROM members WHERE token_hash = @token_hash;
SELECT * FROM members WHERE id = (SELECT member_id FROM member_sessions WHERE token_hash = @token_hash);
-- name: UpdateMember :one
-- 只覆盖传入的列,分别修改昵称和头像的两个并发请求因此不会互相还原;头像哈希传入空串表示清除。
-- 只覆盖传入的列,分别修改昵称和头像的两个并发请求因此不会互相还原;头像哈希和邮箱传入空串表示清除。游客的邮箱总是 NULL。
UPDATE members
SET name = coalesce(sqlc.narg(name), name),
avatar_hash = CASE WHEN sqlc.narg(avatar_hash)::text IS NULL THEN avatar_hash ELSE NULLIF(sqlc.narg(avatar_hash)::text, '') END
avatar_hash = CASE WHEN sqlc.narg(avatar_hash)::text IS NULL THEN avatar_hash ELSE NULLIF(sqlc.narg(avatar_hash)::text, '') END,
email = CASE WHEN username IS NULL THEN NULL WHEN sqlc.narg(email)::text IS NULL THEN email ELSE NULLIF(sqlc.narg(email)::text, '') END
WHERE id = @id
RETURNING *;
-- name: MemberAccountProfile :one
SELECT username, email FROM members WHERE id = @id;
-- name: RegisterMember :execrows
-- 只给游客设置:已经注册过的成员不受影响,影响的行数为 0。
UPDATE members SET username = @username, password_hash = @password_hash
WHERE id = @id AND username IS NULL;
-- name: AccountByUsername :one
SELECT * FROM members WHERE username = @username;
-- name: SetMemberPassword :execrows
UPDATE members SET password_hash = @password_hash WHERE id = @id AND username IS NOT NULL;
-- name: LockMemberPassword :one
-- FOR SHARE 让修改密码等到这次登录提交之后,它随后删除会话时会删掉这次登录签发的会话。
SELECT coalesce(password_hash, '')::text AS password_hash FROM members WHERE id = @id FOR SHARE;
-- name: DeleteSession :execrows
DELETE FROM member_sessions WHERE token_hash = @token_hash;
-- name: DeleteMemberSessions :exec
-- keep 为 NULL 时删除这个成员的全部会话。
DELETE FROM member_sessions WHERE member_id = @member_id AND token_hash IS DISTINCT FROM sqlc.narg(keep)::bytea;