···11+CREATE TABLE IF NOT EXISTS actors (
22+ id INTEGER PRIMARY KEY,
33+ did TEXT NOT NULL UNIQUE,
44+ handle TEXT NOT NULL UNIQUE,
55+ email TEXT NOT NULL UNIQUE,
66+ password_hash TEXT NOT NULL,
77+ signing_key TEXT NOT NULL,
88+ preferences TEXT NOT NULL,
99+ created_at INTEGER NOT NULL,
1010+ deactivated_at INTEGER
1111+);
1212+1313+CREATE INDEX IF NOT EXISTS actors_did_idx ON actors (did);
1414+CREATE INDEX IF NOT EXISTS actors_handle_idx ON actors (handle);
1515+CREATE INDEX IF NOT EXISTS actors_email_idx ON actors (email);
1616+1717+CREATE TABLE IF NOT EXISTS invite_codes (
1818+ code TEXT PRIMARY KEY,
1919+ did TEXT NOT NULL,
2020+ remaining INTEGER NOT NULL
2121+);
2222+2323+CREATE TABLE IF NOT EXISTS firehose (
2424+ seq INTEGER PRIMARY KEY,
2525+ time INTEGER NOT NULL,
2626+ t TEXT NOT NULL,
2727+ data BLOB NOT NULL
2828+);
2929+3030+CREATE TABLE IF NOT EXISTS revoked_tokens (
3131+ did TEXT NOT NULL,
3232+ jti TEXT NOT NULL,
3333+ revoked_at INTEGER NOT NULL,
3434+ PRIMARY KEY (did, jti)
3535+);
3636+3737+CREATE TABLE IF NOT EXISTS oauth_requests (
3838+ request_id TEXT PRIMARY KEY,
3939+ client_id TEXT NOT NULL,
4040+ request_data TEXT NOT NULL,
4141+ dpop_jkt TEXT,
4242+ expires_at INTEGER NOT NULL,
4343+ created_at INTEGER NOT NULL
4444+);
4545+4646+CREATE INDEX IF NOT EXISTS oauth_requests_expires_idx ON oauth_requests(expires_at);
4747+4848+CREATE TABLE IF NOT EXISTS oauth_codes (
4949+ code TEXT PRIMARY KEY,
5050+ request_id TEXT NOT NULL REFERENCES oauth_requests(request_id) ON DELETE CASCADE,
5151+ authorized_by TEXT,
5252+ authorized_at INTEGER,
5353+ expires_at INTEGER NOT NULL,
5454+ used BOOLEAN DEFAULT FALSE
5555+);
5656+5757+CREATE INDEX IF NOT EXISTS oauth_codes_expires_idx ON oauth_codes(expires_at);
5858+5959+CREATE TABLE IF NOT EXISTS oauth_tokens (
6060+ refresh_token TEXT UNIQUE NOT NULL,
6161+ client_id TEXT NOT NULL,
6262+ did TEXT NOT NULL,
6363+ dpop_jkt TEXT,
6464+ scope TEXT NOT NULL,
6565+ expires_at INTEGER NOT NULL
6666+);
6767+6868+CREATE INDEX IF NOT EXISTS oauth_tokens_refresh_idx ON oauth_tokens(refresh_token);
6969+7070+CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_requests
7171+AFTER INSERT ON oauth_requests
7272+BEGIN
7373+ DELETE FROM oauth_requests WHERE expires_at < unixepoch() * 1000;
7474+END;
7575+7676+CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_codes
7777+AFTER INSERT ON oauth_codes
7878+BEGIN
7979+ DELETE FROM oauth_codes WHERE expires_at < unixepoch() * 1000 OR used = 1;
8080+END;
8181+8282+CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_tokens
8383+AFTER INSERT ON oauth_tokens
8484+BEGIN
8585+ DELETE FROM oauth_tokens WHERE expires_at < unixepoch() * 1000;
8686+END;
+4
migrations/002_track_oauth_sessions.sql
···11+ALTER TABLE oauth_tokens ADD COLUMN created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP;
22+ALTER TABLE oauth_tokens ADD COLUMN last_refreshed_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP;
33+44+CREATE INDEX IF NOT EXISTS oauth_tokens_did_idx ON oauth_tokens(did);
+3
pegasus/lib/api/oauth_/token.ml
···7676 () )
7777 in
7878 let now_sec = int_of_float (Unix.gettimeofday ()) in
7979+ let now_ms = Util.now_ms () in
7980 let expires_in =
8081 Constants.access_token_expiry_ms / 1000
8182 in
···101102 ; did
102103 ; dpop_jkt= proof.jkt
103104 ; scope= orig_req.scope
105105+ ; created_at= now_ms
106106+ ; last_refreshed_at= now_ms
104107 ; expires_at }
105108 in
106109 let nonce = Dpop.next_nonce () in
+1-153
pegasus/lib/data_store.ml
···2121open Types
22222323module Queries = struct
2424- let create_tables conn =
2525- let$! () =
2626- [%rapper
2727- execute
2828- {sql| CREATE TABLE IF NOT EXISTS actors (
2929- id INTEGER PRIMARY KEY,
3030- did TEXT NOT NULL UNIQUE,
3131- handle TEXT NOT NULL UNIQUE,
3232- email TEXT NOT NULL UNIQUE,
3333- password_hash TEXT NOT NULL,
3434- signing_key TEXT NOT NULL,
3535- preferences TEXT NOT NULL,
3636- created_at INTEGER NOT NULL,
3737- deactivated_at INTEGER
3838- )
3939- |sql}]
4040- () conn
4141- in
4242- let$! () =
4343- [%rapper
4444- execute
4545- {sql| CREATE INDEX IF NOT EXISTS actors_did_idx ON actors (did);
4646- CREATE INDEX IF NOT EXISTS actors_handle_idx ON actors (handle);
4747- CREATE INDEX IF NOT EXISTS actors_email_idx ON actors (email);
4848- |sql}]
4949- () conn
5050- in
5151- let$! () =
5252- [%rapper
5353- execute
5454- {sql| CREATE TABLE IF NOT EXISTS invite_codes (
5555- code TEXT PRIMARY KEY,
5656- did TEXT NOT NULL,
5757- remaining INTEGER NOT NULL
5858- )
5959- |sql}]
6060- () conn
6161- in
6262- let$! () =
6363- [%rapper
6464- execute
6565- {sql| CREATE TABLE IF NOT EXISTS firehose (
6666- seq INTEGER PRIMARY KEY,
6767- time INTEGER NOT NULL,
6868- t TEXT NOT NULL,
6969- data BLOB NOT NULL
7070- )
7171- |sql}]
7272- () conn
7373- in
7474- let$! () =
7575- [%rapper
7676- execute
7777- (* no need to store issued tokens, just revoked ones; stolen from millipds https://github.com/DavidBuchanan314/millipds/blob/8f89a01e7d367a2a46f379960e9ca50347dcce71/src/millipds/database.py#L253 *)
7878- {sql| CREATE TABLE IF NOT EXISTS revoked_tokens (
7979- did TEXT NOT NULL,
8080- jti TEXT NOT NULL,
8181- revoked_at INTEGER NOT NULL,
8282- PRIMARY KEY (did, jti)
8383- )
8484- |sql}]
8585- () conn
8686- in
8787- let$! () =
8888- [%rapper
8989- execute
9090- {sql| CREATE TABLE IF NOT EXISTS oauth_requests (
9191- request_id TEXT PRIMARY KEY,
9292- client_id TEXT NOT NULL,
9393- request_data TEXT NOT NULL,
9494- dpop_jkt TEXT,
9595- expires_at INTEGER NOT NULL,
9696- created_at INTEGER NOT NULL
9797- )
9898- |sql}]
9999- () conn
100100- in
101101- let$! () =
102102- [%rapper
103103- execute
104104- {sql| CREATE TABLE IF NOT EXISTS oauth_codes (
105105- code TEXT PRIMARY KEY,
106106- request_id TEXT NOT NULL REFERENCES oauth_requests(request_id) ON DELETE CASCADE,
107107- authorized_by TEXT,
108108- authorized_at INTEGER,
109109- expires_at INTEGER NOT NULL,
110110- used BOOLEAN DEFAULT FALSE
111111- )
112112- |sql}]
113113- () conn
114114- in
115115- let$! () =
116116- [%rapper
117117- execute
118118- {sql| CREATE TABLE IF NOT EXISTS oauth_tokens (
119119- refresh_token TEXT UNIQUE NOT NULL,
120120- client_id TEXT NOT NULL,
121121- did TEXT NOT NULL,
122122- dpop_jkt TEXT,
123123- scope TEXT NOT NULL,
124124- expires_at INTEGER NOT NULL
125125- )
126126- |sql}]
127127- () conn
128128- in
129129- let$! () =
130130- [%rapper
131131- execute
132132- {sql| CREATE INDEX IF NOT EXISTS oauth_requests_expires_idx ON oauth_requests(expires_at);
133133- CREATE INDEX IF NOT EXISTS oauth_codes_expires_idx ON oauth_codes(expires_at);
134134- CREATE INDEX IF NOT EXISTS oauth_tokens_refresh_idx ON oauth_tokens(refresh_token);
135135- |sql}]
136136- () conn
137137- in
138138- let$! () =
139139- [%rapper
140140- execute
141141- {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_requests
142142- AFTER INSERT ON oauth_requests
143143- BEGIN
144144- DELETE FROM oauth_requests WHERE expires_at < unixepoch() * 1000;
145145- END
146146- |sql}
147147- syntax_off]
148148- () conn
149149- in
150150- let$! () =
151151- [%rapper
152152- execute
153153- {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_codes
154154- AFTER INSERT ON oauth_codes
155155- BEGIN
156156- DELETE FROM oauth_codes WHERE expires_at < unixepoch() * 1000 OR used = 1;
157157- END
158158- |sql}
159159- syntax_off]
160160- () conn
161161- in
162162- let$! () =
163163- [%rapper
164164- execute
165165- {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_tokens
166166- AFTER INSERT ON oauth_tokens
167167- BEGIN
168168- DELETE FROM oauth_tokens WHERE expires_at < unixepoch() * 1000;
169169- END
170170- |sql}
171171- syntax_off]
172172- () conn
173173- in
174174- Lwt.return_ok ()
175175-17624 let create_actor =
17725 [%rapper
17826 execute
···315163 Util.mkfile_p Util.Constants.pegasus_db_filepath ~perm:0o644 ;
316164 Util.connect_sqlite ?create ?write Util.Constants.pegasus_db_location
317165318318-let init conn : unit Lwt.t = Util.use_pool conn Queries.create_tables
166166+let init conn : unit Lwt.t = Migrations.run_migrations conn
319167320168let create_actor ~did ~handle ~email ~password ~signing_key conn =
321169 let password_hash = Bcrypt.hash password |> Bcrypt.string_of_hash in
+114
pegasus/lib/migrations.ml
···11+[@@@ocaml.warning "-33"]
22+33+open Lwt.Infix
44+55+type migration = {id: int; name: string; applied_at: int}
66+77+module Queries = struct
88+ open Util.Rapper
99+ open Util.Syntax
1010+1111+ let create_migrations_table =
1212+ [%rapper
1313+ execute
1414+ {sql| CREATE TABLE IF NOT EXISTS schema_migrations (
1515+ id INTEGER PRIMARY KEY,
1616+ name TEXT NOT NULL,
1717+ applied_at INTEGER NOT NULL
1818+ )
1919+ |sql}]
2020+ ()
2121+2222+ let get_applied_migrations =
2323+ [%rapper
2424+ get_many
2525+ {sql| SELECT @int{id}, @string{name}, @int{applied_at}
2626+ FROM schema_migrations
2727+ ORDER BY id ASC
2828+ |sql}
2929+ record_out]
3030+ ()
3131+3232+ let record_migration =
3333+ [%rapper
3434+ execute
3535+ {sql| INSERT INTO schema_migrations (id, name, applied_at)
3636+ VALUES (%int{id}, %string{name}, %int{applied_at})
3737+ |sql}]
3838+end
3939+4040+let execute_raw db_path sql =
4141+ let db = Sqlite3.db_open db_path in
4242+ try
4343+ let rc = Sqlite3.exec db sql in
4444+ let _ = Sqlite3.db_close db in
4545+ match rc with
4646+ | Sqlite3.Rc.OK ->
4747+ Lwt.return_ok ()
4848+ | _ ->
4949+ let err_msg = Sqlite3.errmsg db in
5050+ Lwt.return_error (Failure ("sql error: " ^ err_msg))
5151+ with e ->
5252+ let _ = Sqlite3.db_close db in
5353+ Lwt.return_error e
5454+5555+let parse_migration_filename filename =
5656+ try
5757+ let regex = Str.regexp "^\\([0-9]+\\)_\\(.*\\)\\.sql$" in
5858+ if Str.string_match regex filename 0 then
5959+ let id = Str.matched_group 1 filename |> int_of_string in
6060+ let name = Str.matched_group 2 filename in
6161+ Some (id, name, filename)
6262+ else None
6363+ with _ -> None
6464+6565+let read_migration_files migrations_dir =
6666+ try
6767+ let files = Sys.readdir migrations_dir |> Array.to_list in
6868+ let migrations =
6969+ files
7070+ |> List.filter_map (fun filename ->
7171+ match parse_migration_filename filename with
7272+ | Some (id, name, _) ->
7373+ let full_path = Filename.concat migrations_dir filename in
7474+ Some (id, name, full_path)
7575+ | None ->
7676+ None )
7777+ |> List.sort (fun (id1, _, _) (id2, _, _) -> compare id1 id2)
7878+ in
7979+ Lwt.return migrations
8080+ with Sys_error _ -> Lwt.return []
8181+8282+let run_migration conn (id, name, filepath) =
8383+ let%lwt () = Lwt_io.printlf "running migration %03d: %s" id name in
8484+ let%lwt sql_content =
8585+ Lwt_io.with_file ~mode:Lwt_io.Input filepath (fun ic -> Lwt_io.read ic)
8686+ in
8787+ let%lwt result = execute_raw Util.Constants.pegasus_db_filepath sql_content in
8888+ let%lwt () =
8989+ match result with Ok () -> Lwt.return_unit | Error e -> raise e
9090+ in
9191+ let applied_at = Util.now_ms () in
9292+ let%lwt () =
9393+ Util.use_pool conn (Queries.record_migration ~id ~name ~applied_at)
9494+ in
9595+ Lwt_io.printlf "migration %03d applied successfully" id
9696+9797+let run_migrations ?(migrations_dir = "migrations") conn =
9898+ let%lwt () = Util.use_pool conn Queries.create_migrations_table in
9999+ let%lwt applied =
100100+ Util.use_pool conn Queries.get_applied_migrations
101101+ >|= List.map (fun m -> m.id)
102102+ in
103103+ let%lwt available = read_migration_files migrations_dir in
104104+ let pending =
105105+ List.filter (fun (id, _, _) -> not (List.mem id applied)) available
106106+ in
107107+ match pending with
108108+ | [] ->
109109+ Lwt_io.printl "no pending migrations"
110110+ | _ ->
111111+ let%lwt () =
112112+ Lwt_io.printlf "found %d pending migrations" (List.length pending)
113113+ in
114114+ Lwt_list.iter_s (run_migration conn) pending