mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-04-22 01:10:31 +00:00
sso signup & login
This commit is contained in:
40
.env.example
40
.env.example
@@ -160,6 +160,46 @@ AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
# ALLOW_HTTP_PROXY=1
|
||||
# Custom frontend directory (defaults to ./frontend/dist)
|
||||
# FRONTEND_DIR=/path/to/frontend/dist
|
||||
# =============================================================================
|
||||
# SSO / Social Login
|
||||
# =============================================================================
|
||||
# Each provider requires ENABLED=true plus CLIENT_ID and CLIENT_SECRET.
|
||||
# Register your PDS as an OAuth application with each provider to get credentials.
|
||||
|
||||
# GitHub
|
||||
# SSO_GITHUB_ENABLED=true
|
||||
# SSO_GITHUB_CLIENT_ID=
|
||||
# SSO_GITHUB_CLIENT_SECRET=
|
||||
|
||||
# Discord
|
||||
# SSO_DISCORD_ENABLED=true
|
||||
# SSO_DISCORD_CLIENT_ID=
|
||||
# SSO_DISCORD_CLIENT_SECRET=
|
||||
|
||||
# Google
|
||||
# SSO_GOOGLE_ENABLED=true
|
||||
# SSO_GOOGLE_CLIENT_ID=
|
||||
# SSO_GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# GitLab (set ISSUER for self-hosted instances)
|
||||
# SSO_GITLAB_ENABLED=false
|
||||
# SSO_GITLAB_CLIENT_ID=
|
||||
# SSO_GITLAB_CLIENT_SECRET=
|
||||
# SSO_GITLAB_ISSUER=https://gitlab.com
|
||||
|
||||
# Generic OIDC
|
||||
# SSO_OIDC_ENABLED=false
|
||||
# SSO_OIDC_CLIENT_ID=
|
||||
# SSO_OIDC_CLIENT_SECRET=
|
||||
# SSO_OIDC_ISSUER=https://your-identity-provider.com
|
||||
# SSO_OIDC_NAME=Custom Provider
|
||||
|
||||
# Apple Sign-in
|
||||
# SSO_APPLE_ENABLED=true
|
||||
# SSO_APPLE_CLIENT_ID=com.example.signin # Services ID from Apple Developer Portal
|
||||
# SSO_APPLE_TEAM_ID=XXXXXXXXXX # 10-character Team ID
|
||||
# SSO_APPLE_KEY_ID=XXXXXXXXXX # Key ID from portal
|
||||
# SSO_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
CARGO_MOMMYS_LITTLE=mister
|
||||
CARGO_MOMMYS_PRONOUNS=his
|
||||
CARGO_MOMMYS_ROLES=daddy
|
||||
|
||||
22
.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json
generated
Normal file
22
.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT t.token FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc"
|
||||
}
|
||||
77
.sqlx/query-06eb7c6e1983b6121526ba63612236391290c2e63d37d2bb1cd89ea822950a82.json
generated
Normal file
77
.sqlx/query-06eb7c6e1983b6121526ba63612236391290c2e63d37d2bb1cd89ea822950a82.json
generated
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT token, request_uri, provider as \"provider: SsoProviderType\",\n provider_user_id, provider_username, provider_email, created_at, expires_at\n FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "06eb7c6e1983b6121526ba63612236391290c2e63d37d2bb1cd89ea822950a82"
|
||||
}
|
||||
15
.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json
generated
Normal file
15
.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET deactivated_at = $1 WHERE did = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107"
|
||||
}
|
||||
22
.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json
generated
Normal file
22
.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "body",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99"
|
||||
}
|
||||
22
.sqlx/query-0fae1be7a75bdc58c69a9af97cad4aec23c32a9378764b8d6d7eb2cc89c562b1.json
generated
Normal file
22
.sqlx/query-0fae1be7a75bdc58c69a9af97cad4aec23c32a9378764b8d6d7eb2cc89c562b1.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT token FROM sso_pending_registration WHERE token = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0fae1be7a75bdc58c69a9af97cad4aec23c32a9378764b8d6d7eb2cc89c562b1"
|
||||
}
|
||||
16
.sqlx/query-1abfd9ff7ae1de0ca048b6a67a60a7ba5cfca75af5cc4e7280fea230cf46af7e.json
generated
Normal file
16
.sqlx/query-1abfd9ff7ae1de0ca048b6a67a60a7ba5cfca75af5cc4e7280fea230cf46af7e.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE external_identities\n SET provider_username = COALESCE($2, provider_username),\n provider_email = COALESCE($3, provider_email),\n last_login_at = NOW(),\n updated_at = NOW()\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1abfd9ff7ae1de0ca048b6a67a60a7ba5cfca75af5cc4e7280fea230cf46af7e"
|
||||
}
|
||||
22
.sqlx/query-1c84643fd6bc57c76517849a64d2d877df337e823d4c2c2b077f695bbfc9e9ac.json
generated
Normal file
22
.sqlx/query-1c84643fd6bc57c76517849a64d2d877df337e823d4c2c2b077f695bbfc9e9ac.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1c84643fd6bc57c76517849a64d2d877df337e823d4c2c2b077f695bbfc9e9ac"
|
||||
}
|
||||
28
.sqlx/query-24b823043ab60f36c29029137fef30dfe33922bb06067f2fdbfc1fbb4b0a2a81.json
generated
Normal file
28
.sqlx/query-24b823043ab60f36c29029137fef30dfe33922bb06067f2fdbfc1fbb4b0a2a81.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token, request_uri\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "24b823043ab60f36c29029137fef30dfe33922bb06067f2fdbfc1fbb4b0a2a81"
|
||||
}
|
||||
38
.sqlx/query-2841093a67480e75e1e9e4046bf3eb74afae2d04f5ea0ec17a4d433983e6d71c.json
generated
Normal file
38
.sqlx/query-2841093a67480e75e1e9e4046bf3eb74afae2d04f5ea0ec17a4d433983e6d71c.json
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2841093a67480e75e1e9e4046bf3eb74afae2d04f5ea0ec17a4d433983e6d71c"
|
||||
}
|
||||
14
.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json
generated
Normal file
14
.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5"
|
||||
}
|
||||
32
.sqlx/query-376b72306b50f747bc9161985ff4f50c35c53025a55ccf5e9933dc3795d29313.json
generated
Normal file
32
.sqlx/query-376b72306b50f747bc9161985ff4f50c35c53025a55ccf5e9933dc3795d29313.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "376b72306b50f747bc9161985ff4f50c35c53025a55ccf5e9933dc3795d29313"
|
||||
}
|
||||
22
.sqlx/query-3933ea5b147ab6294936de147b98e116cfae848ecd76ea5d367585eb5117f2ad.json
generated
Normal file
22
.sqlx/query-3933ea5b147ab6294936de147b98e116cfae848ecd76ea5d367585eb5117f2ad.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM external_identities WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3933ea5b147ab6294936de147b98e116cfae848ecd76ea5d367585eb5117f2ad"
|
||||
}
|
||||
16
.sqlx/query-3bed8d4843545f4a9676207513806603c50eb2af92957994abaf1c89c0294c12.json
generated
Normal file
16
.sqlx/query-3bed8d4843545f4a9676207513806603c50eb2af92957994abaf1c89c0294c12.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3bed8d4843545f4a9676207513806603c50eb2af92957994abaf1c89c0294c12"
|
||||
}
|
||||
54
.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json
generated
Normal file
54
.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json
generated
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT subject, body, comms_type as \"comms_type: String\" FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "subject",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "body",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "comms_type: String",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "comms_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"welcome",
|
||||
"email_verification",
|
||||
"password_reset",
|
||||
"email_update",
|
||||
"account_deletion",
|
||||
"admin_email",
|
||||
"plc_operation",
|
||||
"two_factor_code",
|
||||
"channel_verification",
|
||||
"passkey_recovery",
|
||||
"legacy_login_alert",
|
||||
"migration_verification"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe"
|
||||
}
|
||||
33
.sqlx/query-44a1f3f4c515e904e9d5c616a48d7d6a59bcb5e2f415122c1bb1e5f54cacc12f.json
generated
Normal file
33
.sqlx/query-44a1f3f4c515e904e9d5c616a48d7d6a59bcb5e2f415122c1bb1e5f54cacc12f.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "44a1f3f4c515e904e9d5c616a48d7d6a59bcb5e2f415122c1bb1e5f54cacc12f"
|
||||
}
|
||||
22
.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json
generated
Normal file
22
.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM users WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068"
|
||||
}
|
||||
40
.sqlx/query-45fac6171726d2c1990a3bb37a6dac592efa7f1bedcb29824ce8792093872722.json
generated
Normal file
40
.sqlx/query-45fac6171726d2c1990a3bb37a6dac592efa7f1bedcb29824ce8792093872722.json
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT preferred_comms_channel as \"preferred_comms_channel: String\", discord_id FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "preferred_comms_channel: String",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "comms_channel",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"email",
|
||||
"discord",
|
||||
"telegram",
|
||||
"signal"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "discord_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "45fac6171726d2c1990a3bb37a6dac592efa7f1bedcb29824ce8792093872722"
|
||||
}
|
||||
15
.sqlx/query-47faf3cd805673aab801d23dee46c3e802ca3988426863424e2bc2f627d9b758.json
generated
Normal file
15
.sqlx/query-47faf3cd805673aab801d23dee46c3e802ca3988426863424e2bc2f627d9b758.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO handle_reservations (handle, reserved_by)\n SELECT $1, $2\n WHERE NOT EXISTS (\n SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL\n )\n AND NOT EXISTS (\n SELECT 1 FROM handle_reservations WHERE handle = $1 AND expires_at > NOW()\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "47faf3cd805673aab801d23dee46c3e802ca3988426863424e2bc2f627d9b758"
|
||||
}
|
||||
28
.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json
generated
Normal file
28
.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88"
|
||||
}
|
||||
22
.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json
generated
Normal file
22
.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email_verified FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email_verified",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6"
|
||||
}
|
||||
22
.sqlx/query-4fef326fa2d03d04869af3fec702c901d1ecf392545a3a032438b2c1859d46cc.json
generated
Normal file
22
.sqlx/query-4fef326fa2d03d04869af3fec702c901d1ecf392545a3a032438b2c1859d46cc.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT token FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4fef326fa2d03d04869af3fec702c901d1ecf392545a3a032438b2c1859d46cc"
|
||||
}
|
||||
77
.sqlx/query-5031b96c65078d6c54954ce6e57ff9cbba4c48dd8a7546882ab5647114ffab4a.json
generated
Normal file
77
.sqlx/query-5031b96c65078d6c54954ce6e57ff9cbba4c48dd8a7546882ab5647114ffab4a.json
generated
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token, request_uri, provider as \"provider: SsoProviderType\",\n provider_user_id, provider_username, provider_email, created_at, expires_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5031b96c65078d6c54954ce6e57ff9cbba4c48dd8a7546882ab5647114ffab4a"
|
||||
}
|
||||
15
.sqlx/query-575c1e5529874f8f523e6fe22ccf4ee3296806581b1765dfb91a84ffab347f15.json
generated
Normal file
15
.sqlx/query-575c1e5529874f8f523e6fe22ccf4ee3296806581b1765dfb91a84ffab347f15.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_authorization_request (id, client_id, parameters, expires_at)\n VALUES ($1, 'https://test.example.com', $2, NOW() + INTERVAL '1 hour')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "575c1e5529874f8f523e6fe22ccf4ee3296806581b1765dfb91a84ffab347f15"
|
||||
}
|
||||
33
.sqlx/query-596c3400a60c77c7645fd46fcea61fa7898b6832e58c0f647f382b23b81d350e.json
generated
Normal file
33
.sqlx/query-596c3400a60c77c7645fd46fcea61fa7898b6832e58c0f647f382b23b81d350e.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "596c3400a60c77c7645fd46fcea61fa7898b6832e58c0f647f382b23b81d350e"
|
||||
}
|
||||
81
.sqlx/query-59e63c5cf92985714e9586d1ce012efef733d4afaa4ea09974daf8303805e5d2.json
generated
Normal file
81
.sqlx/query-59e63c5cf92985714e9586d1ce012efef733d4afaa4ea09974daf8303805e5d2.json
generated
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, did, provider as \"provider: SsoProviderType\", provider_user_id, provider_username, provider_email\n FROM external_identities\n WHERE provider = $1 AND provider_user_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "59e63c5cf92985714e9586d1ce012efef733d4afaa4ea09974daf8303805e5d2"
|
||||
}
|
||||
28
.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json
generated
Normal file
28
.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT k.key_bytes, k.encryption_version\n FROM user_keys k\n JOIN users u ON k.user_id = u.id\n WHERE u.did = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0"
|
||||
}
|
||||
22
.sqlx/query-5af4a386c1632903ad7102551a5bd148bcf541baab6a84c8649666a695f9c4d1.json
generated
Normal file
22
.sqlx/query-5af4a386c1632903ad7102551a5bd148bcf541baab6a84c8649666a695f9c4d1.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "state",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5af4a386c1632903ad7102551a5bd148bcf541baab6a84c8649666a695f9c4d1"
|
||||
}
|
||||
14
.sqlx/query-5dc0d09cea2415c4053518b7cb5c41da4a8cae66c8c9cd151eee4ea29c0e1c45.json
generated
Normal file
14
.sqlx/query-5dc0d09cea2415c4053518b7cb5c41da4a8cae66c8c9cd151eee4ea29c0e1c45.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_auth_state\n WHERE expires_at < $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5dc0d09cea2415c4053518b7cb5c41da4a8cae66c8c9cd151eee4ea29c0e1c45"
|
||||
}
|
||||
43
.sqlx/query-5e4c0dd92ac3c4b5e2eae5d129f2649cf3a8f068105f44a8dca9625427affc06.json
generated
Normal file
43
.sqlx/query-5e4c0dd92ac3c4b5e2eae5d129f2649cf3a8f068105f44a8dca9625427affc06.json
generated
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT provider_user_id, provider_email_verified\n FROM external_identities\n WHERE did = $1 AND provider = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "provider_email_verified",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5e4c0dd92ac3c4b5e2eae5d129f2649cf3a8f068105f44a8dca9625427affc06"
|
||||
}
|
||||
33
.sqlx/query-5e9c6ec72c2c0ea1c8dff551d01baddd1dd953c828a5656db2ee39dea996f890.json
generated
Normal file
33
.sqlx/query-5e9c6ec72c2c0ea1c8dff551d01baddd1dd953c828a5656db2ee39dea996f890.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, nonce, code_verifier)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5e9c6ec72c2c0ea1c8dff551d01baddd1dd953c828a5656db2ee39dea996f890"
|
||||
}
|
||||
34
.sqlx/query-630c1fabbf37946cbf2f3f77faa2e973875cd8e9176792d79a4bec91d703bbf2.json
generated
Normal file
34
.sqlx/query-630c1fabbf37946cbf2f3f77faa2e973875cd8e9176792d79a4bec91d703bbf2.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "630c1fabbf37946cbf2f3f77faa2e973875cd8e9176792d79a4bec91d703bbf2"
|
||||
}
|
||||
28
.sqlx/query-63f6f2a89650794fe90e10ce7fc785a6b9f7d37c12b31a6ff13f7c5214eef19e.json
generated
Normal file
28
.sqlx/query-63f6f2a89650794fe90e10ce7fc785a6b9f7d37c12b31a6ff13f7c5214eef19e.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT did, email_verified FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email_verified",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "63f6f2a89650794fe90e10ce7fc785a6b9f7d37c12b31a6ff13f7c5214eef19e"
|
||||
}
|
||||
66
.sqlx/query-6c7ace2a64848adc757af6c93b9162e1d95788b372370a7ad0d7540338bb73ee.json
generated
Normal file
66
.sqlx/query-6c7ace2a64848adc757af6c93b9162e1d95788b372370a7ad0d7540338bb73ee.json
generated
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT state, request_uri, provider as \"provider: SsoProviderType\", action, nonce, code_verifier\n FROM sso_auth_state\n WHERE state = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "state",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "action",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "nonce",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "code_verifier",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6c7ace2a64848adc757af6c93b9162e1d95788b372370a7ad0d7540338bb73ee"
|
||||
}
|
||||
22
.sqlx/query-6fbcff0206599484bfb6cef165b6f729d27e7a342f7718ee4ac07f0ca94412ba.json
generated
Normal file
22
.sqlx/query-6fbcff0206599484bfb6cef165b6f729d27e7a342f7718ee4ac07f0ca94412ba.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT state FROM sso_auth_state WHERE state = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "state",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6fbcff0206599484bfb6cef165b6f729d27e7a342f7718ee4ac07f0ca94412ba"
|
||||
}
|
||||
33
.sqlx/query-712459c27fc037f45389e2766cf1057e86e93ef756a784ed12beb453b03c5da1.json
generated
Normal file
33
.sqlx/query-712459c27fc037f45389e2766cf1057e86e93ef756a784ed12beb453b03c5da1.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "712459c27fc037f45389e2766cf1057e86e93ef756a784ed12beb453b03c5da1"
|
||||
}
|
||||
22
.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json
generated
Normal file
22
.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "subject",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f"
|
||||
}
|
||||
22
.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json
generated
Normal file
22
.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT t.token\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631"
|
||||
}
|
||||
28
.sqlx/query-7d24e744a4e63570b1410e50b45b745ce8915ab3715b3eff7efc2d84f27735d0.json
generated
Normal file
28
.sqlx/query-7d24e744a4e63570b1410e50b45b745ce8915ab3715b3eff7efc2d84f27735d0.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT provider_username, last_login_at FROM external_identities WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "last_login_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7d24e744a4e63570b1410e50b45b745ce8915ab3715b3eff7efc2d84f27735d0"
|
||||
}
|
||||
28
.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json
generated
Normal file
28
.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT t.token, t.expires_at FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81"
|
||||
}
|
||||
34
.sqlx/query-85ffc37a77af832d7795f5f37efe304fced4bf56b4f2287fe9aeb3fc97e1b191.json
generated
Normal file
34
.sqlx/query-85ffc37a77af832d7795f5f37efe304fced4bf56b4f2287fe9aeb3fc97e1b191.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "85ffc37a77af832d7795f5f37efe304fced4bf56b4f2287fe9aeb3fc97e1b191"
|
||||
}
|
||||
40
.sqlx/query-8d4753d81bdd340b97c816e160ba532f1838f2441079c11d471f2eddf24f5375.json
generated
Normal file
40
.sqlx/query-8d4753d81bdd340b97c816e160ba532f1838f2441079c11d471f2eddf24f5375.json
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8d4753d81bdd340b97c816e160ba532f1838f2441079c11d471f2eddf24f5375"
|
||||
}
|
||||
84
.sqlx/query-8f070e3bdc3b1bb8cfce9a9b1dd67dd022cc515720fb742cf4bf363895d71cd8.json
generated
Normal file
84
.sqlx/query-8f070e3bdc3b1bb8cfce9a9b1dd67dd022cc515720fb742cf4bf363895d71cd8.json
generated
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token, request_uri, provider as \"provider: SsoProviderType\",\n provider_user_id, provider_username, provider_email, provider_email_verified,\n created_at, expires_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "provider_email_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8f070e3bdc3b1bb8cfce9a9b1dd67dd022cc515720fb742cf4bf363895d71cd8"
|
||||
}
|
||||
84
.sqlx/query-9468c5af2fb0e06e600e6c67e236bd4e368b06ce4af15fed16b8a0bfc5328c36.json
generated
Normal file
84
.sqlx/query-9468c5af2fb0e06e600e6c67e236bd4e368b06ce4af15fed16b8a0bfc5328c36.json
generated
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state, request_uri, provider as \"provider: SsoProviderType\", action,\n nonce, code_verifier, did, created_at, expires_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "state",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "action",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "nonce",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "code_verifier",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9468c5af2fb0e06e600e6c67e236bd4e368b06ce4af15fed16b8a0bfc5328c36"
|
||||
}
|
||||
14
.sqlx/query-946e30fee0e45a99f3fe1ec3671c561c9dc537a848bc94c4740d5a83bf8d2861.json
generated
Normal file
14
.sqlx/query-946e30fee0e45a99f3fe1ec3671c561c9dc537a848bc94c4740d5a83bf8d2861.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_pending_registration\n WHERE expires_at < $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "946e30fee0e45a99f3fe1ec3671c561c9dc537a848bc94c4740d5a83bf8d2861"
|
||||
}
|
||||
22
.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json
generated
Normal file
22
.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "body",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5"
|
||||
}
|
||||
28
.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json
generated
Normal file
28
.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "public_key_did_key",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002"
|
||||
}
|
||||
22
.sqlx/query-9dba64081d4f95b5490c9a9bf30a7175db3429f39df4f25e212f38f33882fc65.json
generated
Normal file
22
.sqlx/query-9dba64081d4f95b5490c9a9bf30a7175db3429f39df4f25e212f38f33882fc65.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id FROM external_identities WHERE did = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9dba64081d4f95b5490c9a9bf30a7175db3429f39df4f25e212f38f33882fc65"
|
||||
}
|
||||
66
.sqlx/query-9fd56986c1c843d386d1e5884acef8573eb55a3e9f5cb0122fcf8b93d6d667a5.json
generated
Normal file
66
.sqlx/query-9fd56986c1c843d386d1e5884acef8573eb55a3e9f5cb0122fcf8b93d6d667a5.json
generated
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT token, request_uri, provider as \"provider: SsoProviderType\", provider_user_id,\n provider_username, provider_email\n FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9fd56986c1c843d386d1e5884acef8573eb55a3e9f5cb0122fcf8b93d6d667a5"
|
||||
}
|
||||
34
.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json
generated
Normal file
34
.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "private_key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "used_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5"
|
||||
}
|
||||
15
.sqlx/query-a3d549a32e76c24e265c73a98dd739067623f275de0740bd576ee288f4444496.json
generated
Normal file
15
.sqlx/query-a3d549a32e76c24e265c73a98dd739067623f275de0740bd576ee288f4444496.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE external_identities\n SET provider_username = $2, last_login_at = NOW()\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a3d549a32e76c24e265c73a98dd739067623f275de0740bd576ee288f4444496"
|
||||
}
|
||||
31
.sqlx/query-a4dc8fb22bd094d414c55b9da20b610f7b122b485ab0fd0d0646d68ae8e64fe6.json
generated
Normal file
31
.sqlx/query-a4dc8fb22bd094d414c55b9da20b610f7b122b485ab0fd0d0646d68ae8e64fe6.json
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a4dc8fb22bd094d414c55b9da20b610f7b122b485ab0fd0d0646d68ae8e64fe6"
|
||||
}
|
||||
22
.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json
generated
Normal file
22
.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT token FROM account_deletion_requests WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5"
|
||||
}
|
||||
99
.sqlx/query-a87afce2ff68221df2e3e1051293217446fa0ed25144755f0da6f4825478506c.json
generated
Normal file
99
.sqlx/query-a87afce2ff68221df2e3e1051293217446fa0ed25144755f0da6f4825478506c.json
generated
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, did, provider as \"provider: SsoProviderType\", provider_user_id,\n provider_username, provider_email, created_at, updated_at, last_login_at\n FROM external_identities\n WHERE provider = $1 AND provider_user_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "last_login_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a87afce2ff68221df2e3e1051293217446fa0ed25144755f0da6f4825478506c"
|
||||
}
|
||||
28
.sqlx/query-aee3e8e1d8924d41bec7d866e274f8bb2ddef833eb03326103c2d0a17ee56154.json
generated
Normal file
28
.sqlx/query-aee3e8e1d8924d41bec7d866e274f8bb2ddef833eb03326103c2d0a17ee56154.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state, request_uri\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "state",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "aee3e8e1d8924d41bec7d866e274f8bb2ddef833eb03326103c2d0a17ee56154"
|
||||
}
|
||||
31
.sqlx/query-ba9684872fad5201b8504c2606c29364a2df9631fe98817e7bfacd3f3f51f6cb.json
generated
Normal file
31
.sqlx/query-ba9684872fad5201b8504c2606c29364a2df9631fe98817e7bfacd3f3f51f6cb.json
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ba9684872fad5201b8504c2606c29364a2df9631fe98817e7bfacd3f3f51f6cb"
|
||||
}
|
||||
12
.sqlx/query-bb4460f75d30f48b79d71b97f2c7d54190260deba2d2ade177dbdaa507ab275b.json
generated
Normal file
12
.sqlx/query-bb4460f75d30f48b79d71b97f2c7d54190260deba2d2ade177dbdaa507ab275b.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM sso_auth_state WHERE expires_at < NOW()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bb4460f75d30f48b79d71b97f2c7d54190260deba2d2ade177dbdaa507ab275b"
|
||||
}
|
||||
84
.sqlx/query-bf7e32cc58dfe85e08d52595f0c3b979f0f7c04f4401b5840f96ff0e47144075.json
generated
Normal file
84
.sqlx/query-bf7e32cc58dfe85e08d52595f0c3b979f0f7c04f4401b5840f96ff0e47144075.json
generated
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, did, provider as \"provider: SsoProviderType\", provider_user_id,\n provider_username, provider_email, created_at, updated_at, last_login_at\n FROM external_identities\n WHERE did = $1\n ORDER BY created_at ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "last_login_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bf7e32cc58dfe85e08d52595f0c3b979f0f7c04f4401b5840f96ff0e47144075"
|
||||
}
|
||||
22
.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json
generated
Normal file
22
.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT password_reset_code FROM users WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "password_reset_code",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee"
|
||||
}
|
||||
22
.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json
generated
Normal file
22
.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as \"count!\" FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3"
|
||||
}
|
||||
34
.sqlx/query-cdba2cc5219e52ee1c23d52c1e099b49b87e45dcfc6edb7a3e73067ed61b312b.json
generated
Normal file
34
.sqlx/query-cdba2cc5219e52ee1c23d52c1e099b49b87e45dcfc6edb7a3e73067ed61b312b.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, nonce, code_verifier, did)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cdba2cc5219e52ee1c23d52c1e099b49b87e45dcfc6edb7a3e73067ed61b312b"
|
||||
}
|
||||
31
.sqlx/query-d0d4fb4b44cda3442b20037b4d5efaa032e1d004c775e2b6077c5050d7d62041.json
generated
Normal file
31
.sqlx/query-d0d4fb4b44cda3442b20037b4d5efaa032e1d004c775e2b6077c5050d7d62041.json
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d0d4fb4b44cda3442b20037b4d5efaa032e1d004c775e2b6077c5050d7d62041"
|
||||
}
|
||||
14
.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json
generated
Normal file
14
.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4"
|
||||
}
|
||||
40
.sqlx/query-dd7d80d4d118a5fc95b574e2ca9ffaccf974e52fb6ac368f716409c55f9d3ab0.json
generated
Normal file
40
.sqlx/query-dd7d80d4d118a5fc95b574e2ca9ffaccf974e52fb6ac368f716409c55f9d3ab0.json
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dd7d80d4d118a5fc95b574e2ca9ffaccf974e52fb6ac368f716409c55f9d3ab0"
|
||||
}
|
||||
32
.sqlx/query-dec3a21a8e60cc8d2c5dad727750bc88f5535dedae244f7b6e4afa95769b8f1a.json
generated
Normal file
32
.sqlx/query-dec3a21a8e60cc8d2c5dad727750bc88f5535dedae244f7b6e4afa95769b8f1a.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dec3a21a8e60cc8d2c5dad727750bc88f5535dedae244f7b6e4afa95769b8f1a"
|
||||
}
|
||||
22
.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json
generated
Normal file
22
.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3"
|
||||
}
|
||||
22
.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json
generated
Normal file
22
.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "used_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9"
|
||||
}
|
||||
30
.sqlx/query-eb54d2ce02cab7c2e7f9926bd469b19e5f0513f47173b2738fc01a57082d7abb.json
generated
Normal file
30
.sqlx/query-eb54d2ce02cab7c2e7f9926bd469b19e5f0513f47173b2738fc01a57082d7abb.json
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eb54d2ce02cab7c2e7f9926bd469b19e5f0513f47173b2738fc01a57082d7abb"
|
||||
}
|
||||
12
.sqlx/query-eb82195792193f432e9abfe5e6ea4d4c45ccb9bd15b025602c64967bd4c85fd3.json
generated
Normal file
12
.sqlx/query-eb82195792193f432e9abfe5e6ea4d4c45ccb9bd15b025602c64967bd4c85fd3.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM handle_reservations WHERE expires_at <= NOW()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eb82195792193f432e9abfe5e6ea4d4c45ccb9bd15b025602c64967bd4c85fd3"
|
||||
}
|
||||
15
.sqlx/query-ec22a8cc89e480c403a239eac44288e144d83364129491de6156760616666d3d.json
generated
Normal file
15
.sqlx/query-ec22a8cc89e480c403a239eac44288e144d83364129491de6156760616666d3d.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM external_identities WHERE id = $1 AND did = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ec22a8cc89e480c403a239eac44288e144d83364129491de6156760616666d3d"
|
||||
}
|
||||
22
.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json
generated
Normal file
22
.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7"
|
||||
}
|
||||
14
.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json
generated
Normal file
14
.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET is_admin = TRUE WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814"
|
||||
}
|
||||
14
.sqlx/query-f4d0d7fbb138a2c3c285d829ffd3a760a5036640291666daf6f51d32ab4f3d2d.json
generated
Normal file
14
.sqlx/query-f4d0d7fbb138a2c3c285d829ffd3a760a5036640291666daf6f51d32ab4f3d2d.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM handle_reservations WHERE handle = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f4d0d7fbb138a2c3c285d829ffd3a760a5036640291666daf6f51d32ab4f3d2d"
|
||||
}
|
||||
28
.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json
generated
Normal file
28
.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "password_reset_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "password_reset_code_expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382"
|
||||
}
|
||||
15
.sqlx/query-ff903cc1839ee69b3c217bc713f9c734fc4a794cefa9f76286facda88bf22f18.json
generated
Normal file
15
.sqlx/query-ff903cc1839ee69b3c217bc713f9c734fc4a794cefa9f76286facda88bf22f18.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM external_identities\n WHERE id = $1 AND did = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ff903cc1839ee69b3c217bc713f9c734fc4a794cefa9f76286facda88bf22f18"
|
||||
}
|
||||
84
.sqlx/query-ff93791f03c093deff1fdf4a86989548178bac3cbe6ffa73c22cafab61d05ba4.json
generated
Normal file
84
.sqlx/query-ff93791f03c093deff1fdf4a86989548178bac3cbe6ffa73c22cafab61d05ba4.json
generated
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT token, request_uri, provider as \"provider: SsoProviderType\",\n provider_user_id, provider_username, provider_email, provider_email_verified,\n created_at, expires_at\n FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "request_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "provider: SsoProviderType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "sso_provider_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"github",
|
||||
"discord",
|
||||
"google",
|
||||
"gitlab",
|
||||
"oidc",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "provider_user_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "provider_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "provider_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "provider_email_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ff93791f03c093deff1fdf4a86989548178bac3cbe6ffa73c22cafab61d05ba4"
|
||||
}
|
||||
47
Cargo.lock
generated
47
Cargo.lock
generated
@@ -3192,6 +3192,29 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "10.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
"getrandom 0.2.16",
|
||||
"hmac",
|
||||
"js-sys",
|
||||
"p256 0.13.2",
|
||||
"p384",
|
||||
"pem",
|
||||
"rand 0.8.5",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"signature 2.2.0",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.4"
|
||||
@@ -3883,6 +3906,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -5029,6 +5062,18 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sketches-ddsketch"
|
||||
version = "0.3.0"
|
||||
@@ -6040,6 +6085,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
"aws-sdk-s3",
|
||||
"axum",
|
||||
@@ -6067,6 +6113,7 @@ dependencies = [
|
||||
"iroh-car",
|
||||
"jacquard-common",
|
||||
"jacquard-repo",
|
||||
"jsonwebtoken",
|
||||
"k256",
|
||||
"metrics",
|
||||
"metrics-exporter-prometheus",
|
||||
|
||||
@@ -7,6 +7,7 @@ mod infra;
|
||||
mod oauth;
|
||||
mod repo;
|
||||
mod session;
|
||||
mod sso;
|
||||
mod user;
|
||||
|
||||
pub use backlink::{Backlink, BacklinkRepository};
|
||||
@@ -40,15 +41,18 @@ pub use session::{
|
||||
AppPasswordCreate, AppPasswordRecord, RefreshSessionResult, SessionForRefresh, SessionListItem,
|
||||
SessionMfaStatus, SessionRefreshData, SessionRepository, SessionToken, SessionTokenCreate,
|
||||
};
|
||||
pub use sso::{
|
||||
ExternalIdentity, SsoAuthState, SsoPendingRegistration, SsoProviderType, SsoRepository,
|
||||
};
|
||||
pub use user::{
|
||||
AccountSearchResult, CompletePasskeySetupInput, CreateAccountError,
|
||||
CreateDelegatedAccountInput, CreatePasskeyAccountInput, CreatePasswordAccountInput,
|
||||
CreatePasswordAccountResult, DidWebOverrides, MigrationReactivationError,
|
||||
MigrationReactivationInput, NotificationPrefs, OAuthTokenWithUser, PasswordResetResult,
|
||||
ReactivatedAccountInfo, RecoverPasskeyAccountInput, RecoverPasskeyAccountResult,
|
||||
ScheduledDeletionAccount, StoredBackupCode, StoredPasskey, TotpRecord, User2faStatus,
|
||||
UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo,
|
||||
UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery,
|
||||
CreatePasswordAccountResult, CreateSsoAccountInput, DidWebOverrides,
|
||||
MigrationReactivationError, MigrationReactivationInput, NotificationPrefs, OAuthTokenWithUser,
|
||||
PasswordResetResult, ReactivatedAccountInfo, RecoverPasskeyAccountInput,
|
||||
RecoverPasskeyAccountResult, ScheduledDeletionAccount, StoredBackupCode, StoredPasskey,
|
||||
TotpRecord, User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo,
|
||||
UserEmailInfo, UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery,
|
||||
UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle,
|
||||
UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId,
|
||||
UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo,
|
||||
|
||||
176
crates/tranquil-db-traits/src/sso.rs
Normal file
176
crates/tranquil-db-traits/src/sso.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tranquil_types::Did;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::DbError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(type_name = "sso_provider_type", rename_all = "lowercase")]
|
||||
pub enum SsoProviderType {
|
||||
Github,
|
||||
Discord,
|
||||
Google,
|
||||
Gitlab,
|
||||
Oidc,
|
||||
Apple,
|
||||
}
|
||||
|
||||
impl SsoProviderType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Github => "github",
|
||||
Self::Discord => "discord",
|
||||
Self::Google => "google",
|
||||
Self::Gitlab => "gitlab",
|
||||
Self::Oidc => "oidc",
|
||||
Self::Apple => "apple",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"github" => Some(Self::Github),
|
||||
"discord" => Some(Self::Discord),
|
||||
"google" => Some(Self::Google),
|
||||
"gitlab" => Some(Self::Gitlab),
|
||||
"oidc" => Some(Self::Oidc),
|
||||
"apple" => Some(Self::Apple),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Github => "GitHub",
|
||||
Self::Discord => "Discord",
|
||||
Self::Google => "Google",
|
||||
Self::Gitlab => "GitLab",
|
||||
Self::Oidc => "SSO",
|
||||
Self::Apple => "Apple",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Github => "github",
|
||||
Self::Discord => "discord",
|
||||
Self::Google => "google",
|
||||
Self::Gitlab => "gitlab",
|
||||
Self::Oidc => "oidc",
|
||||
Self::Apple => "apple",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternalIdentity {
|
||||
pub id: Uuid,
|
||||
pub did: Did,
|
||||
pub provider: SsoProviderType,
|
||||
pub provider_user_id: String,
|
||||
pub provider_username: Option<String>,
|
||||
pub provider_email: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub last_login_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SsoAuthState {
|
||||
pub state: String,
|
||||
pub request_uri: String,
|
||||
pub provider: SsoProviderType,
|
||||
pub action: String,
|
||||
pub nonce: Option<String>,
|
||||
pub code_verifier: Option<String>,
|
||||
pub did: Option<Did>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SsoPendingRegistration {
|
||||
pub token: String,
|
||||
pub request_uri: String,
|
||||
pub provider: SsoProviderType,
|
||||
pub provider_user_id: String,
|
||||
pub provider_username: Option<String>,
|
||||
pub provider_email: Option<String>,
|
||||
pub provider_email_verified: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait SsoRepository: Send + Sync {
|
||||
async fn create_external_identity(
|
||||
&self,
|
||||
did: &Did,
|
||||
provider: SsoProviderType,
|
||||
provider_user_id: &str,
|
||||
provider_username: Option<&str>,
|
||||
provider_email: Option<&str>,
|
||||
) -> Result<Uuid, DbError>;
|
||||
|
||||
async fn get_external_identity_by_provider(
|
||||
&self,
|
||||
provider: SsoProviderType,
|
||||
provider_user_id: &str,
|
||||
) -> Result<Option<ExternalIdentity>, DbError>;
|
||||
|
||||
async fn get_external_identities_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Vec<ExternalIdentity>, DbError>;
|
||||
|
||||
async fn update_external_identity_login(
|
||||
&self,
|
||||
id: Uuid,
|
||||
provider_username: Option<&str>,
|
||||
provider_email: Option<&str>,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn delete_external_identity(&self, id: Uuid, did: &Did) -> Result<bool, DbError>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_sso_auth_state(
|
||||
&self,
|
||||
state: &str,
|
||||
request_uri: &str,
|
||||
provider: SsoProviderType,
|
||||
action: &str,
|
||||
nonce: Option<&str>,
|
||||
code_verifier: Option<&str>,
|
||||
did: Option<&Did>,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn consume_sso_auth_state(&self, state: &str) -> Result<Option<SsoAuthState>, DbError>;
|
||||
|
||||
async fn cleanup_expired_sso_auth_states(&self) -> Result<u64, DbError>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_pending_registration(
|
||||
&self,
|
||||
token: &str,
|
||||
request_uri: &str,
|
||||
provider: SsoProviderType,
|
||||
provider_user_id: &str,
|
||||
provider_username: Option<&str>,
|
||||
provider_email: Option<&str>,
|
||||
provider_email_verified: bool,
|
||||
) -> Result<(), DbError>;
|
||||
|
||||
async fn get_pending_registration(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> Result<Option<SsoPendingRegistration>, DbError>;
|
||||
|
||||
async fn consume_pending_registration(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> Result<Option<SsoPendingRegistration>, DbError>;
|
||||
|
||||
async fn cleanup_expired_pending_registrations(&self) -> Result<u64, DbError>;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
||||
use tranquil_types::{Did, Handle};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{CommsChannel, DbError};
|
||||
use crate::{CommsChannel, DbError, SsoProviderType};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserRow {
|
||||
@@ -480,6 +480,11 @@ pub trait UserRepository: Send + Sync {
|
||||
input: &CreatePasskeyAccountInput,
|
||||
) -> Result<CreatePasswordAccountResult, CreateAccountError>;
|
||||
|
||||
async fn create_sso_account(
|
||||
&self,
|
||||
input: &CreateSsoAccountInput,
|
||||
) -> Result<CreatePasswordAccountResult, CreateAccountError>;
|
||||
|
||||
async fn reactivate_migration_account(
|
||||
&self,
|
||||
input: &MigrationReactivationInput,
|
||||
@@ -490,6 +495,12 @@ pub trait UserRepository: Send + Sync {
|
||||
handle: &Handle,
|
||||
) -> Result<bool, DbError>;
|
||||
|
||||
async fn reserve_handle(&self, handle: &Handle, reserved_by: &str) -> Result<bool, DbError>;
|
||||
|
||||
async fn release_handle_reservation(&self, handle: &Handle) -> Result<(), DbError>;
|
||||
|
||||
async fn cleanup_expired_handle_reservations(&self) -> Result<u64, DbError>;
|
||||
|
||||
async fn check_and_consume_invite_code(&self, code: &str) -> Result<bool, DbError>;
|
||||
|
||||
async fn complete_passkey_setup(
|
||||
@@ -842,6 +853,7 @@ pub enum CreateAccountError {
|
||||
HandleTaken,
|
||||
EmailTaken,
|
||||
DidExists,
|
||||
InvalidToken,
|
||||
Database(String),
|
||||
}
|
||||
|
||||
@@ -882,6 +894,30 @@ pub struct CreatePasskeyAccountInput {
|
||||
pub birthdate_pref: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateSsoAccountInput {
|
||||
pub handle: Handle,
|
||||
pub email: Option<String>,
|
||||
pub did: Did,
|
||||
pub preferred_comms_channel: CommsChannel,
|
||||
pub discord_id: Option<String>,
|
||||
pub telegram_username: Option<String>,
|
||||
pub signal_number: Option<String>,
|
||||
pub encrypted_key_bytes: Vec<u8>,
|
||||
pub encryption_version: i32,
|
||||
pub commit_cid: String,
|
||||
pub repo_rev: String,
|
||||
pub genesis_block_cids: Vec<Vec<u8>>,
|
||||
pub invite_code: Option<String>,
|
||||
pub birthdate_pref: Option<serde_json::Value>,
|
||||
pub sso_provider: SsoProviderType,
|
||||
pub sso_provider_user_id: String,
|
||||
pub sso_provider_username: Option<String>,
|
||||
pub sso_provider_email: Option<String>,
|
||||
pub sso_provider_email_verified: bool,
|
||||
pub pending_registration_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompletePasskeySetupInput {
|
||||
pub user_id: Uuid,
|
||||
|
||||
@@ -7,6 +7,7 @@ mod infra;
|
||||
mod oauth;
|
||||
mod repo;
|
||||
mod session;
|
||||
mod sso;
|
||||
mod user;
|
||||
|
||||
use sqlx::PgPool;
|
||||
@@ -21,9 +22,11 @@ pub use infra::PostgresInfraRepository;
|
||||
pub use oauth::PostgresOAuthRepository;
|
||||
pub use repo::PostgresRepoRepository;
|
||||
pub use session::PostgresSessionRepository;
|
||||
pub use sso::PostgresSsoRepository;
|
||||
use tranquil_db_traits::{
|
||||
BacklinkRepository, BackupRepository, BlobRepository, DelegationRepository, InfraRepository,
|
||||
OAuthRepository, RepoEventNotifier, RepoRepository, SessionRepository, UserRepository,
|
||||
OAuthRepository, RepoEventNotifier, RepoRepository, SessionRepository, SsoRepository,
|
||||
UserRepository,
|
||||
};
|
||||
pub use user::PostgresUserRepository;
|
||||
|
||||
@@ -38,6 +41,7 @@ pub struct PostgresRepositories {
|
||||
pub infra: Arc<dyn InfraRepository>,
|
||||
pub backup: Arc<dyn BackupRepository>,
|
||||
pub backlink: Arc<dyn BacklinkRepository>,
|
||||
pub sso: Arc<dyn SsoRepository>,
|
||||
pub event_notifier: Arc<dyn RepoEventNotifier>,
|
||||
}
|
||||
|
||||
@@ -54,6 +58,7 @@ impl PostgresRepositories {
|
||||
infra: Arc::new(PostgresInfraRepository::new(pool.clone())),
|
||||
backup: Arc::new(PostgresBackupRepository::new(pool.clone())),
|
||||
backlink: Arc::new(PostgresBacklinkRepository::new(pool.clone())),
|
||||
sso: Arc::new(PostgresSsoRepository::new(pool.clone())),
|
||||
event_notifier: Arc::new(PostgresRepoEventNotifier::new(pool)),
|
||||
}
|
||||
}
|
||||
|
||||
337
crates/tranquil-db/src/postgres/sso.rs
Normal file
337
crates/tranquil-db/src/postgres/sso.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use sqlx::PgPool;
|
||||
use tranquil_db_traits::{
|
||||
DbError, ExternalIdentity, SsoAuthState, SsoPendingRegistration, SsoProviderType, SsoRepository,
|
||||
};
|
||||
use tranquil_types::Did;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::user::map_sqlx_error;
|
||||
|
||||
pub struct PostgresSsoRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PostgresSsoRepository {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SsoRepository for PostgresSsoRepository {
|
||||
async fn create_external_identity(
|
||||
&self,
|
||||
did: &Did,
|
||||
provider: SsoProviderType,
|
||||
provider_user_id: &str,
|
||||
provider_username: Option<&str>,
|
||||
provider_email: Option<&str>,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let id = sqlx::query_scalar!(
|
||||
r#"
|
||||
INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
"#,
|
||||
did.as_str(),
|
||||
provider as SsoProviderType,
|
||||
provider_user_id,
|
||||
provider_username,
|
||||
provider_email,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn get_external_identity_by_provider(
|
||||
&self,
|
||||
provider: SsoProviderType,
|
||||
provider_user_id: &str,
|
||||
) -> Result<Option<ExternalIdentity>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, did, provider as "provider: SsoProviderType", provider_user_id,
|
||||
provider_username, provider_email, created_at, updated_at, last_login_at
|
||||
FROM external_identities
|
||||
WHERE provider = $1 AND provider_user_id = $2
|
||||
"#,
|
||||
provider as SsoProviderType,
|
||||
provider_user_id,
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(|r| ExternalIdentity {
|
||||
id: r.id,
|
||||
did: Did::new_unchecked(&r.did),
|
||||
provider: r.provider,
|
||||
provider_user_id: r.provider_user_id,
|
||||
provider_username: r.provider_username,
|
||||
provider_email: r.provider_email,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
last_login_at: r.last_login_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_external_identities_by_did(
|
||||
&self,
|
||||
did: &Did,
|
||||
) -> Result<Vec<ExternalIdentity>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, did, provider as "provider: SsoProviderType", provider_user_id,
|
||||
provider_username, provider_email, created_at, updated_at, last_login_at
|
||||
FROM external_identities
|
||||
WHERE did = $1
|
||||
ORDER BY created_at ASC
|
||||
"#,
|
||||
did.as_str(),
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| ExternalIdentity {
|
||||
id: r.id,
|
||||
did: Did::new_unchecked(&r.did),
|
||||
provider: r.provider,
|
||||
provider_user_id: r.provider_user_id,
|
||||
provider_username: r.provider_username,
|
||||
provider_email: r.provider_email,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
last_login_at: r.last_login_at,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn update_external_identity_login(
|
||||
&self,
|
||||
id: Uuid,
|
||||
provider_username: Option<&str>,
|
||||
provider_email: Option<&str>,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE external_identities
|
||||
SET provider_username = COALESCE($2, provider_username),
|
||||
provider_email = COALESCE($3, provider_email),
|
||||
last_login_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
id,
|
||||
provider_username,
|
||||
provider_email,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_external_identity(&self, id: Uuid, did: &Did) -> Result<bool, DbError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM external_identities
|
||||
WHERE id = $1 AND did = $2
|
||||
"#,
|
||||
id,
|
||||
did.as_str(),
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
async fn create_sso_auth_state(
|
||||
&self,
|
||||
state: &str,
|
||||
request_uri: &str,
|
||||
provider: SsoProviderType,
|
||||
action: &str,
|
||||
nonce: Option<&str>,
|
||||
code_verifier: Option<&str>,
|
||||
did: Option<&Did>,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO sso_auth_state (state, request_uri, provider, action, nonce, code_verifier, did)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
"#,
|
||||
state,
|
||||
request_uri,
|
||||
provider as SsoProviderType,
|
||||
action,
|
||||
nonce,
|
||||
code_verifier,
|
||||
did.map(|d| d.as_str()),
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn consume_sso_auth_state(&self, state: &str) -> Result<Option<SsoAuthState>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM sso_auth_state
|
||||
WHERE state = $1 AND expires_at > NOW()
|
||||
RETURNING state, request_uri, provider as "provider: SsoProviderType", action,
|
||||
nonce, code_verifier, did, created_at, expires_at
|
||||
"#,
|
||||
state,
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(|r| SsoAuthState {
|
||||
state: r.state,
|
||||
request_uri: r.request_uri,
|
||||
provider: r.provider,
|
||||
action: r.action,
|
||||
nonce: r.nonce,
|
||||
code_verifier: r.code_verifier,
|
||||
did: r.did.map(|d| Did::new_unchecked(&d)),
|
||||
created_at: r.created_at,
|
||||
expires_at: r.expires_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn cleanup_expired_sso_auth_states(&self) -> Result<u64, DbError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM sso_auth_state
|
||||
WHERE expires_at < $1
|
||||
"#,
|
||||
Utc::now(),
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn create_pending_registration(
|
||||
&self,
|
||||
token: &str,
|
||||
request_uri: &str,
|
||||
provider: SsoProviderType,
|
||||
provider_user_id: &str,
|
||||
provider_username: Option<&str>,
|
||||
provider_email: Option<&str>,
|
||||
provider_email_verified: bool,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email, provider_email_verified)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
"#,
|
||||
token,
|
||||
request_uri,
|
||||
provider as SsoProviderType,
|
||||
provider_user_id,
|
||||
provider_username,
|
||||
provider_email,
|
||||
provider_email_verified,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_pending_registration(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> Result<Option<SsoPendingRegistration>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT token, request_uri, provider as "provider: SsoProviderType",
|
||||
provider_user_id, provider_username, provider_email, provider_email_verified,
|
||||
created_at, expires_at
|
||||
FROM sso_pending_registration
|
||||
WHERE token = $1 AND expires_at > NOW()
|
||||
"#,
|
||||
token,
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(|r| SsoPendingRegistration {
|
||||
token: r.token,
|
||||
request_uri: r.request_uri,
|
||||
provider: r.provider,
|
||||
provider_user_id: r.provider_user_id,
|
||||
provider_username: r.provider_username,
|
||||
provider_email: r.provider_email,
|
||||
provider_email_verified: r.provider_email_verified,
|
||||
created_at: r.created_at,
|
||||
expires_at: r.expires_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn consume_pending_registration(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> Result<Option<SsoPendingRegistration>, DbError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM sso_pending_registration
|
||||
WHERE token = $1 AND expires_at > NOW()
|
||||
RETURNING token, request_uri, provider as "provider: SsoProviderType",
|
||||
provider_user_id, provider_username, provider_email, provider_email_verified,
|
||||
created_at, expires_at
|
||||
"#,
|
||||
token,
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(row.map(|r| SsoPendingRegistration {
|
||||
token: r.token,
|
||||
request_uri: r.request_uri,
|
||||
provider: r.provider,
|
||||
provider_user_id: r.provider_user_id,
|
||||
provider_username: r.provider_username,
|
||||
provider_email: r.provider_email,
|
||||
provider_email_verified: r.provider_email_verified,
|
||||
created_at: r.created_at,
|
||||
expires_at: r.expires_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn cleanup_expired_pending_registrations(&self) -> Result<u64, DbError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM sso_pending_registration
|
||||
WHERE expires_at < $1
|
||||
"#,
|
||||
Utc::now(),
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ use uuid::Uuid;
|
||||
|
||||
use tranquil_db_traits::{
|
||||
AccountSearchResult, CommsChannel, DbError, DidWebOverrides, NotificationPrefs,
|
||||
OAuthTokenWithUser, PasswordResetResult, StoredBackupCode, StoredPasskey, TotpRecord,
|
||||
User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo, UserEmailInfo,
|
||||
UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery,
|
||||
OAuthTokenWithUser, PasswordResetResult, SsoProviderType, StoredBackupCode, StoredPasskey,
|
||||
TotpRecord, User2faStatus, UserAuthInfo, UserCommsPrefs, UserConfirmSignup, UserDidWebInfo,
|
||||
UserEmailInfo, UserForDeletion, UserForDidDoc, UserForDidDocBuild, UserForPasskeyRecovery,
|
||||
UserForPasskeySetup, UserForRecovery, UserForVerification, UserIdAndHandle,
|
||||
UserIdAndPasswordHash, UserIdHandleEmail, UserInfoForAuth, UserKeyInfo, UserKeyWithId,
|
||||
UserLegacyLoginPref, UserLoginCheck, UserLoginFull, UserLoginInfo, UserPasswordInfo,
|
||||
@@ -2671,6 +2671,173 @@ impl UserRepository for PostgresUserRepository {
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_sso_account(
|
||||
&self,
|
||||
input: &tranquil_db_traits::CreateSsoAccountInput,
|
||||
) -> Result<
|
||||
tranquil_db_traits::CreatePasswordAccountResult,
|
||||
tranquil_db_traits::CreateAccountError,
|
||||
> {
|
||||
let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
let token_consumed: Option<(String,)> = sqlx::query_as(
|
||||
r#"
|
||||
DELETE FROM sso_pending_registration
|
||||
WHERE token = $1 AND expires_at > NOW()
|
||||
RETURNING token
|
||||
"#,
|
||||
)
|
||||
.bind(&input.pending_registration_token)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
if token_consumed.is_none() {
|
||||
return Err(tranquil_db_traits::CreateAccountError::InvalidToken);
|
||||
}
|
||||
|
||||
let is_first_user: bool = sqlx::query_scalar!("SELECT COUNT(*) as count FROM users")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map(|c| c.unwrap_or(0) == 0)
|
||||
.unwrap_or(false);
|
||||
|
||||
let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as(
|
||||
r#"INSERT INTO users (
|
||||
handle, email, did, password_hash, password_required,
|
||||
preferred_comms_channel, discord_id, telegram_username, signal_number,
|
||||
is_admin
|
||||
) VALUES ($1, $2, $3, NULL, FALSE, $4, $5, $6, $7, $8) RETURNING id"#,
|
||||
)
|
||||
.bind(input.handle.as_str())
|
||||
.bind(&input.email)
|
||||
.bind(input.did.as_str())
|
||||
.bind(input.preferred_comms_channel)
|
||||
.bind(&input.discord_id)
|
||||
.bind(&input.telegram_username)
|
||||
.bind(&input.signal_number)
|
||||
.bind(is_first_user)
|
||||
.fetch_one(&mut *tx)
|
||||
.await;
|
||||
|
||||
let user_id = match user_insert {
|
||||
Ok((id,)) => id,
|
||||
Err(e) => {
|
||||
if let Some(db_err) = e.as_database_error()
|
||||
&& db_err.code().as_deref() == Some("23505")
|
||||
{
|
||||
let constraint = db_err.constraint().unwrap_or("");
|
||||
if constraint.contains("handle") {
|
||||
return Err(tranquil_db_traits::CreateAccountError::HandleTaken);
|
||||
} else if constraint.contains("email") {
|
||||
return Err(tranquil_db_traits::CreateAccountError::EmailTaken);
|
||||
}
|
||||
}
|
||||
return Err(tranquil_db_traits::CreateAccountError::Database(
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())",
|
||||
user_id,
|
||||
&input.encrypted_key_bytes[..],
|
||||
input.encryption_version
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| tranquil_db_traits::CreateAccountError::Database(e.to_string()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)",
|
||||
user_id,
|
||||
input.commit_cid,
|
||||
input.repo_rev
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_blocks (user_id, block_cid, repo_rev)
|
||||
SELECT $1, block_cid, $3 FROM UNNEST($2::bytea[]) AS t(block_cid)
|
||||
ON CONFLICT (user_id, block_cid) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(&input.genesis_block_cids)
|
||||
.bind(&input.repo_rev)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
if let Some(code) = &input.invite_code {
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
|
||||
code
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)",
|
||||
code,
|
||||
user_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(birthdate_pref) = &input.birthdate_pref {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, name) DO NOTHING",
|
||||
user_id,
|
||||
"app.bsky.actor.defs#personalDetailsPref",
|
||||
birthdate_pref
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email, provider_email_verified)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
"#,
|
||||
input.did.as_str(),
|
||||
input.sso_provider as SsoProviderType,
|
||||
&input.sso_provider_user_id,
|
||||
input.sso_provider_username.as_deref(),
|
||||
input.sso_provider_email.as_deref(),
|
||||
input.sso_provider_email_verified,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
tx.commit().await.map_err(|e: sqlx::Error| {
|
||||
tranquil_db_traits::CreateAccountError::Database(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(tranquil_db_traits::CreatePasswordAccountResult {
|
||||
user_id,
|
||||
is_admin: is_first_user,
|
||||
})
|
||||
}
|
||||
|
||||
async fn reactivate_migration_account(
|
||||
&self,
|
||||
input: &tranquil_db_traits::MigrationReactivationInput,
|
||||
@@ -2744,16 +2911,70 @@ impl UserRepository for PostgresUserRepository {
|
||||
&self,
|
||||
handle: &Handle,
|
||||
) -> Result<bool, DbError> {
|
||||
let exists: Option<(i32,)> =
|
||||
sqlx::query_as("SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL")
|
||||
.bind(handle.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
let exists: Option<(i32,)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL
|
||||
UNION ALL
|
||||
SELECT 1 FROM handle_reservations WHERE handle = $1 AND expires_at > NOW()
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(handle.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(exists.is_none())
|
||||
}
|
||||
|
||||
async fn reserve_handle(&self, handle: &Handle, reserved_by: &str) -> Result<bool, DbError> {
|
||||
sqlx::query!("DELETE FROM handle_reservations WHERE expires_at <= NOW()")
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO handle_reservations (handle, reserved_by)
|
||||
SELECT $1, $2
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE handle = $1 AND deactivated_at IS NULL
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM handle_reservations WHERE handle = $1 AND expires_at > NOW()
|
||||
)
|
||||
"#,
|
||||
handle.as_str(),
|
||||
reserved_by,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
async fn release_handle_reservation(&self, handle: &Handle) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"DELETE FROM handle_reservations WHERE handle = $1",
|
||||
handle.as_str()
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cleanup_expired_handle_reservations(&self) -> Result<u64, DbError> {
|
||||
let result = sqlx::query!("DELETE FROM handle_reservations WHERE expires_at <= NOW()")
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn check_and_consume_invite_code(&self, code: &str) -> Result<bool, DbError> {
|
||||
let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ tranquil-db = { workspace = true }
|
||||
tranquil-db-traits = { workspace = true }
|
||||
|
||||
aes-gcm = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
backon = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
aws-config = { workspace = true }
|
||||
@@ -44,6 +45,7 @@ ipld-core = { workspace = true }
|
||||
iroh-car = { workspace = true }
|
||||
jacquard-common = { workspace = true }
|
||||
jacquard-repo = { workspace = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
k256 = { workspace = true }
|
||||
metrics = { workspace = true }
|
||||
metrics-exporter-prometheus = { workspace = true }
|
||||
|
||||
12
crates/tranquil-pds/build.rs
Normal file
12
crates/tranquil-pds/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let timestamp = Command::new("date")
|
||||
.arg("+%Y-%m-%d %H:%M:%S UTC")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
|
||||
println!("cargo:rustc-env=BUILD_TIMESTAMP={}", timestamp);
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
@@ -107,6 +107,13 @@ pub enum ApiError {
|
||||
error: Option<String>,
|
||||
message: Option<String>,
|
||||
},
|
||||
SsoProviderNotFound,
|
||||
SsoProviderNotEnabled,
|
||||
SsoInvalidAction,
|
||||
SsoNotAuthenticated,
|
||||
SsoSessionExpired,
|
||||
SsoAlreadyLinked,
|
||||
SsoLinkNotFound,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
@@ -197,8 +204,14 @@ impl ApiError {
|
||||
| Self::InvalidVerificationChannel
|
||||
| Self::SelfHostedDidWebDisabled
|
||||
| Self::AccountAlreadyExists
|
||||
| Self::TokenRequired => StatusCode::BAD_REQUEST,
|
||||
Self::PasskeyNotFound => StatusCode::NOT_FOUND,
|
||||
| Self::TokenRequired
|
||||
| Self::SsoProviderNotFound
|
||||
| Self::SsoProviderNotEnabled
|
||||
| Self::SsoInvalidAction
|
||||
| Self::SsoNotAuthenticated
|
||||
| Self::SsoSessionExpired
|
||||
| Self::SsoAlreadyLinked => StatusCode::BAD_REQUEST,
|
||||
Self::PasskeyNotFound | Self::SsoLinkNotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
fn error_name(&self) -> Cow<'static, str> {
|
||||
@@ -293,6 +306,13 @@ impl ApiError {
|
||||
Self::AccountAlreadyExists => Cow::Borrowed("AccountAlreadyExists"),
|
||||
Self::HandleNotFound => Cow::Borrowed("HandleNotFound"),
|
||||
Self::SubjectNotFound => Cow::Borrowed("SubjectNotFound"),
|
||||
Self::SsoProviderNotFound => Cow::Borrowed("SsoProviderNotFound"),
|
||||
Self::SsoProviderNotEnabled => Cow::Borrowed("SsoProviderNotEnabled"),
|
||||
Self::SsoInvalidAction => Cow::Borrowed("SsoInvalidAction"),
|
||||
Self::SsoNotAuthenticated => Cow::Borrowed("SsoNotAuthenticated"),
|
||||
Self::SsoSessionExpired => Cow::Borrowed("SsoSessionExpired"),
|
||||
Self::SsoAlreadyLinked => Cow::Borrowed("SsoAlreadyLinked"),
|
||||
Self::SsoLinkNotFound => Cow::Borrowed("SsoLinkNotFound"),
|
||||
}
|
||||
}
|
||||
fn message(&self) -> Option<String> {
|
||||
@@ -392,6 +412,19 @@ impl ApiError {
|
||||
Self::AccountAlreadyExists => Some("Account already exists".to_string()),
|
||||
Self::HandleNotFound => Some("Unable to resolve handle".to_string()),
|
||||
Self::SubjectNotFound => Some("Subject not found".to_string()),
|
||||
Self::SsoProviderNotFound => Some("Unknown SSO provider".to_string()),
|
||||
Self::SsoProviderNotEnabled => Some("SSO provider is not enabled".to_string()),
|
||||
Self::SsoInvalidAction => {
|
||||
Some("Action must be login, link, or register".to_string())
|
||||
}
|
||||
Self::SsoNotAuthenticated => {
|
||||
Some("Must be authenticated to link SSO account".to_string())
|
||||
}
|
||||
Self::SsoSessionExpired => Some("SSO session expired or invalid".to_string()),
|
||||
Self::SsoAlreadyLinked => {
|
||||
Some("This SSO account is already linked to a different user".to_string())
|
||||
}
|
||||
Self::SsoLinkNotFound => Some("Linked account not found".to_string()),
|
||||
Self::IdentifierMismatch => {
|
||||
Some("The identifier does not match the verification token".to_string())
|
||||
}
|
||||
@@ -467,6 +500,13 @@ impl From<sqlx::Error> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tranquil_db_traits::DbError> for ApiError {
|
||||
fn from(e: tranquil_db_traits::DbError) -> Self {
|
||||
tracing::error!("Database error: {:?}", e);
|
||||
Self::DatabaseError
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::auth::TokenValidationError> for ApiError {
|
||||
fn from(e: crate::auth::TokenValidationError) -> Self {
|
||||
match e {
|
||||
|
||||
@@ -428,7 +428,10 @@ pub async fn activate_account(
|
||||
let _ = state.cache.delete(&format!("plc:doc:{}", did)).await;
|
||||
let _ = state.cache.delete(&format!("plc:data:{}", did)).await;
|
||||
if state.did_resolver.refresh_did(did.as_str()).await.is_none() {
|
||||
warn!("[MIGRATION] activateAccount: Failed to refresh DID cache for {}", did);
|
||||
warn!(
|
||||
"[MIGRATION] activateAccount: Failed to refresh DID cache for {}",
|
||||
did
|
||||
);
|
||||
}
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: Sequencing account event (active=true) for did={}",
|
||||
|
||||
@@ -7,14 +7,45 @@ use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::time::Duration;
|
||||
use subtle::ConstantTimeEq;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
const EMAIL_UPDATE_TTL: Duration = Duration::from_secs(30 * 60);
|
||||
|
||||
fn email_update_cache_key(did: &str) -> String {
|
||||
format!("email_update:{}", did)
|
||||
}
|
||||
|
||||
fn hash_token(token: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token.as_bytes());
|
||||
URL_SAFE_NO_PAD.encode(hasher.finalize())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PendingEmailUpdate {
|
||||
new_email: String,
|
||||
token_hash: String,
|
||||
authorized: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RequestEmailUpdateInput {
|
||||
#[serde(default)]
|
||||
pub new_email: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn request_email_update(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: BearerAuth,
|
||||
input: Option<Json<RequestEmailUpdateInput>>,
|
||||
) -> Response {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
@@ -60,11 +91,30 @@ pub async fn request_email_update(
|
||||
);
|
||||
let formatted_code = crate::auth::verification_token::format_token_for_display(&code);
|
||||
|
||||
if let Some(Json(ref inp)) = input
|
||||
&& let Some(ref new_email) = inp.new_email {
|
||||
let new_email = new_email.trim().to_lowercase();
|
||||
if !new_email.is_empty() && crate::api::validation::is_valid_email(&new_email) {
|
||||
let pending = PendingEmailUpdate {
|
||||
new_email,
|
||||
token_hash: hash_token(&code),
|
||||
authorized: false,
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&pending) {
|
||||
let cache_key = email_update_cache_key(&auth.0.did);
|
||||
if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await {
|
||||
warn!("Failed to cache pending email update: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_email_update_token(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user.id,
|
||||
&code,
|
||||
&formatted_code,
|
||||
&hostname,
|
||||
)
|
||||
@@ -223,34 +273,48 @@ pub async fn update_email(
|
||||
}
|
||||
|
||||
if email_verified {
|
||||
let Some(ref t) = input.token else {
|
||||
return ApiError::TokenRequired.into_response();
|
||||
};
|
||||
let confirmation_token = crate::auth::verification_token::normalize_token_input(t.trim());
|
||||
let mut authorized_via_link = false;
|
||||
|
||||
let current_email_lower = current_email
|
||||
.as_ref()
|
||||
.map(|e| e.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
let cache_key = email_update_cache_key(did);
|
||||
if let Some(pending_json) = state.cache.get(&cache_key).await
|
||||
&& let Ok(pending) = serde_json::from_str::<PendingEmailUpdate>(&pending_json)
|
||||
&& pending.authorized && pending.new_email == new_email {
|
||||
authorized_via_link = true;
|
||||
let _ = state.cache.delete(&cache_key).await;
|
||||
info!(did = %did, "Email update completed via link authorization");
|
||||
}
|
||||
|
||||
let verified = crate::auth::verification_token::verify_channel_update_token(
|
||||
&confirmation_token,
|
||||
"email_update",
|
||||
¤t_email_lower,
|
||||
);
|
||||
if !authorized_via_link {
|
||||
let Some(ref t) = input.token else {
|
||||
return ApiError::TokenRequired.into_response();
|
||||
};
|
||||
let confirmation_token =
|
||||
crate::auth::verification_token::normalize_token_input(t.trim());
|
||||
|
||||
match verified {
|
||||
Ok(token_data) => {
|
||||
if token_data.did != did.as_str() {
|
||||
let current_email_lower = current_email
|
||||
.as_ref()
|
||||
.map(|e| e.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = crate::auth::verification_token::verify_channel_update_token(
|
||||
&confirmation_token,
|
||||
"email_update",
|
||||
¤t_email_lower,
|
||||
);
|
||||
|
||||
match verified {
|
||||
Ok(token_data) => {
|
||||
if token_data.did != did.as_str() {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
return ApiError::ExpiredToken(None).into_response();
|
||||
}
|
||||
Err(_) => {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
return ApiError::ExpiredToken(None).into_response();
|
||||
}
|
||||
Err(_) => {
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,3 +396,148 @@ pub async fn check_email_verified(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AuthorizeEmailUpdateQuery {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
pub async fn authorize_email_update(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
axum::extract::Query(query): axum::extract::Query<AuthorizeEmailUpdateQuery>,
|
||||
) -> Response {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::VerificationCheck, &client_ip)
|
||||
.await
|
||||
{
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
}
|
||||
|
||||
let verified = crate::auth::verification_token::verify_token_signature(&query.token);
|
||||
|
||||
let token_data = match verified {
|
||||
Ok(data) => data,
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
warn!("authorize_email_update: token expired");
|
||||
return ApiError::ExpiredToken(None).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("authorize_email_update: token verification failed: {:?}", e);
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if token_data.purpose != crate::auth::verification_token::VerificationPurpose::ChannelUpdate {
|
||||
warn!(
|
||||
"authorize_email_update: wrong purpose: {:?}",
|
||||
token_data.purpose
|
||||
);
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
if token_data.channel != "email_update" {
|
||||
warn!(
|
||||
"authorize_email_update: wrong channel: {}",
|
||||
token_data.channel
|
||||
);
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
|
||||
let did = token_data.did;
|
||||
info!("authorize_email_update: token valid for did={}", did);
|
||||
|
||||
let cache_key = email_update_cache_key(&did);
|
||||
let pending_json = match state.cache.get(&cache_key).await {
|
||||
Some(json) => json,
|
||||
None => {
|
||||
warn!(
|
||||
"authorize_email_update: no pending email update in cache for did={}",
|
||||
did
|
||||
);
|
||||
return ApiError::InvalidRequest("No pending email update found".into())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let token_hash = hash_token(&query.token);
|
||||
if pending
|
||||
.token_hash
|
||||
.as_bytes()
|
||||
.ct_eq(token_hash.as_bytes())
|
||||
.unwrap_u8()
|
||||
!= 1
|
||||
{
|
||||
warn!("authorize_email_update: token hash mismatch");
|
||||
return ApiError::InvalidToken(None).into_response();
|
||||
}
|
||||
|
||||
pending.authorized = true;
|
||||
if let Ok(json) = serde_json::to_string(&pending)
|
||||
&& let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await {
|
||||
warn!("Failed to update pending email authorization: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
|
||||
info!(did = %did, "Email update authorized via link click");
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let redirect_url = format!(
|
||||
"https://{}/app/verify?type=email-authorize-success",
|
||||
hostname
|
||||
);
|
||||
|
||||
axum::response::Redirect::to(&redirect_url).into_response()
|
||||
}
|
||||
|
||||
pub async fn check_email_update_status(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: BearerAuth,
|
||||
) -> Response {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::VerificationCheck, &client_ip)
|
||||
.await
|
||||
{
|
||||
return ApiError::RateLimitExceeded(None).into_response();
|
||||
}
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth.0.is_oauth,
|
||||
auth.0.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Read,
|
||||
) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let cache_key = email_update_cache_key(&auth.0.did);
|
||||
let pending_json = match state.cache.get(&cache_key).await {
|
||||
Some(json) => json,
|
||||
None => {
|
||||
return Json(json!({ "pending": false, "authorized": false })).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return Json(json!({ "pending": false, "authorized": false })).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
"pending": true,
|
||||
"authorized": pending.authorized,
|
||||
"newEmail": pending.new_email,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ pub use account_status::{
|
||||
request_account_delete,
|
||||
};
|
||||
pub use app_password::{create_app_password, list_app_passwords, revoke_app_password};
|
||||
pub use email::{check_email_verified, confirm_email, request_email_update, update_email};
|
||||
pub use email::{
|
||||
authorize_email_update, check_email_update_status, check_email_verified, confirm_email,
|
||||
request_email_update, update_email,
|
||||
};
|
||||
pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes};
|
||||
pub use logo::get_logo;
|
||||
pub use meta::{describe_server, health, robots_txt};
|
||||
|
||||
@@ -366,12 +366,33 @@ pub async fn set_password(
|
||||
auth: BearerAuth,
|
||||
Json(input): Json<SetPasswordInput>,
|
||||
) -> Response {
|
||||
if crate::api::server::reauth::check_reauth_required_cached(
|
||||
&*state.session_repo,
|
||||
&state.cache,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await
|
||||
let has_password = state
|
||||
.user_repo
|
||||
.has_password_by_did(&auth.0.did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
let has_passkeys = state
|
||||
.user_repo
|
||||
.has_passkeys(&auth.0.did)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let has_totp = state
|
||||
.user_repo
|
||||
.has_totp_enabled(&auth.0.did)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
let has_any_reauth_method = has_password || has_passkeys || has_totp;
|
||||
|
||||
if has_any_reauth_method
|
||||
&& crate::api::server::reauth::check_reauth_required_cached(
|
||||
&*state.session_repo,
|
||||
&state.cache,
|
||||
&auth.0.did,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return crate::api::server::reauth::reauth_required_response(
|
||||
&*state.user_repo,
|
||||
|
||||
@@ -366,7 +366,8 @@ pub mod repo {
|
||||
user_repo: &dyn UserRepository,
|
||||
infra_repo: &dyn InfraRepository,
|
||||
user_id: Uuid,
|
||||
code: &str,
|
||||
raw_token: &str,
|
||||
display_code: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, DbError> {
|
||||
let prefs = user_repo
|
||||
@@ -375,17 +376,17 @@ pub mod repo {
|
||||
.ok_or(DbError::NotFound)?;
|
||||
let strings = get_strings(prefs.preferred_locale.as_deref().unwrap_or("en"));
|
||||
let current_email = prefs.email.unwrap_or_default();
|
||||
let verify_page = format!("https://{}/app/verify?type=email-update", hostname);
|
||||
let verify_page = format!("https://{}/app/settings", hostname);
|
||||
let verify_link = format!(
|
||||
"https://{}/app/verify?type=email-update&token={}",
|
||||
"https://{}/xrpc/_account.authorizeEmailUpdate?token={}",
|
||||
hostname,
|
||||
urlencoding::encode(code)
|
||||
urlencoding::encode(raw_token)
|
||||
);
|
||||
let body = format_message(
|
||||
strings.email_update_body,
|
||||
&[
|
||||
("handle", &prefs.handle),
|
||||
("code", code),
|
||||
("code", display_code),
|
||||
("verify_page", &verify_page),
|
||||
("verify_link", &verify_link),
|
||||
],
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod plc;
|
||||
pub mod rate_limit;
|
||||
pub mod repo;
|
||||
pub mod scheduled;
|
||||
pub mod sso;
|
||||
pub mod state;
|
||||
pub mod storage;
|
||||
pub mod sync;
|
||||
@@ -287,6 +288,14 @@ pub fn app(state: AppState) -> Router {
|
||||
"/com.atproto.server.updateEmail",
|
||||
post(api::server::update_email),
|
||||
)
|
||||
.route(
|
||||
"/_account.authorizeEmailUpdate",
|
||||
get(api::server::authorize_email_update),
|
||||
)
|
||||
.route(
|
||||
"/_account.checkEmailUpdateStatus",
|
||||
get(api::server::check_email_update_status),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.reserveSigningKey",
|
||||
post(api::server::reserve_signing_key),
|
||||
@@ -569,7 +578,27 @@ pub fn app(state: AppState) -> Router {
|
||||
)
|
||||
.route("/token", post(oauth::endpoints::token_endpoint))
|
||||
.route("/revoke", post(oauth::endpoints::revoke_token))
|
||||
.route("/introspect", post(oauth::endpoints::introspect_token));
|
||||
.route("/introspect", post(oauth::endpoints::introspect_token))
|
||||
.route("/sso/providers", get(sso::endpoints::get_sso_providers))
|
||||
.route("/sso/initiate", post(sso::endpoints::sso_initiate))
|
||||
.route(
|
||||
"/sso/callback",
|
||||
get(sso::endpoints::sso_callback).post(sso::endpoints::sso_callback_post),
|
||||
)
|
||||
.route("/sso/linked", get(sso::endpoints::get_linked_accounts))
|
||||
.route("/sso/unlink", post(sso::endpoints::unlink_account))
|
||||
.route(
|
||||
"/sso/pending-registration",
|
||||
get(sso::endpoints::get_pending_registration),
|
||||
)
|
||||
.route(
|
||||
"/sso/complete-registration",
|
||||
post(sso::endpoints::complete_registration),
|
||||
)
|
||||
.route(
|
||||
"/sso/check-handle-available",
|
||||
get(sso::endpoints::check_handle_available),
|
||||
);
|
||||
|
||||
let well_known_router = Router::new()
|
||||
.route("/did.json", get(api::identity::well_known_did))
|
||||
|
||||
@@ -4,6 +4,13 @@ use std::sync::Arc;
|
||||
use tokio::sync::watch;
|
||||
use tracing::{error, info, warn};
|
||||
use tranquil_pds::comms::{CommsService, DiscordSender, EmailSender, SignalSender, TelegramSender};
|
||||
|
||||
const BUILD_VERSION: &str = concat!(
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (built ",
|
||||
env!("BUILD_TIMESTAMP"),
|
||||
")"
|
||||
);
|
||||
use tranquil_pds::crawlers::{Crawlers, start_crawlers_service};
|
||||
use tranquil_pds::scheduled::{
|
||||
backfill_genesis_commit_blocks, backfill_record_blobs, backfill_repo_rev, backfill_user_blocks,
|
||||
@@ -106,6 +113,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
state.user_repo.clone(),
|
||||
state.blob_repo.clone(),
|
||||
state.blob_store.clone(),
|
||||
state.sso_repo.clone(),
|
||||
shutdown_rx,
|
||||
));
|
||||
|
||||
@@ -121,7 +129,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid SERVER_HOST or SERVER_PORT: {}", e))?;
|
||||
|
||||
info!("listening on {}", addr);
|
||||
info!("tranquil-pds {} listening on {}", BUILD_VERSION, addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr)
|
||||
.await
|
||||
|
||||
@@ -459,9 +459,7 @@ pub async fn delegation_auth_token(
|
||||
headers: HeaderMap,
|
||||
Json(form): Json<DelegationTokenAuthSubmit>,
|
||||
) -> Response {
|
||||
let auth_header = headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
let auth_header = headers.get("authorization").and_then(|v| v.to_str().ok());
|
||||
|
||||
let extracted = match extract_auth_token_from_header(auth_header) {
|
||||
Some(e) => e,
|
||||
|
||||
@@ -176,7 +176,7 @@ pub async fn frontend_client_metadata(
|
||||
"refresh_token".to_string(),
|
||||
],
|
||||
response_types: vec!["code".to_string()],
|
||||
scope: "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:* identity:*"
|
||||
scope: "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage identity:*"
|
||||
.to_string(),
|
||||
token_endpoint_auth_method: "none".to_string(),
|
||||
application_type: "web".to_string(),
|
||||
|
||||
@@ -33,6 +33,9 @@ pub struct RateLimiters {
|
||||
pub handle_update: Arc<KeyedRateLimiter>,
|
||||
pub handle_update_daily: Arc<KeyedRateLimiter>,
|
||||
pub verification_check: Arc<KeyedRateLimiter>,
|
||||
pub sso_initiate: Arc<KeyedRateLimiter>,
|
||||
pub sso_callback: Arc<KeyedRateLimiter>,
|
||||
pub sso_unlink: Arc<KeyedRateLimiter>,
|
||||
}
|
||||
|
||||
impl Default for RateLimiters {
|
||||
@@ -95,6 +98,15 @@ impl RateLimiters {
|
||||
verification_check: Arc::new(RateLimiter::keyed(Quota::per_minute(
|
||||
NonZeroU32::new(60).unwrap(),
|
||||
))),
|
||||
sso_initiate: Arc::new(RateLimiter::keyed(Quota::per_minute(
|
||||
NonZeroU32::new(10).unwrap(),
|
||||
))),
|
||||
sso_callback: Arc::new(RateLimiter::keyed(Quota::per_minute(
|
||||
NonZeroU32::new(30).unwrap(),
|
||||
))),
|
||||
sso_unlink: Arc::new(RateLimiter::keyed(Quota::per_minute(
|
||||
NonZeroU32::new(10).unwrap(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +151,13 @@ impl RateLimiters {
|
||||
)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_sso_initiate_limit(mut self, per_minute: u32) -> Self {
|
||||
self.sso_initiate = Arc::new(RateLimiter::keyed(Quota::per_minute(
|
||||
NonZeroU32::new(per_minute).unwrap_or(NonZeroU32::new(10).unwrap()),
|
||||
)));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_client_ip(headers: &HeaderMap, addr: Option<SocketAddr>) -> String {
|
||||
|
||||
@@ -9,7 +9,8 @@ use tokio::sync::watch;
|
||||
use tokio::time::interval;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tranquil_db_traits::{
|
||||
BackupRepository, BlobRepository, BrokenGenesisCommit, RepoRepository, UserRepository,
|
||||
BackupRepository, BlobRepository, BrokenGenesisCommit, RepoRepository, SsoRepository,
|
||||
UserRepository,
|
||||
};
|
||||
use tranquil_types::{AtUri, CidLink, Did};
|
||||
|
||||
@@ -390,6 +391,7 @@ pub async fn start_scheduled_tasks(
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
blob_repo: Arc<dyn BlobRepository>,
|
||||
blob_store: Arc<dyn BlobStorage>,
|
||||
sso_repo: Arc<dyn SsoRepository>,
|
||||
mut shutdown_rx: watch::Receiver<bool>,
|
||||
) {
|
||||
let check_interval = Duration::from_secs(
|
||||
@@ -423,6 +425,36 @@ pub async fn start_scheduled_tasks(
|
||||
).await {
|
||||
error!("Error processing scheduled deletions: {}", e);
|
||||
}
|
||||
|
||||
match sso_repo.cleanup_expired_sso_auth_states().await {
|
||||
Ok(count) if count > 0 => {
|
||||
info!(count = count, "Cleaned up expired SSO auth states");
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Error cleaning up SSO auth states: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match sso_repo.cleanup_expired_pending_registrations().await {
|
||||
Ok(count) if count > 0 => {
|
||||
info!(count = count, "Cleaned up expired SSO pending registrations");
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Error cleaning up SSO pending registrations: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match user_repo.cleanup_expired_handle_reservations().await {
|
||||
Ok(count) if count > 0 => {
|
||||
info!(count = count, "Cleaned up expired handle reservations");
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Error cleaning up handle reservations: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
211
crates/tranquil-pds/src/sso/config.rs
Normal file
211
crates/tranquil-pds/src/sso/config.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use std::sync::OnceLock;
|
||||
use tranquil_db_traits::SsoProviderType;
|
||||
|
||||
static SSO_CONFIG: OnceLock<SsoConfig> = OnceLock::new();
|
||||
static SSO_REDIRECT_URI: OnceLock<String> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderConfig {
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub issuer: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppleProviderConfig {
|
||||
pub client_id: String,
|
||||
pub team_id: String,
|
||||
pub key_id: String,
|
||||
pub private_key_pem: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SsoConfig {
|
||||
pub github: Option<ProviderConfig>,
|
||||
pub discord: Option<ProviderConfig>,
|
||||
pub google: Option<ProviderConfig>,
|
||||
pub gitlab: Option<ProviderConfig>,
|
||||
pub oidc: Option<ProviderConfig>,
|
||||
pub apple: Option<AppleProviderConfig>,
|
||||
}
|
||||
|
||||
impl SsoConfig {
|
||||
pub fn init() -> &'static Self {
|
||||
SSO_CONFIG.get_or_init(|| {
|
||||
let github = Self::load_provider("GITHUB", false);
|
||||
let discord = Self::load_provider("DISCORD", false);
|
||||
let google = Self::load_provider("GOOGLE", false);
|
||||
let gitlab = Self::load_provider("GITLAB", true);
|
||||
let oidc = Self::load_provider("OIDC", true);
|
||||
let apple = Self::load_apple_provider();
|
||||
|
||||
let config = SsoConfig {
|
||||
github,
|
||||
discord,
|
||||
google,
|
||||
gitlab,
|
||||
oidc,
|
||||
apple,
|
||||
};
|
||||
|
||||
if config.is_any_enabled() {
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_default();
|
||||
if hostname.is_empty() || hostname == "localhost" {
|
||||
panic!(
|
||||
"PDS_HOSTNAME must be set to a valid hostname when SSO is enabled. \
|
||||
SSO redirect URIs require a proper hostname for security."
|
||||
);
|
||||
}
|
||||
SSO_REDIRECT_URI
|
||||
.set(format!("https://{}/oauth/sso/callback", hostname))
|
||||
.expect("SSO_REDIRECT_URI already set");
|
||||
tracing::info!(
|
||||
hostname = %hostname,
|
||||
providers = ?config.enabled_providers().iter().map(|p| p.as_str()).collect::<Vec<_>>(),
|
||||
"SSO initialized"
|
||||
);
|
||||
}
|
||||
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_redirect_uri() -> &'static str {
|
||||
SSO_REDIRECT_URI
|
||||
.get()
|
||||
.map(|s| s.as_str())
|
||||
.expect("SSO redirect URI not initialized - call SsoConfig::init() first")
|
||||
}
|
||||
|
||||
fn load_provider(name: &str, needs_issuer: bool) -> Option<ProviderConfig> {
|
||||
let enabled = std::env::var(format!("SSO_{}_ENABLED", name))
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false);
|
||||
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let client_id = std::env::var(format!("SSO_{}_CLIENT_ID", name)).ok()?;
|
||||
let client_secret = std::env::var(format!("SSO_{}_CLIENT_SECRET", name)).ok()?;
|
||||
|
||||
if client_id.is_empty() || client_secret.is_empty() {
|
||||
tracing::warn!(
|
||||
"SSO_{} enabled but missing client_id or client_secret",
|
||||
name
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let issuer = if needs_issuer {
|
||||
let issuer_val = std::env::var(format!("SSO_{}_ISSUER", name)).ok();
|
||||
if issuer_val.is_none() || issuer_val.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
|
||||
tracing::warn!("SSO_{} requires ISSUER but none provided", name);
|
||||
return None;
|
||||
}
|
||||
issuer_val
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let display_name = std::env::var(format!("SSO_{}_NAME", name)).ok();
|
||||
|
||||
Some(ProviderConfig {
|
||||
client_id,
|
||||
client_secret,
|
||||
issuer,
|
||||
display_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_apple_provider() -> Option<AppleProviderConfig> {
|
||||
let enabled = std::env::var("SSO_APPLE_ENABLED")
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false);
|
||||
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let client_id = std::env::var("SSO_APPLE_CLIENT_ID").ok()?;
|
||||
let team_id = std::env::var("SSO_APPLE_TEAM_ID").ok()?;
|
||||
let key_id = std::env::var("SSO_APPLE_KEY_ID").ok()?;
|
||||
let private_key_pem = std::env::var("SSO_APPLE_PRIVATE_KEY").ok()?;
|
||||
|
||||
if client_id.is_empty() {
|
||||
tracing::warn!("SSO_APPLE enabled but missing CLIENT_ID");
|
||||
return None;
|
||||
}
|
||||
if team_id.is_empty() || team_id.len() != 10 {
|
||||
tracing::warn!("SSO_APPLE enabled but TEAM_ID is invalid (must be 10 characters)");
|
||||
return None;
|
||||
}
|
||||
if key_id.is_empty() {
|
||||
tracing::warn!("SSO_APPLE enabled but missing KEY_ID");
|
||||
return None;
|
||||
}
|
||||
if private_key_pem.is_empty() || !private_key_pem.contains("PRIVATE KEY") {
|
||||
tracing::warn!("SSO_APPLE enabled but PRIVATE_KEY is invalid");
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(AppleProviderConfig {
|
||||
client_id,
|
||||
team_id,
|
||||
key_id,
|
||||
private_key_pem,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get() -> &'static Self {
|
||||
SSO_CONFIG.get_or_init(SsoConfig::default)
|
||||
}
|
||||
|
||||
pub fn get_provider_config(&self, provider: SsoProviderType) -> Option<&ProviderConfig> {
|
||||
match provider {
|
||||
SsoProviderType::Github => self.github.as_ref(),
|
||||
SsoProviderType::Discord => self.discord.as_ref(),
|
||||
SsoProviderType::Google => self.google.as_ref(),
|
||||
SsoProviderType::Gitlab => self.gitlab.as_ref(),
|
||||
SsoProviderType::Oidc => self.oidc.as_ref(),
|
||||
SsoProviderType::Apple => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_apple_config(&self) -> Option<&AppleProviderConfig> {
|
||||
self.apple.as_ref()
|
||||
}
|
||||
|
||||
pub fn enabled_providers(&self) -> Vec<SsoProviderType> {
|
||||
let mut providers = Vec::new();
|
||||
if self.github.is_some() {
|
||||
providers.push(SsoProviderType::Github);
|
||||
}
|
||||
if self.discord.is_some() {
|
||||
providers.push(SsoProviderType::Discord);
|
||||
}
|
||||
if self.google.is_some() {
|
||||
providers.push(SsoProviderType::Google);
|
||||
}
|
||||
if self.gitlab.is_some() {
|
||||
providers.push(SsoProviderType::Gitlab);
|
||||
}
|
||||
if self.oidc.is_some() {
|
||||
providers.push(SsoProviderType::Oidc);
|
||||
}
|
||||
if self.apple.is_some() {
|
||||
providers.push(SsoProviderType::Apple);
|
||||
}
|
||||
providers
|
||||
}
|
||||
|
||||
pub fn is_any_enabled(&self) -> bool {
|
||||
self.github.is_some()
|
||||
|| self.discord.is_some()
|
||||
|| self.google.is_some()
|
||||
|| self.gitlab.is_some()
|
||||
|| self.oidc.is_some()
|
||||
|| self.apple.is_some()
|
||||
}
|
||||
}
|
||||
1306
crates/tranquil-pds/src/sso/endpoints.rs
Normal file
1306
crates/tranquil-pds/src/sso/endpoints.rs
Normal file
File diff suppressed because it is too large
Load Diff
6
crates/tranquil-pds/src/sso/mod.rs
Normal file
6
crates/tranquil-pds/src/sso/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod config;
|
||||
pub mod endpoints;
|
||||
pub mod providers;
|
||||
|
||||
pub use config::SsoConfig;
|
||||
pub use providers::{AuthUrlResult, SsoError, SsoManager, SsoProvider, SsoUserInfo};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user