feat: migrate one-time and signup tokens to an actor (#1611)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Alessandro (Ale) Segala
2026-07-26 15:32:43 +02:00
committed by GitHub
co-authored by Elias Schneider
parent 531bb5f0cf
commit a1b4e1d2b2
37 changed files with 7989 additions and 905 deletions
@@ -0,0 +1,56 @@
-- Recreate the one_time_access_tokens table with the schema it had before it was dropped.
CREATE TABLE one_time_access_tokens
(
id UUID NOT NULL PRIMARY KEY,
created_at TIMESTAMPTZ,
token VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
device_token VARCHAR(16)
);
CREATE INDEX IF NOT EXISTS idx_one_time_access_tokens_expires_at ON one_time_access_tokens (expires_at);
-- Recreate the signup token tables with the schema they had before they were frozen.
CREATE TABLE signup_tokens (
id UUID NOT NULL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
token VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
usage_limit INTEGER NOT NULL DEFAULT 1,
usage_count INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_signup_tokens_token ON signup_tokens(token);
CREATE INDEX idx_signup_tokens_expires_at ON signup_tokens(expires_at);
CREATE TABLE signup_tokens_user_groups
(
signup_token_id UUID NOT NULL,
user_group_id UUID NOT NULL,
PRIMARY KEY (signup_token_id, user_group_id),
FOREIGN KEY (signup_token_id) REFERENCES signup_tokens (id) ON DELETE CASCADE,
FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE
);
-- Restore the signup tokens from the frozen JSON document stored in the "kv" table.
-- json_array_elements expands the JSON array into one row per token object.
INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count)
SELECT
(e ->> 'id')::uuid,
to_timestamp((e ->> 'createdAt')::bigint),
e ->> 'token',
to_timestamp((e ->> 'expiresAt')::bigint),
(e ->> 'usageLimit')::int,
(e ->> 'usageCount')::int
FROM kv, json_array_elements(kv."value"::json) AS e
WHERE kv."key" = 'signup_tokens_migrated';
-- Restore the token/user-group associations, expanding each token's nested userGroupIds array.
INSERT INTO signup_tokens_user_groups (signup_token_id, user_group_id)
SELECT
(e ->> 'id')::uuid,
g.value::uuid
FROM kv, json_array_elements(kv."value"::json) AS e, json_array_elements_text(e -> 'userGroupIds') AS g
WHERE kv."key" = 'signup_tokens_migrated';
-- Remove the frozen signup tokens from the "kv" table.
DELETE FROM kv WHERE "key" = 'signup_tokens_migrated';
@@ -0,0 +1,28 @@
-- One-time access tokens are now stored in the actor state store, so the table is no longer needed.
DROP TABLE IF EXISTS one_time_access_tokens;
-- Freeze the signup tokens.
-- Encode every signup token (with its user group IDs) as a single JSON array and store it in the "kv" table under the "signup_tokens_migrated" key, so the singleton signup token actor can seed its state from it on first startup.
-- The "HAVING count(*) > 0" clause ensures nothing is written to the "kv" table when there are no signup tokens.
-- Timestamps are stored as Unix seconds so the frozen format is identical across databases.
INSERT INTO kv ("key", "value")
SELECT 'signup_tokens_migrated', json_agg(
json_build_object(
'id', st.id,
'token', st.token,
'expiresAt', extract(epoch FROM st.expires_at)::bigint,
'usageLimit', st.usage_limit,
'usageCount', st.usage_count,
'createdAt', extract(epoch FROM st.created_at)::bigint,
'userGroupIds', COALESCE(
(SELECT json_agg(stug.user_group_id) FROM signup_tokens_user_groups stug WHERE stug.signup_token_id = st.id),
'[]'::json
)
)
)::text
FROM signup_tokens st
HAVING count(*) > 0;
-- Drop the now-frozen signup token tables.
DROP TABLE signup_tokens_user_groups;
DROP TABLE signup_tokens;