From 81ea7e331a30562ea337113d2f1a6e20d0087007 Mon Sep 17 00:00:00 2001 From: Michael Riezler Date: Sun, 3 Jul 2022 14:04:12 +0200 Subject: [PATCH] update(server): add missing project id's --- VERSION | 2 +- packages/rust/sdk/src/test.rs | 5 - scripts/seeds/data/projects.ts | 2 +- scripts/seeds/dev/init.ts | 3 + .../2020-11-28-215246_create_session/up.sql | 2 +- .../2021-01-24-203105_create_passwords/up.sql | 1 + .../2021-02-12-210113_create_template/up.sql | 6 +- .../up.sql | 1 + .../up.sql | 1 + .../2021-10-02-203303_create_api_keys/up.sql | 1 + .../2022-05-02-141736_create_oauth/up.sql | 1 + server/sqlx-data.json | 500 +++++++++--------- server/src/admin/data/admin.rs | 2 +- server/src/api_key/data.rs | 4 +- server/src/api_key/generate.rs | 17 +- server/src/api_key/sql/create_api_key.sql | 4 +- server/src/oauth/data/mod.rs | 4 +- server/src/oauth/google.rs | 3 +- .../oauth/sql/insert_oauth_request_state.sql | 4 +- server/src/password/data/password.rs | 8 +- server/src/password/reset.rs | 4 +- server/src/password/signin.rs | 5 +- server/src/password/signup.rs | 2 +- server/src/password/sql/create_password.sql | 4 +- server/src/password/sql/set_password.sql | 4 +- .../src/passwordless/request_passwordless.rs | 2 +- server/src/passwordless/verify.rs | 3 +- server/src/session/data/session.rs | 9 +- server/src/session/refresh.rs | 2 +- server/src/session/sql/token_is_valid.sql | 4 +- .../sql/insert_default_template_data.sql | 4 +- .../sql/insert_default_translations.sql | 4 +- server/src/template/sql/set_template.sql | 3 +- server/src/template/sql/set_translation.sql | 7 +- server/src/user/change_email.rs | 2 +- server/src/user/data/email.rs | 142 ++--- server/src/user/data/user.rs | 9 + server/src/user/delete_account.rs | 2 +- server/src/user/set_password.rs | 2 +- server/src/user/sign_out.rs | 7 +- server/src/user/sql/create_user.sql | 3 +- .../user/sql/email/insert_change_request.sql | 4 +- server/src/user/sql/get_project_id.sql | 4 + tests/api_tests/src/api_key/utils.ts | 7 +- .../src/email_password/reset.test.ts | 105 ++-- .../src/template/translations.test.ts | 6 +- tests/api_tests/src/user/change_email.test.ts | 20 +- tests/api_tests/src/user/delete.test.ts | 6 +- tests/api_tests/src/user/sign_out.test.ts | 12 +- tests/api_tests/src/utils/user.ts | 6 +- tests/ui_e2e/tests/password/signup.spec.ts | 2 + tests/ui_e2e/tests/utils/user.ts | 6 +- 52 files changed, 554 insertions(+), 419 deletions(-) create mode 100644 server/src/user/sql/get_project_id.sql diff --git a/VERSION b/VERSION index ed2d50d1..c0a08f45 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -22.06.0-alpha \ No newline at end of file +22.7.0-alpha \ No newline at end of file diff --git a/packages/rust/sdk/src/test.rs b/packages/rust/sdk/src/test.rs index 550630ba..a4742a01 100644 --- a/packages/rust/sdk/src/test.rs +++ b/packages/rust/sdk/src/test.rs @@ -98,7 +98,6 @@ async fn verify_jwt() { let expire_at = Utc::now() + Duration::minutes(15); for keypair in keypairs.iter() { let payload = Claims { - iss: keypair.id, exp: expire_at.timestamp(), sub: Uuid::new_v4(), traits: vec![], @@ -121,7 +120,6 @@ async fn verify_token_expired() { let expire_at = Utc::now() - Duration::minutes(15); for keypair in keypairs.iter() { let payload = Claims { - iss: keypair.id, exp: expire_at.timestamp(), sub: Uuid::new_v4(), traits: vec![], @@ -145,7 +143,6 @@ async fn verify_token_fails_for_missing_key() { let keypair = keypairs.get(0).unwrap(); let payload = Claims { - iss: Uuid::new_v4(), exp: expire_at.timestamp(), sub: Uuid::new_v4(), traits: vec![], @@ -165,11 +162,9 @@ async fn verify_token_invalid_token() { let auth = AuthKeys::init("http://localhost:7000").await.unwrap(); let expire_at = Utc::now() + Duration::minutes(15); - let keypair = keypairs.get(0).unwrap(); let keypair2 = keypairs.get(1).unwrap(); let payload = Claims { - iss: keypair.id, exp: expire_at.timestamp(), sub: Uuid::new_v4(), traits: vec![], diff --git a/scripts/seeds/data/projects.ts b/scripts/seeds/data/projects.ts index f00c88c4..a89a4385 100644 --- a/scripts/seeds/data/projects.ts +++ b/scripts/seeds/data/projects.ts @@ -38,7 +38,7 @@ export let project = { export let projectSettings = { name: 'Development', - domain: 'http://localhost:4200', + domain: 'http://localhost:5000', project_id: project.id, } diff --git a/scripts/seeds/dev/init.ts b/scripts/seeds/dev/init.ts index e5d189c2..91809a7a 100644 --- a/scripts/seeds/dev/init.ts +++ b/scripts/seeds/dev/init.ts @@ -62,6 +62,7 @@ exports.seed = async function(knex: Knex) { user_id: u.id, alg: 'bcrypt', hash: password, + project_id: u.project_id, } }) ) @@ -93,6 +94,7 @@ exports.seed = async function(knex: Knex) { subject: t.subject, redirect_to: t.redirect_to, of_type: t.of_type, + project_id: t.project_id, } }) ) @@ -104,6 +106,7 @@ exports.seed = async function(knex: Knex) { template_id: t.id, language: 'en', content: t.translation, + project_id: t.project_id, } }) ) diff --git a/server/migrations/2020-11-28-215246_create_session/up.sql b/server/migrations/2020-11-28-215246_create_session/up.sql index 3630d307..96227dff 100644 --- a/server/migrations/2020-11-28-215246_create_session/up.sql +++ b/server/migrations/2020-11-28-215246_create_session/up.sql @@ -3,7 +3,7 @@ create table if not exists sessions ( id uuid primary key default uuid_generate_v4() , expire_at timestamptz not null default now() + '30 days' - , project_id uuid references projects(id) on delete cascade + , project_id uuid not null references projects(id) on delete cascade , created_at timestamptz not null default now() , public_key bytea not null , user_id uuid references users(id) on delete cascade diff --git a/server/migrations/2021-01-24-203105_create_passwords/up.sql b/server/migrations/2021-01-24-203105_create_passwords/up.sql index 2bab0bfd..d87e11a3 100644 --- a/server/migrations/2021-01-24-203105_create_passwords/up.sql +++ b/server/migrations/2021-01-24-203105_create_passwords/up.sql @@ -4,6 +4,7 @@ create type password_alg as enum('bcrypt', 'argon2id', 'sha1', 'scrypt', 'pbkdf2 create table if not exists passwords ( user_id uuid primary key references users(id) on delete cascade + , project_id uuid not null references projects(id) on delete cascade , alg password_alg not null default 'argon2id' , hash text not null , updated_at timestamptz not null default now() diff --git a/server/migrations/2021-02-12-210113_create_template/up.sql b/server/migrations/2021-02-12-210113_create_template/up.sql index ae19798f..a8fcd261 100644 --- a/server/migrations/2021-02-12-210113_create_template/up.sql +++ b/server/migrations/2021-02-12-210113_create_template/up.sql @@ -10,7 +10,8 @@ create table if not exists templates ); create table if not exists template_data - ( from_name text not null + ( project_id uuid not null references projects(id) on delete cascade + , from_name text not null , subject text not null , template_id uuid not null references templates(id) on delete cascade , redirect_to text not null @@ -19,7 +20,8 @@ create table if not exists template_data ); create table if not exists template_translations - ( template_id uuid not null references templates(id) on delete cascade + ( project_id uuid not null references projects(id) on delete cascade + , template_id uuid not null references templates(id) on delete cascade , language text not null default 'en' , content jsonb not null , primary key(template_id, language) diff --git a/server/migrations/2021-03-05-204923_create_refresh_access_tokens/up.sql b/server/migrations/2021-03-05-204923_create_refresh_access_tokens/up.sql index a92b2e68..72e15fa8 100644 --- a/server/migrations/2021-03-05-204923_create_refresh_access_tokens/up.sql +++ b/server/migrations/2021-03-05-204923_create_refresh_access_tokens/up.sql @@ -2,6 +2,7 @@ create table if not exists refresh_access_tokens ( id uuid primary key + , project_id uuid not null references projects(id) on delete cascade , session_id uuid not null references sessions(id) on delete cascade , expire_at timestamptz not null default now() + '30 minutes' ); \ No newline at end of file diff --git a/server/migrations/2021-09-18-195903_create_email_change_request/up.sql b/server/migrations/2021-09-18-195903_create_email_change_request/up.sql index 71365e6a..82f6a6e4 100644 --- a/server/migrations/2021-09-18-195903_create_email_change_request/up.sql +++ b/server/migrations/2021-09-18-195903_create_email_change_request/up.sql @@ -7,6 +7,7 @@ create table if not exists email_change_request , old_email text not null , new_email text not null , user_id uuid not null references users(id) on delete cascade + , project_id uuid not null references projects(id) on delete cascade , token text not null , reset_token text not null , state email_change_state not null default 'request' diff --git a/server/migrations/2021-10-02-203303_create_api_keys/up.sql b/server/migrations/2021-10-02-203303_create_api_keys/up.sql index 30f9a64b..9cf3dc1b 100644 --- a/server/migrations/2021-10-02-203303_create_api_keys/up.sql +++ b/server/migrations/2021-10-02-203303_create_api_keys/up.sql @@ -5,6 +5,7 @@ create table if not exists api_keys , token text not null , name text , user_id uuid not null references users(id) on delete cascade + , project_id uuid not null references projects(id) on delete cascade , expire_at timestamptz , created_at timestamptz not null default now() ); \ No newline at end of file diff --git a/server/migrations/2022-05-02-141736_create_oauth/up.sql b/server/migrations/2022-05-02-141736_create_oauth/up.sql index c84b3304..e9b735bc 100644 --- a/server/migrations/2022-05-02-141736_create_oauth/up.sql +++ b/server/migrations/2022-05-02-141736_create_oauth/up.sql @@ -24,6 +24,7 @@ create table if not exists oauth_data create table if not exists oauth_request_state ( request_id uuid primary key default uuid_generate_v4() + , project_id uuid not null references projects(id) on delete cascade , csrf_token text not null , pkce_code_verifier text , created_at timestamptz not null default now() diff --git a/server/sqlx-data.json b/server/sqlx-data.json index 7d258df5..512e34d4 100644 --- a/server/sqlx-data.json +++ b/server/sqlx-data.json @@ -1,23 +1,5 @@ { "db": "PostgreSQL", - "010a192687891d6df57b1cf699eb0453d50eae119aa812876a922d794127614d": { - "query": "with insert_template as (\r\n insert into templates(body, name, project_id, of_type)\r\n values ($1, $2, $3, 'view')\r\n on conflict (project_id, name) do update set body = $1\r\n returning id\r\n)\r\ninsert into template_data(from_name, subject, template_id, redirect_to, of_type)\r\nselect $4 as from_name\r\n , $5 as subject\r\n , insert_template.id as template_id\r\n , $6 as redirect_to\r\n , $7 as of_type\r\n from insert_template \r\non conflict (template_id, of_type)\r\n do update set from_name = $4\r\n , subject = $5\r\n , redirect_to = $6", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Uuid", - "Text", - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - } - }, "0138f9e9a57bc2abff5e20ec00a5e3e4787930f055bd64c5c6203ab913134304": { "query": "select project_id as id\r\n , public_key as key\r\n from project_keys ", "describe": { @@ -42,77 +24,33 @@ ] } }, - "01bab3838d76efb6fce3091fb00bbb819fa8eba91160107e12631a029e532a1d": { - "query": "with raw_template_data as (\r\n select *\r\n from jsonb_to_recordset($1)\r\n as x(\r\n id uuid\r\n , from_name text\r\n , subject text\r\n , redirect_to text\r\n , of_type text\r\n , template_type text\r\n )\r\n)\r\ninsert into template_data\r\nselect raw_template_data.from_name\r\n , '{{t.subject}}' as subject\r\n , raw_template_data.id as template_id\r\n , raw_template_data.redirect_to\r\n , raw_template_data.of_type\r\n from raw_template_data\r\n where raw_template_data.template_type = 'view'", + "0c3109c025e6384a9050ca914ede1c4faad7f6c5972102c90696e7eff973dc87": { + "query": "\r\nwith update_password as (\r\n insert into passwords (hash, user_id, alg, project_id)\r\n values ($2, $1, $3, $4)\r\n\ton conflict (user_id) do update\r\n set hash = $2\r\n , alg = $3\r\n\treturning user_id\r\n)\r\nupdate users\r\n set state = case when state = 'set_password'\r\n then 'active'\r\n else state\r\n end\r\n from update_password\r\n where users.id = update_password.user_id\r\n", "describe": { "columns": [], "parameters": { "Left": [ - "Jsonb" - ] - }, - "nullable": [] - } - }, - "08f5785fd03da32047a0331aa2ebe43a135c24de9886f89e5c81d926ea714753": { - "query": "\r\nselect users.id as \"sub\"\r\n , users.traits as \"traits\"\r\n , users.project_id as \"iss\"\r\n , extract(epoch from now() + interval '15 minutes')::numeric::bigint as \"exp!\"\r\n from api_keys\r\n join users on users.id = api_keys.user_id\r\n where api_keys.id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "sub", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "traits", - "type_info": "TextArray" - }, - { - "ordinal": 2, - "name": "iss", - "type_info": "Uuid" - }, - { - "ordinal": 3, - "name": "exp!", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - null - ] - } - }, - "0cc9374c85dfbb62e95cdc7797abe47cce3833cc151fd97d9f858bc33526cee9": { - "query": "\r\ninsert into api_keys(token, user_id, expire_at, name)\r\nvalues($1, $2, $3, $4)\r\nreturning id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Text", "Uuid", - "Timestamptz", - "Text" + "Text", + { + "Custom": { + "name": "password_alg", + "kind": { + "Enum": [ + "bcrypt", + "argon2id", + "sha1", + "scrypt", + "pbkdf2", + "md5" + ] + } + } + }, + "Uuid" ] }, - "nullable": [ - false - ] + "nullable": [] } }, "0d55d8318c4c8df2f34966d42cf7eb90354aeda4f181d8ab6717bc06a5699d35": { @@ -173,18 +111,6 @@ "nullable": [] } }, - "185559486165216884fc348ff29359b300e9421a1f24d7cbc02ead5c782389f6": { - "query": "with raw_template_data as (\r\n select *\r\n from jsonb_to_recordset($1)\r\n as x(\r\n id uuid\r\n , translation jsonb\r\n , template_type text\r\n )\r\n)\r\ninsert into template_translations\r\nselect raw_template_data.id as template_id\r\n , 'en' as language\r\n , raw_template_data.translation\r\n from raw_template_data\r\n where raw_template_data.template_type = 'view'", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Jsonb" - ] - }, - "nullable": [] - } - }, "206e748963b01c6332cc10ddc1addb99925d4418511c7a0a66673a53ca3bde8f": { "query": "\r\nselect password_alg as \"alg: PasswordAlg\"\r\n from users\r\n join project_settings on project_settings.project_id = users.project_id\r\n where users.id = $1", "describe": { @@ -219,29 +145,6 @@ ] } }, - "23516858fe2c9e78934be8c8dfb282071229814878933ca7275332edfc00840b": { - "query": "\r\nwith insert_user as (\r\n insert into users(email, project_id, provider_id, device_languages)\r\n values($1, $3, 'password', $4)\r\n returning id\r\n)\r\ninsert into passwords(user_id, alg, hash)\r\nselect insert_user.id as \"user_id\"\r\n , 'bcrypt' as \"alg\"\r\n , $2 as \"hash\"\r\n from insert_user \r\nreturning user_id as \"id\"", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Uuid", - "TextArray" - ] - }, - "nullable": [ - false - ] - } - }, "2ba1a6cdc73d23d31f1accab094eeca32f6588190f34f490ea327aeddfa5a01f": { "query": "update users\r\n set state = 'active'\r\n where id = $1\r\n and project_id = $2", "describe": { @@ -400,6 +303,31 @@ "nullable": [] } }, + "39267e5dafd250f26fb4707847665215730eb6b0021c8686f0a177c4e9c68dfe": { + "query": "\r\ninsert into email_change_request(old_email, new_email, user_id, token, reset_token, project_id)\r\nvalues($1, $2, $3, $4, $5, $6)\r\nreturning id\r\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Uuid", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + } + }, "3a388d6d2bcf254c7cdcb66582094090727e9925ac4c07ca0cb92c5ef4bc8bcc": { "query": "\r\nwith insert_admin as (\r\n insert into users\r\n ( email\r\n , project_id\r\n , traits\r\n , data\r\n , provider_id\r\n )\r\n values\r\n ( $1\r\n , $3\r\n , '{ \"Admin\" }'\r\n , '{}'::jsonb\r\n , 'email'\r\n )\r\n returning id\r\n)\r\ninsert into passwords(user_id, alg, hash)\r\nselect insert_admin.id as \"user_id\"\r\n , 'bcrypt' as \"alg\"\r\n , $2 as \"hash\"\r\n from insert_admin \r\nreturning user_id as \"id\"", "describe": { @@ -422,15 +350,30 @@ ] } }, - "3ad2d5c06ab2346c3721e62919c438ebb6ad8ccd4754e200bc03949e0db8b71a": { - "query": "\r\ninsert into oauth_request_state(request_id, csrf_token, pkce_code_verifier)\r\nvalues($1, $2, $3)\r\n", + "416a4fe50274643bcb47e71e42396e69cf6fa955dbd4ae04b5a7f70e1d4f8c8e": { + "query": "\r\ninsert into passwords (hash, user_id, alg, project_id)\r\nvalues ($2, $1, $3, $4)\r\non conflict (user_id) do update\r\n set hash = $2\r\n , alg = $3", "describe": { "columns": [], "parameters": { "Left": [ "Uuid", "Text", - "Text" + { + "Custom": { + "name": "password_alg", + "kind": { + "Enum": [ + "bcrypt", + "argon2id", + "sha1", + "scrypt", + "pbkdf2", + "md5" + ] + } + } + }, + "Uuid" ] }, "nullable": [] @@ -450,6 +393,18 @@ "nullable": [] } }, + "4460703e91a3cb158015326be2a05e87bf1364ee8aed8afe8b89e2430285da23": { + "query": "with raw_template_data as (\r\n select *\r\n from jsonb_to_recordset($1)\r\n as x(\r\n id uuid\r\n , from_name text\r\n , subject text\r\n , redirect_to text\r\n , of_type text\r\n , template_type text\r\n , project_id uuid\r\n )\r\n)\r\ninsert into template_data\r\nselect raw_template_data.project_id\r\n , raw_template_data.from_name\r\n , '{{t.subject}}' as subject\r\n , raw_template_data.id as template_id\r\n , raw_template_data.redirect_to\r\n , raw_template_data.of_type\r\n from raw_template_data\r\n where raw_template_data.template_type = 'view'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Jsonb" + ] + }, + "nullable": [] + } + }, "4af88c85db71db29320531dde8e69c63e7e4cfa34e88272cd2ca1af9088b164f": { "query": "\r\ndelete from projects\r\n where id = $1\r\n and is_admin = false", "describe": { @@ -488,6 +443,21 @@ ] } }, + "4e9e55e37c80ec9571dd9378856aec501dcc6508f08ab0f6264a26ba5f94054c": { + "query": "with template as (\r\n\tselect id, project_id\r\n\t from templates\r\n\t where project_id = $1\r\n\t and name = $2\r\n)\r\ninsert into template_translations(project_id, template_id, language, content)\r\nselect template.project_id as \"project_id\"\r\n\t , template.id as template_id\r\n , $3 as language\r\n , $4 as content\r\n from template\r\non conflict (template_id, language)\r\n do update set content = $4", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Jsonb" + ] + }, + "nullable": [] + } + }, "4fedcf9d80fdffa38567618645d7818e996c374433da5a0ee98bbd680e70e48b": { "query": "\r\nselect settings\r\n from oauth\r\n where project_id = $1\r\n and provider = $2\r\n", "describe": { @@ -587,10 +557,22 @@ false, false, true, - true + false ] } }, + "5c26ece35126112e67f3a78599ce39a0ee35112257f3c3dc13298a4234cf25bd": { + "query": "with raw_template_data as (\r\n select *\r\n from jsonb_to_recordset($1)\r\n as x(\r\n id uuid\r\n , translation jsonb\r\n , template_type text\r\n , project_id uuid\r\n )\r\n)\r\ninsert into template_translations\r\nselect raw_template_data.project_id \r\n , raw_template_data.id as template_id\r\n , 'en' as language\r\n , raw_template_data.translation\r\n from raw_template_data\r\n where raw_template_data.template_type = 'view'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Jsonb" + ] + }, + "nullable": [] + } + }, "5ce52abf02c9c80245ce76c5cfc9912d64902d54ca53d387703ab38e7dc4a007": { "query": "\r\ninsert into oauth_data(provider_id, provider, email, user_id, project_id)\r\nvalues($1, $2, $3, $4, $5)\r\non conflict (provider_id, provider, project_id, user_id)\r\n do update set email = excluded.email\r\n", "describe": { @@ -1033,7 +1015,7 @@ false, false, true, - true + false ] } }, @@ -1079,7 +1061,46 @@ false, true, false, - true + false + ] + } + }, + "7e5add493c20fb35bc954cc9b4a7a720058bd8bcd35a7d1a0faafcc580b50c80": { + "query": "\r\ninsert into oauth_request_state(request_id, csrf_token, pkce_code_verifier, project_id)\r\nvalues($1, $2, $3, $4)\r\n", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Uuid" + ] + }, + "nullable": [] + } + }, + "7ecd6e134f8a6fda6407195806ae92f1db9aedf5d4cb3c006d79212f21786966": { + "query": "\r\ninsert into api_keys(token, user_id, expire_at, name, project_id)\r\nvalues($1, $2, $3, $4, $5)\r\nreturning id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid", + "Timestamptz", + "Text", + "Uuid" + ] + }, + "nullable": [ + false ] } }, @@ -1115,6 +1136,26 @@ "nullable": [] } }, + "896132321ee9da5370e5360c0377e9566335278f2fdbc46efcba21039f6c57d8": { + "query": "\r\nselect project_id\r\n from users\r\n where users.id = $1\r\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + } + }, "8b372f5603257a2a217b7fcbde685fea1a1e877b300796c40f2432ee194b1c00": { "query": "\r\nselect api_keys.token\r\n , api_keys.expire_at\r\n from api_keys\r\n where id = $1\r\n", "describe": { @@ -1178,6 +1219,29 @@ "nullable": [] } }, + "90c6bd29578dabf01973a17751fdb4cc19694100fb1dc5a68fafad5ecce87b4b": { + "query": "\r\nwith insert_user as (\r\n insert into users(email, project_id, provider_id, device_languages)\r\n values($1, $3, 'password', $4)\r\n returning id\r\n)\r\ninsert into passwords(user_id, alg, hash, project_id)\r\nselect insert_user.id as \"user_id\"\r\n , 'bcrypt' as \"alg\"\r\n , $2 as \"hash\"\r\n , $3 as \"project_id\"\r\n from insert_user \r\nreturning user_id as \"id\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Uuid", + "TextArray" + ] + }, + "nullable": [ + false + ] + } + }, "950a688987b96583dddec23083020816b69cc6b6b8bb759e7b37276229dcfdc6": { "query": "select template_data.from_name\r\n , template_data.subject\r\n , templates.body\r\n , template_data.redirect_to\r\n , template_data.of_type\r\n , templates.project_id\r\n from templates\r\n join template_data on template_data.template_id = templates.id\r\n where templates.project_id = $1\r\n and templates.name = $2", "describe": { @@ -1229,6 +1293,24 @@ ] } }, + "9671f0797a1713fae7f5749b86102c9427862707ca2265f79b187bda12d0b9b5": { + "query": "with insert_template as (\r\n insert into templates(body, name, project_id, of_type)\r\n values ($1, $2, $3, 'view')\r\n on conflict (project_id, name) do update set body = $1\r\n returning id\r\n)\r\ninsert into template_data(from_name, subject, template_id, redirect_to, of_type, project_id)\r\nselect $4 as from_name\r\n , $5 as subject\r\n , insert_template.id as template_id\r\n , $6 as redirect_to\r\n , $7 as of_type\r\n , $3 as \"project_id\"\r\n from insert_template \r\non conflict (template_id, of_type)\r\n do update set from_name = $4\r\n , subject = $5\r\n , redirect_to = $6", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Uuid", + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + } + }, "9b8764aafecaf838cda3e6706b35bf72d0893d426918b654435bc47e279a6c7f": { "query": "with confirm_token as (\r\n update passwordless\r\n set confirmed = True\r\n where id = $1\r\n returning id, email, project_id\r\n)\r\nupdate passwordless\r\n set is_valid = False\r\n from confirm_token\r\n where passwordless.email = confirm_token.email\r\n and passwordless.project_id = confirm_token.project_id\r\n and passwordless.id != confirm_token.id", "describe": { @@ -1241,6 +1323,61 @@ "nullable": [] } }, + "9b90ed6b61a4673a6bcbb83926104686107e3b2334b881b5e3db7e384bf67311": { + "query": "with add_token as (\r\n insert into refresh_access_tokens(id, session_id, expire_at, project_id)\r\n values($1, $2, $3, $4)\r\n on conflict(id) do nothing\r\n returning id\r\n)\r\nselect count(add_token.id) = 1 as is_valid\r\n from add_token", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "is_valid", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Timestamptz", + "Uuid" + ] + }, + "nullable": [ + null + ] + } + }, + "a109638fd631d36aa396e8cca03a5a48ecb7f90af263648df521909a36618849": { + "query": "\r\nselect users.id as \"sub\"\r\n , users.traits as \"traits\"\r\n , extract(epoch from now() + interval '15 minutes')::numeric::bigint as \"exp!\"\r\n from api_keys\r\n join users on users.id = api_keys.user_id\r\n where api_keys.id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sub", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "traits", + "type_info": "TextArray" + }, + { + "ordinal": 2, + "name": "exp!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + null + ] + } + }, "a34b6dece676dae560a64665c0ed7498254d360162fe7902076f366c9f735bc6": { "query": "select body\r\n , name\r\n from templates\r\n where of_type = 'index'\r\n or of_type = 'component'", "describe": { @@ -1265,21 +1402,6 @@ ] } }, - "a6116447548c62619883e58ac80a216d7ee585f378370d01a2de673b0f39b917": { - "query": "with template as (\r\n\tselect id\r\n\t from templates\r\n\t where project_id = $1\r\n\t and name = $2\r\n)\r\ninsert into template_translations(template_id, language, content)\r\nselect template.id as template_id\r\n , $3 as language\r\n , $4 as content\r\n from template\r\non conflict (template_id, language)\r\n do update set content = $4", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Jsonb" - ] - }, - "nullable": [] - } - }, "a66a8b419e00097a011163435afed4b18704f2e785d56bf6852f0ecaa2605d42": { "query": "update projects\r\n set flags = $2\r\n where id = $1 ", "describe": { @@ -1512,28 +1634,6 @@ "nullable": [] } }, - "ba82bc0f28255dab9075f69ec7dd2a06bbf1c5d3f755ad6cd0de42ed244adbef": { - "query": "with add_token as (\r\n insert into refresh_access_tokens(id, session_id, expire_at)\r\n values($1, $2, $3)\r\n on conflict(id) do nothing\r\n returning id\r\n)\r\nselect count(add_token.id) = 1 as is_valid\r\n from add_token", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "is_valid", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Timestamptz" - ] - }, - "nullable": [ - null - ] - } - }, "ba8a195cc1204bb3fa12ee4e975d9e7c784c4765b681e4e39d20d77110c25c43": { "query": "select count(users.id) as total_users\r\n from users\r\n where project_id = $1", "describe": { @@ -1768,62 +1868,6 @@ ] } }, - "d2626d6b6f997289e21d78c74cb39f30fa99758edb955c047ceb32045ca04072": { - "query": "\r\nwith update_password as (\r\n insert into passwords (hash, user_id, alg)\r\n values ($2, $1, $3)\r\n\ton conflict (user_id) do update\r\n set hash = $2\r\n , alg = $3\r\n\treturning user_id\r\n)\r\nupdate users\r\n set state = case when state = 'set_password'\r\n then 'active'\r\n else state\r\n end\r\n from update_password\r\n where users.id = update_password.user_id\r\n", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - { - "Custom": { - "name": "password_alg", - "kind": { - "Enum": [ - "bcrypt", - "argon2id", - "sha1", - "scrypt", - "pbkdf2", - "md5" - ] - } - } - } - ] - }, - "nullable": [] - } - }, - "d2f72625f2ccadec3be094ec627557037e142759485bb76a2945c407d83a17d8": { - "query": "\r\ninsert into passwords (hash, user_id, alg)\r\nvalues ($2, $1, $3)\r\non conflict (user_id) do update\r\n set hash = $2\r\n , alg = $3", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - { - "Custom": { - "name": "password_alg", - "kind": { - "Enum": [ - "bcrypt", - "argon2id", - "sha1", - "scrypt", - "pbkdf2", - "md5" - ] - } - } - } - ] - }, - "nullable": [] - } - }, "d3596e0e0fdaf69f4e9c502119f1d62b37bc423a6222868c06649b154bd1dade": { "query": "select domain\r\n from project_settings\r\n where project_id = $1", "describe": { @@ -2127,30 +2171,6 @@ "nullable": [] } }, - "ee2492403a550bdcb1b9a22795268caec937a80d707ac7597f1980ba961b2cd1": { - "query": "\r\ninsert into email_change_request(old_email, new_email, user_id, token, reset_token)\r\nvalues($1, $2, $3, $4, $5)\r\nreturning id\r\n", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Uuid", - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - } - }, "f2b9665414dba813517eee498f03ca85f747ff4543c7a0cffe4f321de4fdcd67": { "query": "update projects\r\n set is_admin = true\r\n , flags = '{ \"auth::signin\", \"method::email_password\" }'\r\n where id = $1", "describe": { diff --git a/server/src/admin/data/admin.rs b/server/src/admin/data/admin.rs index ff2133ea..c02f5f17 100644 --- a/server/src/admin/data/admin.rs +++ b/server/src/admin/data/admin.rs @@ -173,7 +173,7 @@ impl Admin { if let Some(password) = user.password { let alg = ProjectData::password_alg(&pool, &user.project_id).await?; - Password::create_password(&pool, &row.id, &password, &alg).await?; + Password::create_password(&pool, &row.id, &password, &alg, &user.project_id).await?; } Ok(row.id) diff --git a/server/src/api_key/data.rs b/server/src/api_key/data.rs index e402f819..037c7402 100644 --- a/server/src/api_key/data.rs +++ b/server/src/api_key/data.rs @@ -1,12 +1,12 @@ use crate::crypto::Token; use crate::response::error::ApiError; -use vulpo::Claims; use base64; use chrono::{DateTime, Utc}; use sqlx::PgPool; use std::str::{self, FromStr}; use uuid::Uuid; +use vulpo::Claims; pub struct ApiKey { pub api_key: String, @@ -24,6 +24,7 @@ impl ApiKey { user_id: &Uuid, expire_at: &Option>, name: &Option, + project_id: &Uuid, ) -> Result { let default_name = String::from(""); let name = name.as_ref().unwrap_or(&default_name); @@ -33,6 +34,7 @@ impl ApiKey { user_id, *expire_at, name, + project_id, ) .fetch_one(pool) .await diff --git a/server/src/api_key/generate.rs b/server/src/api_key/generate.rs index f8d03920..60670d3c 100644 --- a/server/src/api_key/generate.rs +++ b/server/src/api_key/generate.rs @@ -1,6 +1,8 @@ use crate::api_key::data::ApiKey; + use crate::response::error::ApiError; use crate::session::data::AccessToken; +use crate::user::data::User; use crate::{crypto::Token, db::Db}; use base64; @@ -29,7 +31,20 @@ pub async fn generate( let hashed_token = Token::hash(&token)?; let user_id = access_token.sub(); - let id = ApiKey::insert(&pool, &hashed_token, &user_id, &body.expire_at, &body.name).await?; + + let project_id = User::project(&pool, &user_id) + .await? + .ok_or(ApiError::BadRequest)?; + + let id = ApiKey::insert( + &pool, + &hashed_token, + &user_id, + &body.expire_at, + &body.name, + &project_id, + ) + .await?; let token = format!("{}:{}", id, token); let api_key = base64::encode(token); diff --git a/server/src/api_key/sql/create_api_key.sql b/server/src/api_key/sql/create_api_key.sql index 50b4b83d..6e74b9d8 100644 --- a/server/src/api_key/sql/create_api_key.sql +++ b/server/src/api_key/sql/create_api_key.sql @@ -1,4 +1,4 @@ -insert into api_keys(token, user_id, expire_at, name) -values($1, $2, $3, $4) +insert into api_keys(token, user_id, expire_at, name, project_id) +values($1, $2, $3, $4, $5) returning id \ No newline at end of file diff --git a/server/src/oauth/data/mod.rs b/server/src/oauth/data/mod.rs index 79cb50d1..fe8cf5bc 100644 --- a/server/src/oauth/data/mod.rs +++ b/server/src/oauth/data/mod.rs @@ -19,12 +19,14 @@ impl OAuthRequestState { request_id: Uuid, csrf_token: Option<&str>, pkce_code_verifier: Option<&str>, + project_id: &Uuid, ) -> Result<(), ApiError> { sqlx::query_file!( "src/oauth/sql/insert_oauth_request_state.sql", request_id, csrf_token, - pkce_code_verifier + pkce_code_verifier, + project_id, ) .execute(pool) .await diff --git a/server/src/oauth/google.rs b/server/src/oauth/google.rs index 5501701b..a505c7a4 100644 --- a/server/src/oauth/google.rs +++ b/server/src/oauth/google.rs @@ -63,6 +63,7 @@ pub async fn get_auth_url( body.request_id, Some(csrf_state.secret()), Some(pkce_code_verifier.secret()), + &project.id, ) .await?; @@ -165,7 +166,7 @@ pub async fn exchange_code( public_key: body.public_key.to_owned(), user_id: Some(user.id), expire_at: Utc::now() + Duration::days(30), - project_id: Some(project.id), + project_id: project.id, }; let session = Session::create(&db, session).await?; diff --git a/server/src/oauth/sql/insert_oauth_request_state.sql b/server/src/oauth/sql/insert_oauth_request_state.sql index 2f5bec39..87eed683 100644 --- a/server/src/oauth/sql/insert_oauth_request_state.sql +++ b/server/src/oauth/sql/insert_oauth_request_state.sql @@ -1,3 +1,3 @@ -insert into oauth_request_state(request_id, csrf_token, pkce_code_verifier) -values($1, $2, $3) +insert into oauth_request_state(request_id, csrf_token, pkce_code_verifier, project_id) +values($1, $2, $3, $4) diff --git a/server/src/password/data/password.rs b/server/src/password/data/password.rs index 76d027fe..aca84714 100644 --- a/server/src/password/data/password.rs +++ b/server/src/password/data/password.rs @@ -48,6 +48,7 @@ impl Password { user_id: &Uuid, password: &str, alg: &PasswordAlg, + project_id: &Uuid, ) -> Result<(), ApiError> { let password = Password::hash(password, alg).map_err(|_| ApiError::InternalServerError)?; @@ -55,7 +56,8 @@ impl Password { "src/password/sql/set_password.sql", user_id, password, - alg as &PasswordAlg + alg as &PasswordAlg, + project_id, ) .execute(pool) .await @@ -69,6 +71,7 @@ impl Password { user_id: &Uuid, password: &str, alg: &PasswordAlg, + project_id: &Uuid, ) -> Result<(), ApiError> { let password = Password::hash(password, alg).map_err(|_| ApiError::InternalServerError)?; @@ -76,7 +79,8 @@ impl Password { "src/password/sql/create_password.sql", user_id, password, - alg as &PasswordAlg + alg as &PasswordAlg, + project_id, ) .execute(pool) .await diff --git a/server/src/password/reset.rs b/server/src/password/reset.rs index d6709d6d..9bbc2c5a 100644 --- a/server/src/password/reset.rs +++ b/server/src/password/reset.rs @@ -91,7 +91,7 @@ pub struct ResetPassword { pub async fn password_reset( pool: Db, body: Json, - _project: Project, + project: Project, ) -> Result { if body.password1 != body.password2 { return Err(ApiError::ResetPasswordMismatch); @@ -114,7 +114,7 @@ pub async fn password_reset( // todo: move "remove password reset token" into "set_password" query PasswordReset::remove(&pool, &reset.user_id).await?; let alg = ProjectData::password_alg_by_user(&pool, &reset.user_id).await?; - Password::set_password(&pool, &reset.user_id, &body.password1, &alg).await?; + Password::set_password(&pool, &reset.user_id, &body.password1, &alg, &project.id).await?; Ok(Status::Ok) } diff --git a/server/src/password/signin.rs b/server/src/password/signin.rs index a11a9ea5..0ada96dc 100644 --- a/server/src/password/signin.rs +++ b/server/src/password/signin.rs @@ -55,7 +55,8 @@ pub async fn sign_in( let current_alg = ProjectData::password_alg(&pool, &project.id).await?; if current_alg != password.alg { - Password::create_password(&pool, &user.id, &body.password, ¤t_alg).await?; + Password::create_password(&pool, &user.id, &body.password, ¤t_alg, &project.id) + .await?; } if user.state == UserState::Disabled { @@ -67,7 +68,7 @@ pub async fn sign_in( public_key: body.public_key.to_owned(), user_id: Some(user.id), expire_at: Utc::now() + Duration::days(30), - project_id: Some(project.id), + project_id: project.id, }; let session = Session::create(&pool, session).await?; diff --git a/server/src/password/signup.rs b/server/src/password/signup.rs index d231be74..228f2bae 100644 --- a/server/src/password/signup.rs +++ b/server/src/password/signup.rs @@ -63,7 +63,7 @@ pub async fn sign_up( public_key: body.public_key.to_owned(), user_id: Some(user_id), expire_at: Utc::now() + Duration::days(30), - project_id: Some(project.id), + project_id: project.id, }; let session = Session::create(&pool, session).await?; diff --git a/server/src/password/sql/create_password.sql b/server/src/password/sql/create_password.sql index 78ccb818..e65dbe53 100644 --- a/server/src/password/sql/create_password.sql +++ b/server/src/password/sql/create_password.sql @@ -1,6 +1,6 @@ -insert into passwords (hash, user_id, alg) -values ($2, $1, $3) +insert into passwords (hash, user_id, alg, project_id) +values ($2, $1, $3, $4) on conflict (user_id) do update set hash = $2 , alg = $3 \ No newline at end of file diff --git a/server/src/password/sql/set_password.sql b/server/src/password/sql/set_password.sql index cc8819a3..53a0b5df 100644 --- a/server/src/password/sql/set_password.sql +++ b/server/src/password/sql/set_password.sql @@ -1,7 +1,7 @@ with update_password as ( - insert into passwords (hash, user_id, alg) - values ($2, $1, $3) + insert into passwords (hash, user_id, alg, project_id) + values ($2, $1, $3, $4) on conflict (user_id) do update set hash = $2 , alg = $3 diff --git a/server/src/passwordless/request_passwordless.rs b/server/src/passwordless/request_passwordless.rs index 3bee59da..a67b0340 100644 --- a/server/src/passwordless/request_passwordless.rs +++ b/server/src/passwordless/request_passwordless.rs @@ -54,7 +54,7 @@ pub async fn request_passwordless( public_key: body.public_key.to_owned(), user_id, expire_at: Utc::now() + Duration::days(30), - project_id: Some(project.id), + project_id: project.id, }; let session = Session::create(&pool, session).await?; diff --git a/server/src/passwordless/verify.rs b/server/src/passwordless/verify.rs index 92fbb47f..2aab0a6e 100644 --- a/server/src/passwordless/verify.rs +++ b/server/src/passwordless/verify.rs @@ -54,7 +54,8 @@ pub async fn handler( }; let claims = Session::validate_token(¤t_session, &rat)?; - let is_valid = Session::is_valid(&pool, &claims, ¤t_session.id).await?; + let is_valid = + Session::is_valid(&pool, &claims, ¤t_session.id, &token.project_id).await?; if !is_valid { return Err(ApiError::Forbidden); diff --git a/server/src/session/data/session.rs b/server/src/session/data/session.rs index 4486b57c..8882e040 100644 --- a/server/src/session/data/session.rs +++ b/server/src/session/data/session.rs @@ -1,7 +1,7 @@ use crate::response::error::ApiError; use crate::session::data::SessionClaims; -use chrono::{DateTime, Utc, TimeZone}; +use chrono::{DateTime, TimeZone, Utc}; use jsonwebtoken as jwt; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use serde::Deserialize; @@ -10,7 +10,7 @@ use uuid::Uuid; pub struct Session { pub id: Uuid, - pub project_id: Option, + pub project_id: Uuid, pub public_key: Vec, pub expire_at: DateTime, pub user_id: Option, @@ -103,13 +103,15 @@ impl Session { pool: &PgPool, claims: &SessionClaims, session: &Uuid, + project_id: &Uuid, ) -> Result { let exp = Utc.timestamp(claims.exp.into(), 0); let row = sqlx::query_file!( "src/session/sql/token_is_valid.sql", claims.jti, session, - exp + exp, + project_id, ) .fetch_one(pool) .await @@ -120,7 +122,6 @@ impl Session { Some(value) => Ok(value), } } - } #[derive(Deserialize)] diff --git a/server/src/session/refresh.rs b/server/src/session/refresh.rs index 3d47cccb..2fb33858 100644 --- a/server/src/session/refresh.rs +++ b/server/src/session/refresh.rs @@ -27,7 +27,7 @@ pub async fn handler( } let claims = Session::validate_token(&session, &rat)?; - let is_valid = Session::is_valid(&pool, &claims, &session_id).await?; + let is_valid = Session::is_valid(&pool, &claims, &session_id, &project.id).await?; if !is_valid { return Err(ApiError::Forbidden); diff --git a/server/src/session/sql/token_is_valid.sql b/server/src/session/sql/token_is_valid.sql index 4abcffb9..cd749f62 100644 --- a/server/src/session/sql/token_is_valid.sql +++ b/server/src/session/sql/token_is_valid.sql @@ -1,6 +1,6 @@ with add_token as ( - insert into refresh_access_tokens(id, session_id, expire_at) - values($1, $2, $3) + insert into refresh_access_tokens(id, session_id, expire_at, project_id) + values($1, $2, $3, $4) on conflict(id) do nothing returning id ) diff --git a/server/src/template/sql/insert_default_template_data.sql b/server/src/template/sql/insert_default_template_data.sql index 2b8e77d0..547c8d34 100644 --- a/server/src/template/sql/insert_default_template_data.sql +++ b/server/src/template/sql/insert_default_template_data.sql @@ -8,10 +8,12 @@ with raw_template_data as ( , redirect_to text , of_type text , template_type text + , project_id uuid ) ) insert into template_data -select raw_template_data.from_name +select raw_template_data.project_id + , raw_template_data.from_name , '{{t.subject}}' as subject , raw_template_data.id as template_id , raw_template_data.redirect_to diff --git a/server/src/template/sql/insert_default_translations.sql b/server/src/template/sql/insert_default_translations.sql index 930dc3ba..d1be82c9 100644 --- a/server/src/template/sql/insert_default_translations.sql +++ b/server/src/template/sql/insert_default_translations.sql @@ -5,10 +5,12 @@ with raw_template_data as ( id uuid , translation jsonb , template_type text + , project_id uuid ) ) insert into template_translations -select raw_template_data.id as template_id +select raw_template_data.project_id + , raw_template_data.id as template_id , 'en' as language , raw_template_data.translation from raw_template_data diff --git a/server/src/template/sql/set_template.sql b/server/src/template/sql/set_template.sql index dd51da95..b340506a 100644 --- a/server/src/template/sql/set_template.sql +++ b/server/src/template/sql/set_template.sql @@ -4,12 +4,13 @@ with insert_template as ( on conflict (project_id, name) do update set body = $1 returning id ) -insert into template_data(from_name, subject, template_id, redirect_to, of_type) +insert into template_data(from_name, subject, template_id, redirect_to, of_type, project_id) select $4 as from_name , $5 as subject , insert_template.id as template_id , $6 as redirect_to , $7 as of_type + , $3 as "project_id" from insert_template on conflict (template_id, of_type) do update set from_name = $4 diff --git a/server/src/template/sql/set_translation.sql b/server/src/template/sql/set_translation.sql index a4129e01..0e814edd 100644 --- a/server/src/template/sql/set_translation.sql +++ b/server/src/template/sql/set_translation.sql @@ -1,11 +1,12 @@ with template as ( - select id + select id, project_id from templates where project_id = $1 and name = $2 ) -insert into template_translations(template_id, language, content) -select template.id as template_id +insert into template_translations(project_id, template_id, language, content) +select template.project_id as "project_id" + , template.id as template_id , $3 as language , $4 as content from template diff --git a/server/src/user/change_email.rs b/server/src/user/change_email.rs index 1a63f04c..366a53fe 100644 --- a/server/src/user/change_email.rs +++ b/server/src/user/change_email.rs @@ -48,7 +48,7 @@ pub async fn create_email_change_request( reset_token: hashed_reset_token, }; - let request_id = EmailChangeRequest::create(&pool, &change_request).await?; + let request_id = EmailChangeRequest::create(&pool, &change_request, &project.id).await?; let (settings, reset_settings) = join( ProjectEmail::from_project_template(&pool, &project.id, Templates::ConfirmEmailChange), diff --git a/server/src/user/data/email.rs b/server/src/user/data/email.rs index 7ffdbff6..ff4b55f7 100644 --- a/server/src/user/data/email.rs +++ b/server/src/user/data/email.rs @@ -1,87 +1,99 @@ use crate::response::error::ApiError; -use uuid::Uuid; -use sqlx::PgPool; use chrono::{DateTime, Utc}; - +use sqlx::PgPool; +use uuid::Uuid; #[derive(sqlx::Type, PartialEq)] #[sqlx(type_name = "email_change_state")] #[sqlx(rename_all = "lowercase")] pub enum EmailChangeState { - Request, - Reject, - Accept, - Reset, + Request, + Reject, + Accept, + Reset, } - pub struct NewChangeRequest { - pub old_email: String, - pub new_email: String, - pub user_id: Uuid, - pub token: String, - pub reset_token: String, + pub old_email: String, + pub new_email: String, + pub user_id: Uuid, + pub token: String, + pub reset_token: String, } - pub struct ConfirmToken { - pub token: String, - pub state: EmailChangeState, - pub expire_at: DateTime, + pub token: String, + pub state: EmailChangeState, + pub expire_at: DateTime, } - pub struct ResetToken { - pub token: String, - pub state: EmailChangeState, + pub token: String, + pub state: EmailChangeState, } - pub struct EmailChangeRequest; impl EmailChangeRequest { - pub async fn create(pool: &PgPool, request: &NewChangeRequest) -> Result { - sqlx::query_file!("src/user/sql/email/insert_change_request.sql", - request.old_email, - request.new_email, - request.user_id, - request.token, - request.reset_token, - ) - .fetch_one(pool) - .await - .map(|row| row.id) - .map_err(|_| ApiError::InternalServerError) - } - - pub async fn get_confirm_token(pool: &PgPool, token_id: &Uuid) -> Result { - sqlx::query_file_as!(ConfirmToken, "src/user/sql/email/get_confirm_token.sql", token_id) - .fetch_one(pool) - .await - .map_err(|_| ApiError::InternalServerError) - } - - pub async fn get_reset_token(pool: &PgPool, token_id: &Uuid) -> Result { - sqlx::query_file_as!(ResetToken, "src/user/sql/email/get_reset_token.sql", token_id) - .fetch_one(pool) - .await - .map_err(|_| ApiError::InternalServerError) - } - - pub async fn set_email(pool: &PgPool, token_id: &Uuid) -> Result<(), ApiError> { - sqlx::query_file!("src/user/sql/email/set_email.sql", token_id) - .execute(pool) - .await - .map(|_| ()) - .map_err(|_| ApiError::InternalServerError) - } - - pub async fn reset_email(pool: &PgPool, token_id: &Uuid) -> Result<(), ApiError> { - sqlx::query_file!("src/user/sql/email/reset_email.sql", token_id) - .execute(pool) - .await - .map(|_| ()) - .map_err(|_| ApiError::InternalServerError) - } -} \ No newline at end of file + pub async fn create( + pool: &PgPool, + request: &NewChangeRequest, + project_id: &Uuid, + ) -> Result { + sqlx::query_file!( + "src/user/sql/email/insert_change_request.sql", + request.old_email, + request.new_email, + request.user_id, + request.token, + request.reset_token, + project_id, + ) + .fetch_one(pool) + .await + .map(|row| row.id) + .map_err(|_| ApiError::InternalServerError) + } + + pub async fn get_confirm_token( + pool: &PgPool, + token_id: &Uuid, + ) -> Result { + sqlx::query_file_as!( + ConfirmToken, + "src/user/sql/email/get_confirm_token.sql", + token_id + ) + .fetch_one(pool) + .await + .map_err(|_| ApiError::InternalServerError) + } + + pub async fn get_reset_token(pool: &PgPool, token_id: &Uuid) -> Result { + sqlx::query_file_as!( + ResetToken, + "src/user/sql/email/get_reset_token.sql", + token_id + ) + .fetch_one(pool) + .await + .map_err(|_| ApiError::InternalServerError) + } + + pub async fn set_email(pool: &PgPool, token_id: &Uuid) -> Result<(), ApiError> { + sqlx::query_file!("src/user/sql/email/set_email.sql", token_id) + .execute(pool) + .await + .map(|_| ()) + .map_err(|_| ApiError::InternalServerError) + } + + pub async fn reset_email(pool: &PgPool, token_id: &Uuid) -> Result<(), ApiError> { + sqlx::query_file!("src/user/sql/email/reset_email.sql", token_id) + .execute(pool) + .await + .map(|_| ()) + .map_err(|_| ApiError::InternalServerError) + } +} diff --git a/server/src/user/data/user.rs b/server/src/user/data/user.rs index 2f44c40d..b786db22 100644 --- a/server/src/user/data/user.rs +++ b/server/src/user/data/user.rs @@ -261,6 +261,15 @@ impl User { Ok(()) } + + pub async fn project(pool: &PgPool, user_id: &Uuid) -> Result, ApiError> { + let row = sqlx::query_file!("src/user/sql/get_project_id.sql", user_id) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::InternalServerError)?; + + Ok(row.map(|r| r.project_id)) + } } pub enum SortDirection { diff --git a/server/src/user/delete_account.rs b/server/src/user/delete_account.rs index 1a3df7c4..b321a827 100644 --- a/server/src/user/delete_account.rs +++ b/server/src/user/delete_account.rs @@ -24,7 +24,7 @@ pub async fn delete_account( let session = Session::get(&pool, &session_id).await?; let claims = Session::validate_token(&session, &rat)?; - let is_valid = Session::is_valid(&pool, &claims, &session_id).await?; + let is_valid = Session::is_valid(&pool, &claims, &session_id, &session.project_id).await?; if !is_valid { return Err(ApiError::Forbidden); diff --git a/server/src/user/set_password.rs b/server/src/user/set_password.rs index 43738d02..e5c86104 100644 --- a/server/src/user/set_password.rs +++ b/server/src/user/set_password.rs @@ -35,7 +35,7 @@ pub async fn set_password( password::validate_password_length(&body.password)?; let alg = ProjectData::password_alg(&pool, &project.id).await?; - Password::set_password(&pool, &user_id, &body.password, &alg).await?; + Password::set_password(&pool, &user_id, &body.password, &alg, &project.id).await?; let settings = ProjectEmail::from_project_template(&pool, &project.id, Templates::PasswordReset).await; diff --git a/server/src/user/sign_out.rs b/server/src/user/sign_out.rs index ab482c0a..7dff2e1b 100644 --- a/server/src/user/sign_out.rs +++ b/server/src/user/sign_out.rs @@ -1,5 +1,6 @@ use crate::admin::data::Admin; use crate::db::Db; +use crate::project::Project; use crate::response::error::ApiError; use crate::session::data::{RefreshAccessToken, Session}; @@ -12,10 +13,11 @@ pub async fn sign_out( pool: Db, session_id: Uuid, rat: Json, + project: Project, ) -> Result<(), ApiError> { let session = Session::get(&pool, &session_id).await?; let claims = Session::validate_token(&session, &rat)?; - let is_valid = Session::is_valid(&pool, &claims, &session_id).await?; + let is_valid = Session::is_valid(&pool, &claims, &session_id, &project.id).await?; if !is_valid { return Err(ApiError::Forbidden); @@ -37,10 +39,11 @@ pub async fn sign_out_all( pool: Db, session_id: Uuid, rat: Json, + project: Project, ) -> Result<(), ApiError> { let session = Session::get(&pool, &session_id).await?; let claims = Session::validate_token(&session, &rat)?; - let is_valid = Session::is_valid(&pool, &claims, &session_id).await?; + let is_valid = Session::is_valid(&pool, &claims, &session_id, &project.id).await?; if !is_valid { return Err(ApiError::Forbidden); diff --git a/server/src/user/sql/create_user.sql b/server/src/user/sql/create_user.sql index 1eacba37..eb214c2c 100644 --- a/server/src/user/sql/create_user.sql +++ b/server/src/user/sql/create_user.sql @@ -4,9 +4,10 @@ with insert_user as ( values($1, $3, 'password', $4) returning id ) -insert into passwords(user_id, alg, hash) +insert into passwords(user_id, alg, hash, project_id) select insert_user.id as "user_id" , 'bcrypt' as "alg" , $2 as "hash" + , $3 as "project_id" from insert_user returning user_id as "id" \ No newline at end of file diff --git a/server/src/user/sql/email/insert_change_request.sql b/server/src/user/sql/email/insert_change_request.sql index 9dd10c9f..5bfb1845 100644 --- a/server/src/user/sql/email/insert_change_request.sql +++ b/server/src/user/sql/email/insert_change_request.sql @@ -1,4 +1,4 @@ -insert into email_change_request(old_email, new_email, user_id, token, reset_token) -values($1, $2, $3, $4, $5) +insert into email_change_request(old_email, new_email, user_id, token, reset_token, project_id) +values($1, $2, $3, $4, $5, $6) returning id diff --git a/server/src/user/sql/get_project_id.sql b/server/src/user/sql/get_project_id.sql new file mode 100644 index 00000000..e0a09fc1 --- /dev/null +++ b/server/src/user/sql/get_project_id.sql @@ -0,0 +1,4 @@ + +select project_id + from users + where users.id = $1 diff --git a/tests/api_tests/src/api_key/utils.ts b/tests/api_tests/src/api_key/utils.ts index a04e2671..22b7f0f9 100644 --- a/tests/api_tests/src/api_key/utils.ts +++ b/tests/api_tests/src/api_key/utils.ts @@ -1,6 +1,7 @@ import Db from '../utils/db' import { v4 as uuid } from 'uuid' import * as bcrypt from 'bcryptjs' +import { project } from '@seeds/data/projects' export async function generateApiKey(userId: string, expire_at: null | string) { let id = uuid() @@ -10,10 +11,10 @@ export async function generateApiKey(userId: string, expire_at: null | string) { let hashedToken = bcrypt.hashSync(token) await Db.query(` - insert into api_keys(id, token, user_id, expire_at) - values($1, $2, $3, $4) + insert into api_keys(id, token, user_id, expire_at, project_id) + values($1, $2, $3, $4, $5) returning id - `, [id, hashedToken, userId, expire_at]) + `, [id, hashedToken, userId, expire_at, project.id]) return { id, token, value, expire_at } } diff --git a/tests/api_tests/src/email_password/reset.test.ts b/tests/api_tests/src/email_password/reset.test.ts index 7f2649da..7fd5c284 100644 --- a/tests/api_tests/src/email_password/reset.test.ts +++ b/tests/api_tests/src/email_password/reset.test.ts @@ -16,59 +16,63 @@ import { ErrorCode } from '@sdk-js/error' import { PROJECT_ID } from '../utils/env' const SALT = bcrypt.genSaltSync(10); -const EMAIL = getEmail() const PASSWORD = 'password' -const ID = '031eb841-9650-4b52-a62f-1aa2742ceb43' - -beforeAll(() => { - return createUser({ - id: ID, - password: PASSWORD, - email: EMAIL, - }) -}) - -beforeEach(() => { - return Db.query(` - delete from password_change_requests - where user_id = $1 - `, [ID]) -}) afterAll(() => Db.end()) - describe("Reset Password", () => { test("should create reset token", async () => { + let user_id = uuid() + let email = getEmail() + await createUser({ + id: user_id, + password: PASSWORD, + email: email, + }) let payload: PasswordResetPayload = { - email: EMAIL + email: email } let res = await Http.post(Url.RequestPasswordReset, payload) expect(res.status).toBe(200) - let token = await getToken() + let token = await getToken(user_id) expect(token).toBeTruthy() }) test("should return ok for non existing user", async () => { + let user_id = uuid() + let email = getEmail() + await createUser({ + id: user_id, + password: PASSWORD, + email: email, + }) + let payload: PasswordResetPayload = { - email: `wrong-${EMAIL}` + email: `wrong-${email}` } let res = await Http.post(Url.RequestPasswordReset, payload) expect(res.status).toBe(200) - let token = await getToken() + let token = await getToken(user_id) expect(token).toBe(null) }) test("should verify reset token", async () => { + let user_id = uuid() + await createUser({ + id: user_id, + password: PASSWORD, + email: getEmail(), + }) + let { token, hashed } = generateToken() - let tokenId = await insertToken(hashed) + let tokenId = await insertToken(user_id, hashed) let payload: VerifyResetTokenPayload = { id: tokenId, @@ -84,8 +88,15 @@ describe("Reset Password", () => { test("verify should fail when token is expired", async () => { + let user_id = uuid() + await createUser({ + id: user_id, + password: PASSWORD, + email: getEmail(), + }) + let { token, hashed } = generateToken() - let tokenId = await insertToken(hashed, true) + let tokenId = await insertToken(user_id, hashed, true) let payload: VerifyResetTokenPayload = { id: tokenId, @@ -102,8 +113,15 @@ describe("Reset Password", () => { test("verify should fail when token is invalid", async () => { + let user_id = uuid() + await createUser({ + id: user_id, + password: PASSWORD, + email: getEmail(), + }) + let { token, hashed } = generateToken() - let tokenId = await insertToken(hashed) + let tokenId = await insertToken(user_id, hashed) let payload: VerifyResetTokenPayload = { id: tokenId, @@ -120,9 +138,16 @@ describe("Reset Password", () => { test("should set new password", async () => { + let user_id = uuid() + await createUser({ + id: user_id, + password: PASSWORD, + email: getEmail(), + }) + let password = 'password' let { token, hashed } = generateToken() - let tokenId = await insertToken(hashed) + let tokenId = await insertToken(user_id, hashed) let payload: SetPasswordPayload = { id: tokenId, @@ -141,7 +166,7 @@ describe("Reset Password", () => { select hash from passwords where user_id = $1 - `, [ID]) + `, [user_id]) .then(res => res.rows) let passwordSet = await argon2.verify(user.hash, password) @@ -207,9 +232,16 @@ describe("Reset Password", () => { test("set password should fail when token is expired", async () => { + let user_id = uuid() + await createUser({ + id: user_id, + password: PASSWORD, + email: getEmail(), + }) + let password = 'password' let { token, hashed } = generateToken() - let tokenId = await insertToken(hashed, true) + let tokenId = await insertToken(user_id, hashed, true) let payload: SetPasswordPayload = { id: tokenId, @@ -228,9 +260,16 @@ describe("Reset Password", () => { test("set password should fail when token is invalid", async () => { + let user_id = uuid() + await createUser({ + id: user_id, + password: PASSWORD, + email: getEmail(), + }) + let password = 'password' let { token, hashed } = generateToken() - let tokenId = await insertToken(hashed) + let tokenId = await insertToken(user_id, hashed) let payload: SetPasswordPayload = { id: tokenId, @@ -249,14 +288,14 @@ describe("Reset Password", () => { }) -async function getToken(): Promise<{ id: string, token: string } | null> { +async function getToken(id: string): Promise<{ id: string, token: string } | null> { let { rows } = await Db.query(` select id , token from password_change_requests where user_id = $1 and project_id = $2 - `, [ID, PROJECT_ID]) + `, [id, PROJECT_ID]) return rows[0] ?? null } @@ -269,7 +308,7 @@ function generateToken() { } -async function insertToken(token: string, expired: boolean = false): Promise { +async function insertToken(user_id: string, token: string, expired: boolean = false): Promise { let expiredAt = !expired ? new Date(Date.now() + 32 * 60 * 1000) @@ -279,7 +318,7 @@ async function insertToken(token: string, expired: boolean = false): Promise { `, [TEMPLATE_ID, TEMPLATE, PROJECT_ID]) await Db.query(` - insert into template_translations(template_id, language, content) - select template_id, language, content + insert into template_translations(template_id, language, content, project_id) + select template_id, language, content, $2 as project_id from json_to_recordset($1) as x(template_id uuid, language text, content jsonb) - `, [JSON.stringify(translations)]) + `, [JSON.stringify(translations), PROJECT_ID]) }) diff --git a/tests/api_tests/src/user/change_email.test.ts b/tests/api_tests/src/user/change_email.test.ts index 2e6b958e..97ad9040 100644 --- a/tests/api_tests/src/user/change_email.test.ts +++ b/tests/api_tests/src/user/change_email.test.ts @@ -1,10 +1,9 @@ import Db from '../utils/db' import Http from '../utils/http' -import { generateKeyPair } from '../utils/crypto' import { PROJECT_ID } from '../utils/env' import { makeCreateUser } from '../utils/passwordless' import { makeGenerateAccessToken, makeTokenPayload } from '../utils/user' -import { projectKeys } from '@seeds/data/projects' +import { project, projectKeys } from '@seeds/data/projects' import { v4 as uuid } from 'uuid' import * as bcrypt from 'bcryptjs' @@ -12,7 +11,6 @@ import * as bcrypt from 'bcryptjs' const EMAIL = 'api.test+change_email@vulpo.dev' const NEW_EMAIL = 'api.test+change_new_email@vulpo.dev' const USER_ID = '4d883557-1efb-4a4b-9108-40e68d597fbc' -const KEYS = generateKeyPair() let createUser = makeCreateUser( USER_ID, @@ -204,10 +202,18 @@ async function insertChangeRequestToken({ expired = false } = {}) { : new Date(Date.now() + 30 * 60 * 1000) let { rows } = await Db.query(` - insert into email_change_request(old_email, new_email, user_id, token, reset_token, expire_at) - values($1, $2, $3, $4, $5, $6) + insert into email_change_request + ( old_email + , new_email + , user_id + , token + , reset_token + , expire_at + , project_id + ) + values($1, $2, $3, $4, $5, $6, $7) returning id - `, [EMAIL, NEW_EMAIL, USER_ID, hashedToken, hashedResetToken, expireAt]) + `, [EMAIL, NEW_EMAIL, USER_ID, hashedToken, hashedResetToken, expireAt, project.id]) let id = rows[0].id @@ -236,7 +242,7 @@ async function dataIsUnchanged(changeRequestId: string, state: string, email: st -async function confirm(payload) { +async function confirm(payload: any) { let token = generateAccessToken({ payload: tokenPayload() }) diff --git a/tests/api_tests/src/user/delete.test.ts b/tests/api_tests/src/user/delete.test.ts index ffd600b8..9459fb48 100644 --- a/tests/api_tests/src/user/delete.test.ts +++ b/tests/api_tests/src/user/delete.test.ts @@ -80,9 +80,9 @@ describe("Delete Account", () => { }) await Db.query(` - insert into refresh_access_tokens(id, session_id, expire_at) - values($1, $2, now() + '5 minutes') - `, [jti, SESSION_ID]) + insert into refresh_access_tokens(id, session_id, expire_at, project_id) + values($1, $2, now() + '5 minutes', $3) + `, [jti, SESSION_ID, PROJECT_ID]) let url = Url.UserDeleteAccount.replace(':session', SESSION_ID) let res = await Http.post(url, { diff --git a/tests/api_tests/src/user/sign_out.test.ts b/tests/api_tests/src/user/sign_out.test.ts index ce905a55..0667ee3a 100644 --- a/tests/api_tests/src/user/sign_out.test.ts +++ b/tests/api_tests/src/user/sign_out.test.ts @@ -85,9 +85,9 @@ describe("User Sign Out", () => { }) await Db.query(` - insert into refresh_access_tokens(id, session_id, expire_at) - values($1, $2, now() + '5 minutes') - `, [jti, SESSION_ID]) + insert into refresh_access_tokens(id, session_id, expire_at, project_id) + values($1, $2, now() + '5 minutes', $3) + `, [jti, SESSION_ID, PROJECT_ID]) let url = Url.SignOut.replace(':session', SESSION_ID) let res = await Http.post(url, { @@ -153,9 +153,9 @@ describe("User Sign Out", () => { }) await Db.query(` - insert into refresh_access_tokens(id, session_id, expire_at) - values($1, $2, now() + '5 minutes') - `, [jti, SESSION_ID]) + insert into refresh_access_tokens(id, session_id, expire_at, project_id) + values($1, $2, now() + '5 minutes', $3) + `, [jti, SESSION_ID, PROJECT_ID]) let url = Url.SignOutAll.replace(':session', SESSION_ID) let res = await Http.post(url, { diff --git a/tests/api_tests/src/utils/user.ts b/tests/api_tests/src/utils/user.ts index 24ee534e..20b4ca1b 100644 --- a/tests/api_tests/src/utils/user.ts +++ b/tests/api_tests/src/utils/user.ts @@ -114,10 +114,10 @@ export async function createUser({ let hash = await argon2.hash(password, { type: argon2.argon2id }) await Db.query(` - insert into passwords(user_id, alg, hash) - values($1, 'argon2id', $2) + insert into passwords(user_id, alg, hash, project_id) + values($1, 'argon2id', $2, $3) on conflict do nothing - `, [id, hash]) + `, [id, hash, project]) } return { id, email, project, traits, password: password ?? '', state } diff --git a/tests/ui_e2e/tests/password/signup.spec.ts b/tests/ui_e2e/tests/password/signup.spec.ts index 2260d548..2614dd74 100644 --- a/tests/ui_e2e/tests/password/signup.spec.ts +++ b/tests/ui_e2e/tests/password/signup.spec.ts @@ -82,6 +82,8 @@ test('can verify email', async ({ page, browser }) => { let email = await signUp(page) let verifyPage = await followEmail(browser, email, 'Verify Email') + await verifyPage.click('button:has-text("Verify Email")') + let text = "Your email has been verified" await verifyPage.waitForSelector(`//p[contains(@class, vulpo-auth-verify-email-text) and text()="${text}"]`) }) diff --git a/tests/ui_e2e/tests/utils/user.ts b/tests/ui_e2e/tests/utils/user.ts index 08ef815b..6ec718ea 100644 --- a/tests/ui_e2e/tests/utils/user.ts +++ b/tests/ui_e2e/tests/utils/user.ts @@ -35,9 +35,9 @@ export async function createUserWithEmailPassword(email: string, password: strin let hash = bcrypt.hashSync(password, SALT) await Db.query(` - insert into passwords(user_id, hash, alg) - values($1, $2, 'bcrypt') - `, [id, hash]) + insert into passwords(user_id, hash, alg, project_id) + values($1, $2, 'bcrypt', $3) + `, [id, hash, 'ae16cc4a-33be-4b4e-a408-e67018fe453b']) } export async function setState(email: string, state: 'active' | 'disabled' | 'set_password') {