Verify Email + Reset Password flows (#654)
* verifiers * add missing emails * fix gh perms
This commit is contained in:
parent
4bdf9bff3a
commit
0d88ff8dae
23
.env
23
.env
@ -1,6 +1,5 @@
|
||||
DEBUG=true
|
||||
RUST_LOG=info,sqlx::query=warn
|
||||
|
||||
SENTRY_DSN=none
|
||||
|
||||
SITE_URL=https://modrinth.com
|
||||
@ -8,25 +7,26 @@ CDN_URL=https://staging-cdn.modrinth.com
|
||||
LABRINTH_ADMIN_KEY=feedbeef
|
||||
RATE_LIMIT_IGNORE_KEY=feedbeef
|
||||
|
||||
MODERATION_DISCORD_WEBHOOK=
|
||||
PUBLIC_DISCORD_WEBHOOK=
|
||||
CLOUDFLARE_INTEGRATION=false
|
||||
|
||||
DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth
|
||||
DATABASE_MIN_CONNECTIONS=0
|
||||
DATABASE_MAX_CONNECTIONS=16
|
||||
|
||||
REDIS_URL=redis://localhost
|
||||
|
||||
MEILISEARCH_ADDR=http://localhost:7700
|
||||
MEILISEARCH_KEY=modrinth
|
||||
|
||||
REDIS_URL=redis://localhost
|
||||
|
||||
BIND_ADDR=127.0.0.1:8000
|
||||
SELF_ADDR=http://localhost:8000
|
||||
MOCK_FILE_PATH=/tmp/modrinth
|
||||
|
||||
MODERATION_DISCORD_WEBHOOK=
|
||||
PUBLIC_DISCORD_WEBHOOK=
|
||||
CLOUDFLARE_INTEGRATION=false
|
||||
|
||||
STORAGE_BACKEND=local
|
||||
|
||||
MOCK_FILE_PATH=/tmp/modrinth
|
||||
|
||||
BACKBLAZE_KEY_ID=none
|
||||
BACKBLAZE_KEY=none
|
||||
BACKBLAZE_BUCKET_ID=none
|
||||
@ -73,3 +73,10 @@ GOOGLE_CLIENT_SECRET=none
|
||||
STEAM_API_KEY=none
|
||||
|
||||
TURNSTILE_SECRET=none
|
||||
|
||||
SMTP_USERNAME=none
|
||||
SMTP_PASSWORD=none
|
||||
SMTP_HOST=none
|
||||
|
||||
SITE_VERIFY_EMAIL_PATH=none
|
||||
SITE_RESET_PASSWORD_PATH=none
|
||||
279
sqlx-data.json
279
sqlx-data.json
@ -214,6 +214,19 @@
|
||||
},
|
||||
"query": "\n SELECT c.id id, c.category category, c.icon icon, c.header category_header, pt.name project_type\n FROM categories c\n INNER JOIN project_types pt ON c.project_type = pt.id\n ORDER BY c.ordering, c.category\n "
|
||||
},
|
||||
"09f4fba5c0c26457a7415a2196d4f5a9b2c72662b92cae8c96dda9557a024df7": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET email = $1, email_verified = FALSE\n WHERE (id = $2)\n "
|
||||
},
|
||||
"0a1a470c12b84c7e171f0f51e8e541e9abe8bbee17fc441a5054e1dfd5607c05": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1013,19 +1026,6 @@
|
||||
},
|
||||
"query": "\n SELECT m.id id, tm.user_id user_id, tm.payouts_split payouts_split\n FROM mods m\n INNER JOIN team_members tm on m.team_id = tm.team_id AND tm.accepted = TRUE\n WHERE m.id = ANY($1)\n "
|
||||
},
|
||||
"282190042b5ccc85e9b1643072a8703edae76dbbcc9493d5c9db392dca60230d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET google_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"294f264382ad55475b51776cd5d306c4867e8e6966ab79921bba69dc023f8337": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1163,6 +1163,19 @@
|
||||
},
|
||||
"query": "\n SELECT code FROM user_backup_codes\n WHERE user_id = $1\n "
|
||||
},
|
||||
"2df7a4dd792736be89c9da00c039ad7e271f79f4c756daac79ce5622ccb50db2": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET google_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"2e14706127d9822d5a0d7ada02425d224805637d03eda1343e12111f7deba443": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1242,18 +1255,6 @@
|
||||
},
|
||||
"query": "\n SELECT id FROM users\n WHERE email = $1\n "
|
||||
},
|
||||
"31415c0678f9f968181d1b1ee83e0ded54a99afc62a643dce73636f9bb20fd57": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET microsoft_id = NULL\n WHERE (id = $1)\n "
|
||||
},
|
||||
"320d73cd900a6e00f0e74b7a8c34a7658d16034b01a35558cb42fa9c16185eb5": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -1323,6 +1324,18 @@
|
||||
},
|
||||
"query": "\n UPDATE mods\n SET downloads = downloads + $1\n WHERE (id = $2)\n "
|
||||
},
|
||||
"33b9f52f7c67bf6272d0ba90a25185238d12494c9526ab112a854799627a69d7": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET email_verified = TRUE\n WHERE (id = $1)\n "
|
||||
},
|
||||
"33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1375,18 +1388,6 @@
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)"
|
||||
},
|
||||
"36d4b7a18f35cd177cec15e64d3cf4c156980167f0629af37c3947e5ed317faa": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET discord_id = NULL\n WHERE (id = $1)\n "
|
||||
},
|
||||
"371048e45dd74c855b84cdb8a6a565ccbef5ad166ec9511ab20621c336446da6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1551,6 +1552,19 @@
|
||||
},
|
||||
"query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = TRUE\n "
|
||||
},
|
||||
"4242d5d0a6d1d4f22172cdfb06ef47189b69b52e01d00ec2effe580b42eda717": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET password = $1\n WHERE (id = $2)\n "
|
||||
},
|
||||
"447350097928db863d47d756354cd52668f52f7156dd7f3673a826f7b9aca2fd": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -2244,18 +2258,6 @@
|
||||
},
|
||||
"query": "\n UPDATE mods_gallery\n SET ordering = $2\n WHERE id = $1\n "
|
||||
},
|
||||
"5f614c000f1a83ec87ebd0d514c9b484554dc0d097c6e105c42717930fd58058": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET steam_id = NULL\n WHERE (id = $1)\n "
|
||||
},
|
||||
"5f94e9e767ec4be7f9136b991b4a29373dbe48feb2f61281e3212721095ed675": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -2432,19 +2434,6 @@
|
||||
},
|
||||
"query": "\n UPDATE mods_gallery\n SET featured = $2\n WHERE mod_id = $1\n "
|
||||
},
|
||||
"6289dead78b0e128603f996be47e2d2a487668417124c48828eba8e01e18e651": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET discord_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"64d5e7cfb8472fbedcd06143db0db2f4c9677c42f73c540e85ccb5aee1a7b6f9": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -2716,18 +2705,6 @@
|
||||
},
|
||||
"query": "\n SELECT hp.created, hp.amount, hp.status\n FROM historical_payouts hp\n WHERE hp.user_id = $1\n ORDER BY hp.created DESC\n "
|
||||
},
|
||||
"6d2f27f02fe1073153a95ed2f7431a19170c9641588b444e45c94d0c6ba5b52c": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET github_id = NULL\n WHERE (id = $1)\n "
|
||||
},
|
||||
"6d883ea05aead20f571a0f63bfd63f1d432717ec7a0fb9ab29e01fcb061b3afc": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -2740,19 +2717,6 @@
|
||||
},
|
||||
"query": "\n UPDATE files\n SET is_primary = FALSE\n WHERE (version_id = $1)\n "
|
||||
},
|
||||
"6db583cb1c69cffeefaacc728046d62cc7d43018ced98420f9f629145f65f81a": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET steam_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"6db607d629be3047d53ff92bb82c07700595e8f4fcb7b602918540af4ae50d8b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -2827,19 +2791,6 @@
|
||||
},
|
||||
"query": "\n INSERT INTO threads_members (\n thread_id, user_id\n )\n VALUES (\n $1, $2\n )\n "
|
||||
},
|
||||
"71650a60acac690130e53c347c5882ead19c9b071f72de45e8737dc077c593d6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET gitlab_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"71abd207410d123f9a50345ddcddee335fea0d0cc6f28762713ee01a36aee8a0": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -3173,19 +3124,6 @@
|
||||
},
|
||||
"query": "\n SELECT n.id FROM notifications n\n WHERE n.user_id = $1\n "
|
||||
},
|
||||
"7b284f2c766ab64c57309f903d2c456ff74e06ea5f8454f0303215a5ad2cc93f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET password = $1\n WHERE (id = $2)\n "
|
||||
},
|
||||
"7c0cdacf0898155c94008a96a0b918550df4475b9e3362a926d4d00e001880c1": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -3239,6 +3177,32 @@
|
||||
},
|
||||
"query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n "
|
||||
},
|
||||
"7e030d43f3412e7df63c970f873d0a73dd2deb9857aa6f201ec5eec628eb336c": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET github_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"81e2e17bfbaadbb3d25072cf6cb8e8d7b3842252b3c72fcbd24aadd2ad933472": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET microsoft_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"83d428e1c07d16e356ef26bdf1d707940b1683b5f631ded1f6674a081453d67b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -3701,18 +3665,6 @@
|
||||
},
|
||||
"query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n "
|
||||
},
|
||||
"a04c2455d4ad53e2559927f1783322196c2e067666716407da737a7a44b23e66": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET google_id = NULL\n WHERE (id = $1)\n "
|
||||
},
|
||||
"a0c91184d5a02b986decac3c34e78b61451ff90e103bcf1ec46f8da3bbcc1ff2": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -4010,18 +3962,6 @@
|
||||
},
|
||||
"query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status,\n JSONB_AGG(DISTINCT jsonb_build_object('version', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes,\n JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.date_published ASC;\n "
|
||||
},
|
||||
"aa1d5c33e4ef370e372c6ea467cef103e494bbd167e8d1608ebd6786b87d4129": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET gitlab_id = NULL\n WHERE (id = $1)\n "
|
||||
},
|
||||
"aaec611bae08eac41c163367dc508208178170de91165095405f1b41e47f5e7f": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -5129,6 +5069,19 @@
|
||||
},
|
||||
"query": "\n SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h\n INNER JOIN files f on h.file_id = f.id\n INNER JOIN versions v on f.version_id = v.id\n WHERE h.algorithm = 'sha1' AND h.hash = ANY($1)\n "
|
||||
},
|
||||
"cfd80c4417c0534d24d65c782753927ba446e6ba542095c211ae5ee9b06b2753": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET gitlab_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"d1566672369ea22cb1f638f073f8e3fb467b354351ae71c67941323749ec9bcd": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -5219,6 +5172,19 @@
|
||||
},
|
||||
"query": "\n INSERT INTO pats (\n id, name, access_token, scopes, user_id,\n expires\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6\n )\n "
|
||||
},
|
||||
"d3f317f7d767f5188bace4064d548d3049df0d06420e3a23ebd8f326703a448e": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET discord_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"d59a0ca4725d40232eae8bf5735787e1b76282c390d2a8d07fb34e237a0b2132": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -5493,19 +5459,6 @@
|
||||
},
|
||||
"query": "\n DELETE FROM user_backup_codes\n WHERE user_id = $1 AND code = $2\n "
|
||||
},
|
||||
"de644eab84656a7e2b0d3108580763ebcd149cd6256d474521423d3e6ffaae65": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET microsoft_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"debb47a2718f79684c8776da7f289b8d178c302bb5a69562b963b8d008973b8d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -5519,19 +5472,6 @@
|
||||
},
|
||||
"query": "\n UPDATE threads_messages\n SET body = '{\"type\": \"deleted\"}', author_id = $2\n WHERE author_id = $1\n "
|
||||
},
|
||||
"df816c410a7bd4c0616ed24436bfc84308477b6d40de0b70adbc0c723a1f5cea": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET github_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"df871bd959ba97f105ac575f34d8d2a39cbc44a07e0339750a0e477e6fd582ed": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -6104,6 +6044,19 @@
|
||||
},
|
||||
"query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1))\n "
|
||||
},
|
||||
"f9bc19beaa70db45b058e80ba86599d393fad4c7d4af98426a8a9d9ca9b24035": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET steam_id = $2\n WHERE (id = $1)\n "
|
||||
},
|
||||
"fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
197
src/auth/email/auth_notif.html
Normal file
197
src/auth/email/auth_notif.html
Normal file
@ -0,0 +1,197 @@
|
||||
<!doctype html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title>{{ email_title }}</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a{padding:0;}body{margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}table,td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}img{border:0;height:auto;line-height:100%;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;}p{display:block;margin:0;}
|
||||
</style>
|
||||
<!--[if mso]> <noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.ogf{width:100% !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Inter:700,400" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:599px){.xc568{width:568px!important;max-width:568px;}.xc536{width:536px!important;max-width:536px;}.pc100{width:100%!important;max-width:100%;}.pc48-5915{width:48.5915%!important;max-width:48.5915%;}.pc2-8169{width:2.8169%!important;max-width:2.8169%;}}
|
||||
</style>
|
||||
<style media="screen and (min-width:599px)">.moz-text-html .xc568{width:568px!important;max-width:568px;}.moz-text-html .xc536{width:536px!important;max-width:536px;}.moz-text-html .pc100{width:100%!important;max-width:100%;}.moz-text-html .pc48-5915{width:48.5915%!important;max-width:48.5915%;}.moz-text-html .pc2-8169{width:2.8169%!important;max-width:2.8169%;}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:598px){table.fwm{width:100%!important;}td.fwm{width:auto!important;}}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
u+.modrinth-email .gs{background:#000;mix-blend-mode:screen;display:inline-block;padding:0;margin:0;}u+.modrinth-email .gd{background:#000;mix-blend-mode:difference;display:inline-block;padding:0;margin:0;}p{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}u+.modrinth-email a,#MessageViewBody a,a[x-apple-data-detectors]{color:inherit!important;text-decoration:none!important;font-size:inherit!important;font-family:inherit!important;font-weight:inherit!important;line-height:inherit!important;}td.b .klaviyo-image-block{display:inline;vertical-align:middle;}
|
||||
@media only screen and (max-width:599px){.modrinth-email{height:100%!important;margin:0!important;padding:0!important;width:100%!important;}u+.modrinth-email .glist{margin-left:1em!important;}td.ico.v>div.il>a.l.m,td.ico.v .mn-label{padding-right:0!important;padding-bottom:16px!important;}td.x{padding-left:0!important;padding-right:0!important;}.fwm img{max-width:100%!important;height:auto!important;}.aw img{width:auto!important;margin-left:auto!important;margin-right:auto!important;}.ah img{height:auto!important;}td.b.nw>table,td.b.nw a{width:auto!important;}td.stk{border:0!important;}td.u{height:auto!important;}br.sb{display:none!important;}.thd-1 .i-thumbnail{display:inline-block!important;height:auto!important;overflow:hidden!important;}.hd-1{display:block!important;height:auto!important;overflow:visible!important;}.ht-1{display:table!important;height:auto!important;overflow:visible!important;}.hr-1{display:table-row!important;height:auto!important;overflow:visible!important;}.hc-1{display:table-cell!important;height:auto!important;overflow:visible!important;}div.r.pr-16>table>tbody>tr>td,div.r.pr-16>div>table>tbody>tr>td{padding-right:16px!important}div.r.pl-16>table>tbody>tr>td,div.r.pl-16>div>table>tbody>tr>td{padding-left:16px!important}td.b.fw-1>table{width:100%!important}td.fw-1>table>tbody>tr>td>a{display:block!important;width:100%!important;padding-left:0!important;padding-right:0!important;}td.b.fw-1>table{width:100%!important}td.fw-1>table>tbody>tr>td{width:100%!important;padding-left:0!important;padding-right:0!important;}td.b.hvt-D9D9D9>table>tbody>tr>td:hover *{color:#D9D9D9!important}td.b.hvb-00954E>table>tbody>tr>td:hover,td.b.hvb-00954E>table>tbody>tr>td:hover>a{background-color:#00954E!important;}.hvr-fade *{-webkit-transition-duration:0.2s;transition-duration:0.2s;-webkit-transition-property:background-color,color;transition-property:background-color,color;}}
|
||||
@media (prefers-color-scheme:light) and (max-width:599px){.ds-1.hd-1{display:none!important;height:0!important;overflow:hidden!important;}}
|
||||
@media (prefers-color-scheme:dark) and (max-width:599px){.ds-1.hd-1{display:block!important;height:auto!important;overflow:visible!important;}}
|
||||
@media (prefers-color-scheme:dark){div.r.db-000000,div.r.db-000000>table{background-color:#000000!important;}div.r.dt-FFFFFE *{color:#FFFFFE!important}.dh-1{display:none!important;max-width:0!important;max-height:0!important;overflow:hidden!important;mso-hide:all!important;}.ds-1{display:block!important;max-width:none!important;max-height:none!important;height:auto!important;overflow:visible!important;mso-hide:all!important;}td.b.dt-000000 *{color:#000000!important}td.b.db-1BD96A>table>tbody>tr>td,td.b.db-1BD96A>table>tbody>tr>td>a{background-color:#1BD96A!important;}td.x.dt-B0BAC5 *{color:#B0BAC5!important}}.hvr-fade *{-webkit-transition-duration:0.2s;transition-duration:0.2s;-webkit-transition-property:background-color,color;transition-property:background-color,color;}td.b.hvt-D9D9D9>table>tbody>tr>td:hover *{color:#000000!important}td.b.hvb-00954E>table>tbody>tr>td:hover,td.b.hvb-00954E>table>tbody>tr>td:hover>a{background-color:#12b95a!important;}
|
||||
</style>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<!--[if gte mso 9]>
|
||||
<style>a:link,span.MsoHyperlink{mso-style-priority:99;color:inherit;text-decoration:none;}a:visited,span.MsoHyperlinkFollowed{mso-style-priority:99;color:inherit;text-decoration:none;}li{text-indent:-1em;}table,td,p,div,span,ul,ol,li,a{mso-hyphenate:none;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body lang="en" link="#DD0000" vlink="#DD0000" class="modrinth-email" style="mso-line-height-rule:exactly;mso-hyphenate:none;word-spacing:normal;background-color:#1e1e1e;"><div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">{{ email_description }}͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ </div><div class="bg" style="background-color:#1e1e1e;" lang="en">
|
||||
<!--[if mso | IE]>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="r-outlook -outlook pr-16-outlook pl-16-outlook db-000000-outlook dt-FFFFFE-outlook -outlook" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
|
||||
<![endif]--><div class="r pr-16 pl-16 db-000000 dt-FFFFFE" style="background:#eeeeee;background-color:#eeeeee;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#eeeeee;background-color:#eeeeee;width:100%;"><tbody><tr><td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="c-outlook -outlook -outlook" style="vertical-align:middle;width:568px;">
|
||||
<![endif]--><div class="xc568 ogf c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="left" class="i dh-1 m" style="font-size:0;padding:0;padding-bottom:8px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0;"><tbody><tr><td style="width:29px;"> <img alt src="https://cdn.modrinth.com/email/f740e2decee8764a4629bff677a284f9.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="29" height="auto">
|
||||
</td></tr></tbody></table>
|
||||
</td></tr><tr><td align="left" class="i ds-1" style="mso-hide:all;display:none;height:0;overflow:hidden;font-size:0;padding:0;padding-bottom:0;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;border-collapse:collapse;border-spacing:0;"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;width:29px;" width="29"> <img alt src="https://cdn.modrinth.com/email/d3cbe6edea372a9884c705303f4147f1.png" style="mso-hide:all;border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="29" height="auto">
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="r-outlook -outlook pr-16-outlook pl-16-outlook db-000000-outlook dt-FFFFFE-outlook -outlook" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
|
||||
<![endif]--><div class="r pr-16 pl-16 db-000000 dt-FFFFFE" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#fffffe;background-color:#fffffe;width:100%;"><tbody><tr><td style="border:none;direction:ltr;font-size:0;padding:32px 32px 32px 32px;text-align:left;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="c-outlook -outlook -outlook" style="vertical-align:middle;width:536px;">
|
||||
<![endif]--><div class="xc536 ogf c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="left" class="x m" style="font-size:0;padding-bottom:8px;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:32px;mso-ansi-font-size:28px;"><span style="font-size:28px;font-family:Inter,Arial,sans-serif;font-weight:700;color:#000000;line-height:114%;mso-line-height-alt:32px;mso-ansi-font-size:28px;">{{ email_title }}</span></p></div>
|
||||
</td></tr><tr><td align="left" class="x m" style="font-size:0;padding-bottom:8px;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:24px;mso-ansi-font-size:16px;"><span style="font-size:16px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">{{ line_one }}</span></p><p style="Margin:0;mso-line-height-alt:24px;mso-ansi-font-size:16px;"><span style="font-size:16px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;"> </span></p><p style="Margin:0;mso-line-height-alt:24px;mso-ansi-font-size:16px;"><span style="font-size:16px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">{{ line_two }}</span></p></div>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="r-outlook -outlook pr-16-outlook pl-16-outlook db-000000-outlook dt-FFFFFE-outlook -outlook" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
|
||||
<![endif]--><div class="r pr-16 pl-16 db-000000 dt-FFFFFE" style="background:#eeeeee;background-color:#eeeeee;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#eeeeee;background-color:#eeeeee;width:100%;"><tbody><tr><td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="width:568px;">
|
||||
<![endif]--><div class="pc100 ogf" style="font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;">
|
||||
<!--[if mso | IE]>
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"><tr><td style="vertical-align:middle;width:275px;">
|
||||
<![endif]--><div class="pc48-5915 ogf m c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:48.5915%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="left" class="i dh-1 m" style="font-size:0;padding:0;padding-bottom:8px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0;"><tbody><tr><td style="width:102px;"> <a href="https://modrinth.com" target="_blank" title> <img alt="modrinth logo" src="https://cdn.modrinth.com/email/bd3357dfae4b1d266250372db3a0988f.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="102" height="auto"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr><tr><td align="left" class="i ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;font-size:0;padding:0;padding-bottom:8px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;border-collapse:collapse;border-spacing:0;"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;width:102px;" width="102"> <a href="https://modrinth.com" target="_blank" title style="mso-hide:all;"> <img alt="modrinth logo" src="https://cdn.modrinth.com/email/4783699dd14602e3d326335cc56ff7ec.png" style="mso-hide:all;border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="102" height="auto"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr><tr><td align="left" class="x m" style="font-size:0;padding-bottom:8px;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#4d4d4d;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">Rinth, Inc.</span></p></div>
|
||||
</td></tr><tr><td align="left" class="x dt-B0BAC5" style="font-size:0;padding-bottom:0;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#aaaaaa;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">410 N Scottsdale Road</span></p><p style="Margin:0;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#aaaaaa;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">Suite 1000</span></p><p style="Margin:0;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#aaaaaa;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">Tempe, AZ 85281</span></p></div>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td><td style="width:15px;">
|
||||
<![endif]--><div class="pc2-8169 ogf g" style="font-size:0;text-align:left;direction:ltr;display:inline-block;width:2.8169%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"><tbody><tr><td style="padding:0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%"><tbody></tbody></table>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td><td style="vertical-align:middle;width:275px;">
|
||||
<![endif]--><div class="pc48-5915 ogf c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:48.5915%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="right" class="o" style="font-size:0;padding:0;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation"><tr><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://discord.gg/EUHuJHt" target="_blank"> <img alt="Discord" title height="20" src="https://cdn.modrinth.com/email/e089a3a07be91c2940beff1fb191b247.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="twitter.com/modrinth" target="_blank"> <img alt="Twitter" title height="20" src="https://cdn.modrinth.com/email/363985aad91cab53854276e12f267b0b.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://floss.social/@modrinth" target="_blank"> <img alt="Mastodon" title height="20" src="https://cdn.modrinth.com/email/e25c30d5707744f31dcf18651a67d3d5.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://github.com/modrinth/" target="_blank"> <img alt="GitHub" title height="20" src="https://cdn.modrinth.com/email/45993023966d64e6138fd65a530a5d03.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://www.youtube.com/@modrinth" target="_blank"> <img alt="YouTube" title height="20" src="https://cdn.modrinth.com/email/a15afae9fc94e105caeb1bb4d33a0a13.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://discord.gg/EUHuJHt" target="_blank" style="mso-hide:all;"> <img alt="Discord" title height="20" src="https://cdn.modrinth.com/email/8a44a64dd39ba64e3a480ef9872351c9.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="twitter.com/modrinth" target="_blank" style="mso-hide:all;"> <img alt="Twitter" title height="20" src="https://cdn.modrinth.com/email/b42fea63b0f099e851cb0ee60649c7aa.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://floss.social/@modrinth" target="_blank" style="mso-hide:all;"> <img alt="Mastodon" title height="20" src="https://cdn.modrinth.com/email/7469febd9dfb6dd61ce7fdac12b4a644.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://github.com/modrinth/" target="_blank" style="mso-hide:all;"> <img alt="GitHub" title height="20" src="https://cdn.modrinth.com/email/431cdb26e1afcff6b6d74c452c581987.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0;padding-right:0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://www.youtube.com/@modrinth" target="_blank" style="mso-hide:all;"> <img alt="YouTube" title height="20" src="https://cdn.modrinth.com/email/44f82a157ad4c4b122e1927a20b62660.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]--></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]--></div>
|
||||
</body>
|
||||
</html>
|
||||
202
src/auth/email/button_notif.html
Normal file
202
src/auth/email/button_notif.html
Normal file
@ -0,0 +1,202 @@
|
||||
<!doctype html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title>{{ email_title }}</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a{padding:0;}body{margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}table,td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}img{border:0;height:auto;line-height:100%;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;}p{display:block;margin:0;}
|
||||
</style>
|
||||
<!--[if mso]> <noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.ogf{width:100% !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Inter:700,400" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:599px){.xc568{width:568px!important;max-width:568px;}.xc536{width:536px!important;max-width:536px;}.pc100{width:100%!important;max-width:100%;}.pc48-5915{width:48.5915%!important;max-width:48.5915%;}.pc2-8169{width:2.8169%!important;max-width:2.8169%;}}
|
||||
</style>
|
||||
<style media="screen and (min-width:599px)">.moz-text-html .xc568{width:568px!important;max-width:568px;}.moz-text-html .xc536{width:536px!important;max-width:536px;}.moz-text-html .pc100{width:100%!important;max-width:100%;}.moz-text-html .pc48-5915{width:48.5915%!important;max-width:48.5915%;}.moz-text-html .pc2-8169{width:2.8169%!important;max-width:2.8169%;}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:598px){table.fwm{width:100%!important;}td.fwm{width:auto!important;}}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
u+.modrinth-email .gs{background:#000;mix-blend-mode:screen;display:inline-block;padding:0;margin:0;}u+.modrinth-email .gd{background:#000;mix-blend-mode:difference;display:inline-block;padding:0;margin:0;}p{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}u+.modrinth-email a,#MessageViewBody a,a[x-apple-data-detectors]{color:inherit!important;text-decoration:none!important;font-size:inherit!important;font-family:inherit!important;font-weight:inherit!important;line-height:inherit!important;}td.b .klaviyo-image-block{display:inline;vertical-align:middle;}
|
||||
@media only screen and (max-width:599px){.modrinth-email{height:100%!important;margin:0!important;padding:0!important;width:100%!important;}u+.modrinth-email .glist{margin-left:1em!important;}td.ico.v>div.il>a.l.m,td.ico.v .mn-label{padding-right:0!important;padding-bottom:16px!important;}td.x{padding-left:0!important;padding-right:0!important;}.fwm img{max-width:100%!important;height:auto!important;}.aw img{width:auto!important;margin-left:auto!important;margin-right:auto!important;}.ah img{height:auto!important;}td.b.nw>table,td.b.nw a{width:auto!important;}td.stk{border:0!important;}td.u{height:auto!important;}br.sb{display:none!important;}.thd-1 .i-thumbnail{display:inline-block!important;height:auto!important;overflow:hidden!important;}.hd-1{display:block!important;height:auto!important;overflow:visible!important;}.ht-1{display:table!important;height:auto!important;overflow:visible!important;}.hr-1{display:table-row!important;height:auto!important;overflow:visible!important;}.hc-1{display:table-cell!important;height:auto!important;overflow:visible!important;}div.r.pr-16>table>tbody>tr>td,div.r.pr-16>div>table>tbody>tr>td{padding-right:16px!important}div.r.pl-16>table>tbody>tr>td,div.r.pl-16>div>table>tbody>tr>td{padding-left:16px!important}td.b.fw-1>table{width:100%!important}td.fw-1>table>tbody>tr>td>a{display:block!important;width:100%!important;padding-left:0!important;padding-right:0!important;}td.b.fw-1>table{width:100%!important}td.fw-1>table>tbody>tr>td{width:100%!important;padding-left:0!important;padding-right:0!important;}td.b.hvt-D9D9D9>table>tbody>tr>td:hover *{color:#D9D9D9!important}td.b.hvb-00954E>table>tbody>tr>td:hover,td.b.hvb-00954E>table>tbody>tr>td:hover>a{background-color:#00954E!important;}.hvr-fade *{-webkit-transition-duration:0.2s;transition-duration:0.2s;-webkit-transition-property:background-color,color;transition-property:background-color,color;}}
|
||||
@media (prefers-color-scheme:light) and (max-width:599px){.ds-1.hd-1{display:none!important;height:0!important;overflow:hidden!important;}}
|
||||
@media (prefers-color-scheme:dark) and (max-width:599px){.ds-1.hd-1{display:block!important;height:auto!important;overflow:visible!important;}}
|
||||
@media (prefers-color-scheme:dark){div.r.db-000000,div.r.db-000000>table{background-color:#000000!important;}div.r.dt-FFFFFE *{color:#FFFFFE!important}.dh-1{display:none!important;max-width:0!important;max-height:0!important;overflow:hidden!important;mso-hide:all!important;}.ds-1{display:block!important;max-width:none!important;max-height:none!important;height:auto!important;overflow:visible!important;mso-hide:all!important;}td.b.dt-000000 *{color:#000000!important}td.b.db-1BD96A>table>tbody>tr>td,td.b.db-1BD96A>table>tbody>tr>td>a{background-color:#1BD96A!important;}td.x.dt-B0BAC5 *{color:#B0BAC5!important}}.hvr-fade *{-webkit-transition-duration:0.2s;transition-duration:0.2s;-webkit-transition-property:background-color,color;transition-property:background-color,color;}td.b.hvt-D9D9D9>table>tbody>tr>td:hover *{color:#000000!important}td.b.hvb-00954E>table>tbody>tr>td:hover,td.b.hvb-00954E>table>tbody>tr>td:hover>a{background-color:#12b95a!important;}
|
||||
</style>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<!--[if gte mso 9]>
|
||||
<style>a:link,span.MsoHyperlink{mso-style-priority:99;color:inherit;text-decoration:none;}a:visited,span.MsoHyperlinkFollowed{mso-style-priority:99;color:inherit;text-decoration:none;}li{text-indent:-1em;}table,td,p,div,span,ul,ol,li,a{mso-hyphenate:none;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body lang="en" link="#DD0000" vlink="#DD0000" class="modrinth-email" style="mso-line-height-rule:exactly;mso-hyphenate:none;word-spacing:normal;background-color:#1e1e1e;"><div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">{{ email_description }}͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ </div><div class="bg" style="background-color:#1e1e1e;" lang="en">
|
||||
<!--[if mso | IE]>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="r-outlook -outlook pr-16-outlook pl-16-outlook db-000000-outlook dt-FFFFFE-outlook -outlook" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
|
||||
<![endif]--><div class="r pr-16 pl-16 db-000000 dt-FFFFFE" style="background:#eeeeee;background-color:#eeeeee;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#eeeeee;background-color:#eeeeee;width:100%;"><tbody><tr><td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="c-outlook -outlook -outlook" style="vertical-align:middle;width:568px;">
|
||||
<![endif]--><div class="xc568 ogf c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="left" class="i dh-1 m" style="font-size:0;padding:0;padding-bottom:8px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0;"><tbody><tr><td style="width:29px;"> <img alt src="https://cdn.modrinth.com/email/f740e2decee8764a4629bff677a284f9.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="29" height="auto">
|
||||
</td></tr></tbody></table>
|
||||
</td></tr><tr><td align="left" class="i ds-1" style="mso-hide:all;display:none;height:0;overflow:hidden;font-size:0;padding:0;padding-bottom:0;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;border-collapse:collapse;border-spacing:0;"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;width:29px;" width="29"> <img alt src="https://cdn.modrinth.com/email/d3cbe6edea372a9884c705303f4147f1.png" style="mso-hide:all;border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="29" height="auto">
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="r-outlook -outlook pr-16-outlook pl-16-outlook db-000000-outlook dt-FFFFFE-outlook -outlook" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
|
||||
<![endif]--><div class="r pr-16 pl-16 db-000000 dt-FFFFFE" style="background:#fffffe;background-color:#fffffe;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#fffffe;background-color:#fffffe;width:100%;"><tbody><tr><td style="border:none;direction:ltr;font-size:0;padding:32px 32px 32px 32px;text-align:left;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="c-outlook -outlook -outlook" style="vertical-align:middle;width:536px;">
|
||||
<![endif]--><div class="xc536 ogf c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="left" class="x m" style="font-size:0;padding-bottom:8px;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:32px;mso-ansi-font-size:28px;"><span style="font-size:28px;font-family:Inter,Arial,sans-serif;font-weight:700;color:#000000;line-height:114%;mso-line-height-alt:32px;mso-ansi-font-size:28px;">{{ email_title }}</span></p></div>
|
||||
</td></tr><tr><td align="left" class="x m" style="font-size:0;padding-bottom:8px;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:24px;mso-ansi-font-size:16px;"><span style="font-size:16px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">{{ line_one }}</span></p><p style="Margin:0;mso-line-height-alt:24px;mso-ansi-font-size:16px;"><span style="font-size:16px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;"> </span></p><p style="Margin:0;mso-line-height-alt:24px;mso-ansi-font-size:16px;"><span style="font-size:16px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#777777;line-height:150%;mso-line-height-alt:24px;mso-ansi-font-size:16px;">{{ line_two }}</span></p></div>
|
||||
</td></tr><tr><td class="s m" style="font-size:0;padding:0;padding-bottom:8px;word-break:break-word;" aria-hidden="true"><div style="height:4px;line-height:4px;"> </div>
|
||||
</td></tr><tr><td align="left" vertical-align="middle" class="b fw-1 dt-000000 db-1BD96A hvt-D9D9D9 hvb-00954E hvr-fade m" style="font-size:0;padding:0;padding-bottom:8px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;width:146px;line-height:100%;"><tbody><tr><td align="center" bgcolor="#00af5c" role="presentation" style="border:none;border-radius:12px 12px 12px 12px;cursor:auto;mso-padding-alt:12px 0px 12px 0px;background:#00af5c;" valign="middle"> <a href="{{ button_link }}" style="display:inline-block;width:146px;background:#00af5c;color:#ffffff;font-family:Inter,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:100%;margin:0;text-decoration:none;text-transform:none;padding:12px 0px 12px 0px;mso-padding-alt:0;border-radius:12px 12px 12px 12px;" target="_blank"> <span style="font-size:14px;font-family:Inter,Arial,sans-serif;font-weight:700;color:#ffffff;line-height:121%;mso-line-height-alt:18px;mso-ansi-font-size:14px;">{{ button_title }}</span></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr><tr><td align="left" class="x" style="font-size:0;padding-bottom:0;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:22px;mso-ansi-font-size:14px;"><span style="font-size:14px;font-family:Inter,Arial,sans-serif;font-weight:700;color:#777777;line-height:150%;mso-line-height-alt:22px;mso-ansi-font-size:14px;">{{ button_link }}</span></p></div>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="r-outlook -outlook pr-16-outlook pl-16-outlook db-000000-outlook dt-FFFFFE-outlook -outlook" role="presentation" style="width:600px;" width="600"><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;">
|
||||
<![endif]--><div class="r pr-16 pl-16 db-000000 dt-FFFFFE" style="background:#eeeeee;background-color:#eeeeee;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#eeeeee;background-color:#eeeeee;width:100%;"><tbody><tr><td style="border:none;direction:ltr;font-size:0;padding:16px 16px 16px 16px;text-align:left;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="width:568px;">
|
||||
<![endif]--><div class="pc100 ogf" style="font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;">
|
||||
<!--[if mso | IE]>
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"><tr><td style="vertical-align:middle;width:275px;">
|
||||
<![endif]--><div class="pc48-5915 ogf m c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:48.5915%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="left" class="i dh-1 m" style="font-size:0;padding:0;padding-bottom:8px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0;"><tbody><tr><td style="width:102px;"> <a href="https://modrinth.com" target="_blank" title> <img alt="modrinth logo" src="https://cdn.modrinth.com/email/bd3357dfae4b1d266250372db3a0988f.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="102" height="auto"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr><tr><td align="left" class="i ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;font-size:0;padding:0;padding-bottom:8px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;border-collapse:collapse;border-spacing:0;"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;width:102px;" width="102"> <a href="https://modrinth.com" target="_blank" title style="mso-hide:all;"> <img alt="modrinth logo" src="https://cdn.modrinth.com/email/4783699dd14602e3d326335cc56ff7ec.png" style="mso-hide:all;border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" title width="102" height="auto"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr><tr><td align="left" class="x m" style="font-size:0;padding-bottom:8px;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#4d4d4d;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">Rinth, Inc.</span></p></div>
|
||||
</td></tr><tr><td align="left" class="x dt-B0BAC5" style="font-size:0;padding-bottom:0;word-break:break-word;"><div style="text-align:left;"><p style="Margin:0;text-align:left;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#aaaaaa;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">410 N Scottsdale Road</span></p><p style="Margin:0;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#aaaaaa;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">Suite 1000</span></p><p style="Margin:0;mso-line-height-alt:16px;mso-ansi-font-size:14px;"><span style="font-size:13px;font-family:Inter,Arial,sans-serif;font-weight:400;color:#aaaaaa;line-height:123%;mso-line-height-alt:16px;mso-ansi-font-size:14px;">Tempe, AZ 85281</span></p></div>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td><td style="width:15px;">
|
||||
<![endif]--><div class="pc2-8169 ogf g" style="font-size:0;text-align:left;direction:ltr;display:inline-block;width:2.8169%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"><tbody><tr><td style="padding:0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%"><tbody></tbody></table>
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td><td style="vertical-align:middle;width:275px;">
|
||||
<![endif]--><div class="pc48-5915 ogf c" style="font-size:0;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:48.5915%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border:none;vertical-align:middle;" width="100%"><tbody><tr><td align="right" class="o" style="font-size:0;padding:0;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation"><tr><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://discord.gg/EUHuJHt" target="_blank"> <img alt="Discord" title height="20" src="https://cdn.modrinth.com/email/e089a3a07be91c2940beff1fb191b247.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="twitter.com/modrinth" target="_blank"> <img alt="Twitter" title height="20" src="https://cdn.modrinth.com/email/363985aad91cab53854276e12f267b0b.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://floss.social/@modrinth" target="_blank"> <img alt="Mastodon" title height="20" src="https://cdn.modrinth.com/email/e25c30d5707744f31dcf18651a67d3d5.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://github.com/modrinth/" target="_blank"> <img alt="GitHub" title height="20" src="https://cdn.modrinth.com/email/45993023966d64e6138fd65a530a5d03.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e dh-1 m"><td style="padding:0 16px 0 0;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:20px;"><tbody><tr><td style="font-size:0;height:20px;vertical-align:middle;width:20px;"> <a href="https://www.youtube.com/@modrinth" target="_blank"> <img alt="YouTube" title height="20" src="https://cdn.modrinth.com/email/a15afae9fc94e105caeb1bb4d33a0a13.png" style="display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://discord.gg/EUHuJHt" target="_blank" style="mso-hide:all;"> <img alt="Discord" title height="20" src="https://cdn.modrinth.com/email/8a44a64dd39ba64e3a480ef9872351c9.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="twitter.com/modrinth" target="_blank" style="mso-hide:all;"> <img alt="Twitter" title height="20" src="https://cdn.modrinth.com/email/b42fea63b0f099e851cb0ee60649c7aa.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://floss.social/@modrinth" target="_blank" style="mso-hide:all;"> <img alt="Mastodon" title height="20" src="https://cdn.modrinth.com/email/7469febd9dfb6dd61ce7fdac12b4a644.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1 m" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0 16px 0 0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://github.com/modrinth/" target="_blank" style="mso-hide:all;"> <img alt="GitHub" title height="20" src="https://cdn.modrinth.com/email/431cdb26e1afcff6b6d74c452c581987.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td><td>
|
||||
<![endif]-->
|
||||
<table align="right" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"><tbody><tr class="e ds-1" style="mso-hide:all;display:none;height:0;overflow:hidden;"><td style="mso-hide:all;padding:0;padding-right:0;vertical-align:middle;" valign="middle">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-hide:all;width:20px;" width="20"><tbody style="mso-hide:all;"><tr style="mso-hide:all;"><td style="mso-hide:all;font-size:0;height:20px;vertical-align:middle;width:20px;" width="20" height="20" valign="middle"> <a href="https://www.youtube.com/@modrinth" target="_blank" style="mso-hide:all;"> <img alt="YouTube" title height="20" src="https://cdn.modrinth.com/email/44f82a157ad4c4b122e1927a20b62660.png" style="mso-hide:all;display:block;" width="20"></a>
|
||||
</td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]--></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td></tr></tbody></table></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]--></div>
|
||||
</body>
|
||||
</html>
|
||||
72
src/auth/email/mod.rs
Normal file
72
src/auth/email/mod.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::message::Mailbox;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Address, Message, SmtpTransport, Transport};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MailError {
|
||||
#[error("Environment Error")]
|
||||
Env(#[from] dotenvy::Error),
|
||||
#[error("Mail Error: {0}")]
|
||||
Mail(#[from] lettre::error::Error),
|
||||
#[error("Address Parse Error: {0}")]
|
||||
Address(#[from] lettre::address::AddressError),
|
||||
#[error("SMTP Error: {0}")]
|
||||
Smtp(#[from] lettre::transport::smtp::Error),
|
||||
}
|
||||
|
||||
pub fn send_email_raw(to: String, subject: String, body: String) -> Result<(), MailError> {
|
||||
let email = Message::builder()
|
||||
.from(Mailbox::new(
|
||||
Some("Modrinth".to_string()),
|
||||
Address::new("no-reply", "mail.modrinth.com")?,
|
||||
))
|
||||
.to(to.parse().unwrap())
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
|
||||
let username = dotenvy::var("SMTP_USERNAME")?;
|
||||
let password = dotenvy::var("SMTP_PASSWORD")?;
|
||||
let host = dotenvy::var("SMTP_HOST")?;
|
||||
let creds = Credentials::new(username, password);
|
||||
|
||||
let mailer = SmtpTransport::relay(&host)
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
mailer.send(&email)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_email(
|
||||
to: String,
|
||||
email_title: &str,
|
||||
email_description: &str,
|
||||
line_two: &str,
|
||||
button_info: Option<(&str, &str)>,
|
||||
) -> Result<(), MailError> {
|
||||
let mut email = if button_info.is_some() {
|
||||
include_str!("button_notif.html")
|
||||
} else {
|
||||
include_str!("auth_notif.html")
|
||||
}
|
||||
.replace("{{ email_title }}", email_title)
|
||||
.replace("{{ email_description }}", email_description)
|
||||
.replace("{{ line_one }}", email_description)
|
||||
.replace("{{ line_two }}", line_two);
|
||||
|
||||
if let Some((button_title, button_link)) = button_info {
|
||||
email = email
|
||||
.replace("{{ button_title }}", button_title)
|
||||
.replace("{{ button_link }}", button_link);
|
||||
}
|
||||
|
||||
send_email_raw(to, email_title.to_string(), email)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::auth::email::send_email;
|
||||
use crate::auth::session::issue_session;
|
||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||
use crate::auth::{get_user_from_headers, AuthenticationError};
|
||||
@ -14,7 +15,7 @@ use crate::util::captcha::check_turnstile_captcha;
|
||||
use crate::util::ext::{get_image_content_type, get_image_ext};
|
||||
use crate::util::validate::{validation_errors_to_string, RE_URL_SAFE};
|
||||
use actix_web::web::{scope, Data, Query, ServiceConfig};
|
||||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use chrono::{Duration, Utc};
|
||||
@ -29,13 +30,23 @@ use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
pub fn config(cfg: &mut ServiceConfig) {
|
||||
cfg.service(scope("auth").service(auth_callback).service(init))
|
||||
.service(create_account_with_password)
|
||||
.service(login_password)
|
||||
.service(login_2fa)
|
||||
.service(begin_2fa_flow)
|
||||
.service(finish_2fa_flow)
|
||||
.service(remove_2fa);
|
||||
cfg.service(
|
||||
scope("auth")
|
||||
.service(init)
|
||||
.service(auth_callback)
|
||||
.service(delete_auth_provider)
|
||||
.service(create_account_with_password)
|
||||
.service(login_password)
|
||||
.service(login_2fa)
|
||||
.service(begin_2fa_flow)
|
||||
.service(finish_2fa_flow)
|
||||
.service(remove_2fa)
|
||||
.service(reset_password_begin)
|
||||
.service(change_password)
|
||||
.service(resend_verify_email)
|
||||
.service(set_email)
|
||||
.service(verify_email),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Eq, PartialEq, Clone, Copy)]
|
||||
@ -607,6 +618,107 @@ impl AuthProvider {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_user_id(
|
||||
&self,
|
||||
user_id: crate::database::models::UserId,
|
||||
id: Option<&str>,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
match self {
|
||||
AuthProvider::GitHub => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET github_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
user_id as crate::database::models::UserId,
|
||||
id.and_then(|x| x.parse::<i64>().ok())
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Discord => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET discord_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
user_id as crate::database::models::UserId,
|
||||
id.and_then(|x| x.parse::<i64>().ok())
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Microsoft => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET microsoft_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
user_id as crate::database::models::UserId,
|
||||
id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::GitLab => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET gitlab_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
user_id as crate::database::models::UserId,
|
||||
id.and_then(|x| x.parse::<i64>().ok())
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Google => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET google_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
user_id as crate::database::models::UserId,
|
||||
id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Steam => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET steam_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
user_id as crate::database::models::UserId,
|
||||
id.and_then(|x| x.parse::<i64>().ok())
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
AuthProvider::GitHub => "GitHub",
|
||||
AuthProvider::Discord => "Discord",
|
||||
AuthProvider::Microsoft => "Microsoft",
|
||||
AuthProvider::GitLab => "GitLab",
|
||||
AuthProvider::Google => "Google",
|
||||
AuthProvider::Steam => "Steam",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -704,85 +816,19 @@ pub async fn auth_callback(
|
||||
return Err(AuthenticationError::DuplicateUser);
|
||||
}
|
||||
|
||||
match provider {
|
||||
AuthProvider::GitHub => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET github_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::UserId,
|
||||
oauth_user.id.parse::<i64>().ok(),
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Discord => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET discord_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::UserId,
|
||||
oauth_user.id.parse::<i64>().ok(),
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Microsoft => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET microsoft_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::UserId,
|
||||
oauth_user.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::GitLab => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET gitlab_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::UserId,
|
||||
oauth_user.id.parse::<i64>().ok(),
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Google => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET google_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::UserId,
|
||||
oauth_user.id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
AuthProvider::Steam => {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET steam_id = $2
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::UserId,
|
||||
oauth_user.id.parse::<i64>().ok(),
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
provider
|
||||
.update_user_id(id, Some(&oauth_user.id), &mut transaction)
|
||||
.await?;
|
||||
|
||||
let user = crate::database::models::User::get_id(id, &**client, &redis).await?;
|
||||
if let Some(email) = user.and_then(|x| x.email) {
|
||||
send_email(
|
||||
email,
|
||||
"Authentication method added",
|
||||
&format!("When logging into Modrinth, you can now log in using the {} authentication provider.", provider.as_str()),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
crate::database::models::User::clear_caches(&[(id, None)], &redis).await?;
|
||||
@ -991,6 +1037,60 @@ pub async fn auth_callback(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteAuthProvider {
|
||||
pub provider: AuthProvider,
|
||||
}
|
||||
|
||||
#[delete("provider")]
|
||||
pub async fn delete_auth_provider(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<deadpool_redis::Pool>,
|
||||
delete_provider: web::Json<DeleteAuthProvider>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
if !user.auth_providers.map(|x| x.len() > 1).unwrap_or(false)
|
||||
&& !user.has_password.unwrap_or(false)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You must have another authentication method added to this account!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
delete_provider
|
||||
.provider
|
||||
.update_user_id(user.id.into(), None, &mut transaction)
|
||||
.await?;
|
||||
|
||||
if let Some(email) = user.email {
|
||||
send_email(
|
||||
email,
|
||||
"Authentication method removed",
|
||||
&format!("When logging into Modrinth, you can no longer log in using the {} authentication provider.", delete_provider.provider.as_str()),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct NewAccount {
|
||||
#[validate(length(min = 1, max = 39), regex = "RE_URL_SAFE")]
|
||||
@ -1060,6 +1160,19 @@ pub async fn create_account_with_password(
|
||||
));
|
||||
}
|
||||
|
||||
let flow = Flow::ConfirmEmail {
|
||||
user_id,
|
||||
confirm_email: new_account.email.clone(),
|
||||
}
|
||||
.insert(Utc::now() + Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
send_email_verify(
|
||||
new_account.email.clone(),
|
||||
flow,
|
||||
&format!("Welcome to Modritnh, {}!", new_account.username),
|
||||
)?;
|
||||
|
||||
crate::database::models::User {
|
||||
id: user_id,
|
||||
github_id: None,
|
||||
@ -1243,7 +1356,7 @@ pub async fn login_2fa(
|
||||
}
|
||||
}
|
||||
|
||||
#[get("2fa")]
|
||||
#[post("2fa/get_secret")]
|
||||
pub async fn begin_2fa_flow(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
@ -1368,6 +1481,16 @@ pub async fn finish_2fa_flow(
|
||||
codes.push(to_base62(val));
|
||||
}
|
||||
|
||||
if let Some(email) = user.email {
|
||||
send_email(
|
||||
email,
|
||||
"Two-factor authentication enabled",
|
||||
"When logging into Modrinth, you can now enter a code generated by your authenticator app in addition to entering your usual email address and password.",
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
@ -1438,9 +1561,385 @@ pub async fn remove_2fa(
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?;
|
||||
if let Some(email) = user.email {
|
||||
send_email(
|
||||
email,
|
||||
"Two-factor authentication removed",
|
||||
"When logging into Modrinth, you no longer need two-factor authentication to gain access.",
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResetPassword {
|
||||
pub username: String,
|
||||
pub challenge: String,
|
||||
}
|
||||
|
||||
#[post("password/reset")]
|
||||
pub async fn reset_password_begin(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<deadpool_redis::Pool>,
|
||||
reset_password: web::Json<ResetPassword>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if check_turnstile_captcha(&req, &reset_password.challenge).await? {
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
let user = if let Some(user_id) =
|
||||
crate::database::models::User::get_email(&reset_password.username, &**pool).await?
|
||||
{
|
||||
crate::database::models::User::get_id(user_id, &**pool, &redis).await?
|
||||
} else {
|
||||
crate::database::models::User::get(&reset_password.username, &**pool, &redis).await?
|
||||
};
|
||||
|
||||
if let Some(user) = user {
|
||||
let flow = Flow::ForgotPassword { user_id: user.id }
|
||||
.insert(Utc::now() + Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
if let Some(email) = user.email {
|
||||
send_email(
|
||||
email,
|
||||
"Reset your password",
|
||||
"Please visit the following link below to reset your password. If the button does not work, you can copy the link and paste it into your browser.",
|
||||
"If you did not request for your password to be reset, you can safely ignore this email.",
|
||||
Some(("Reset password", &format!("{}/{}?flow={}", dotenvy::var("SITE_URL")?, dotenvy::var("SITE_RESET_PASSWORD_PATH")?, flow))),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct ChangePassword {
|
||||
pub flow: Option<String>,
|
||||
pub old_password: Option<String>,
|
||||
pub new_password: Option<String>,
|
||||
}
|
||||
|
||||
#[patch("password")]
|
||||
pub async fn change_password(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<deadpool_redis::Pool>,
|
||||
change_password: web::Json<ChangePassword>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = if let Some(flow) = &change_password.flow {
|
||||
let flow = Flow::get(flow, &redis).await?;
|
||||
|
||||
if let Some(Flow::ForgotPassword { user_id }) = flow {
|
||||
let user = crate::database::models::User::get_id(user_id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
Some(user)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let user = if let Some(user) = user {
|
||||
user
|
||||
} else {
|
||||
let (scopes, user) =
|
||||
get_user_record_from_bearer_token(&req, None, &**pool, &redis, &session_queue)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
if !scopes.contains(Scopes::USER_AUTH_WRITE) {
|
||||
return Err(ApiError::Authentication(
|
||||
AuthenticationError::InvalidCredentials,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(pass) = user.password.as_ref() {
|
||||
let old_password = change_password.old_password.as_ref().ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You must specify the old password to change your password!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let hasher = Argon2::default();
|
||||
hasher.verify_password(old_password.as_bytes(), &PasswordHash::new(pass)?)?;
|
||||
}
|
||||
|
||||
user
|
||||
};
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let update_password = if let Some(new_password) = &change_password.new_password {
|
||||
let score = zxcvbn::zxcvbn(
|
||||
new_password,
|
||||
&[
|
||||
&user.username,
|
||||
&user.email.clone().unwrap_or_default(),
|
||||
&user.name.unwrap_or_default(),
|
||||
],
|
||||
)?;
|
||||
|
||||
if score.score() < 3 {
|
||||
return Err(ApiError::InvalidInput(
|
||||
if let Some(feedback) = score.feedback().clone().and_then(|x| x.warning()) {
|
||||
format!("Password too weak: {}", feedback)
|
||||
} else {
|
||||
"Specified password is too weak! Please improve its strength.".to_string()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let hasher = Argon2::default();
|
||||
let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy());
|
||||
let password_hash = hasher
|
||||
.hash_password(new_password.as_bytes(), &salt)?
|
||||
.to_string();
|
||||
|
||||
Some(password_hash)
|
||||
} else {
|
||||
if !(user.github_id.is_some()
|
||||
|| user.gitlab_id.is_some()
|
||||
|| user.microsoft_id.is_some()
|
||||
|| user.google_id.is_some()
|
||||
|| user.steam_id.is_some()
|
||||
|| user.discord_id.is_some())
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You must have another authentication method added to remove password authentication!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET password = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
update_password,
|
||||
user.id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if let Some(flow) = &change_password.flow {
|
||||
Flow::remove(flow, &redis).await?;
|
||||
}
|
||||
|
||||
if let Some(email) = user.email {
|
||||
let changed = if update_password.is_some() {
|
||||
"changed"
|
||||
} else {
|
||||
"removed"
|
||||
};
|
||||
|
||||
send_email(
|
||||
email,
|
||||
&format!("Password {}", changed),
|
||||
&format!("Your password has been {} on your account.", changed),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct SetEmail {
|
||||
#[validate(email)]
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[patch("email")]
|
||||
pub async fn set_email(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<deadpool_redis::Pool>,
|
||||
email: web::Json<SetEmail>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
email
|
||||
.0
|
||||
.validate()
|
||||
.map_err(|err| ApiError::InvalidInput(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET email = $1, email_verified = FALSE
|
||||
WHERE (id = $2)
|
||||
",
|
||||
email.email,
|
||||
user.id.0 as i64,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
if let Some(user_email) = user.email {
|
||||
send_email(
|
||||
user_email,
|
||||
"Email changed",
|
||||
&format!("Your email has been updated to {} on your account.", email.email),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
let flow = Flow::ConfirmEmail {
|
||||
user_id: user.id.into(),
|
||||
confirm_email: email.email.clone(),
|
||||
}
|
||||
.insert(Utc::now() + Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
send_email_verify(
|
||||
email.email.clone(),
|
||||
flow,
|
||||
"We need to verify your email address.",
|
||||
)?;
|
||||
|
||||
crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
#[post("email/resend_verify")]
|
||||
pub async fn resend_verify_email(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<deadpool_redis::Pool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
if let Some(email) = user.email {
|
||||
if user.email_verified.unwrap_or(false) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"User email is already verified!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let flow = Flow::ConfirmEmail {
|
||||
user_id: user.id.into(),
|
||||
confirm_email: email.clone(),
|
||||
}
|
||||
.insert(Utc::now() + Duration::hours(24), &redis)
|
||||
.await?;
|
||||
|
||||
send_email_verify(email, flow, "We need to verify your email address.")?;
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(
|
||||
"User does not have an email.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct VerifyEmail {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
#[post("email/verify")]
|
||||
pub async fn verify_email(
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<deadpool_redis::Pool>,
|
||||
email: web::Json<VerifyEmail>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let flow = Flow::get(&email.flow, &redis).await?;
|
||||
|
||||
if let Some(Flow::ConfirmEmail {
|
||||
user_id,
|
||||
confirm_email,
|
||||
}) = flow
|
||||
{
|
||||
let user = crate::database::models::User::get_id(user_id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
if user.email != Some(confirm_email) {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"E-mail does not match verify email. Try re-requesting the verification link."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET email_verified = TRUE
|
||||
WHERE (id = $1)
|
||||
",
|
||||
user.id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Flow::remove(&email.flow, &redis).await?;
|
||||
crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(
|
||||
"Flow does not exist. Try re-requesting the verification link.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn send_email_verify(
|
||||
email: String,
|
||||
flow: String,
|
||||
opener: &str,
|
||||
) -> Result<(), super::email::MailError> {
|
||||
send_email(
|
||||
email,
|
||||
"Verify your email",
|
||||
opener,
|
||||
"Please visit the following link below to verify your email. If the button does not work, you can copy the link and paste it into your browser. This link expires in 24 hours.",
|
||||
Some(("Reset password", &format!("{}/{}?flow={}", dotenvy::var("SITE_VERIFY_EMAIL_PATH")?, dotenvy::var("SITE_RESET_PASSWORD_PATH")?, flow))),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod checks;
|
||||
pub mod email;
|
||||
pub mod flows;
|
||||
pub mod pats;
|
||||
pub mod session;
|
||||
@ -32,6 +33,8 @@ pub enum AuthenticationError {
|
||||
FileHosting(#[from] FileHostingError),
|
||||
#[error("Error while decoding PAT: {0}")]
|
||||
Decoding(#[from] crate::models::ids::DecodingError),
|
||||
#[error("{0}")]
|
||||
Mail(#[from] email::MailError),
|
||||
#[error("Invalid Authentication Credentials")]
|
||||
InvalidCredentials,
|
||||
#[error("Authentication method was not valid")]
|
||||
@ -54,6 +57,7 @@ impl actix_web::ResponseError for AuthenticationError {
|
||||
AuthenticationError::Reqwest(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthenticationError::InvalidCredentials => StatusCode::UNAUTHORIZED,
|
||||
AuthenticationError::Decoding(..) => StatusCode::BAD_REQUEST,
|
||||
AuthenticationError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthenticationError::InvalidAuthMethod => StatusCode::UNAUTHORIZED,
|
||||
AuthenticationError::InvalidClientId => StatusCode::UNAUTHORIZED,
|
||||
AuthenticationError::Url => StatusCode::BAD_REQUEST,
|
||||
@ -72,6 +76,7 @@ impl actix_web::ResponseError for AuthenticationError {
|
||||
AuthenticationError::Reqwest(..) => "network_error",
|
||||
AuthenticationError::InvalidCredentials => "invalid_credentials",
|
||||
AuthenticationError::Decoding(..) => "decoding_error",
|
||||
AuthenticationError::Mail(..) => "mail_error",
|
||||
AuthenticationError::InvalidAuthMethod => "invalid_auth_method",
|
||||
AuthenticationError::InvalidClientId => "invalid_client_id",
|
||||
AuthenticationError::Url => "url_error",
|
||||
|
||||
@ -154,7 +154,7 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
user.map(|x| (Scopes::ALL, x))
|
||||
user.map(|x| (Scopes::NOT_RESTRICTED, x))
|
||||
}
|
||||
_ => return Err(AuthenticationError::InvalidAuthMethod),
|
||||
};
|
||||
|
||||
42
src/main.rs
42
src/main.rs
@ -378,16 +378,6 @@ fn check_env_vars() -> bool {
|
||||
check
|
||||
}
|
||||
|
||||
if parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").is_none() {
|
||||
warn!("Variable `WHITELISTED_MODPACK_DOMAINS` missing in dotenv or not a json array of strings");
|
||||
failed |= true;
|
||||
}
|
||||
|
||||
if parse_strings_from_var("ALLOWED_CALLBACK_URLS").is_none() {
|
||||
warn!("Variable `ALLOWED_CALLBACK_URLS` missing in dotenv or not a json array of strings");
|
||||
failed |= true;
|
||||
}
|
||||
|
||||
failed |= check_var::<String>("SITE_URL");
|
||||
failed |= check_var::<String>("CDN_URL");
|
||||
failed |= check_var::<String>("LABRINTH_ADMIN_KEY");
|
||||
@ -395,11 +385,10 @@ fn check_env_vars() -> bool {
|
||||
failed |= check_var::<String>("DATABASE_URL");
|
||||
failed |= check_var::<String>("MEILISEARCH_ADDR");
|
||||
failed |= check_var::<String>("MEILISEARCH_KEY");
|
||||
failed |= check_var::<String>("REDIS_URL");
|
||||
failed |= check_var::<String>("BIND_ADDR");
|
||||
failed |= check_var::<String>("SELF_ADDR");
|
||||
|
||||
failed |= check_var::<String>("REDIS_URL");
|
||||
|
||||
failed |= check_var::<String>("STORAGE_BACKEND");
|
||||
|
||||
let storage_backend = dotenvy::var("STORAGE_BACKEND").ok();
|
||||
@ -428,10 +417,27 @@ fn check_env_vars() -> bool {
|
||||
failed |= true;
|
||||
}
|
||||
}
|
||||
failed |= check_var::<usize>("LOCAL_INDEX_INTERVAL");
|
||||
|
||||
failed |= check_var::<usize>("LOCAL_INDEX_INTERVAL");
|
||||
failed |= check_var::<usize>("VERSION_INDEX_INTERVAL");
|
||||
|
||||
if parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").is_none() {
|
||||
warn!("Variable `WHITELISTED_MODPACK_DOMAINS` missing in dotenv or not a json array of strings");
|
||||
failed |= true;
|
||||
}
|
||||
|
||||
if parse_strings_from_var("ALLOWED_CALLBACK_URLS").is_none() {
|
||||
warn!("Variable `ALLOWED_CALLBACK_URLS` missing in dotenv or not a json array of strings");
|
||||
failed |= true;
|
||||
}
|
||||
|
||||
failed |= check_var::<String>("ARIADNE_ADMIN_KEY");
|
||||
failed |= check_var::<String>("ARIADNE_URL");
|
||||
|
||||
failed |= check_var::<String>("PAYPAL_API_URL");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");
|
||||
|
||||
failed |= check_var::<String>("GITHUB_CLIENT_ID");
|
||||
failed |= check_var::<String>("GITHUB_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("GITLAB_CLIENT_ID");
|
||||
@ -446,12 +452,12 @@ fn check_env_vars() -> bool {
|
||||
|
||||
failed |= check_var::<String>("TURNSTILE_SECRET");
|
||||
|
||||
failed |= check_var::<String>("ARIADNE_ADMIN_KEY");
|
||||
failed |= check_var::<String>("ARIADNE_URL");
|
||||
failed |= check_var::<String>("SMTP_USERNAME");
|
||||
failed |= check_var::<String>("SMTP_PASSWORD");
|
||||
failed |= check_var::<String>("SMTP_HOST");
|
||||
|
||||
failed |= check_var::<String>("PAYPAL_API_URL");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("SITE_VERIFY_EMAIL_PATH");
|
||||
failed |= check_var::<String>("SITE_RESET_PASSWORD_PATH");
|
||||
|
||||
failed
|
||||
}
|
||||
|
||||
@ -82,7 +82,8 @@ bitflags::bitflags! {
|
||||
// delete a session22
|
||||
const SESSION_DELETE = 1 << 29;
|
||||
|
||||
const ALL = 0b11111111111111111111111111;
|
||||
const ALL = 0b11111111111111111111111111111;
|
||||
const NOT_RESTRICTED = 0b00000011111111111111100111;
|
||||
const NONE = 0b0;
|
||||
}
|
||||
}
|
||||
@ -97,7 +98,8 @@ impl Scopes {
|
||||
| Scopes::PAT_DELETE
|
||||
| Scopes::SESSION_READ
|
||||
| Scopes::SESSION_DELETE
|
||||
| Scopes::USER_AUTH_WRITE,
|
||||
| Scopes::USER_AUTH_WRITE
|
||||
| Scopes::USER_DELETE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +72,8 @@ pub enum ApiError {
|
||||
PasswordHashing(#[from] argon2::password_hash::Error),
|
||||
#[error("Password strength checking error: {0}")]
|
||||
PasswordStrengthCheck(#[from] zxcvbn::ZxcvbnError),
|
||||
#[error("{0}")]
|
||||
Mail(#[from] crate::auth::email::MailError),
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for ApiError {
|
||||
@ -97,6 +99,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::ImageParse(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::PasswordHashing(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::PasswordStrengthCheck(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +126,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::ImageParse(..) => "invalid_image",
|
||||
ApiError::PasswordHashing(..) => "password_hashing_error",
|
||||
ApiError::PasswordStrengthCheck(..) => "strength_check_error",
|
||||
ApiError::Mail(..) => "mail_error",
|
||||
},
|
||||
description: &self.to_string(),
|
||||
})
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
use crate::auth::flows::AuthProvider;
|
||||
use crate::auth::{get_user_from_headers, AuthenticationError};
|
||||
use crate::database::models::User;
|
||||
use crate::file_hosting::FileHost;
|
||||
@ -12,12 +11,8 @@ use crate::routes::ApiError;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use chrono::{DateTime, Utc};
|
||||
use lazy_static::lazy_static;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use regex::Regex;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -193,8 +188,6 @@ pub struct EditUser {
|
||||
)]
|
||||
#[validate]
|
||||
pub payout_data: Option<Option<EditPayoutData>>,
|
||||
pub password: Option<(Option<String>, Option<String>)>,
|
||||
pub remove_auth_providers: Option<Vec<AuthProvider>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate)]
|
||||
@ -407,194 +400,6 @@ pub async fn user_edit(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((old_password, new_password)) = &new_user.password {
|
||||
if !scopes.contains(Scopes::USER_AUTH_WRITE) {
|
||||
return Err(ApiError::Authentication(
|
||||
AuthenticationError::InvalidCredentials,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(pass) = actual_user.password.as_ref() {
|
||||
let old_password = old_password.as_ref().ok_or_else(|| {
|
||||
ApiError::CustomAuthentication(
|
||||
"You must specify the old password to change your password!"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let hasher = Argon2::default();
|
||||
hasher.verify_password(old_password.as_bytes(), &PasswordHash::new(pass)?)?;
|
||||
}
|
||||
|
||||
let update_password = if let Some(new_password) = new_password {
|
||||
let score = zxcvbn::zxcvbn(
|
||||
new_password,
|
||||
&[
|
||||
&actual_user.username,
|
||||
&actual_user.email.unwrap_or_default(),
|
||||
&actual_user.name.unwrap_or_default(),
|
||||
],
|
||||
)?;
|
||||
|
||||
if score.score() < 3 {
|
||||
return Err(ApiError::InvalidInput(
|
||||
if let Some(feedback) =
|
||||
score.feedback().clone().and_then(|x| x.warning())
|
||||
{
|
||||
format!("Password too weak: {}", feedback)
|
||||
} else {
|
||||
"Specified password is too weak! Please improve its strength."
|
||||
.to_string()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let hasher = Argon2::default();
|
||||
let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy());
|
||||
let password_hash = hasher
|
||||
.hash_password(new_password.as_bytes(), &salt)?
|
||||
.to_string();
|
||||
|
||||
Some(password_hash)
|
||||
} else {
|
||||
if !(actual_user.github_id.is_some()
|
||||
|| actual_user.gitlab_id.is_some()
|
||||
|| actual_user.microsoft_id.is_some()
|
||||
|| actual_user.google_id.is_some()
|
||||
|| actual_user.steam_id.is_some()
|
||||
|| actual_user.discord_id.is_some())
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You must have another authentication method added to remove password authentication!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET password = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
update_password,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(remove_auth_providers) = &new_user.remove_auth_providers {
|
||||
if !scopes.contains(Scopes::USER_AUTH_WRITE) {
|
||||
return Err(ApiError::Authentication(
|
||||
AuthenticationError::InvalidCredentials,
|
||||
));
|
||||
}
|
||||
|
||||
let mut auth_providers = Vec::new();
|
||||
if actual_user.github_id.is_some() {
|
||||
auth_providers.push(AuthProvider::GitHub)
|
||||
}
|
||||
if actual_user.gitlab_id.is_some() {
|
||||
auth_providers.push(AuthProvider::GitLab)
|
||||
}
|
||||
if actual_user.discord_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Discord)
|
||||
}
|
||||
if actual_user.google_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Google)
|
||||
}
|
||||
if actual_user.microsoft_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Microsoft)
|
||||
}
|
||||
if actual_user.steam_id.is_some() {
|
||||
auth_providers.push(AuthProvider::Steam)
|
||||
}
|
||||
|
||||
if auth_providers.len() <= remove_auth_providers.len()
|
||||
&& actual_user.password.is_none()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You must have another authentication method added to this method!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if remove_auth_providers.contains(&AuthProvider::GitHub) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET github_id = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if remove_auth_providers.contains(&AuthProvider::GitLab) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET gitlab_id = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if remove_auth_providers.contains(&AuthProvider::Google) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET google_id = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if remove_auth_providers.contains(&AuthProvider::Steam) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET steam_id = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if remove_auth_providers.contains(&AuthProvider::Discord) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET discord_id = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
if remove_auth_providers.contains(&AuthProvider::Microsoft) {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET microsoft_id = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?;
|
||||
transaction.commit().await?;
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user