mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
92
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4897cffb8d | ||
|
|
339beddcbc | ||
|
|
73232b3b7f | ||
|
|
bc073581d2 | ||
|
|
4eabe2a126 | ||
|
|
24285baebb | ||
|
|
ae939cd4ca | ||
|
|
8faa36b014 | ||
|
|
a45a320512 | ||
|
|
98c70633ee | ||
|
|
d858901568 | ||
|
|
b9b03796e3 | ||
|
|
e8475086ec | ||
|
|
655f899785 | ||
|
|
9bcee5ab52 | ||
|
|
9f050f21e6 | ||
|
|
782c82d3e8 | ||
|
|
da828f4db8 | ||
|
|
f6f2cb8d8b | ||
|
|
1a0633b6db | ||
|
|
3ae50a2abb | ||
|
|
408927a23e | ||
|
|
3c29a62036 | ||
|
|
407aa4b4cf | ||
|
|
d709bbeb54 | ||
|
|
b5662371d8 | ||
|
|
db9689b335 | ||
|
|
0f1483d9df | ||
|
|
d48ad873fb | ||
|
|
ff7c0cb5b8 | ||
|
|
faed8c3f6d | ||
|
|
63d00cb660 | ||
|
|
27d54a98af | ||
|
|
05a1a10788 | ||
|
|
addb6c6045 | ||
|
|
e08da23fb5 | ||
|
|
ecd69f88b2 | ||
|
|
d834ce3645 | ||
|
|
cf58559381 | ||
|
|
01a7ee8d1e | ||
|
|
5480826a53 | ||
|
|
9184de92df | ||
|
|
d753b8735c | ||
|
|
c7a5336a51 | ||
|
|
292b8f4cd2 | ||
|
|
c391fb12b9 | ||
|
|
62f20b9aee | ||
|
|
2aca44bbcc | ||
|
|
b9be6abf0b | ||
|
|
a5be0114c5 | ||
|
|
f32f732c31 | ||
|
|
e974672e71 | ||
|
|
d6356ea8e5 | ||
|
|
872dcbcf78 | ||
|
|
44da0ed5ff | ||
|
|
ec7c893681 | ||
|
|
3ce8f85463 | ||
|
|
4ce82b01e6 | ||
|
|
f4d65437df | ||
|
|
9646083025 | ||
|
|
652a9b0b5c | ||
|
|
918c4a9c83 | ||
|
|
0a0b19233d | ||
|
|
6f2e04c3de | ||
|
|
ece35f9158 | ||
|
|
3ccb8132d8 | ||
|
|
cad6bd4f92 | ||
|
|
65bd8937a6 | ||
|
|
abcac5f40a | ||
|
|
43893158ac | ||
|
|
fcab720943 | ||
|
|
4183702a72 | ||
|
|
8946e4c394 | ||
|
|
f1ce31001a | ||
|
|
c99564f8ea | ||
|
|
71c1deb5cd | ||
|
|
042dde075b | ||
|
|
3bfe670d47 | ||
|
|
ed53fe5330 | ||
|
|
547921badc | ||
|
|
aac61dcfaa | ||
|
|
52a972d949 | ||
|
|
bdc2b4abf8 | ||
|
|
c19ce11ea4 | ||
|
|
c54906e6de | ||
|
|
57b4f21080 | ||
|
|
66f3c39c13 | ||
|
|
7484afa18e | ||
|
|
7ea0635d86 | ||
|
|
fe3aba52ab | ||
|
|
64f87551ff | ||
|
|
c8a586c6f1 |
@@ -81,6 +81,6 @@ Please follow these rules precisely:
|
||||
Use existing patterns from our codebase:
|
||||
|
||||
- Variables/plurals: see `apps/frontend/src/pages/frog.vue`
|
||||
- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`
|
||||
- Rich-text link tags: see `apps/frontend/src/error.vue`
|
||||
|
||||
When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or `<IntlFormatted>`.
|
||||
|
||||
Generated
+427
-43
@@ -477,7 +477,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
@@ -494,7 +494,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"utoipa",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -530,6 +530,45 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.5.6"
|
||||
@@ -1000,7 +1039,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tracing",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1369,6 +1408,17 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "base64urlsafedata"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"pastey",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
@@ -1831,7 +1881,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"fnv",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1856,6 +1906,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardetng"
|
||||
version = "0.1.17"
|
||||
@@ -2004,7 +2065,7 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2332,6 +2393,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.3.0"
|
||||
@@ -2706,7 +2776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2726,6 +2796,20 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.4"
|
||||
@@ -3871,11 +3955,25 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"wasi 0.14.7+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.3"
|
||||
@@ -4720,6 +4818,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -5308,8 +5412,10 @@ dependencies = [
|
||||
"utoipa",
|
||||
"utoipa-actix-web",
|
||||
"utoipa-scalar",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"validator",
|
||||
"webauthn-rs",
|
||||
"webauthn-rs-proto",
|
||||
"webp",
|
||||
"woothee",
|
||||
"yaserde",
|
||||
@@ -5342,6 +5448,12 @@ dependencies = [
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.3"
|
||||
@@ -5755,7 +5867,7 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"yaup",
|
||||
@@ -5895,7 +6007,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"smallvec",
|
||||
"tagptr",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5946,7 +6058,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"strum",
|
||||
"utoipa",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6581,6 +6693,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -6611,15 +6732,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
version = "0.10.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
@@ -6643,9 +6763,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
version = "0.9.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -6949,6 +7069,12 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "path-util"
|
||||
version = "0.0.0"
|
||||
@@ -7777,6 +7903,12 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
version = "0.8.10"
|
||||
@@ -7829,6 +7961,17 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
@@ -7886,6 +8029,12 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
@@ -8313,7 +8462,7 @@ dependencies = [
|
||||
"rkyv_derive",
|
||||
"seahash",
|
||||
"tinyvec",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8452,6 +8601,15 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
@@ -8696,7 +8854,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8971,7 +9129,7 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9028,6 +9186,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor_2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
|
||||
dependencies = [
|
||||
"half 2.7.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
@@ -9274,7 +9442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -9291,7 +9459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -9547,7 +9715,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
@@ -9630,7 +9798,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@@ -9670,7 +9838,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@@ -9697,7 +9865,7 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10162,7 +10330,7 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -10470,7 +10638,7 @@ dependencies = [
|
||||
"toml 0.9.8",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -10626,7 +10794,7 @@ dependencies = [
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"whoami",
|
||||
"windows",
|
||||
"windows-core 0.61.2",
|
||||
@@ -10674,7 +10842,7 @@ dependencies = [
|
||||
"tracing-error",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows-core 0.61.2",
|
||||
@@ -11163,9 +11331,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
@@ -11183,14 +11351,14 @@ dependencies = [
|
||||
"mutually_exclusive_features",
|
||||
"pin-project",
|
||||
"tracing",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11199,9 +11367,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@@ -11597,7 +11765,7 @@ dependencies = [
|
||||
"regex",
|
||||
"syn 2.0.106",
|
||||
"url",
|
||||
"uuid 1.18.1",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11623,14 +11791,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
version = "1.23.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"rand 0.10.1",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -11805,7 +11973,16 @@ version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
"wit-bindgen 0.46.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11873,6 +12050,28 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap 2.11.4",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
@@ -11899,6 +12098,18 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap 2.11.4",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.11"
|
||||
@@ -11979,6 +12190,74 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-attestation-ca"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e"
|
||||
dependencies = [
|
||||
"base64urlsafedata",
|
||||
"openssl",
|
||||
"openssl-sys",
|
||||
"serde",
|
||||
"tracing",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-rs"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422"
|
||||
dependencies = [
|
||||
"base64urlsafedata",
|
||||
"serde",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.23.3",
|
||||
"webauthn-rs-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-rs-core"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64urlsafedata",
|
||||
"der-parser",
|
||||
"hex",
|
||||
"nom 7.1.3",
|
||||
"openssl",
|
||||
"openssl-sys",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
"serde",
|
||||
"serde_cbor_2",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.23.3",
|
||||
"webauthn-attestation-ca",
|
||||
"webauthn-rs-proto",
|
||||
"x509-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-rs-proto"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64urlsafedata",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
version = "2.0.2"
|
||||
@@ -12701,6 +12980,94 @@ version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"indexmap 2.11.4",
|
||||
"prettyplease",
|
||||
"syn 2.0.106",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"indexmap 2.11.4",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap 2.11.4",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "woothee"
|
||||
version = "0.13.0"
|
||||
@@ -12792,6 +13159,23 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"data-encoding",
|
||||
"der-parser",
|
||||
"lazy_static",
|
||||
"nom 7.1.3",
|
||||
"oid-registry",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
|
||||
@@ -220,6 +220,8 @@ utoipa-actix-web = { version = "0.1.2" }
|
||||
utoipa-scalar = { version = "0.3.0", default-features = false }
|
||||
uuid = "1.18.1"
|
||||
validator = "0.20.0"
|
||||
webauthn-rs = "0.5.5"
|
||||
webauthn-rs-proto = "0.5.5"
|
||||
webp = { version = "0.3.1", default-features = false }
|
||||
webview2-com = "0.38.0" # Should be updated in lockstep with wry
|
||||
whoami = "1.6.1"
|
||||
|
||||
@@ -14,6 +14,8 @@ import { I18nDebugPanel, LoadingBar, NotificationPanel } from '@modrinth/ui'
|
||||
|
||||
import { setupProviders } from '~/providers/setup.ts'
|
||||
|
||||
import { useAuth } from './composables/auth'
|
||||
|
||||
const auth = await useAuth()
|
||||
setupProviders(auth)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div
|
||||
class="shadow-card mx-auto flex w-full max-w-[27rem] flex-col gap-6 rounded-2xl border border-solid border-button-bg bg-surface-3 !p-6"
|
||||
>
|
||||
<h1
|
||||
class="mx-auto my-0 flex w-full justify-center text-center text-2xl font-semibold text-contrast"
|
||||
>
|
||||
{{ formatMessage(messages.title) }}
|
||||
</h1>
|
||||
|
||||
<section v-if="requiresDob" class="flex flex-col gap-2.5">
|
||||
<label class="text-md font-semibold text-contrast" for="create-account-dob">
|
||||
{{ formatMessage(messages.dateOfBirthLabel) }}
|
||||
</label>
|
||||
<DatePicker
|
||||
id="create-account-dob"
|
||||
v-model="dateOfBirthModel"
|
||||
wrapper-class="w-full"
|
||||
min-date="1900-01-01"
|
||||
:max-date="maxInputDate"
|
||||
:default-view-date="defaultDobViewDate"
|
||||
preserve-day
|
||||
:placeholder="formatMessage(messages.dateOfBirthPlaceholder)"
|
||||
/>
|
||||
<Admonition :type="'info'">
|
||||
<template #header>
|
||||
<div class="-mb-3 -mt-1 flex flex-col gap-0 text-sm font-normal leading-normal">
|
||||
<div>
|
||||
{{ formatMessage(messages.infoPanelText) }}
|
||||
</div>
|
||||
<a
|
||||
class="w-fit text-link underline"
|
||||
:href="SOURCE_CODE_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ formatMessage(messages.relevantSourceCodeText) }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</Admonition>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-2.5">
|
||||
<label class="text-md font-semibold text-contrast" for="create-account-username">
|
||||
{{ formatMessage(messages.usernameOptionalLabel) }}
|
||||
</label>
|
||||
<StyledInput
|
||||
id="create-account-username"
|
||||
v-model="usernameModel"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="globals?.captcha_enabled" class="flex flex-col gap-2.5">
|
||||
<label class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.securityCheckLabel)
|
||||
}}</label>
|
||||
<HCaptcha v-if="globals?.captcha_enabled" :ref="onSetCaptchaRef" v-model="tokenModel" />
|
||||
</section>
|
||||
|
||||
<div
|
||||
class="flex gap-2.5 rounded-2xl border border-solid border-surface-5 bg-surface-3 transition-all hover:brightness-110"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="subscribeModel"
|
||||
class="p-3 text-left leading-snug text-primary transition-all"
|
||||
:label="formatMessage(messages.subscribeLabel)"
|
||||
:description="formatMessage(messages.subscribeLabel)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full font-bold"
|
||||
:disabled="globals?.captcha_enabled ? !tokenModel : false"
|
||||
@click="onCompleteSignUpClick"
|
||||
>
|
||||
{{ formatMessage(messages.completeSignUpButton) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RightArrowIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
|
||||
interface AuthGlobals {
|
||||
captcha_enabled?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface Props {
|
||||
globals?: AuthGlobals | null
|
||||
requiresDob?: boolean
|
||||
onCompleteSignUp?: () => void
|
||||
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
|
||||
}
|
||||
|
||||
const {
|
||||
globals = null,
|
||||
requiresDob = true,
|
||||
onCompleteSignUp = () => {},
|
||||
onSetCaptchaRef = undefined,
|
||||
} = defineProps<Props>()
|
||||
|
||||
const SOURCE_CODE_URL =
|
||||
'https://github.com/modrinth/code/blob/main/apps/frontend/src/components/ui/auth/CreateAccount.vue'
|
||||
|
||||
const dateOfBirthModel = defineModel<string | null>('dateOfBirth', { default: '' })
|
||||
const usernameModel = defineModel<string>('username', { default: '' })
|
||||
const tokenModel = defineModel<string>('token', { default: '' })
|
||||
const subscribeModel = defineModel<boolean>('subscribe', { default: false })
|
||||
|
||||
const maxInputDate = computed(() => `${new Date().getFullYear()}-12-31`)
|
||||
|
||||
const defaultDobViewDate = computed(() => `${new Date().getFullYear()}-01-01`)
|
||||
|
||||
const maxBirthDate = computed(() => {
|
||||
const date = new Date()
|
||||
date.setFullYear(date.getFullYear() - 13)
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day}`
|
||||
})
|
||||
|
||||
const getBirthYear = (dateOfBirth: string | null): number | null => {
|
||||
if (!dateOfBirth) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [yearPart = ''] = dateOfBirth.split('-')
|
||||
const year = Number(yearPart)
|
||||
return Number.isInteger(year) ? year : null
|
||||
}
|
||||
|
||||
const isDateOfBirthMissing = computed(() => requiresDob && !dateOfBirthModel.value)
|
||||
|
||||
const isDateOfBirthYearZero = computed(() => {
|
||||
if (!requiresDob || !dateOfBirthModel.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return getBirthYear(dateOfBirthModel.value) === 0
|
||||
})
|
||||
|
||||
const isUnder13 = computed(
|
||||
() => requiresDob && !!dateOfBirthModel.value && dateOfBirthModel.value > maxBirthDate.value,
|
||||
)
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
function onCompleteSignUpClick() {
|
||||
if (isDateOfBirthMissing.value) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.dateOfBirthRequiredTitle),
|
||||
text: formatMessage(messages.dateOfBirthRequiredText),
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isDateOfBirthYearZero.value) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.dateOfBirthInvalidTitle),
|
||||
text: formatMessage(messages.dateOfBirthInvalidText),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isUnder13.value) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.ageRequirementWarningTitle),
|
||||
text: formatMessage(messages.under13HelperText),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
onCompleteSignUp()
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'auth.create-account.title',
|
||||
defaultMessage: 'Create an Account',
|
||||
},
|
||||
dateOfBirthLabel: {
|
||||
id: 'auth.create-account.date-of-birth.label',
|
||||
defaultMessage: 'Date of birth',
|
||||
},
|
||||
dateOfBirthPlaceholder: {
|
||||
id: 'auth.create-account.date-of-birth.placeholder',
|
||||
defaultMessage: 'Select your date of birth',
|
||||
},
|
||||
dateOfBirthRequiredTitle: {
|
||||
id: 'auth.create-account.date-of-birth.required.title',
|
||||
defaultMessage: 'Date of birth required',
|
||||
},
|
||||
dateOfBirthRequiredText: {
|
||||
id: 'auth.create-account.date-of-birth.required.text',
|
||||
defaultMessage: 'Please enter your date of birth before continuing.',
|
||||
},
|
||||
dateOfBirthInvalidTitle: {
|
||||
id: 'auth.create-account.date-of-birth.invalid.title',
|
||||
defaultMessage: 'Invalid date of birth',
|
||||
},
|
||||
dateOfBirthInvalidText: {
|
||||
id: 'auth.create-account.date-of-birth.invalid.text',
|
||||
defaultMessage: 'Please enter a valid date of birth. Year cannot be 0000.',
|
||||
},
|
||||
under13HelperText: {
|
||||
id: 'auth.create-account.date-of-birth.under13-helper',
|
||||
defaultMessage: 'You do not meet the age requirement to create an account at Modrinth.',
|
||||
},
|
||||
ageRequirementWarningTitle: {
|
||||
id: 'auth.create-account.age-requirement.warning-title',
|
||||
defaultMessage: 'Age requirement',
|
||||
},
|
||||
infoPanelText: {
|
||||
id: 'auth.create-account.info-panel.text',
|
||||
defaultMessage:
|
||||
'We do not store your date of birth. We only use it during sign-up to validate COPPA compliance.',
|
||||
},
|
||||
relevantSourceCodeText: {
|
||||
id: 'auth.create-account.info-panel.source-code-link',
|
||||
defaultMessage: 'Relevant source code',
|
||||
},
|
||||
usernameOptionalLabel: {
|
||||
id: 'auth.create-account.username.label',
|
||||
defaultMessage: 'Username',
|
||||
},
|
||||
usernamePlaceholder: {
|
||||
id: 'auth.create-account.username.placeholder',
|
||||
defaultMessage: 'Enter username',
|
||||
},
|
||||
securityCheckLabel: {
|
||||
id: 'auth.create-account.security-check.label',
|
||||
defaultMessage: 'Security check',
|
||||
},
|
||||
subscribeLabel: {
|
||||
id: 'auth.create-account.subscribe.label',
|
||||
defaultMessage: 'Keep me updated on the cool things Modrinth is working on via email',
|
||||
},
|
||||
completeSignUpButton: {
|
||||
id: 'auth.create-account.complete-sign-up',
|
||||
defaultMessage: 'Complete sign up',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
+21
-4
@@ -47,17 +47,34 @@ defineExpose({
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style>
|
||||
.h-captcha {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid var(--color-button-bg);
|
||||
height: 78px;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
margin: -1px;
|
||||
.h-captcha iframe {
|
||||
transform: scale(1.33);
|
||||
transform-origin: center;
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.h-captcha {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.h-captcha iframe {
|
||||
transform: scale(1.03);
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="contents">
|
||||
<ConfirmModal
|
||||
ref="removePasskeyModal"
|
||||
:title="formatMessage(messages.removePasskeyConfirmTitle)"
|
||||
:description="
|
||||
formatMessage(messages.removePasskeyConfirmDescription, { name: passkeyToRemove?.name })
|
||||
"
|
||||
:proceed-label="formatMessage(commonMessages.removeButton)"
|
||||
@proceed="removePasskey()"
|
||||
/>
|
||||
|
||||
<NewModal
|
||||
ref="managePasskeyModal"
|
||||
width="600px"
|
||||
:header="formatMessage(messages.managePasskeyTitle)"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-if="passkeysLoading" class="flex items-center gap-2 text-secondary">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
{{ formatMessage(messages.managePasskeyModalLoading) }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<p v-if="passkeys.length === 0" class="m-0 text-secondary">
|
||||
{{ formatMessage(messages.managePasskeyModalNoPasskeys) }}
|
||||
</p>
|
||||
<div
|
||||
v-for="passkey in passkeys"
|
||||
:key="passkey.id"
|
||||
class="universal-card recessed flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<strong>{{ passkey.name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span v-tooltip="formatDateTime(passkey.created_at)">
|
||||
{{
|
||||
formatMessage(messages.managePasskeyModalAdded, {
|
||||
ago: formatRelativeTime(passkey.created_at),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
⋅
|
||||
<span v-if="passkey.last_used" v-tooltip="formatDateTime(passkey.last_used)">
|
||||
{{
|
||||
formatMessage(messages.managePasskeyModalLastUsed, {
|
||||
ago: formatRelativeTime(passkey.last_used),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ formatMessage(messages.managePasskeyModalNeverUsed) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.renameButton)"
|
||||
@click="
|
||||
() => {
|
||||
passkeyToRename = { ...passkey }
|
||||
renamePasskeyModal?.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.removeButton)"
|
||||
@click="
|
||||
() => {
|
||||
passkeyToRemove = passkey
|
||||
removePasskeyModal?.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="input-group self-end">
|
||||
<ButtonStyled>
|
||||
<button @click="registerPasskey()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.managePasskeyAddPasskey) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="managePasskeyModal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.closeButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
|
||||
<NewModal
|
||||
ref="addPasskeyModal"
|
||||
width="500px"
|
||||
:header="formatMessage(messages.managePasskeyAddPasskey)"
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="text-md font-semibold text-contrast">
|
||||
{{ formatMessage(messages.passkeyNameLabel) }}
|
||||
</div>
|
||||
<StyledInput
|
||||
id="passkey-name"
|
||||
v-model="pendingPasskeyName"
|
||||
:maxlength="255"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
|
||||
@keyup.enter="pendingPasskeyName && finishRegisterPasskey()"
|
||||
/>
|
||||
<div class="label__description">
|
||||
{{ formatMessage(messages.passkeyNameDescription) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="addPasskeyModal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!pendingPasskeyName" @click="finishRegisterPasskey()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.managePasskeyAddPasskey) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
|
||||
<NewModal
|
||||
ref="renamePasskeyModal"
|
||||
width="500px"
|
||||
:header="formatMessage(messages.renamePasskeyModalHeader)"
|
||||
>
|
||||
<div v-if="passkeyToRename" class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="text-md font-semibold text-contrast">
|
||||
{{ formatMessage(messages.passkeyNameLabel) }}
|
||||
</div>
|
||||
<StyledInput
|
||||
id="passkey-rename"
|
||||
v-model="passkeyToRenameName"
|
||||
:maxlength="255"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
|
||||
@keyup.enter="passkeyToRenameName && renamePasskey()"
|
||||
/>
|
||||
<div class="label__description">
|
||||
{{ formatMessage(messages.passkeyNameDescription) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="renamePasskeyModal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!passkeyToRenameName" @click="renamePasskey()">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
|
||||
<div class="adjacent-input mb-0">
|
||||
<label for="manage-passkeys">
|
||||
<span class="label__title">{{ formatMessage(messages.managePasskeyTitle) }}</span>
|
||||
<span class="label__description">{{
|
||||
formatMessage(messages.managePasskeyDescription)
|
||||
}}</span>
|
||||
</label>
|
||||
<div>
|
||||
<ButtonStyled>
|
||||
<button id="manage-passkeys" @click="showPasskeyModal">
|
||||
<UserKeyIcon /> {{ formatMessage(messages.managePasskeyTitle) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
EditIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
UserKeyIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
ConfirmModal,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { createPasskeyCredential } from '~/helpers/passkey.ts'
|
||||
|
||||
const { labrinth } = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
|
||||
function notifyError(err: unknown) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err instanceof Error ? err.message : String(err),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
managePasskeyTitle: {
|
||||
id: 'settings.account.security.passkey.title',
|
||||
defaultMessage: 'Manage passkeys',
|
||||
},
|
||||
managePasskeyDescription: {
|
||||
id: 'settings.account.security.passkey.description',
|
||||
defaultMessage: 'Manage your registered passkeys, or add a new one.',
|
||||
},
|
||||
managePasskeyAddPasskey: {
|
||||
id: 'settings.account.security.passkey.add',
|
||||
defaultMessage: 'Add passkey',
|
||||
},
|
||||
managePasskeyModalLoading: {
|
||||
id: 'settings.account.security.passkey.modal.loading',
|
||||
defaultMessage: 'Loading passkeys…',
|
||||
},
|
||||
managePasskeyModalNoPasskeys: {
|
||||
id: 'settings.account.security.passkey.modal.no-passkeys',
|
||||
defaultMessage: 'You do not have any passkeys registered.',
|
||||
},
|
||||
managePasskeyModalAdded: {
|
||||
id: 'settings.account.security.passkey.modal.added',
|
||||
defaultMessage: 'Added {ago}',
|
||||
},
|
||||
managePasskeyModalLastUsed: {
|
||||
id: 'settings.account.security.passkey.modal.last-used',
|
||||
defaultMessage: 'Last used {ago}',
|
||||
},
|
||||
managePasskeyModalNeverUsed: {
|
||||
id: 'settings.account.security.passkey.modal.never-used',
|
||||
defaultMessage: 'Never used',
|
||||
},
|
||||
passkeyNameLabel: {
|
||||
id: 'settings.account.security.passkey.add-modal.name.label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
passkeyNameDescription: {
|
||||
id: 'settings.account.security.passkey.add-modal.name.description',
|
||||
defaultMessage:
|
||||
'Make sure to pick something memorable, so you can identify this passkey later.',
|
||||
},
|
||||
passkeyNamePlaceholder: {
|
||||
id: 'settings.account.security.passkey.add-modal.name.placeholder',
|
||||
defaultMessage: 'My passkey',
|
||||
},
|
||||
renamePasskeyModalHeader: {
|
||||
id: 'settings.account.security.passkey.rename-modal.header',
|
||||
defaultMessage: 'Rename passkey',
|
||||
},
|
||||
removePasskeyConfirmTitle: {
|
||||
id: 'settings.account.security.passkey.remove.title',
|
||||
defaultMessage: 'Are you sure you want to remove this passkey?',
|
||||
},
|
||||
removePasskeyConfirmDescription: {
|
||||
id: 'settings.account.security.passkey.remove.description',
|
||||
defaultMessage:
|
||||
'This will permanently remove the passkey "{name}". You will no longer be able to sign in with it.',
|
||||
},
|
||||
})
|
||||
|
||||
const removePasskeyModal = useTemplateRef<InstanceType<typeof ConfirmModal>>('removePasskeyModal')
|
||||
const managePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('managePasskeyModal')
|
||||
const addPasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('addPasskeyModal')
|
||||
const renamePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('renamePasskeyModal')
|
||||
|
||||
const pendingPasskey = ref<Labrinth.Auth.v2.PasskeyRegisterFinishRequest | null>(null)
|
||||
const passkeyToRemove = ref<Labrinth.Auth.v2.Passkey | null>(null)
|
||||
const passkeyToRename = ref<Labrinth.Auth.v2.Passkey | null>(null)
|
||||
const pendingPasskeyName = computed({
|
||||
get: () => pendingPasskey.value?.name ?? '',
|
||||
set: (name) => {
|
||||
if (pendingPasskey.value) {
|
||||
pendingPasskey.value.name = name
|
||||
}
|
||||
},
|
||||
})
|
||||
const passkeyToRenameName = computed({
|
||||
get: () => passkeyToRename.value?.name ?? '',
|
||||
set: (name) => {
|
||||
if (passkeyToRename.value) {
|
||||
passkeyToRename.value.name = name
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const passkeys = ref<Labrinth.Auth.v2.Passkey[]>([])
|
||||
const passkeysLoading = ref(false)
|
||||
|
||||
async function fetchPasskeys() {
|
||||
passkeysLoading.value = true
|
||||
try {
|
||||
passkeys.value = await labrinth.auth_v2.listPasskeys()
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
passkeysLoading.value = false
|
||||
}
|
||||
|
||||
async function showPasskeyModal() {
|
||||
managePasskeyModal.value?.show()
|
||||
await fetchPasskeys()
|
||||
}
|
||||
|
||||
async function registerPasskey() {
|
||||
startLoading()
|
||||
try {
|
||||
const res = await labrinth.auth_v2.registerPasskeyStart()
|
||||
|
||||
const credential = await createPasskeyCredential(res.options.publicKey)
|
||||
pendingPasskey.value = {
|
||||
flow: res.flow,
|
||||
credential: credential,
|
||||
name: '',
|
||||
}
|
||||
|
||||
addPasskeyModal.value?.show()
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function finishRegisterPasskey() {
|
||||
if (!pendingPasskey.value) return
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
const passkey = await labrinth.auth_v2.registerPasskeyFinish(pendingPasskey.value)
|
||||
passkeys.value.unshift(passkey)
|
||||
pendingPasskey.value = null
|
||||
addPasskeyModal.value?.hide()
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function renamePasskey() {
|
||||
if (!passkeyToRename.value) return
|
||||
const { id, name } = passkeyToRename.value
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
await labrinth.auth_v2.renamePasskey(id, { name })
|
||||
const existing = passkeys.value.find((passkey) => passkey.id === id)
|
||||
if (existing) {
|
||||
existing.name = name
|
||||
}
|
||||
passkeyToRename.value = null
|
||||
renamePasskeyModal.value?.hide()
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function removePasskey() {
|
||||
if (!passkeyToRemove.value) return
|
||||
const { id } = passkeyToRemove.value
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
await labrinth.auth_v2.deletePasskey(id)
|
||||
passkeys.value = passkeys.value.filter((passkey) => passkey.id !== id)
|
||||
passkeyToRemove.value = null
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div v-if="subtleLauncherRedirectUri">
|
||||
<iframe
|
||||
:src="subtleLauncherRedirectUri"
|
||||
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
||||
></iframe>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="universal-card mx-auto flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
|
||||
>
|
||||
<template v-if="flow && !subtleLauncherRedirectUri">
|
||||
<div class="flex flex-col items-end gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="two-factor-code">
|
||||
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.twoFactorCodeLabelDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCodeModel"
|
||||
:maxlength="11"
|
||||
inputmode="numeric"
|
||||
:placeholder="formatMessage(messages.twoFactorCodeInputPlaceholder)"
|
||||
autocomplete="one-time-code"
|
||||
@keyup.enter="onTwoFactorSignIn()"
|
||||
/>
|
||||
</div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="onTwoFactorSignIn()">
|
||||
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="text-center text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.signInWithLabel) }}
|
||||
</div>
|
||||
|
||||
<section class="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
|
||||
<ButtonStyled v-for="provider in oauthProviders" :key="provider.id">
|
||||
<a
|
||||
class="relative w-full !justify-center overflow-visible !shadow-none"
|
||||
:class="{
|
||||
'!border !border-[var(--color-green)]': lastSignInProvider === provider.id,
|
||||
}"
|
||||
:href="getAuthUrl(provider.id, redirectTarget)"
|
||||
:aria-label="
|
||||
formatMessage(messages.continueWithProvider, { provider: provider.name })
|
||||
"
|
||||
@click="onOAuthProviderClick(provider.id)"
|
||||
>
|
||||
<component :is="provider.icon" />
|
||||
<span>{{ provider.name }}</span>
|
||||
<span
|
||||
v-if="lastSignInProvider === provider.id"
|
||||
class="oauth-provider-last-sign-in-badge"
|
||||
>
|
||||
{{ formatMessage(messages.lastSignInLabel) }}
|
||||
</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
class="relative w-full !justify-center overflow-visible !shadow-none sm:col-span-2"
|
||||
:class="{ '!border !border-[var(--color-green)]': lastSignInProvider === 'passkey' }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="onPasskeySignIn"
|
||||
@keydown.enter="onPasskeySignIn"
|
||||
>
|
||||
<UserKeyIcon />
|
||||
<span class="ml-1">{{ formatMessage(messages.continueWithPasskey) }}</span>
|
||||
<span
|
||||
v-if="lastSignInProvider === 'passkey'"
|
||||
class="oauth-provider-last-sign-in-badge"
|
||||
>
|
||||
{{ formatMessage(messages.lastSignInLabel) }}
|
||||
</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
|
||||
<div class="h-px w-full bg-surface-5"></div>
|
||||
|
||||
<section class="mx-auto flex w-full flex-col gap-2.5">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailUsernameLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="emailModel"
|
||||
:icon="MailIcon"
|
||||
type="text"
|
||||
inputmode="email"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.emailUsernameLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="passwordModel"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<HCaptcha
|
||||
v-if="globals?.captcha_enabled && emailModel && passwordModel"
|
||||
:ref="onSetCaptchaRef"
|
||||
v-model="tokenModel"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full"
|
||||
:disabled="globals?.captcha_enabled ? !tokenModel : false"
|
||||
@click="onPasswordSignIn()"
|
||||
>
|
||||
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-2.5 !text-base">
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/reset-password',
|
||||
query: routeQuery,
|
||||
}"
|
||||
>
|
||||
{{ formatMessage(messages.forgotPasswordLabel) }}
|
||||
</NuxtLink>
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
||||
<NuxtLink
|
||||
class="inline text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-up',
|
||||
query: routeQuery,
|
||||
}"
|
||||
>
|
||||
{{ formatMessage(messages.createAccountLabel) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
UserKeyIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages, defineMessages, StyledInput, useVIntl } from '@modrinth/ui'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
import {
|
||||
getAuthUrl,
|
||||
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
} from '@/composables/auth.ts'
|
||||
|
||||
const oauthProviders = [
|
||||
{ id: 'discord', name: 'Discord', icon: DiscordColorIcon },
|
||||
{ id: 'github', name: 'GitHub', icon: GitHubColorIcon },
|
||||
{ id: 'microsoft', name: 'Microsoft', icon: MicrosoftColorIcon },
|
||||
{ id: 'google', name: 'Google', icon: GoogleColorIcon },
|
||||
{ id: 'steam', name: 'Steam', icon: SteamColorIcon },
|
||||
{ id: 'gitlab', name: 'GitLab', icon: GitLabColorIcon },
|
||||
] as const
|
||||
|
||||
type AuthProvider = (typeof oauthProviders)[number]['id'] | 'passkey'
|
||||
|
||||
interface AuthGlobals {
|
||||
captcha_enabled?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface Props {
|
||||
subtleLauncherRedirectUri?: string
|
||||
flow?: string
|
||||
redirectTarget?: string
|
||||
routeQuery?: LocationQuery
|
||||
globals?: AuthGlobals | null
|
||||
onPasswordSignIn?: () => void
|
||||
onTwoFactorSignIn?: () => void
|
||||
onPasskeySignIn?: () => void
|
||||
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
|
||||
}
|
||||
|
||||
const {
|
||||
subtleLauncherRedirectUri = '',
|
||||
flow = '',
|
||||
redirectTarget = '',
|
||||
routeQuery = {},
|
||||
globals = null,
|
||||
onPasswordSignIn = () => {},
|
||||
onTwoFactorSignIn = () => {},
|
||||
onPasskeySignIn = () => {},
|
||||
onSetCaptchaRef = undefined,
|
||||
} = defineProps<Props>()
|
||||
|
||||
const emailModel = defineModel<string>('email', { default: '' })
|
||||
const passwordModel = defineModel<string>('password', { default: '' })
|
||||
const tokenModel = defineModel<string>('token', { default: '' })
|
||||
const twoFactorCodeModel = defineModel<string>('twoFactorCode', { default: '' })
|
||||
|
||||
const lastSignInOAuthProvider = useStorage<AuthProvider | null>(
|
||||
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
const pendingSignInOAuthProvider = useStorage<AuthProvider | null>(
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
const lastSignInProvider = computed(() => lastSignInOAuthProvider.value)
|
||||
const onOAuthProviderClick = (provider: AuthProvider) => {
|
||||
pendingSignInOAuthProvider.value = provider
|
||||
}
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
forgotPasswordLabel: {
|
||||
id: 'auth.sign-in.forgot-password',
|
||||
defaultMessage: 'Forgot password',
|
||||
},
|
||||
noAccountLabel: {
|
||||
id: 'auth.sign-in.no-account',
|
||||
defaultMessage: "Don't have an account?",
|
||||
},
|
||||
createAccountLabel: {
|
||||
id: 'auth.sign-in.create-account',
|
||||
defaultMessage: 'Sign up',
|
||||
},
|
||||
signInWithLabel: {
|
||||
id: 'auth.sign-in.sign-in-with',
|
||||
defaultMessage: 'Sign into Modrinth',
|
||||
},
|
||||
twoFactorCodeInputPlaceholder: {
|
||||
id: 'auth.sign-in.2fa.placeholder',
|
||||
defaultMessage: 'Enter code...',
|
||||
},
|
||||
twoFactorCodeLabel: {
|
||||
id: 'auth.sign-in.2fa.label',
|
||||
defaultMessage: 'Two-factor authentication',
|
||||
},
|
||||
twoFactorCodeLabelDescription: {
|
||||
id: 'auth.sign-in.2fa.description',
|
||||
defaultMessage:
|
||||
'Enter the 6-digit code from your authenticator app, or one of your backup codes.',
|
||||
},
|
||||
continueWithProvider: {
|
||||
id: 'auth.continue-with-provider',
|
||||
defaultMessage: 'Continue with {provider}',
|
||||
},
|
||||
continueWithEmail: {
|
||||
id: 'auth.sign-in.continue-with-email',
|
||||
defaultMessage: 'Continue with Email',
|
||||
},
|
||||
lastSignInLabel: {
|
||||
id: 'auth.sign-in.last-sign-in',
|
||||
defaultMessage: 'Last used',
|
||||
},
|
||||
continueWithPasskey: {
|
||||
id: 'auth.sign-in.continue-with-passkey',
|
||||
defaultMessage: 'Continue with passkey',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.oauth-provider-last-sign-in-badge {
|
||||
position: absolute;
|
||||
top: -0.75rem;
|
||||
right: 0.25rem;
|
||||
z-index: 1;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--surface-3);
|
||||
color: var(--color-green);
|
||||
border: 1px solid var(--color-green);
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.oauth-provider-last-sign-in-badge::before {
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background-color: var(--color-green-highlight);
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div
|
||||
class="universal-card flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
|
||||
>
|
||||
<div class="mx-auto text-center text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.signUpWithTitle) }}
|
||||
</div>
|
||||
<section class="flex flex-col gap-2.5">
|
||||
<ButtonStyled>
|
||||
<a
|
||||
class="!shadow-none"
|
||||
:href="getAuthUrl('google', redirectTarget)"
|
||||
@click="onOAuthProviderClick('google')"
|
||||
>
|
||||
<GoogleColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Google' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
class="!shadow-none"
|
||||
:href="getAuthUrl('microsoft', redirectTarget)"
|
||||
@click="onOAuthProviderClick('microsoft')"
|
||||
>
|
||||
<MicrosoftColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Microsoft' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
class="!shadow-none"
|
||||
:href="getAuthUrl('discord', redirectTarget)"
|
||||
@click="onOAuthProviderClick('discord')"
|
||||
>
|
||||
<DiscordColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Discord' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<template v-if="showOtherOptions">
|
||||
<ButtonStyled>
|
||||
<a
|
||||
class="!shadow-none"
|
||||
:href="getAuthUrl('github', redirectTarget)"
|
||||
@click="onOAuthProviderClick('github')"
|
||||
>
|
||||
<GitHubColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'GitHub' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
class="!shadow-none"
|
||||
:href="getAuthUrl('gitlab', redirectTarget)"
|
||||
@click="onOAuthProviderClick('gitlab')"
|
||||
>
|
||||
<GitLabColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'GitLab' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
class="!shadow-none"
|
||||
:href="getAuthUrl('steam', redirectTarget)"
|
||||
@click="onOAuthProviderClick('steam')"
|
||||
>
|
||||
<SteamColorIcon />
|
||||
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Steam' }) }}</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<button
|
||||
class="mx-auto -mb-3 bg-transparent pt-1 text-center text-base font-semibold text-secondary transition-all hover:text-primary"
|
||||
@click="onToggleOtherOptions()"
|
||||
>
|
||||
{{
|
||||
showOtherOptions
|
||||
? formatMessage(messages.showFewerOptions)
|
||||
: formatMessage(messages.showOtherOptions)
|
||||
}}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div class="h-px w-full bg-surface-5"></div>
|
||||
|
||||
<section class="flex flex-col gap-2.5">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="emailModel"
|
||||
:icon="MailIcon"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
:placeholder="formatMessage(commonMessages.emailLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="passwordModel"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full"
|
||||
:disabled="!emailModel || !passwordModel"
|
||||
@click="onContinueWithEmail()"
|
||||
>
|
||||
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<p v-if="!routeQuery.launcher" class="m-0 text-center">
|
||||
<IntlFormatted :message-id="messages.legalDisclaimer">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/legal/terms" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #privacy-policy-link="{ children }">
|
||||
<NuxtLink to="/legal/privacy" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<div class="mx-auto flex flex-wrap items-center justify-start gap-2 text-center">
|
||||
{{ formatMessage(messages.alreadyHaveAccountLabel) }}
|
||||
<NuxtLink
|
||||
class="mr-1 text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-in',
|
||||
query: routeQuery,
|
||||
}"
|
||||
>
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
|
||||
import { getAuthUrl, PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY } from '@/composables/auth.ts'
|
||||
|
||||
type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft'
|
||||
|
||||
interface Props {
|
||||
redirectTarget?: string
|
||||
showOtherOptions?: boolean
|
||||
routeQuery?: LocationQuery
|
||||
onToggleOtherOptions?: () => void
|
||||
onContinueWithEmail?: () => void
|
||||
}
|
||||
|
||||
const {
|
||||
redirectTarget = '',
|
||||
showOtherOptions = false,
|
||||
routeQuery = {},
|
||||
onToggleOtherOptions = () => {},
|
||||
onContinueWithEmail = () => {},
|
||||
} = defineProps<Props>()
|
||||
|
||||
const emailModel = defineModel<string>('email', { default: '' })
|
||||
const passwordModel = defineModel<string>('password', { default: '' })
|
||||
|
||||
const pendingSignInOAuthProvider = useStorage<AuthProvider | null>(
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
)
|
||||
const onOAuthProviderClick = (provider: AuthProvider) => {
|
||||
pendingSignInOAuthProvider.value = provider
|
||||
}
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
signUpWithTitle: {
|
||||
id: 'auth.sign-up.title.sign-up-with',
|
||||
defaultMessage: 'Create an Account',
|
||||
},
|
||||
continueWithProvider: {
|
||||
id: 'auth.continue-with-provider',
|
||||
defaultMessage: 'Continue with {provider}',
|
||||
},
|
||||
legalDisclaimer: {
|
||||
id: 'auth.sign-up.legal-dislaimer',
|
||||
defaultMessage:
|
||||
"By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
|
||||
},
|
||||
alreadyHaveAccountLabel: {
|
||||
id: 'auth.sign-up.sign-in-option.title',
|
||||
defaultMessage: 'Already have an account?',
|
||||
},
|
||||
continueWithEmail: {
|
||||
id: 'auth.sign-up.continue-with-email',
|
||||
defaultMessage: 'Continue with Email',
|
||||
},
|
||||
showFewerOptions: {
|
||||
id: 'auth.sign-up.show-fewer-options',
|
||||
defaultMessage: 'Show fewer options',
|
||||
},
|
||||
showOtherOptions: {
|
||||
id: 'auth.sign-up.show-other-options',
|
||||
defaultMessage: 'Show other options',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -166,9 +166,7 @@ defineExpose({
|
||||
show,
|
||||
})
|
||||
|
||||
const auth = (await useAuth()) as Ref<{
|
||||
user: { id: string; username: string; avatar_url: string } | null
|
||||
}>
|
||||
const auth = await useAuth()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
|
||||
+1
-2
@@ -120,7 +120,7 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
|
||||
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
|
||||
import { getAuthUrl, removeAuthProvider, useAuth } from '@/composables/auth.js'
|
||||
import { getAuthUrl, removeAuthProvider, useAuth } from '@/composables/auth.ts'
|
||||
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
|
||||
|
||||
const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees, saveStateToStorage } =
|
||||
@@ -193,7 +193,6 @@ async function saveVenmoHandle() {
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error auth.js is not typed
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
initialVenmoHandle.value = venmoHandle.value.trim()
|
||||
|
||||
+1
-1
@@ -358,8 +358,8 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
|
||||
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
|
||||
import { useAuth } from '@/composables/auth.js'
|
||||
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
|
||||
import { useAuth } from '~/composables/auth.ts'
|
||||
|
||||
const debug = useDebugLogger('TremendousDetailsStage')
|
||||
const { withdrawData, maxWithdrawAmount, availableMethods, paymentOptions, calculateFees } =
|
||||
|
||||
@@ -190,7 +190,6 @@ import {
|
||||
LinkIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { type ExtendedReport, reportQuickReplies } from '@modrinth/moderation'
|
||||
import { type OverflowMenuOption, useFormatDateTime } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
@@ -198,6 +197,8 @@ import {
|
||||
getProjectTypeIcon,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
@@ -31,13 +31,13 @@ import {
|
||||
getProjectTypeIcon,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
useFormatBytes,
|
||||
useFormatDateTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { NavTabs } from '@modrinth/ui'
|
||||
import {
|
||||
capitalizeString,
|
||||
formatProjectType,
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ const currentProjectId = computed(() => projectV3.value?.id)
|
||||
const { selectedProjectId, selectedVersionId } = injectServerCompatibilityContext()
|
||||
const { labrinth } = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const auth = (await useAuth()) as { user?: { id: string } }
|
||||
const auth = await useAuth()
|
||||
|
||||
interface VersionInfo {
|
||||
id: string
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
function normalizeAuthToken(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const useAuth = async (oldToken = null) => {
|
||||
const auth = useState('auth', () => ({
|
||||
user: null,
|
||||
token: '',
|
||||
headers: {},
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken = null) => {
|
||||
const auth = {
|
||||
user: null,
|
||||
token: '',
|
||||
}
|
||||
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
const authCookie = useCookie('auth-token', {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
secure: config.public.cookieSecure,
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (oldToken) {
|
||||
const normalized = normalizeAuthToken(oldToken)
|
||||
if (normalized) {
|
||||
authCookie.value = normalized
|
||||
}
|
||||
}
|
||||
|
||||
const oauthCode = normalizeAuthToken(route.query.code)
|
||||
if (oauthCode && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = oauthCode
|
||||
}
|
||||
|
||||
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${oauthCode}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
const tokenStr = normalizeAuthToken(authCookie.value)
|
||||
|
||||
if (authCookie.value != null && tokenStr === '') {
|
||||
authCookie.value = null
|
||||
} else if (tokenStr) {
|
||||
auth.token = tokenStr
|
||||
|
||||
if (!auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token && typeof auth.token === 'string') {
|
||||
try {
|
||||
const session = await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
auth.token = normalizeAuthToken(session.session)
|
||||
if (auth.token) {
|
||||
authCookie.value = auth.token
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
authCookie.value = null
|
||||
auth.token = ''
|
||||
}
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getSignInRedirectPath = (route) => {
|
||||
const fullPath = route.fullPath
|
||||
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
|
||||
return '/dashboard'
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export const getSignInRouteObj = (route, redirectOverride) => ({
|
||||
path: '/auth/sign-in',
|
||||
query: {
|
||||
redirect: redirectOverride ?? getSignInRedirectPath(route),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAuthUrl = (provider, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
|
||||
const fullURL = route.query.launcher
|
||||
? getLauncherRedirectUrl(route)
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider) => {
|
||||
startLoading()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const getLauncherRedirectUrl = (route) => {
|
||||
const usesLocalhostRedirectionScheme =
|
||||
['4', '6'].includes(route.query.ipver) && Number(route.query.port) < 65536
|
||||
|
||||
return usesLocalhostRedirectionScheme
|
||||
? `http://${route.query.ipver === '4' ? '127.0.0.1' : '[::1]'}:${route.query.port}`
|
||||
: `https://launcher-files.modrinth.com`
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { LocationQueryValue, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import type { CookieOptions } from '#app'
|
||||
|
||||
type AuthState = {
|
||||
user: Labrinth.Users.v2.User | null
|
||||
token: string
|
||||
}
|
||||
|
||||
type QueryValue = LocationQueryValue | LocationQueryValue[] | undefined
|
||||
type FullPathRoute = Pick<RouteLocationNormalizedLoaded, 'fullPath'>
|
||||
type LauncherRoute = Pick<RouteLocationNormalizedLoaded, 'query'>
|
||||
|
||||
export const LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY = 'auth-last-sign-in-oauth-provider'
|
||||
export const PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY = 'auth-pending-sign-in-oauth-provider'
|
||||
|
||||
const AUTH_COOKIE_OPTIONS = {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
} satisfies CookieOptions<string | null>
|
||||
|
||||
const normalizeAuthToken = (value: unknown) => {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getQueryString = (value: QueryValue) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? null
|
||||
}
|
||||
return value ?? null
|
||||
}
|
||||
|
||||
export const useAuth = async (oldToken: string | null | undefined = null) => {
|
||||
const auth = useState<AuthState>('auth', () => ({
|
||||
user: null,
|
||||
token: '',
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken: string | null | undefined = null) => {
|
||||
const auth: AuthState = {
|
||||
user: null,
|
||||
token: '',
|
||||
}
|
||||
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const config = useRuntimeConfig()
|
||||
const authCookie = useCookie<string | null>('auth-token', {
|
||||
...AUTH_COOKIE_OPTIONS,
|
||||
secure: config.public.cookieSecure,
|
||||
})
|
||||
|
||||
if (oldToken) {
|
||||
const normalized = normalizeAuthToken(oldToken)
|
||||
if (normalized) {
|
||||
authCookie.value = normalized
|
||||
}
|
||||
}
|
||||
|
||||
const oauthCode = normalizeAuthToken(route.query.code)
|
||||
if (oauthCode && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = oauthCode
|
||||
}
|
||||
|
||||
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${oauthCode}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
const tokenStr = normalizeAuthToken(authCookie.value)
|
||||
|
||||
if (authCookie.value != null && tokenStr === '') {
|
||||
authCookie.value = null
|
||||
} else if (tokenStr) {
|
||||
auth.token = tokenStr
|
||||
|
||||
if (!auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = (await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as Labrinth.Users.v2.User
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = (await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as { session: unknown }
|
||||
|
||||
auth.token = normalizeAuthToken(session.session)
|
||||
if (auth.token) {
|
||||
authCookie.value = auth.token
|
||||
auth.user = (await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
apiVersion: 3,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)) as Labrinth.Users.v2.User
|
||||
} else {
|
||||
authCookie.value = null
|
||||
auth.token = ''
|
||||
}
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getSignInRedirectPath = (route: FullPathRoute) => {
|
||||
const fullPath = route.fullPath
|
||||
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
|
||||
return '/dashboard'
|
||||
}
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export const getSignInRouteObj = (route: FullPathRoute, redirectOverride?: string | null) => ({
|
||||
path: '/auth/sign-in',
|
||||
query: {
|
||||
redirect: redirectOverride ?? getSignInRedirectPath(route),
|
||||
},
|
||||
})
|
||||
|
||||
export const getAuthUrl = (provider: string, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
const launcher = getQueryString(route.query.launcher)
|
||||
|
||||
const fullURL = launcher
|
||||
? (() => {
|
||||
const callbackUrl = new URL('/auth/sign-in', config.public.siteUrl)
|
||||
callbackUrl.searchParams.set('launcher', launcher)
|
||||
|
||||
const ipver = getQueryString(route.query.ipver)
|
||||
const port = getQueryString(route.query.port)
|
||||
|
||||
if (ipver) {
|
||||
callbackUrl.searchParams.set('ipver', ipver)
|
||||
}
|
||||
|
||||
if (port) {
|
||||
callbackUrl.searchParams.set('port', port)
|
||||
}
|
||||
|
||||
return callbackUrl.toString()
|
||||
})()
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const promotePendingSignInOAuthProvider = () => {
|
||||
if (!import.meta.client) return
|
||||
const pending = useStorage<string | null>(
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
if (!pending.value) return
|
||||
const last = useStorage<string | null>(LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY, null, undefined, {
|
||||
initOnMounted: true,
|
||||
})
|
||||
last.value = pending.value
|
||||
pending.value = null
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider: string) => {
|
||||
startLoading()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const getLauncherRedirectUrl = (route: LauncherRoute) => {
|
||||
const ipver = getQueryString(route.query.ipver)
|
||||
const port = Number(getQueryString(route.query.port))
|
||||
const usesLocalhostRedirectionScheme = ['4', '6'].includes(ipver ?? '') && port < 65536
|
||||
|
||||
return usesLocalhostRedirectionScheme
|
||||
? `http://${ipver === '4' ? '127.0.0.1' : '[::1]'}:${port}`
|
||||
: 'https://launcher-files.modrinth.com'
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
function ensurePasskeySupported() {
|
||||
const supported =
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.PublicKeyCredential !== 'undefined' &&
|
||||
typeof navigator !== 'undefined' &&
|
||||
!!navigator.credentials
|
||||
if (!supported) {
|
||||
throw new Error('Passkeys are not supported by this browser.')
|
||||
}
|
||||
}
|
||||
|
||||
function base64urlToBuffer(base64url: string) {
|
||||
return Uint8Array.from(atob(base64url.replace(/-/g, '+').replace(/_/g, '/')), (char) =>
|
||||
char.charCodeAt(0),
|
||||
)
|
||||
}
|
||||
|
||||
function bufferToBase64url(buffer: ArrayBuffer) {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
let str = ''
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
str += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a passkey credential using the browser's WebAuthn API.
|
||||
*
|
||||
* @param options The public key options for creating the passkey credential, provided by the server.
|
||||
*/
|
||||
export async function createPasskeyCredential(options: any) {
|
||||
ensurePasskeySupported()
|
||||
|
||||
const publicKey = {
|
||||
...options,
|
||||
challenge: base64urlToBuffer(options.challenge),
|
||||
user: {
|
||||
...options.user,
|
||||
id: base64urlToBuffer(options.user.id),
|
||||
},
|
||||
excludeCredentials: options.excludeCredentials?.map((cred: any) => ({
|
||||
...cred,
|
||||
id: base64urlToBuffer(cred.id),
|
||||
})),
|
||||
}
|
||||
|
||||
const credential = (await navigator.credentials.create({ publicKey })) as PublicKeyCredential
|
||||
const response = credential.response as AuthenticatorAttestationResponse
|
||||
|
||||
return {
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64url(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
||||
attestationObject: bufferToBase64url(response.attestationObject),
|
||||
},
|
||||
extensions: credential.getClientExtensionResults(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a user using a passkey credential.
|
||||
*
|
||||
* @param options The public key options for authenticating the passkey credential, provided by the server.
|
||||
*/
|
||||
export async function getPasskeyCredential(options: any) {
|
||||
ensurePasskeySupported()
|
||||
|
||||
const publicKey = {
|
||||
...options,
|
||||
challenge: base64urlToBuffer(options.challenge),
|
||||
allowCredentials: options.allowCredentials?.map((cred: any) => ({
|
||||
...cred,
|
||||
id: base64urlToBuffer(cred.id),
|
||||
})),
|
||||
}
|
||||
|
||||
const credential = (await navigator.credentials.get({ publicKey })) as PublicKeyCredential
|
||||
const response = credential.response as AuthenticatorAssertionResponse
|
||||
|
||||
return {
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64url(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
||||
authenticatorData: bufferToBase64url(response.authenticatorData),
|
||||
signature: bufferToBase64url(response.signature),
|
||||
userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : null,
|
||||
},
|
||||
extensions: credential.getClientExtensionResults(),
|
||||
}
|
||||
}
|
||||
@@ -799,7 +799,7 @@ import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.
|
||||
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
|
||||
import ProjectCreateModal from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import ModrinthFooter from '~/components/ui/ModrinthFooter.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { errors as generatedStateErrors } from '~/generated/state.json'
|
||||
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
|
||||
import { hasActiveMidas } from '~/utils/user-membership.ts'
|
||||
|
||||
@@ -365,39 +365,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "أدخل الرمز..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>هل نسيت كلمة المرور؟</forgot-password-link> • <create-account-link>إنشاء حساب</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "تسجيل الدخول باستخدام"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "تسجيل الدخول"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "أو استخدم كلمة المرور"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "إنشاء حساب"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link> الخاصة بها."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "كلمتا المرور غير متطابقتين!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "هل لديك حساب بالفعل؟"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "اشترك لتصلك التحديثات حول مودرنث"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "إنشاء حساب"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "أو أنشئ حسابًا بنفسك"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "سجّل باستخدام"
|
||||
},
|
||||
@@ -431,21 +413,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "تحقّق من البريد الإلكتروني"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "اشترك في التحديثات حول مودرنث"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "أنت الآن جزء من مجتمع رائع من المبدعين والمستكشفين الذين يقومون بالفعل بإنشاء المودات المذهلة، تثبيتها، والبقاء على اطلاع بآخر التحديثات."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "مرحبًا بك في مودرنث!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "مرحبًا"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "تعديل الأيقونة"
|
||||
},
|
||||
|
||||
@@ -407,39 +407,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Zadejte kód..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Zapomněli jste heslo?</forgot-password-link> • <create-account-link>Vytvořte si účet</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Přihlášte se pomocí"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Přihlásit se"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Nebo použijte heslo"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Vytvořte účet"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Vytvořením účtu souhlasíte s <terms-link>Podmínkami</terms-link> a <privacy-policy-link>Zásadami ochrany osobních údajů</privacy-policy-link> Modrinthu."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Hesla se neshodují!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Již máte účet?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Přihlásit se k odběru informací o Modrinthu"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registrace"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Nebo si vytvořte účet"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Registrujte se pomocí"
|
||||
},
|
||||
@@ -473,21 +455,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Ověřit e-mail"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Přihlásit se k odběru informací o Modrinthu"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Nyní jste součástí úžasné komunity tvůrců a průzkumníků, kteří již vytvářejí, stahují a aktualizují úžasné módy."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Vytvořením účtu souhlasíte s <terms-link>Podmínkami</terms-link> a <privacy-policy-link>Zásadami ochrany osobních údajů</privacy-policy-link> Modrinthu."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Vítejte v Modrinthu!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Vítejte"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Upravit ikonu"
|
||||
},
|
||||
|
||||
@@ -263,39 +263,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Indtast kode..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Glemt adgangskode?</forgot-password-link> • <create-account-link>Opret en konto</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Log ind med"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Log ind"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Eller brug en adgangskode"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Opret konto"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Ved at oprette en konto, accepterer du Modrinth's <terms-link>Vilkår</terms-link> og <privacy-policy-link>Privatlivspolitik</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Adgangskoderne er ikke ens!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Har du allerede en konto?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Abonner på opdateringer om Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Tilmeld"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Eller opret en konto selv"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Tilmeld dig med"
|
||||
},
|
||||
@@ -329,21 +311,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Bekræft E-mail"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Abonner på opdateringer om Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Du er nu en del af det awesome fællesskab og skabere og udforskere der allerede bygger, downloader, og forbliver opdateret med fantastiske mods."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Ved at oprette en konto, accepterer du Modrinth's <terms-link>Vilkår</terms-link> og <privacy-policy-link>Privatlivspolitik</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Velkommen til Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Velkommen"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Rediger ikon"
|
||||
},
|
||||
|
||||
@@ -737,39 +737,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Code eingeben..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Passwort vergessen?</forgot-password-link> • <create-account-link>Konto erstellen</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Anmelden mit"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Anmelden"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Oder verwende ein Passwort"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Konto erstellen"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Durch das erstellen eines Kontos stimmst du den <terms-link>Nutzungsbedingungen</terms-link> und <privacy-policy-link>Datenschutzrichtlinien</privacy-policy-link> von Modrinth zu."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Passwörter stimmen nicht überein!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Du hast bereits ein Konto?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Folge den Updates über Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registrieren"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Oder erstelle selbst ein Konto"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Registrieren mit"
|
||||
},
|
||||
@@ -803,21 +785,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "E-Mail verifizieren"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Folge updates über Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Du bist nun teil der grossartigen Community von Entwicklern und Entdeckern, welche bereits tolle Projekte erstellen, herunterladen und auf den neusten Stand halten."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Durch das erstellen eines Kontos hast du den <terms-link>Nutzungsbedingungen</terms-link> und <privacy-policy-link>Datenschutzrichtlinien</privacy-policy-link> von Modrinth zugestimmt."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Willkommen bei Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Willkommen"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Icon bearbeiten"
|
||||
},
|
||||
|
||||
@@ -737,39 +737,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Code eingeben..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Passwort vergessen?</forgot-password-link> • <create-account-link>Erstelle ein Konto</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Anmelden mit"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Anmelden"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Oder Passwort verwenden"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Konto erstellen"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Durch die Erstellung eines Kontos stimmst du den <terms-link>Nutzungsbedingungen</terms-link> und der <privacy-policy-link>Datenschutzerklärung</privacy-policy-link> von Modrinth zu."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Passwörter stimmen nicht überein!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Du hast bereits ein Konto?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Updates zu Modrinth abonnieren"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registrieren"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Oder erstelle selbst ein Konto"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Registrieren mit"
|
||||
},
|
||||
@@ -803,21 +785,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "E-Mail bestätigen"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Erhalte Neuigkeiten zu Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Du bist jetzt Teil der großartigen Community von Entwicklern und Entdeckern die bereits fantastische Mods erstellen, herunterladen und auf dem neuesten Stand halten."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Durch die Erstellung eines Kontos hast du den <terms-link>Nutzungsbedingungen</terms-link> und der <privacy-policy-link>Datenschutzerklärung</privacy-policy-link> von Modrinth zugestimmt."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Willkommen bei Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Willkommen"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Icon bearbeiten"
|
||||
},
|
||||
|
||||
@@ -731,12 +731,72 @@
|
||||
"auth.authorize.authorize-app-name": {
|
||||
"message": "Authorize {appName}"
|
||||
},
|
||||
"auth.authorize.error.missing-parameters": {
|
||||
"message": "Missing required OAuth query parameters."
|
||||
},
|
||||
"auth.authorize.error.no-redirect-url": {
|
||||
"message": "No redirect location found in response"
|
||||
},
|
||||
"auth.authorize.errro-title": {
|
||||
"message": "An Error Occured"
|
||||
},
|
||||
"auth.authorize.redirect-url": {
|
||||
"message": "You will be redirected to <redirect-url>{url}</redirect-url>"
|
||||
},
|
||||
"auth.continue-with-provider": {
|
||||
"message": "Continue with {provider}"
|
||||
},
|
||||
"auth.create-account.age-requirement.warning-title": {
|
||||
"message": "Age requirement"
|
||||
},
|
||||
"auth.create-account.complete-sign-up": {
|
||||
"message": "Complete sign up"
|
||||
},
|
||||
"auth.create-account.date-of-birth.invalid.text": {
|
||||
"message": "Please enter a valid date of birth. Year cannot be 0000."
|
||||
},
|
||||
"auth.create-account.date-of-birth.invalid.title": {
|
||||
"message": "Invalid date of birth"
|
||||
},
|
||||
"auth.create-account.date-of-birth.label": {
|
||||
"message": "Date of birth"
|
||||
},
|
||||
"auth.create-account.date-of-birth.placeholder": {
|
||||
"message": "Select your date of birth"
|
||||
},
|
||||
"auth.create-account.date-of-birth.required.text": {
|
||||
"message": "Please enter your date of birth before continuing."
|
||||
},
|
||||
"auth.create-account.date-of-birth.required.title": {
|
||||
"message": "Date of birth required"
|
||||
},
|
||||
"auth.create-account.date-of-birth.under13-helper": {
|
||||
"message": "You do not meet the age requirement to create an account at Modrinth."
|
||||
},
|
||||
"auth.create-account.info-panel.source-code-link": {
|
||||
"message": "Relevant source code"
|
||||
},
|
||||
"auth.create-account.info-panel.text": {
|
||||
"message": "We do not store your date of birth. We only use it during sign-up to validate COPPA compliance."
|
||||
},
|
||||
"auth.create-account.page-title": {
|
||||
"message": "Create Account"
|
||||
},
|
||||
"auth.create-account.security-check.label": {
|
||||
"message": "Security check"
|
||||
},
|
||||
"auth.create-account.subscribe.label": {
|
||||
"message": "Keep me updated on the cool things Modrinth is working on via email"
|
||||
},
|
||||
"auth.create-account.title": {
|
||||
"message": "Create an Account"
|
||||
},
|
||||
"auth.create-account.username.label": {
|
||||
"message": "Username"
|
||||
},
|
||||
"auth.create-account.username.placeholder": {
|
||||
"message": "Enter username"
|
||||
},
|
||||
"auth.reset-password.method-choice.action": {
|
||||
"message": "Send recovery email"
|
||||
},
|
||||
@@ -771,53 +831,68 @@
|
||||
"message": "Reset your password"
|
||||
},
|
||||
"auth.sign-in.2fa.description": {
|
||||
"message": "Please enter a two-factor code to proceed."
|
||||
"message": "Enter the 6-digit code from your authenticator app, or one of your backup codes."
|
||||
},
|
||||
"auth.sign-in.2fa.label": {
|
||||
"message": "Enter two-factor code"
|
||||
"message": "Two-factor authentication"
|
||||
},
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Enter code..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Forgot password?</forgot-password-link> • <create-account-link>Create an account</create-account-link>"
|
||||
"auth.sign-in.continue-with-email": {
|
||||
"message": "Continue with Email"
|
||||
},
|
||||
"auth.sign-in.continue-with-passkey": {
|
||||
"message": "Continue with passkey"
|
||||
},
|
||||
"auth.sign-in.create-account": {
|
||||
"message": "Sign up"
|
||||
},
|
||||
"auth.sign-in.forgot-password": {
|
||||
"message": "Forgot password"
|
||||
},
|
||||
"auth.sign-in.last-sign-in": {
|
||||
"message": "Last used"
|
||||
},
|
||||
"auth.sign-in.no-account": {
|
||||
"message": "Don't have an account?"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Sign in with"
|
||||
"message": "Sign into Modrinth"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Sign In"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Or use a password"
|
||||
"auth.sign-up.age-requirement.warning-title": {
|
||||
"message": "Age requirement"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Create account"
|
||||
"auth.sign-up.continue-with-email": {
|
||||
"message": "Continue with Email"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Passwords do not match!"
|
||||
"auth.sign-up.show-fewer-options": {
|
||||
"message": "Show fewer options"
|
||||
},
|
||||
"auth.sign-up.show-other-options": {
|
||||
"message": "Show other options"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Already have an account?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Subscribe to updates about Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Sign Up"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Or create an account yourself"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Sign up with"
|
||||
"message": "Create an Account"
|
||||
},
|
||||
"auth.verify-email.action.account-settings": {
|
||||
"message": "Account settings"
|
||||
},
|
||||
"auth.verify-email.action.discover-mods": {
|
||||
"message": "Discover mods"
|
||||
},
|
||||
"auth.verify-email.already-verified.description": {
|
||||
"message": "Your email is already verified!"
|
||||
},
|
||||
@@ -836,6 +911,15 @@
|
||||
"auth.verify-email.failed-verification.title": {
|
||||
"message": "Email verification failed"
|
||||
},
|
||||
"auth.verify-email.notification.email-sent.description": {
|
||||
"message": "An email with a link to verify your account has been sent to {email}."
|
||||
},
|
||||
"auth.verify-email.notification.email-sent.title": {
|
||||
"message": "Email sent"
|
||||
},
|
||||
"auth.verify-email.notification.error-occurred.title": {
|
||||
"message": "An error occurred"
|
||||
},
|
||||
"auth.verify-email.post-verification.description": {
|
||||
"message": "Your email address has been successfully verified!"
|
||||
},
|
||||
@@ -845,21 +929,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verify Email"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Subscribe to updates about Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with amazing mods."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Welcome to Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Welcome"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Edit icon"
|
||||
},
|
||||
@@ -4031,6 +4100,48 @@
|
||||
"settings.account.security.email.title": {
|
||||
"message": "Email"
|
||||
},
|
||||
"settings.account.security.passkey.add": {
|
||||
"message": "Add passkey"
|
||||
},
|
||||
"settings.account.security.passkey.add-modal.name.description": {
|
||||
"message": "Make sure to pick something memorable, so you can identify this passkey later."
|
||||
},
|
||||
"settings.account.security.passkey.add-modal.name.label": {
|
||||
"message": "Name"
|
||||
},
|
||||
"settings.account.security.passkey.add-modal.name.placeholder": {
|
||||
"message": "My passkey"
|
||||
},
|
||||
"settings.account.security.passkey.description": {
|
||||
"message": "Manage your registered passkeys, or add a new one."
|
||||
},
|
||||
"settings.account.security.passkey.modal.added": {
|
||||
"message": "Added {ago}"
|
||||
},
|
||||
"settings.account.security.passkey.modal.last-used": {
|
||||
"message": "Last used {ago}"
|
||||
},
|
||||
"settings.account.security.passkey.modal.loading": {
|
||||
"message": "Loading passkeys…"
|
||||
},
|
||||
"settings.account.security.passkey.modal.never-used": {
|
||||
"message": "Never used"
|
||||
},
|
||||
"settings.account.security.passkey.modal.no-passkeys": {
|
||||
"message": "You do not have any passkeys registered."
|
||||
},
|
||||
"settings.account.security.passkey.remove.description": {
|
||||
"message": "This will permanently remove the passkey \"{name}\". You will no longer be able to sign in with it."
|
||||
},
|
||||
"settings.account.security.passkey.remove.title": {
|
||||
"message": "Are you sure you want to remove this passkey?"
|
||||
},
|
||||
"settings.account.security.passkey.rename-modal.header": {
|
||||
"message": "Rename passkey"
|
||||
},
|
||||
"settings.account.security.passkey.title": {
|
||||
"message": "Manage passkeys"
|
||||
},
|
||||
"settings.account.security.password.action.add": {
|
||||
"message": "Add password"
|
||||
},
|
||||
|
||||
@@ -779,39 +779,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Introduce el código..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>¿Olvidaste tu contraseña?</forgot-password-link> • <create-account-link>Crear una cuenta</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Iniciar sesión con"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Iniciar sesión"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "O usa una contraseña"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Crear cuenta"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Al crear una cuenta, aceptas los <terms-link>Términos</terms-link> y la <privacy-policy-link>Política de privacidad</privacy-policy-link> de Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "¡Las contraseñas no coinciden!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "¿Ya tienes una cuenta?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Suscríbete para recibir actualizaciones sobre Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registrarse"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "O crea tu propia cuenta"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Registrarse con"
|
||||
},
|
||||
@@ -845,21 +827,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verificar correo electrónico"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Suscríbete para recibir actualizaciones sobre Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Ahora formas parte de la increíble comunidad de creadores y exploradores que ya están construyendo, descargando y manteniéndose al día con mods asombrosos."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Al crear una cuenta, has aceptado los <terms-link>Términos</terms-link> y la <privacy-policy-link>Política de privacidad</privacy-policy-link> de Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "¡Bienvenido a Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Bienvenido"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Editar ícono"
|
||||
},
|
||||
|
||||
@@ -779,39 +779,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Introducir código..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>¿Olvidaste tu contraseña?</forgot-password-link> • <create-account-link>Crear una cuenta</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Iniciar sesión con"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Iniciar sesión"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "O usa una contraseña"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Crear cuenta"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Al crear una cuenta, tú aceptas los <terms-link>Términos</terms-link> y <privacy-policy-link>Política de Privacidad</privacy-policy-link> de Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "¡Las contraseñas no coinciden!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "¿Ya tienes una cuenta?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Suscríbete para recibir actualizaciones sobre Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registro"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "O crea una cuenta tú mismo"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Registrarse con"
|
||||
},
|
||||
@@ -845,21 +827,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verificar correo electrónico"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Suscríbete a las novedades sobre Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Ahora formas parte de la increíble comunidad de creadores y exploradores que ya están creando, descargando y manteniéndose al día con mods increíbles."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Al crear esta cuenta, aceptas los <terms-link>términos de Modrinth</terms-link> y la <privacy-policy-link>Política de Privacidad</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "¡Bienvenido a Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Bienvenido"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Editar icono"
|
||||
},
|
||||
|
||||
@@ -269,30 +269,15 @@
|
||||
"auth.sign-in.title": {
|
||||
"message": "Kirjaudu sisään"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Tai käytä salasanaa"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Luo tili"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Luomalla tilin, hyväksyt Modrinthin <terms-link>-käyttöoikeussopimuksen</terms-link> ja <privacy-policy-link>Tietosuojakäytännön</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Salasanat eivät täsmää!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Onko sinulla jo käyttäjätili?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Tilaa päivityksiä Modrinthista"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Rekisteröidy"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Tai luo tili itse"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Reskiteröidy käyttäen"
|
||||
},
|
||||
@@ -326,18 +311,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Vahvista sähköposti"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Tilaa päivityksiä Modrinthista"
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Luomalla tilin, olet hyväksynyt Modrinthin <terms-link>-käyttöoikeussopimuksen</terms-link> ja <privacy-policy-link>Tietosuojakäytännön</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Tervetuloa Modrinthiin!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Tervetuloa"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Muokkaa kuvaketta"
|
||||
},
|
||||
|
||||
@@ -395,39 +395,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Ilagay ang code..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Nalimutan ang password?</forgot-password-link> • <create-account-link>Lumikha ng account</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Mag-sign in gamit ang"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Mag-sign In"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "O gamitin ang password"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Lumikha ng account"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Sa paggawa ng account, sumasang-ayon ka sa <terms-link>Mga Tuntunin</terms-link> at <privacy-policy-link>Patakaran sa Pribasiya</privacy-policy-link> ng Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Hindi magkatugma ang mga password!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Mayroon ka nang account?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Mag-subscribe ukol sa mga update sa Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Mag-sign Up"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "O sariling lumkiha account"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Mag-sign up gamit ang"
|
||||
},
|
||||
@@ -461,21 +443,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Patunayan ang Email"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Mag-subscribe ukol sa mga update ng Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Ikaw ay parte na ngayon ng kahanga-hangang komunidad na ito ng mga tagagawa at manlalalakbay na nagtataguyod, nagda-download, at nananatiling napapanahon sa mga nakamamanghang na mga mod."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Sa paggawa ng account, sumang-ayon ka na sa <terms-link>Mga Tuntunin</terms-link> at <privacy-policy-link>Patakaran sa Pribasiya</privacy-policy-link> ng Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Maligayang pagdating sa Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Maligayang Pagdating"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Baguhin ang ikono"
|
||||
},
|
||||
|
||||
@@ -779,39 +779,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Saisir le code..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Mot de passe oublié ?</forgot-password-link> • <create-account-link>Créer un compte</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Se connecter avec"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Se connecter"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Ou utiliser un mot de passe"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Créer un compte"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "En créant un compte, vous acceptez les <terms-link>Conditions d'utilisation</terms-link> et la <privacy-policy-link>Politique de confidentialité</privacy-policy-link> de Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Les mots de passe ne correspondent pas !"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Vous avez déjà un compte ?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "S'abonner aux mises à jour de Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "S'inscrire"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Ou créez vous-même un compte"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "S'inscrire avec"
|
||||
},
|
||||
@@ -845,21 +827,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Vérifier l'adresse e-mail"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "S'abonner aux mises à jour de Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Vous faites maintenant partie de la formidable communauté de créateurs et d'explorateurs qui construisent, téléchargent et se tiennent au courant des incroyables mods."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "En créant un compte, vous acceptez les <terms-link>Conditions d'utilisation</terms-link> et la <privacy-policy-link>Politique de confidentialité</privacy-policy-link> de Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Bienvenue sur Modrinth !"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Bienvenue"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Modifier l'icône"
|
||||
},
|
||||
|
||||
@@ -263,39 +263,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "כתבו קוד..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>שככו את הסיסמה?</forgot-password-link>\n<create-account-link>ליצור חשבון</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "הכנס אם"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "הכנס"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "או להשתמש בסיסמה"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "ליצור חשבון"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "כליצור את החשבון, אתם מסכימים ל<terms-link>תנאים</terms-link> ו<privacy-policy-link>מדיניות פרטיות</privacy-policy-link> של Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "סיסמאות לא תואמות!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "כבר יש לכם חשבון?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "להרשם לעדכונים על Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "הרשם"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "או עצרו חשבון בעצמכם"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "הרשם אם"
|
||||
},
|
||||
@@ -329,21 +311,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "אמת אימייל"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "הירשם לעדכונים על Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "אתה עכשיו חלק מהקהילה המדהימה של יוצרים ומשתמשים שכבר בונים, מורידים ומתעדכנים עם מודים מגניבים."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "ע\"י יצירת חשבון, אתה הסכמת ל<terms-link>תנאים</terms-link> של מודרינפ ו<privacy-policy-link>מדיניות הפרטיות</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "ברוכים הבאים ל-Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "ברוכים הבאים"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "ערוך סמל"
|
||||
},
|
||||
|
||||
@@ -689,39 +689,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Kód beírása..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Elfelejtetted a jelszavad?</forgot-password-link> • <create-account-link>Hozz létre egy fiókot</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Bejelentkezés ezzel"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Bejelentkezés"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Vagy használj jelszót"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Fiók létrehozása"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Fiók létrehozásával elfogadod a Modrinth <terms-link>Felhasználási feltételeit</terms-link> és <privacy-policy-link>Adatvédelmi irányelveit</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "A jelszavak nem jegyeznek!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Már van fiókod?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Feliratkozás a Modrinth híreire és frissítéseire"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Regisztráció"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Vagy hozz létre egyet magadnak"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Regisztráció ezzel"
|
||||
},
|
||||
@@ -755,21 +737,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Email-cím hitelesítése"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Feliratkozás a Modrinth híreire és frissítéseire"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Most már része vagy annak a fantasztikus fejlesztők és felfedezők közösségének, akik már építenek, letöltenek és naprakészek maradnak a csodálatos modokkal kapcsolatban."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Fiók létrehozásával elfogadod a Modrinth <terms-link>Felhasználási feltételeit</terms-link> és <privacy-policy-link>Adatvédelmi irányelveit</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Üdvözlünk a Modrinthon!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Üdvözlünk"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Ikon szerkesztése"
|
||||
},
|
||||
|
||||
@@ -482,39 +482,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Masukkan kode..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Lupa kata sandi?</forgot-password-link> • <create-account-link>Buat akun</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Masuk dengan"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Masuk"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Atau gunakan kata sandi"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Buat akun"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Dengan membuat sebuah akun, Anda menyetujui <terms-link>Ketentuan</terms-link> dan <privacy-policy-link>Kebijakan Privasi</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Kata sandi tidak cocok!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Sudah memiliki akun?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Berlangganan pembaruan tentang Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Daftar"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Atau buat akun Anda sendiri"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Daftar dengan"
|
||||
},
|
||||
@@ -548,21 +530,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verifikasi Sur-el"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Berlangganan pembaruan tentang Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Anda kini menjadi bagian dari komunitas kreator dan penjelajah hebat yang sudah membangun, mengunduh, dan terus mengikuti perkembangan terkini dari mod-mod yang luar biasa."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Dengan membuat sebuah akun, Anda telah menyetujui <terms-link>Ketentuan</terms-link> dan <privacy-policy-link>Kebijakan Privasi</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Selamat datang di Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Selamat Datang"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Sunting ikon"
|
||||
},
|
||||
|
||||
@@ -773,39 +773,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Inserisci il codice..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Password dimenticata?</forgot-password-link> • <create-account-link>Crea un account</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Accedi con"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Accedi"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "O usa la password"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Crea account"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Creando un''account, accetti i <terms-link>termini d''uso</terms-link> e l''<privacy-policy-link>informativa sulla privacy</privacy-policy-link> di Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Le password non corrispondono!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Hai già un account?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Ricevi novità da Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registrati"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "O crea un account"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Accedi con"
|
||||
},
|
||||
@@ -839,21 +821,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verifica email"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Ricevi novità riguardanti Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Fai ora parte della grandiosa comunità di creatori ed esploratori che già costruiscono, aggiornano e stanno sul pezzo con le fantastiche mod."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Creando un''account, hai accettato i <terms-link>termini d''utilizzo</terms-link> e l''<privacy-policy-link>informativa sulla privacy</privacy-policy-link> di Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Modrinth ti dà il benvenuto!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Ciao"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Modifica icona"
|
||||
},
|
||||
|
||||
@@ -539,39 +539,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "コードを入力…"
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>パスワードをお忘れですか?</forgot-password-link> • <create-account-link>アカウントを作成する</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "外部アカウントでログイン"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "ログイン"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "パスワードでログイン"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "アカウントを作成"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "アカウントを作成すると、Modrinthの <terms-link>利用規約</terms-link> および <privacy-policy-link>プライバシーポリシー</privacy-policy-link>に同意したものとみなされます。"
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "パスワードが一致しません!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "すでにアカウントをお持ちですか?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Modrinthのアップデートを受け取る"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "アカウント作成"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "メールアドレスで作成"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "外部アカウントで作成"
|
||||
},
|
||||
@@ -605,21 +587,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "メールアドレスを確認"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Modrinthのアップデートを受け取る"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "素晴らしいModの開発や導入、アップデートを日々行っている、クリエイターと冒険者たちの活気あるコミュニティへようこそ。"
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "アカウントを作成すると、Modrinthの <terms-link>利用規約</terms-link> および <privacy-policy-link>プライバシーポリシー</privacy-policy-link>に同意したものとみなされます。"
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Modrinthへようこそ!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "ようこそ"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "アイコンを編集"
|
||||
},
|
||||
|
||||
@@ -275,39 +275,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "코드 입력..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>비밀번호를 잊으셨나요?</forgot-password-link> • <create-account-link>계정 만들기</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "다음을 통해 로그인"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "로그인"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "또는 비밀번호 사용하기"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "계정 생성"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "계정을 생성함으로써, Modrinth의 <terms-link>이용 약관</terms-link> 및 <privacy-policy-link>개인정보 보호정책</privacy-policy-link>에 동의합니다."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "비밀번호가 일치하지 않습니다!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "이미 계정이 있으신가요?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Modrinth의 업데이트 소식 전달받기"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "회원 가입"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "또는 직접 계정을 만드세요"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "다음 계정으로 가입"
|
||||
},
|
||||
@@ -341,21 +323,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "이메일 인증"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Modrinth의 업데이트 소식 전달받기"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "이제 당신은 놀라운 모드를 만들고, 다운로드하고, 최신 상태로 유지하는 훌륭한 크리에이터 및 탐험가들의 모임의 일원이 되었습니다."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "계정을 생성함으로써, Modrinth의 <terms-link>이용 약관</terms-link> 및 <privacy-policy-link>개인정보 보호정책</privacy-policy-link>에 동의하게 됩니다."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Modrinth에 오신 걸 환영합니다!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "환영합니다"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "아이콘 수정"
|
||||
},
|
||||
|
||||
@@ -665,39 +665,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Masukkan kod..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Lupa kata laluan?</forgot-password-link> • <create-account-link>Cipta akaun</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Daftar masuk dengan"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Daftar Masuk"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Atau gunakan kata laluan"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Cipta akaun"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Dengan mencipta akaun, anda bersetuju dengan <terms-link>Terma</terms-link> dan <privacy-policy-link>Dasar Privasi</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Kata laluan tidak sepadan!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Sudah mempunyai akaun?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Langgan kepada kemas kini tentang Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Daftar"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Atau cipta akaun anda sendiri"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Daftar dengan"
|
||||
},
|
||||
@@ -731,21 +713,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Sahkan E-mel"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Langgan kepada kemas kini tentang Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Anda kini sebahagian daripada komuniti hebat pencipta & peneroka yang sudah membina, memuat turun dan sentiasa mengikuti perkembangan terkini mengenai mod yang menakjubkan."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Dengan mencipta akaun, anda telah bersetuju dengan <terms-link>Terma</terms-link> dan <privacy-policy-link>Dasar Privasi</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Selamat Datang ke Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Selamat Datang"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Sunting ikon"
|
||||
},
|
||||
|
||||
@@ -626,36 +626,18 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Voer code in..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Wachtwoord vergeten?</forgot-password-link> • <create-account-link>Maak een account aan</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Log in met"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Log in"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Of gebruik een wachtwoord"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Account aanmaken"
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Wachtwoorden komen niet overeen!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Heb je al een account?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Schrijf je in voor updates van Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Aanmelden"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Of maak zelf een account"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Meld je aan met"
|
||||
},
|
||||
@@ -689,18 +671,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verifieer e-mail"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Abonneer op updates over Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Je bent nu deel van de geweldige community van makers en ontdekkers die mods bouwen, downloaden en up-to-date blijven van geweldige mods."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Welkom op Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Welkom"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Bewerk icoon"
|
||||
},
|
||||
|
||||
@@ -263,39 +263,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Skriv inn kode..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Glemt passord?</forgot-password-link> • <create-account-link>Opprett en konto</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Logg inn med"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Logg inn"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Eller bruk et passord"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Opprett konto"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Ved å opprette en konto godtar du Modrinths <terms-link>Vilkår</terms-link> og <privacy-policy-link>Personvernpolicy</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Passordene stemmer ikke overens!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Har du allerede en konto?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Abonner på oppdateringer om Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registrer deg"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Eller lag en konko selv"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Registrer deg med"
|
||||
},
|
||||
@@ -329,21 +311,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verifiser Email"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Abonner på oppdateringer om Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Nå er du med i det fantastiske fellesskapet av skapere og utforskere som bygger, laster ned og holder seg oppdatert på fantastiske mods."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Ved å opprette en konto godtar du Modrinths <terms-link>Vilkår</terms-link> og <privacy-policy-link>Personvernpolicy</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Velkommen til Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Velkommen"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Rediger ikon"
|
||||
},
|
||||
|
||||
@@ -779,39 +779,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Wprowadź kod..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Zapomniałeś hasła?</forgot-password-link> • <create-account-link>Utwórz konto</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Zaloguj się przez"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Zaloguj się"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Lub użyj hasła"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Utwórz konto"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Tworząc konto, wyrażasz zgodę na <terms-link>Warunki</terms-link> i <privacy-policy-link>Politykę Prywatności</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Hasła się różnią!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Masz już konto?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Otrzymuj aktualności o Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Zarejestruj się"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Lub utwórz własne konto"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Zarejestruj się przez"
|
||||
},
|
||||
@@ -845,21 +827,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Zweryfikuj e-mail"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Otrzymuj aktualności o Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Teraz jesteś częścią wspaniałej społeczności twórców i odkrywców, którzy już tworzą, pobierają i na bieżąco korzystają z niesamowitych modów."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Przez stworzenie konta, zgadzasz się na nasze<terms-link>Warunki</terms-link> oraz <privacy-policy-link>Politykę prywatności</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Witaj w Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Witaj"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Edytuj ikonę"
|
||||
},
|
||||
|
||||
@@ -779,39 +779,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Insira o código..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Esqueceu sua senha?</forgot-password-link> • <create-account-link>Criar uma conta</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Iniciar sessão com"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Iniciar sessão"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Ou use a senha"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Criar conta"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Ao criar uma conta, você concorda com os <terms-link>Termos</terms-link> e a <privacy-policy-link>Política de Privacidade</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "As senhas não correspondem!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Já possui uma conta?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Inscreva-se para receber novidades do Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Criar conta"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Ou crie uma conta você mesmo"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Criar conta com"
|
||||
},
|
||||
@@ -845,21 +827,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verificar e-mail"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Inscreva-se para receber atualizações sobre o Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Agora você faz parte da incrível comunidade de criadores e exploradores que já estão criando, baixando e acompanhando mods incríveis."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Ao criar uma conta, você concorda com os <terms-link>Termos</terms-link> e a <privacy-policy-link>Política de Privacidade</privacy-policy-link> do Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Bem-vindo(a) ao Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Bem-vindo"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Editar ícone"
|
||||
},
|
||||
|
||||
@@ -263,39 +263,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Inserir código..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Esqueceste-te da palavra-passe?</forgot-password-link> • <create-account-link>Criar uma conta</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Iniciar sessão com"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Iniciar sessão"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Ou utiliza uma palavra-passe"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Criar conta"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Ao criares uma conta, concordas com os <terms-link>Termos</terms-link> e com a <privacy-policy-link>Política de Privacidade </privacy-policy-link> do Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "As palavras-passe não coincidem!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Já tens uma conta?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Receber novidades sobre o Modrinth por e-mail"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Criar conta"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Ou cria uma conta tu mesmo"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Criar conta com"
|
||||
},
|
||||
@@ -329,21 +311,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verificar E-mail"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Receber novidades sobre o Modrinth por e-mail"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Agora fazes parte da incrível comunidade de criadores e exploradores que criam, transferem e se mantêm atualizados com mods fantásticos."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Ao criares uma conta, concordas com os <terms-link>Termos</terms-link> e com a <privacy-policy-link>Política de Privacidade </privacy-policy-link> do Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Bem-vindo(a) ao Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Boas-vindas"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Editar ícone"
|
||||
},
|
||||
|
||||
@@ -257,39 +257,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Introdu codul..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Ați uitat parola?</forgot-password-link> • <create-account-link>Creați un cont</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Conectează-te cu"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Conectare"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Sau folosiți o parolă"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Creare cont"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Prin crearea unui cont, ești de acord cu <terms-link>Termenii</terms-link> și <privacy-policy-link>Politica de confidențialitate</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Parolele nu se potrivesc!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Aveți deja un cont?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Abonați-vă la actualizările despre Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Înregistrare"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Sau creează-ți singur un cont"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Înregistrare cu"
|
||||
},
|
||||
@@ -323,21 +305,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verifică email-ul"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Abonați-vă la actualizările despre Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Acum faci parte din minunata comunitate de creatori și exploratori care deja creează, descarcă și rămân la curent cu moduri extraordinare."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Prin crearea unui cont, ești de acord cu <terms-link>Termenii</terms-link> și <privacy-policy-link>Politica de confidențialitate</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Bine ați venit pe Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Bine ați venit"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Editează pictograma"
|
||||
},
|
||||
|
||||
@@ -761,39 +761,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Введите код..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Забыли пароль?</forgot-password-link> • <create-account-link>Создать аккаунт</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Войдите через"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Вход"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Или используйте пароль"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Создать аккаунт"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Регистрируясь, вы принимаете наши <terms-link>условия</terms-link> и <privacy-policy-link>политику конфиденциальности</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Пароли не совпадают!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Уже есть аккаунт?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Получать новости о Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Регистрация"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Или создайте аккаунт сами"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Зарегистрируйтесь через"
|
||||
},
|
||||
@@ -827,21 +809,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Подтверждение почты"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Получать новости о Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Теперь вы часть потрясающего сообщества авторов и игроков, которые вместе создают, находят и следят за любимыми модами."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Зарегистрировавшись, вы приняли наши <terms-link>условия</terms-link> и <privacy-policy-link>политику конфиденциальности</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Добро пожаловать!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Добро пожаловать"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Изменить иконку"
|
||||
},
|
||||
|
||||
@@ -122,39 +122,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Unesite kod..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Zaboravili ste lozinku?</forgot-password-link> • <create-account-link>Kreirajte nalog</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Prijavi se sa"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Prijavi se"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Ili koristite lozinku"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Kreirajte nalog"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Kreiranjem naloga prihvatate <terms-link>Uslove</terms-link> i <privacy-policy-link>Pravila privatnosti</privacy-policy-link> Modrintha."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Lozinke se ne poklapaju!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Već imate nalog?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Pretplatite se za novosti o Modrinthu"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registracija"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Ili kreirajte nalog sami"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Prijavite se sa"
|
||||
},
|
||||
@@ -188,18 +170,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verifikujte email"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Pretplatite se za novosti o Modrinthu"
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Kreiranjem naloga pristali ste na <terms-link>Uslove</terms-link> i <privacy-policy-link>Pravila privatnosti</privacy-policy-link> Modrintha."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Dobrodošli na Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Dobrodošli"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Ikona za izmenu"
|
||||
},
|
||||
|
||||
@@ -665,39 +665,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Ange kod..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Har du glömt lösenordet?</forgot-password-link> • <create-account-link>Skapa ett konto</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Logga in med"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Logga in"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Eller använd ett lösenord"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Skapa konto"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Genom att skapa ett konto godkänner du Modrinths <terms-link>villkor</terms-link> och <privacy-policy-link>sekretesspolicy</privacy-policy-link>."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Lösenorden stämmer inte överens!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Har du redan ett konto?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Jag vill få uppdateringar om Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Registrera dig"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Eller skapa ett konto själv"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Registrera dig med"
|
||||
},
|
||||
@@ -731,21 +713,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Verifiera e-post"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Prenumerera för uppdateringar om Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Du är nu en del av ett grymt community av skapare och utforskare som redan bygger, laddar ner och håller sig uppdaterade med fantastiska moddar."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "När du skapar ett konto godkänner du Modrinths <terms-link>villkor</terms-link> och <privacy-policy-link>sekretesspolicy</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Välkommen till Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Välkommen"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Redigera-ikon"
|
||||
},
|
||||
|
||||
@@ -455,36 +455,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "ป้อนรหัส"
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link> ลืมรหัสผ่าน? </forgot-password-link> • <create-account-link> สร้างบัญชีใหม่ </create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "ลงชื่อเข้าใช้ด้วย"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "ลงชื่อเข้าใช้"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "สร้างบัญชี"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "ในการสร้างบัญชี คุณยอมรับ <terms-link> ข้อกำหนด </terms-link> และ <privacy-policy-link> นโยบายความเป็นส่วนตัว </privacy-policy-link> ของ Modrinth"
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "รหัสผ่านไม่ตรงกัน!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "มีบัญชีอยู่แล้ว?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "สมัครรับข้อมูลการอัพเดทเกี่ยวกับ Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "ลงทะเบียน"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "หรือจะสร้างบัญชีด้วยตัวคุณเอง"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "สมัครใช้งานด้วย"
|
||||
},
|
||||
@@ -518,18 +503,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "ยืนยันอีเมล"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "สมัครรับข้อมูลอัปเดตเกี่ยวกับ Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "ตอนนี้คุณได้เป็นส่วนหนึ่งของคอมมูนิตี้สุดเจ๋งของเหล่าครีเอเตอร์และนักสำรวจ ที่กำลังสร้างสรรค์ ดาวน์โหลด และอัปเดตม็อดสุดยอดอยู่เสมอ"
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "ยินดีต้อนรับเข้าสู่ modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "ยินดีต้อนรับ"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "แก้ไขไอคอน"
|
||||
},
|
||||
|
||||
@@ -737,39 +737,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Kodu girin..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Parolanızı mı unuttunuz?</forgot-password-link> • <create-account-link>Yeni hesap oluştur</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Şununla giriş yap"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Giriş Yap"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Veya parola kullanın"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Hesap oluştur"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Bir hesap oluşturarak, Modrinth'in <terms-link>Kurallar</terms-link> ve <privacy-policy-link>Gizlilik İlkeleri </privacy-policy-link>'ni kabul etmiş bulunursunuz."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Parolalar uyuşmuyor!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Zaten bir hesabınız var mı?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Modrinth hakkında güncellemelere abone ol"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Kaydol"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Veya kendiniz hesap oluşturun"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Şunlarla giriş yapın"
|
||||
},
|
||||
@@ -803,21 +785,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "E-postayı Doğrula"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Modrinth'le ilgili güncellemelere abone ol"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Artık inanılmaz modlar yapan, indiren ve geride kalmayan yaratıcıların ve kaşiflerin havalı topluluğunun bir parçasısın."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Bir hesap oluşturarak, Modrinth'in <terms-link>Şartlar</terms-link>'ını ve <privacy-policy-link>Gizlilik İlkeleri</privacy-policy-link>'ni kabul etmiş sayılırsınız."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Modrinth'e hoş geldiniz!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Hoş geldiniz"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Simgeyi düzenle"
|
||||
},
|
||||
|
||||
@@ -569,39 +569,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Уведіть код…"
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Забули пароль?</forgot-password-link> • <create-account-link>Створити обліковий запис</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Увійдіть за допомогою"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Вхід"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Або ж з паролем"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Створити обліковий запис"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Створивши обліковий запис, ви погоджуєтеся з <terms-link>Умовами використання</terms-link> та <privacy-policy-link>Політикою конфіденційности</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Паролі не збігаються!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Уже маєте обліковий запис?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Підписатися на оновлення Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Реєстрація"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Або зробіть обліковий запис самотужки"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Реєстрація за допомогою"
|
||||
},
|
||||
@@ -635,21 +617,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Підтвердити електронну пошту"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Підписатися на оновлення Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Ви тепер частина чудової спільноти авторів і дослідників, що вже створюють, завантажують та оновлюють дивовижні моди."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Створивши обліковий запис, ви погодилися з <terms-link>Умовами використання</terms-link> та <privacy-policy-link>Політикою конфіденційности</privacy-policy-link> Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Вітаємо в Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Вітаємо"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Редагувати значок"
|
||||
},
|
||||
|
||||
@@ -737,39 +737,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Nhập mã..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Quên mật khẩu à?</forgot-password-link> • <create-account-link>Tạo tài khoản</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Đăng nhập bằng"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Đăng nhập"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Hoặc dùng mật khẩu"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Tạo tài khoản"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "Khi bạn tạo tài khoản, bạn đồng ý với <terms-link>Điều khoản sử dụng</terms-link> và <privacy-policy-link>Chính sách quyền riêng tư</privacy-policy-link> của Modrinth."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "Mật khẩu không khớp!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "Đã có tài khoản rồi à?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "Đăng kí để nhận cập nhật về Modrinth"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "Đăng ký"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "Hoặc tự tạo một tài khoản"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "Đăng ký bằng"
|
||||
},
|
||||
@@ -803,21 +785,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "Xác nhận email"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "Đăng kí để nhận cập nhật về Modrinth"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Bạn hiện đã là một phần của cộng đồng tuyệt vời gồm những người sáng tạo và khám phá, những người đang xây dựng, tải xuống và luôn cập nhật với các mod tuyệt vời."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "Khi bạn tạo tài khoản, bạn đồng ý với <terms-link>Điều khoản sử dụng</terms-link> và <privacy-policy-link>Chính sách quyền riêng tư</privacy-policy-link> của Modrinth."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "Chào mừng đến với Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Chào mừng"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Sửa biểu tượng"
|
||||
},
|
||||
|
||||
@@ -734,39 +734,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "输入验证码…"
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>忘记密码?</forgot-password-link> • <create-account-link>创建新账户</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "通过以下方式登录"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "登录"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "或使用密码"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "创建账户"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "创建账户即表示你同意 Modrinth 的<terms-link>使用条款</terms-link>和<privacy-policy-link>隐私政策</privacy-policy-link>。"
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "密码不一致!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "已有账户?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "订阅有关 Modrinth 更新的邮件"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "注册"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "或者自己创建一个账户"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "通过以下方式注册"
|
||||
},
|
||||
@@ -800,21 +782,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "验证电子邮箱"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "订阅有关 Modrinth 的更新邮件"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "你现在是这个超棒社区的一员了——与创作者和社区玩家们一起创作、探索、获取模组并持续追踪最新的精彩模组动态吧。"
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "创建账户即表示你已同意 Modrinth 的<terms-link>使用条款</terms-link>和<privacy-policy-link>隐私政策</privacy-policy-link>。"
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "欢迎来到 Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "欢迎"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "编辑图标"
|
||||
},
|
||||
|
||||
@@ -779,39 +779,21 @@
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "輸入驗證碼..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>忘記密碼?</forgot-password-link> • <create-account-link>建立帳號</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "使用以下方式登入"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "登入"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "或使用密碼"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "建立帳號"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "建立帳號即表示您同意 Modrinth 的《<terms-link>使用條款</terms-link>》及《<privacy-policy-link>隱私權政策</privacy-policy-link>》。"
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "密碼不相符!"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "已經有帳號了?"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "訂閱 Modrinth 的最新消息"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "註冊"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "或自行建立帳號"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "使用以下方式註冊"
|
||||
},
|
||||
@@ -845,21 +827,6 @@
|
||||
"auth.verify-email.title": {
|
||||
"message": "驗證電子郵件"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "訂閱 Modrinth 的最新消息"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "你現在已加入由創作者與探索者組成的精彩社群,大家都在積極打造、下載並持續掌握優質模組的最新動態。"
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "建立帳號即表示您已同意 Modrinth 的《<terms-link>使用條款</terms-link>》及《<privacy-policy-link>隱私權政策</privacy-policy-link>》。"
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "歡迎使用 Modrinth!"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "歡迎"
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "編輯圖示"
|
||||
},
|
||||
|
||||
@@ -1125,7 +1125,7 @@ import MessageBanner from '~/components/ui/MessageBanner.vue'
|
||||
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
|
||||
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
|
||||
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
|
||||
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
|
||||
import { versionQueryOptions } from '~/composables/queries/version'
|
||||
|
||||
@@ -348,7 +348,7 @@ import {
|
||||
import { useTemplateRef, watch } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -502,7 +502,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { onServerPrefetch } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { projectQueryOptions, STALE_TIME } from '~/composables/queries/project'
|
||||
import { versionQueryOptions } from '~/composables/queries/version'
|
||||
import { createDataPackVersion } from '~/helpers/package.js'
|
||||
|
||||
@@ -269,7 +269,7 @@ import {
|
||||
import { onMounted, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -8,7 +8,9 @@ useSeoMeta({
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage class="auth-container universal-card" />
|
||||
<div class="grid min-h-[calc(100vh-14.5rem)] place-items-center px-2.5 pb-20 pt-10">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -31,19 +33,17 @@ useSeoMeta({
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-container .btn-wrapper :is(a, button) {
|
||||
.auth-container .btn {
|
||||
font-weight: 700;
|
||||
min-height: 2.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.centered-btn :is(a, button),
|
||||
.centered-btn:is(a, button) {
|
||||
.centered-btn {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.continue-btn :is(a, button) svg,
|
||||
.continue-btn:is(a, button) svg {
|
||||
.btn.continue-btn svg {
|
||||
margin: 0 0 0 0.5rem;
|
||||
}
|
||||
|
||||
@@ -54,19 +54,19 @@ useSeoMeta({
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.third-party a {
|
||||
.third-party .btn {
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.third-party a svg {
|
||||
.third-party .btn svg {
|
||||
margin-right: var(--gap-sm);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 25.5rem) {
|
||||
.third-party a {
|
||||
.third-party .btn {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="error" class="oauth-items">
|
||||
<div>
|
||||
<h1>{{ formatMessage(commonMessages.errorLabel) }}</h1>
|
||||
</div>
|
||||
<p>
|
||||
<span>{{ error.data?.error }}: </span>
|
||||
{{ error.data?.description }}
|
||||
<div
|
||||
class="universal-card flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
|
||||
>
|
||||
<div v-if="authorizationError" class="flex flex-col gap-2.5">
|
||||
<h1 class="m-0 mx-auto text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.errorTitle) }}
|
||||
</h1>
|
||||
<p class="m-0">
|
||||
<span>{{ authorizationError.error }}: </span>
|
||||
{{ authorizationError.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="app && createdBy && authorizationData" class="oauth-items">
|
||||
<div class="connected-items">
|
||||
<div class="profile-pics">
|
||||
<div v-else-if="app && createdBy && authorizationData" class="flex flex-col gap-6">
|
||||
<div class="mt-4 flex items-center justify-center">
|
||||
<div class="flex w-full flex-row items-center justify-evenly">
|
||||
<Avatar size="md" :src="app.icon_url" />
|
||||
<!-- <img class="profile-pic" :src="app.icon_url" alt="User profile picture" /> -->
|
||||
<div class="connection-indicator">→</div>
|
||||
<Avatar size="md" circle :src="auth.user.avatar_url" />
|
||||
<div class="flex select-none items-center justify-center text-[2rem] text-primary">→</div>
|
||||
<Avatar size="md" circle :src="auth.user?.avatar_url" />
|
||||
<!-- <img class="profile-pic" :src="auth.user.avatar_url" alt="User profile picture" /> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">
|
||||
<h1>{{ formatMessage(messages.title, { appName: app.name }) }}</h1>
|
||||
<div class="mx-auto">
|
||||
<h1 class="mb-0 ml-0 mr-0 mt-0 text-xl text-contrast">
|
||||
{{ formatMessage(messages.title, { appName: app.name }) }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="auth-info">
|
||||
<div class="scope-heading">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="mb-3">
|
||||
<IntlFormatted
|
||||
:message-id="messages.appInfo"
|
||||
:values="{
|
||||
@@ -43,11 +47,11 @@
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
<div class="scope-items">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="scopeItem in scopeDefinitions" :key="scopeItem">
|
||||
<div class="scope-item">
|
||||
<div class="scope-icon">
|
||||
<CheckIcon />
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="grid h-5 min-h-5 w-5 min-w-5 place-content-center rounded-full bg-green">
|
||||
<CheckIcon class="text-sm text-black" />
|
||||
</div>
|
||||
{{ scopeItem }}
|
||||
</div>
|
||||
@@ -68,11 +72,11 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="redirection-notice">
|
||||
<p class="redirect-instructions">
|
||||
<div class="flex flex-col gap-2 text-center">
|
||||
<p class="m-0 text-sm">
|
||||
<IntlFormatted :message-id="messages.redirectUrl" :values="{ url: redirectUri }">
|
||||
<template #redirect-url="{ children }">
|
||||
<span class="redirect-url">
|
||||
<span class="font-bold">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
@@ -83,7 +87,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
@@ -98,10 +102,35 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
import type { LocationQueryValue } from 'vue-router'
|
||||
|
||||
import { useAuth } from '@/composables/auth.js'
|
||||
import { useScopes } from '@/composables/auth/scopes.ts'
|
||||
|
||||
interface ApiErrorShape {
|
||||
data?: {
|
||||
description?: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
const getQueryString = (
|
||||
value: LocationQueryValue | LocationQueryValue[] | null | undefined,
|
||||
): string => {
|
||||
const firstValue = Array.isArray(value) ? value[0] : value
|
||||
return typeof firstValue === 'string' ? firstValue : ''
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
const apiError = error as ApiErrorShape
|
||||
if (typeof apiError?.data?.description === 'string') {
|
||||
return apiError.data.description
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -124,6 +153,10 @@ const messages = defineMessages({
|
||||
id: 'auth.authorize.error.no-redirect-url',
|
||||
defaultMessage: 'No redirect location found in response',
|
||||
},
|
||||
missingParametersError: {
|
||||
id: 'auth.authorize.error.missing-parameters',
|
||||
defaultMessage: 'Missing required OAuth query parameters.',
|
||||
},
|
||||
redirectUrl: {
|
||||
id: 'auth.authorize.redirect-url',
|
||||
defaultMessage: 'You will be redirected to <redirect-url>{url}</redirect-url>',
|
||||
@@ -132,33 +165,35 @@ const messages = defineMessages({
|
||||
id: 'auth.authorize.authorize-app-name',
|
||||
defaultMessage: 'Authorize {appName}',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'auth.authorize.errro-title',
|
||||
defaultMessage: 'An Error Occured',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useNativeRoute()
|
||||
const auth = await useAuth()
|
||||
const { scopesToDefinitions } = useScopes()
|
||||
|
||||
const clientId = router.query?.client_id || false
|
||||
const redirectUri = router.query?.redirect_uri || false
|
||||
const scope = router.query?.scope || false
|
||||
const state = router.query?.state || false
|
||||
const clientId = computed(() => getQueryString(router.query.client_id))
|
||||
const redirectUri = computed(() => getQueryString(router.query.redirect_uri))
|
||||
const scope = computed(() => getQueryString(router.query.scope))
|
||||
const state = computed(() => getQueryString(router.query.state))
|
||||
const hasRequiredParams = computed(() => !!clientId.value && !!redirectUri.value && !!scope.value)
|
||||
|
||||
const getFlowIdAuthorization = async () => {
|
||||
const params = {
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope,
|
||||
}
|
||||
if (state) {
|
||||
params.state = state
|
||||
}
|
||||
|
||||
const authorization = await client.labrinth.oauth_internal.authorize(params)
|
||||
const authorization = await client.labrinth.oauth_internal.authorize({
|
||||
client_id: clientId.value,
|
||||
redirect_uri: redirectUri.value,
|
||||
scope: scope.value,
|
||||
...(state.value ? { state: state.value } : {}),
|
||||
})
|
||||
|
||||
if (typeof authorization === 'string') {
|
||||
await navigateTo(authorization, {
|
||||
external: true,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
return authorization
|
||||
@@ -170,27 +205,60 @@ const {
|
||||
error,
|
||||
suspense: authSusp,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['authorization', clientId, redirectUri, scope, state]),
|
||||
queryKey: computed(() => [
|
||||
'authorization',
|
||||
clientId.value,
|
||||
redirectUri.value,
|
||||
scope.value,
|
||||
state.value,
|
||||
]),
|
||||
queryFn: getFlowIdAuthorization,
|
||||
enabled: computed(() => !!clientId && !!redirectUri && !!scope),
|
||||
enabled: hasRequiredParams,
|
||||
})
|
||||
|
||||
const authorizationError = computed(() => {
|
||||
if (!hasRequiredParams.value) {
|
||||
return {
|
||||
error: 'invalid_request',
|
||||
description: formatMessage(messages.missingParametersError),
|
||||
}
|
||||
}
|
||||
|
||||
const apiError = (error.value as ApiErrorShape | null)?.data
|
||||
if (apiError) {
|
||||
return apiError
|
||||
}
|
||||
|
||||
if (error.value) {
|
||||
return {
|
||||
error: 'server_error',
|
||||
description: getErrorMessage(error.value),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const { data: app, suspense: appSusp } = useQuery({
|
||||
queryKey: computed(() => ['oauth/app', clientId]),
|
||||
queryFn: () => client.labrinth.oauth_internal.getApp(clientId),
|
||||
enabled: computed(() => !!clientId),
|
||||
queryKey: computed(() => ['oauth/app', clientId.value]),
|
||||
queryFn: () => client.labrinth.oauth_internal.getApp(clientId.value),
|
||||
enabled: computed(() => !!clientId.value),
|
||||
})
|
||||
|
||||
const { data: createdBy, suspense: userSusp } = useQuery({
|
||||
queryKey: computed(() => ['user', app.value?.created_by]),
|
||||
queryFn: () => client.labrinth.users_v2.get(app.value.created_by),
|
||||
queryFn: () => {
|
||||
if (!app.value?.created_by) {
|
||||
throw new Error('Missing OAuth app creator')
|
||||
}
|
||||
|
||||
return client.labrinth.users_v2.get(app.value.created_by)
|
||||
},
|
||||
enabled: computed(() => !!app.value?.created_by),
|
||||
})
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
await authSusp()
|
||||
await appSusp()
|
||||
await userSusp()
|
||||
await Promise.allSettled([authSusp(), appSusp(), userSusp()])
|
||||
})
|
||||
|
||||
const scopeDefinitions = computed(() =>
|
||||
@@ -199,6 +267,10 @@ const scopeDefinitions = computed(() =>
|
||||
|
||||
const onAuthorize = async () => {
|
||||
try {
|
||||
if (!authorizationData.value?.flow_id) {
|
||||
throw new Error(formatMessage(messages.noRedirectUrlError))
|
||||
}
|
||||
|
||||
const res = await client.labrinth.oauth_internal.accept({
|
||||
flow: authorizationData.value.flow_id,
|
||||
})
|
||||
@@ -214,7 +286,7 @@ const onAuthorize = async () => {
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -222,6 +294,10 @@ const onAuthorize = async () => {
|
||||
|
||||
const onReject = async () => {
|
||||
try {
|
||||
if (!authorizationData.value?.flow_id) {
|
||||
throw new Error(formatMessage(messages.noRedirectUrlError))
|
||||
}
|
||||
|
||||
const res = await client.labrinth.oauth_internal.reject({
|
||||
flow: authorizationData.value.flow_id,
|
||||
})
|
||||
@@ -237,7 +313,7 @@ const onReject = async () => {
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -247,130 +323,3 @@ definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.oauth-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xl);
|
||||
}
|
||||
|
||||
.scope-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.scope-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.scope-icon {
|
||||
display: flex;
|
||||
|
||||
color: var(--color-raised-bg);
|
||||
background-color: var(--color-green);
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
padding: var(--gap-xs);
|
||||
}
|
||||
.title {
|
||||
margin-inline: auto;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.redirection-notice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
text-align: center;
|
||||
|
||||
.redirect-instructions {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.redirect-url {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.wide-button {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-xs);
|
||||
justify-content: center;
|
||||
}
|
||||
.auth-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.scope-heading {
|
||||
margin-bottom: var(--gap-sm);
|
||||
}
|
||||
|
||||
.profile-pics {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.connection-indicator {
|
||||
// Make sure the text sits in the middle and is centered.
|
||||
// Make the text large, and make sure it's not selectable.
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
user-select: none;
|
||||
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 50%;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.dotted-border-line {
|
||||
width: 75%;
|
||||
border: 0.1rem dashed var(--color-divider);
|
||||
}
|
||||
|
||||
.connected-items {
|
||||
// Display dotted-border-line under profile-pics and centered behind them
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 1rem;
|
||||
|
||||
// Display profile-pics on top of dotted-border-line
|
||||
.profile-pics {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Display dotted-border-line behind profile-pics
|
||||
.dotted-border-line {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div v-if="subtleLauncherRedirectUri">
|
||||
<iframe
|
||||
:src="subtleLauncherRedirectUri"
|
||||
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
||||
></iframe>
|
||||
</div>
|
||||
<CreateAccountView
|
||||
v-else
|
||||
v-model:date-of-birth="dateOfBirth"
|
||||
v-model:username="username"
|
||||
v-model:token="token"
|
||||
v-model:subscribe="subscribe"
|
||||
:globals="globals"
|
||||
:requires-dob="requiresDob"
|
||||
:on-complete-sign-up="completeOAuthSignUp"
|
||||
:on-set-captcha-ref="setCaptchaRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { LocationQueryValue } from 'vue-router'
|
||||
|
||||
import CreateAccountView from '@/components/ui/auth/CreateAccount.vue'
|
||||
import { getLauncherRedirectUrl, promotePendingSignInOAuthProvider } from '@/composables/auth.ts'
|
||||
|
||||
interface AuthGlobalsResponse {
|
||||
captcha_enabled?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ApiErrorShape {
|
||||
data?: {
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
const getQueryString = (
|
||||
value: LocationQueryValue | LocationQueryValue[] | null | undefined,
|
||||
): string => {
|
||||
const firstValue = Array.isArray(value) ? value[0] : value
|
||||
return typeof firstValue === 'string' ? firstValue : ''
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
const apiError = error as ApiErrorShape
|
||||
if (typeof apiError?.data?.description === 'string') {
|
||||
return apiError.data.description
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const route = useNativeRoute()
|
||||
const auth = await useAuth()
|
||||
|
||||
const messages = defineMessages({
|
||||
createAccountTitle: {
|
||||
id: 'auth.create-account.page-title',
|
||||
defaultMessage: 'Create Account',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
title() {
|
||||
return `${formatMessage(messages.createAccountTitle)} - Modrinth`
|
||||
},
|
||||
})
|
||||
|
||||
const requiresDob = computed(() => {
|
||||
const raw = route.query.requires_dob
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
|
||||
if (!value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return value === 'true' || value === '1'
|
||||
})
|
||||
|
||||
const oauthFlowState = computed(() => {
|
||||
const state = route.query.state
|
||||
const value = Array.isArray(state) ? state[0] : state
|
||||
return typeof value === 'string' ? value : ''
|
||||
})
|
||||
|
||||
const defaultUsername = computed(() => {
|
||||
const queryUsername = route.query.username
|
||||
const value = Array.isArray(queryUsername) ? queryUsername[0] : queryUsername
|
||||
return typeof value === 'string' && value.length > 0 ? value : ''
|
||||
})
|
||||
|
||||
const dateOfBirth = ref('')
|
||||
const username = ref(defaultUsername.value)
|
||||
const token = ref('')
|
||||
const subscribe = ref(false)
|
||||
const subtleLauncherRedirectUri = ref<string>()
|
||||
|
||||
const captcha = ref<{ reset?: () => void } | null>(null)
|
||||
const setCaptchaRef = (captchaRef: unknown) => {
|
||||
captcha.value = (captchaRef as { reset?: () => void } | null) ?? null
|
||||
}
|
||||
|
||||
const { data: globals } = useQuery<AuthGlobalsResponse>({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await client.labrinth.globals_internal.get()
|
||||
} catch (err) {
|
||||
console.error('Error fetching globals:', err)
|
||||
return { captcha_enabled: true, tax_compliance_thresholds: {} }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
async function completeOAuthSignUp() {
|
||||
startLoading()
|
||||
try {
|
||||
if (!oauthFlowState.value) {
|
||||
throw new Error('Missing OAuth flow state')
|
||||
}
|
||||
|
||||
const res = await client.labrinth.auth_v2.createOAuthAccount({
|
||||
username: username.value.trim() || defaultUsername.value,
|
||||
state: oauthFlowState.value,
|
||||
challenge: token.value,
|
||||
sign_up_newsletter: subscribe.value,
|
||||
})
|
||||
|
||||
await finishSignIn(res.session)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset?.()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function finishSignIn(sessionToken?: string | null) {
|
||||
if (route.query.launcher) {
|
||||
let token = sessionToken
|
||||
if (!token) {
|
||||
token = auth.value.token
|
||||
}
|
||||
|
||||
promotePendingSignInOAuthProvider()
|
||||
|
||||
const redirectUrl = `${getLauncherRedirectUrl(route)}/?code=${token}`
|
||||
|
||||
if (redirectUrl.startsWith('https://launcher-files.modrinth.com/')) {
|
||||
await navigateTo(redirectUrl, {
|
||||
external: true,
|
||||
})
|
||||
} else {
|
||||
subtleLauncherRedirectUri.value = redirectUrl
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (sessionToken) {
|
||||
await useAuth(sessionToken)
|
||||
await useUser()
|
||||
queryClient.clear()
|
||||
|
||||
promotePendingSignInOAuthProvider()
|
||||
}
|
||||
|
||||
if (route.query.redirect) {
|
||||
const redirect = decodeURIComponent(getQueryString(route.query.redirect))
|
||||
await navigateTo(redirect, {
|
||||
replace: true,
|
||||
})
|
||||
} else {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ formatMessage(messages.longTitle) }}</h1>
|
||||
<section class="auth-form">
|
||||
<template v-if="step === 'choose_method'">
|
||||
<p>
|
||||
{{ formatMessage(methodChoiceMessages.description) }}
|
||||
</p>
|
||||
<div
|
||||
class="universal-card flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
|
||||
>
|
||||
<h1 class="m-0 mx-auto text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.longTitle) }}
|
||||
</h1>
|
||||
<template v-if="step === 'choose_method'">
|
||||
<Admonition :type="'info'">
|
||||
<template #header>
|
||||
<div class="-mb-2 flex flex-col gap-1.5 font-normal leading-normal">
|
||||
<div>
|
||||
{{ formatMessage(methodChoiceMessages.description) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Admonition>
|
||||
|
||||
<label for="email" hidden>
|
||||
{{ formatMessage(commonMessages.emailUsernameLabel) }}
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label class="text-md font-semibold text-contrast" for="email">
|
||||
{{ formatMessage(commonMessages.emailLabel) }}
|
||||
</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
@@ -19,22 +29,37 @@
|
||||
:placeholder="formatMessage(commonMessages.emailLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HCaptcha v-if="globals?.captcha_enabled" ref="captcha" v-model="token" />
|
||||
<div v-if="globals?.captcha_enabled" class="flex flex-col gap-2.5">
|
||||
<label class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.securityCheckLabel)
|
||||
}}</label>
|
||||
<HCaptcha ref="captcha" v-model="token" />
|
||||
</div>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="mx-auto"
|
||||
:disabled="globals?.captcha_enabled ? !token : false"
|
||||
@click="recovery"
|
||||
>
|
||||
<SendIcon /> {{ formatMessage(methodChoiceMessages.action) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else-if="step === 'passed_challenge'">
|
||||
<p>{{ formatMessage(postChallengeMessages.description) }}</p>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full"
|
||||
:disabled="(globals?.captcha_enabled ? !token : false) || !email"
|
||||
@click="recovery"
|
||||
>
|
||||
<SendIcon /> {{ formatMessage(methodChoiceMessages.action) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else-if="step === 'passed_challenge'">
|
||||
<Admonition :type="'info'">
|
||||
<template #header>
|
||||
<div class="-mb-2 flex flex-col gap-1.5 font-normal leading-normal">
|
||||
<div>
|
||||
{{ formatMessage(postChallengeMessages.description) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Admonition>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
@@ -60,17 +85,18 @@
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button class="auth-form__input continue-btn" @click="changePassword">
|
||||
<button class="!w-full" @click="changePassword">
|
||||
{{ formatMessage(postChallengeMessages.action) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { KeyIcon, MailIcon, SendIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
@@ -80,8 +106,41 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import type { LocationQueryValue } from 'vue-router'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
|
||||
interface AuthGlobalsResponse {
|
||||
captcha_enabled?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ApiErrorShape {
|
||||
data?: {
|
||||
description?: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
type ResetPasswordStep = 'choose_method' | 'passed_challenge'
|
||||
|
||||
const getQueryString = (
|
||||
value: LocationQueryValue | LocationQueryValue[] | null | undefined,
|
||||
): string => {
|
||||
const firstValue = Array.isArray(value) ? value[0] : value
|
||||
return typeof firstValue === 'string' ? firstValue : ''
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
const apiError = error as ApiErrorShape
|
||||
if (typeof apiError?.data?.description === 'string') {
|
||||
return apiError.data.description
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -149,6 +208,10 @@ const messages = defineMessages({
|
||||
id: 'auth.reset-password.title.long',
|
||||
defaultMessage: 'Reset your password',
|
||||
},
|
||||
securityCheckLabel: {
|
||||
id: 'auth.create-account.security-check.label',
|
||||
defaultMessage: 'Security check',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
@@ -162,15 +225,15 @@ if (auth.value.user) {
|
||||
|
||||
const route = useNativeRoute()
|
||||
|
||||
const step = ref('choose_method')
|
||||
const step = ref<ResetPasswordStep>('choose_method')
|
||||
|
||||
if (route.query.flow) {
|
||||
step.value = 'passed_challenge'
|
||||
}
|
||||
|
||||
const captcha = ref()
|
||||
const captcha = ref<{ reset?: () => void } | null>(null)
|
||||
|
||||
const { data: globals } = useQuery({
|
||||
const { data: globals } = useQuery<AuthGlobalsResponse>({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
@@ -201,10 +264,10 @@ async function recovery() {
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
captcha.value?.reset?.()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
@@ -217,7 +280,7 @@ async function changePassword() {
|
||||
try {
|
||||
await client.labrinth.auth_v2.changePassword({
|
||||
new_password: newPassword.value,
|
||||
flow: route.query.flow,
|
||||
flow: getQueryString(route.query.flow),
|
||||
})
|
||||
|
||||
addNotification({
|
||||
@@ -229,10 +292,10 @@ async function changePassword() {
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
captcha.value?.reset?.()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
@@ -1,171 +1,72 @@
|
||||
<template>
|
||||
<div v-if="subtleLauncherRedirectUri">
|
||||
<iframe
|
||||
:src="subtleLauncherRedirectUri"
|
||||
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
||||
></iframe>
|
||||
</div>
|
||||
<div v-else>
|
||||
<template v-if="flow && !subtleLauncherRedirectUri">
|
||||
<label for="two-factor-code">
|
||||
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.twoFactorCodeLabelDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCode"
|
||||
:maxlength="11"
|
||||
inputmode="numeric"
|
||||
:placeholder="formatMessage(messages.twoFactorCodeInputPlaceholder)"
|
||||
autocomplete="one-time-code"
|
||||
@keyup.enter="begin2FASignIn"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button class="continue-btn" @click="begin2FASignIn">
|
||||
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>{{ formatMessage(messages.signInWithLabel) }}</h1>
|
||||
|
||||
<section class="third-party">
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('discord', redirectTarget)">
|
||||
<DiscordColorIcon />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('github', redirectTarget)">
|
||||
<GitHubColorIcon />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('microsoft', redirectTarget)">
|
||||
<MicrosoftColorIcon />
|
||||
<span>Microsoft</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('google', redirectTarget)">
|
||||
<GoogleColorIcon />
|
||||
<span>Google</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('steam', redirectTarget)">
|
||||
<SteamColorIcon />
|
||||
<span>Steam</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('gitlab', redirectTarget)">
|
||||
<GitLabColorIcon />
|
||||
<span>GitLab</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
|
||||
<h1>{{ formatMessage(messages.usePasswordLabel) }}</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailUsernameLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="email"
|
||||
:icon="MailIcon"
|
||||
type="text"
|
||||
inputmode="email"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.emailUsernameLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="password"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<HCaptcha v-if="globals?.captcha_enabled" ref="captcha" v-model="token" />
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="continue-btn centered-btn"
|
||||
:disabled="globals?.captcha_enabled ? !token : false"
|
||||
@click="beginPasswordSignIn()"
|
||||
>
|
||||
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<div class="auth-form__additional-options">
|
||||
<IntlFormatted :message-id="messages.additionalOptionsLabel">
|
||||
<template #forgot-password-link="{ children }">
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/reset-password',
|
||||
query: route.query,
|
||||
}"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #create-account-link="{ children }">
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-up',
|
||||
query: route.query,
|
||||
}"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
<SignInView
|
||||
v-model:email="email"
|
||||
v-model:password="password"
|
||||
v-model:token="token"
|
||||
v-model:two-factor-code="twoFactorCode"
|
||||
:subtle-launcher-redirect-uri="subtleLauncherRedirectUri"
|
||||
:flow="flow"
|
||||
:redirect-target="redirectTarget"
|
||||
:route-query="route.query"
|
||||
:globals="globals"
|
||||
:on-password-sign-in="beginPasswordSignIn"
|
||||
:on-two-factor-sign-in="begin2FASignIn"
|
||||
:on-passkey-sign-in="beginPasskeySignin"
|
||||
:on-set-captcha-ref="setCaptchaRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { LocationQueryValue } from 'vue-router'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import { getAuthUrl, getLauncherRedirectUrl } from '@/composables/auth.js'
|
||||
import SignInView from '@/components/ui/auth/SignIn.vue'
|
||||
import {
|
||||
getLauncherRedirectUrl,
|
||||
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
promotePendingSignInOAuthProvider,
|
||||
} from '@/composables/auth.ts'
|
||||
import { getPasskeyCredential } from '@/helpers/passkey.ts'
|
||||
|
||||
type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft' | 'passkey'
|
||||
|
||||
interface AuthGlobalsResponse {
|
||||
captcha_enabled?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ApiErrorShape {
|
||||
data?: {
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
const getQueryString = (
|
||||
value: LocationQueryValue | LocationQueryValue[] | null | undefined,
|
||||
): string => {
|
||||
const firstValue = Array.isArray(value) ? value[0] : value
|
||||
return typeof firstValue === 'string' ? firstValue : ''
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
const apiError = error as ApiErrorShape
|
||||
if (typeof apiError?.data?.description === 'string') {
|
||||
return apiError.data.description
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -173,35 +74,10 @@ const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
additionalOptionsLabel: {
|
||||
id: 'auth.sign-in.additional-options',
|
||||
defaultMessage:
|
||||
'<forgot-password-link>Forgot password?</forgot-password-link> • <create-account-link>Create an account</create-account-link>',
|
||||
},
|
||||
signInWithLabel: {
|
||||
id: 'auth.sign-in.sign-in-with',
|
||||
defaultMessage: 'Sign in with',
|
||||
},
|
||||
signInTitle: {
|
||||
id: 'auth.sign-in.title',
|
||||
defaultMessage: 'Sign In',
|
||||
},
|
||||
twoFactorCodeInputPlaceholder: {
|
||||
id: 'auth.sign-in.2fa.placeholder',
|
||||
defaultMessage: 'Enter code...',
|
||||
},
|
||||
twoFactorCodeLabel: {
|
||||
id: 'auth.sign-in.2fa.label',
|
||||
defaultMessage: 'Enter two-factor code',
|
||||
},
|
||||
twoFactorCodeLabelDescription: {
|
||||
id: 'auth.sign-in.2fa.description',
|
||||
defaultMessage: 'Please enter a two-factor code to proceed.',
|
||||
},
|
||||
usePasswordLabel: {
|
||||
id: 'auth.sign-in.use-password',
|
||||
defaultMessage: 'Or use a password',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
@@ -212,11 +88,35 @@ useHead({
|
||||
|
||||
const auth = await useAuth()
|
||||
const route = useNativeRoute()
|
||||
const pendingSignInOAuthProvider = useStorage<AuthProvider | null>(
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
const lastSignInOAuthProvider = useStorage<AuthProvider | null>(
|
||||
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
|
||||
const redirectTarget = route.query.redirect || ''
|
||||
const subtleLauncherRedirectUri = ref()
|
||||
if (route.query.state !== undefined) {
|
||||
await navigateTo(
|
||||
{
|
||||
path: '/auth/create/oauth',
|
||||
query: route.query,
|
||||
},
|
||||
{
|
||||
replace: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (route.query.code && !route.fullPath.includes('new_account=true')) {
|
||||
const redirectTarget = getQueryString(route.query.redirect)
|
||||
const subtleLauncherRedirectUri = ref<string>()
|
||||
|
||||
if (route.query.code) {
|
||||
await finishSignIn()
|
||||
}
|
||||
|
||||
@@ -224,9 +124,12 @@ if (auth.value.user) {
|
||||
await finishSignIn()
|
||||
}
|
||||
|
||||
const captcha = ref()
|
||||
const captcha = ref<{ reset?: () => void } | null>(null)
|
||||
const setCaptchaRef = (captchaRef: unknown) => {
|
||||
captcha.value = (captchaRef as { reset?: () => void } | null) ?? null
|
||||
}
|
||||
|
||||
const { data: globals } = useQuery({
|
||||
const { data: globals } = useQuery<AuthGlobalsResponse>({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
@@ -242,9 +145,11 @@ const email = ref('')
|
||||
const password = ref('')
|
||||
const token = ref('')
|
||||
|
||||
const flow = ref(route.query.flow)
|
||||
const flow = ref(getQueryString(route.query.flow))
|
||||
|
||||
async function beginPasswordSignIn() {
|
||||
pendingSignInOAuthProvider.value = null
|
||||
lastSignInOAuthProvider.value = null
|
||||
startLoading()
|
||||
try {
|
||||
const res = await client.labrinth.auth_v2.login({
|
||||
@@ -261,41 +166,68 @@ async function beginPasswordSignIn() {
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
captcha.value?.reset?.()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const twoFactorCode = ref(null)
|
||||
const twoFactorCode = ref('')
|
||||
async function begin2FASignIn() {
|
||||
startLoading()
|
||||
try {
|
||||
const res = await client.labrinth.auth_v2.login2FA({
|
||||
flow: flow.value,
|
||||
code: twoFactorCode.value ? twoFactorCode.value.toString() : twoFactorCode.value,
|
||||
code: twoFactorCode.value,
|
||||
})
|
||||
|
||||
await finishSignIn(res.session)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
captcha.value?.reset?.()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function finishSignIn(token) {
|
||||
async function beginPasskeySignin() {
|
||||
startLoading()
|
||||
try {
|
||||
const start = await client.labrinth.auth_v2.authenticatePasskeyStart()
|
||||
|
||||
const credential = await getPasskeyCredential(start.options.publicKey)
|
||||
|
||||
const result = await client.labrinth.auth_v2.authenticatePasskeyFinish({
|
||||
flow: start.flow,
|
||||
credential,
|
||||
})
|
||||
|
||||
pendingSignInOAuthProvider.value = 'passkey'
|
||||
await finishSignIn(result.session)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
async function finishSignIn(sessionToken?: string | null) {
|
||||
if (route.query.launcher) {
|
||||
let token = sessionToken
|
||||
if (!token) {
|
||||
token = auth.value.token
|
||||
}
|
||||
|
||||
promotePendingSignInOAuthProvider()
|
||||
|
||||
const redirectUrl = `${getLauncherRedirectUrl(route)}/?code=${token}`
|
||||
|
||||
if (redirectUrl.startsWith('https://launcher-files.modrinth.com/')) {
|
||||
@@ -316,14 +248,16 @@ async function finishSignIn(token) {
|
||||
return
|
||||
}
|
||||
|
||||
if (token) {
|
||||
await useAuth(token)
|
||||
if (sessionToken) {
|
||||
await useAuth(sessionToken)
|
||||
await useUser()
|
||||
queryClient.clear()
|
||||
}
|
||||
|
||||
promotePendingSignInOAuthProvider()
|
||||
|
||||
if (route.query.redirect) {
|
||||
const redirect = decodeURIComponent(route.query.redirect)
|
||||
const redirect = decodeURIComponent(getQueryString(route.query.redirect))
|
||||
await navigateTo(redirect, {
|
||||
replace: true,
|
||||
})
|
||||
|
||||
@@ -1,171 +1,81 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ formatMessage(messages.signUpWithTitle) }}</h1>
|
||||
|
||||
<section class="third-party">
|
||||
<ButtonStyled>
|
||||
<a class="discord-btn" :href="getAuthUrl('discord', redirectTarget)">
|
||||
<DiscordColorIcon />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('github', redirectTarget)">
|
||||
<GitHubColorIcon />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('microsoft', redirectTarget)">
|
||||
<MicrosoftColorIcon />
|
||||
<span>Microsoft</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('google', redirectTarget)">
|
||||
<GoogleColorIcon />
|
||||
<span>Google</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('steam', redirectTarget)">
|
||||
<SteamColorIcon />
|
||||
<span>Steam</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a :href="getAuthUrl('gitlab', redirectTarget)">
|
||||
<GitLabColorIcon />
|
||||
<span>GitLab</span>
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
|
||||
<h1>{{ formatMessage(messages.createAccountTitle) }}</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<label for="email" hidden>{{ formatMessage(commonMessages.emailLabel) }}</label>
|
||||
<StyledInput
|
||||
id="email"
|
||||
v-model="email"
|
||||
:icon="MailIcon"
|
||||
type="email"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.emailLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="username" hidden>{{ formatMessage(commonMessages.usernameLabel) }}</label>
|
||||
<StyledInput
|
||||
id="username"
|
||||
v-model="username"
|
||||
:icon="UserIcon"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
:placeholder="formatMessage(commonMessages.usernameLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="password"
|
||||
v-model="password"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<label for="confirm-password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
||||
<StyledInput
|
||||
id="confirm-password"
|
||||
v-model="confirmPassword"
|
||||
:icon="KeyIcon"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(commonMessages.confirmPasswordLabel)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-model="subscribe"
|
||||
class="subscribe-btn"
|
||||
:label="formatMessage(messages.subscribeLabel)"
|
||||
:description="formatMessage(messages.subscribeLabel)"
|
||||
/>
|
||||
|
||||
<p v-if="!route.query.launcher">
|
||||
<IntlFormatted :message-id="messages.legalDisclaimer">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/legal/terms" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #privacy-policy-link="{ children }">
|
||||
<NuxtLink to="/legal/privacy" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<HCaptcha v-if="globals?.captcha_enabled" ref="captcha" v-model="token" />
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="continue-btn centered-btn"
|
||||
:disabled="globals?.captcha_enabled ? !token : false"
|
||||
@click="createAccount"
|
||||
>
|
||||
{{ formatMessage(messages.createAccountButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<div class="auth-form__additional-options">
|
||||
{{ formatMessage(messages.alreadyHaveAccountLabel) }}
|
||||
<NuxtLink
|
||||
class="text-link"
|
||||
:to="{
|
||||
path: '/auth/sign-in',
|
||||
query: route.query,
|
||||
}"
|
||||
>
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<SignUpView
|
||||
v-if="!isCreateAccountStep"
|
||||
v-model:email="email"
|
||||
v-model:password="password"
|
||||
:redirect-target="redirectTarget"
|
||||
:show-other-options="showOtherOptions"
|
||||
:route-query="route.query"
|
||||
:on-toggle-other-options="toggleOtherOptions"
|
||||
:on-continue-with-email="continueWithEmail"
|
||||
/>
|
||||
<CreateAccountView
|
||||
v-else
|
||||
v-model:date-of-birth="dateOfBirth"
|
||||
v-model:username="username"
|
||||
v-model:token="token"
|
||||
v-model:subscribe="subscribe"
|
||||
:globals="globals"
|
||||
:on-complete-sign-up="createAccount"
|
||||
:on-set-captcha-ref="setCaptchaRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DiscordColorIcon,
|
||||
GitHubColorIcon,
|
||||
GitLabColorIcon,
|
||||
GoogleColorIcon,
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
MicrosoftColorIcon,
|
||||
RightArrowIcon,
|
||||
SteamColorIcon,
|
||||
UserIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { LocationQueryValue } from 'vue-router'
|
||||
|
||||
import HCaptcha from '@/components/ui/HCaptcha.vue'
|
||||
import { getAuthUrl } from '@/composables/auth.js'
|
||||
import CreateAccountView from '@/components/ui/auth/CreateAccount.vue'
|
||||
import SignUpView from '@/components/ui/auth/SignUp.vue'
|
||||
import {
|
||||
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
promotePendingSignInOAuthProvider,
|
||||
} from '@/composables/auth.ts'
|
||||
|
||||
interface AuthGlobalsResponse {
|
||||
captcha_enabled?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ApiErrorShape {
|
||||
data?: {
|
||||
description?: string
|
||||
error?: string
|
||||
}
|
||||
v1Error?: {
|
||||
error?: string
|
||||
}
|
||||
responseData?: {
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
const getQueryString = (
|
||||
value: LocationQueryValue | LocationQueryValue[] | null | undefined,
|
||||
): string => {
|
||||
const firstValue = Array.isArray(value) ? value[0] : value
|
||||
return typeof firstValue === 'string' ? firstValue : ''
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
const apiError = error as ApiErrorShape
|
||||
if (typeof apiError?.data?.description === 'string') {
|
||||
return apiError.data.description
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -176,30 +86,13 @@ const messages = defineMessages({
|
||||
id: 'auth.sign-up.title',
|
||||
defaultMessage: 'Sign Up',
|
||||
},
|
||||
signUpWithTitle: {
|
||||
id: 'auth.sign-up.title.sign-up-with',
|
||||
defaultMessage: 'Sign up with',
|
||||
ageRequirementWarningTitle: {
|
||||
id: 'auth.sign-up.age-requirement.warning-title',
|
||||
defaultMessage: 'Age requirement',
|
||||
},
|
||||
createAccountTitle: {
|
||||
id: 'auth.sign-up.title.create-account',
|
||||
defaultMessage: 'Or create an account yourself',
|
||||
},
|
||||
subscribeLabel: {
|
||||
id: 'auth.sign-up.subscribe.label',
|
||||
defaultMessage: 'Subscribe to updates about Modrinth',
|
||||
},
|
||||
legalDisclaimer: {
|
||||
id: 'auth.sign-up.legal-dislaimer',
|
||||
defaultMessage:
|
||||
"By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
|
||||
},
|
||||
createAccountButton: {
|
||||
id: 'auth.sign-up.action.create-account',
|
||||
defaultMessage: 'Create account',
|
||||
},
|
||||
alreadyHaveAccountLabel: {
|
||||
id: 'auth.sign-up.sign-in-option.title',
|
||||
defaultMessage: 'Already have an account?',
|
||||
under13HelperText: {
|
||||
id: 'auth.create-account.date-of-birth.under13-helper',
|
||||
defaultMessage: 'You cannot create an account at Modrinth unless you are over 13 years old.',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -209,16 +102,36 @@ useHead({
|
||||
|
||||
const auth = await useAuth()
|
||||
const route = useNativeRoute()
|
||||
const pendingSignInOAuthProvider = useStorage(
|
||||
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
const lastSignInOAuthProvider = useStorage(
|
||||
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
||||
null,
|
||||
undefined,
|
||||
{ initOnMounted: true },
|
||||
)
|
||||
|
||||
const redirectTarget = route.query.redirect
|
||||
const redirectTarget = getQueryString(route.query.redirect)
|
||||
const showOtherOptions = ref(false)
|
||||
const isCreateAccountStep = ref(false)
|
||||
|
||||
if (auth.value.user) {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
|
||||
const captcha = ref()
|
||||
const captcha = ref<{ reset?: () => void } | null>(null)
|
||||
const setCaptchaRef = (captchaRef: unknown) => {
|
||||
captcha.value = (captchaRef as { reset?: () => void } | null) ?? null
|
||||
}
|
||||
const toggleOtherOptions = () => {
|
||||
showOtherOptions.value = !showOtherOptions.value
|
||||
}
|
||||
|
||||
const { data: globals } = useQuery({
|
||||
const { data: globals } = useQuery<AuthGlobalsResponse>({
|
||||
queryKey: ['auth-globals'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
@@ -231,29 +144,117 @@ const { data: globals } = useQuery({
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const dateOfBirth = ref('')
|
||||
const username = ref('')
|
||||
const token = ref('')
|
||||
const subscribe = ref(false)
|
||||
const USERNAME_MAX_LENGTH = 39
|
||||
const MAX_USERNAME_SUFFIX_RETRIES = 10
|
||||
const MAX_RANDOM_USERNAME_ATTEMPTS = 10
|
||||
|
||||
async function continueWithEmail() {
|
||||
pendingSignInOAuthProvider.value = null
|
||||
lastSignInOAuthProvider.value = null
|
||||
startLoading()
|
||||
try {
|
||||
const generatedUsername = await findAvailableGeneratedUsername()
|
||||
|
||||
token.value = ''
|
||||
username.value = generatedUsername
|
||||
isCreateAccountStep.value = true
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
function generateUsernameFromEmail(emailAddress: string): string {
|
||||
const [localPart = ''] = emailAddress.trim().toLowerCase().split('@')
|
||||
const sanitized = localPart
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
|
||||
return (sanitized || 'user').slice(0, USERNAME_MAX_LENGTH)
|
||||
}
|
||||
|
||||
async function findAvailableGeneratedUsername(): Promise<string> {
|
||||
const baseUsername = generateUsernameFromEmail(email.value)
|
||||
|
||||
for (let suffixAttempt = 0; suffixAttempt <= MAX_USERNAME_SUFFIX_RETRIES; suffixAttempt++) {
|
||||
const candidateUsername = appendUsernameSuffix(baseUsername, suffixAttempt)
|
||||
|
||||
try {
|
||||
await validateCreateAccountCandidate(candidateUsername)
|
||||
return candidateUsername
|
||||
} catch (err) {
|
||||
if (!isUsernameTakenValidationError(err)) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await findAvailableRandomUsername()
|
||||
}
|
||||
|
||||
function appendUsernameSuffix(baseUsername: string, suffixAttempt: number): string {
|
||||
if (suffixAttempt === 0) {
|
||||
return baseUsername
|
||||
}
|
||||
|
||||
const suffix = String(suffixAttempt)
|
||||
const maxBaseLength = Math.max(1, USERNAME_MAX_LENGTH - suffix.length)
|
||||
return `${baseUsername.slice(0, maxBaseLength)}${suffix}`
|
||||
}
|
||||
|
||||
function generateRandomUsername(): string {
|
||||
const randomSuffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`
|
||||
const base = `user_${randomSuffix}`
|
||||
return base.slice(0, USERNAME_MAX_LENGTH)
|
||||
}
|
||||
|
||||
async function findAvailableRandomUsername(): Promise<string> {
|
||||
for (let attempt = 0; attempt < MAX_RANDOM_USERNAME_ATTEMPTS; attempt++) {
|
||||
const candidateUsername = generateRandomUsername()
|
||||
|
||||
try {
|
||||
await validateCreateAccountCandidate(candidateUsername)
|
||||
return candidateUsername
|
||||
} catch (err) {
|
||||
if (!isUsernameTakenValidationError(err)) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unable to find an available username. Please choose one manually.')
|
||||
}
|
||||
|
||||
async function validateCreateAccountCandidate(candidateUsername: string): Promise<void> {
|
||||
await client.labrinth.auth_v2.validateCreateAccount({
|
||||
username: candidateUsername,
|
||||
password: password.value,
|
||||
email: email.value,
|
||||
})
|
||||
}
|
||||
|
||||
function isUsernameTakenValidationError(error: unknown): boolean {
|
||||
const apiError = error as ApiErrorShape
|
||||
const errorCode =
|
||||
apiError?.data?.error ?? apiError?.v1Error?.error ?? apiError?.responseData?.error
|
||||
return errorCode === 'username_taken'
|
||||
}
|
||||
|
||||
async function createAccount() {
|
||||
startLoading()
|
||||
try {
|
||||
if (confirmPassword.value !== password.value) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: formatMessage({
|
||||
id: 'auth.sign-up.notification.password-mismatch.text',
|
||||
defaultMessage: 'Passwords do not match!',
|
||||
}),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
}
|
||||
|
||||
const res = await client.labrinth.auth_v2.createAccount({
|
||||
username: username.value,
|
||||
username: username.value.trim() || generateUsernameFromEmail(email.value),
|
||||
password: password.value,
|
||||
email: email.value,
|
||||
challenge: token.value,
|
||||
@@ -263,23 +264,25 @@ async function createAccount() {
|
||||
await useAuth(res.session)
|
||||
await useUser()
|
||||
|
||||
promotePendingSignInOAuthProvider()
|
||||
|
||||
if (route.query.launcher) {
|
||||
await navigateTo({ path: '/auth/sign-in', query: route.query })
|
||||
return
|
||||
}
|
||||
|
||||
if (route.query.redirect) {
|
||||
await navigateTo(route.query.redirect)
|
||||
await navigateTo(getQueryString(route.query.redirect))
|
||||
} else {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
captcha.value?.reset()
|
||||
captcha.value?.reset?.()
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
@@ -1,78 +1,126 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="universal-card flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
|
||||
>
|
||||
<template v-if="auth.user && auth.user.email_verified && !success">
|
||||
<h1>{{ formatMessage(alreadyVerifiedMessages.title) }}</h1>
|
||||
<h1 class="m-0 mx-auto text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(alreadyVerifiedMessages.title) }}
|
||||
</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<p>{{ formatMessage(alreadyVerifiedMessages.description) }}</p>
|
||||
<Admonition type="success">
|
||||
{{ formatMessage(alreadyVerifiedMessages.description) }}
|
||||
</Admonition>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/settings/account">
|
||||
<NuxtLink class="shadow-none" to="/settings/account">
|
||||
<SettingsIcon /> {{ formatMessage(messages.accountSettings) }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
<ButtonStyled color="brand">
|
||||
<NuxtLink to="/discover/mods">
|
||||
{{ formatMessage(messages.discoverMods) }}
|
||||
<RightArrowIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="success">
|
||||
<h1>{{ formatMessage(postVerificationMessages.title) }}</h1>
|
||||
<h1 class="m-0 mx-auto text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(postVerificationMessages.title) }}
|
||||
</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<p>{{ formatMessage(postVerificationMessages.description) }}</p>
|
||||
<Admonition type="success">
|
||||
{{ formatMessage(postVerificationMessages.description) }}
|
||||
</Admonition>
|
||||
|
||||
<ButtonStyled v-if="auth.user">
|
||||
<NuxtLink to="/settings/account">
|
||||
<SettingsIcon /> {{ formatMessage(messages.accountSettings) }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<NuxtLink to="/auth/sign-in">
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
<RightArrowIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
<template v-if="auth.user">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/settings/account">
|
||||
<SettingsIcon /> {{ formatMessage(messages.accountSettings) }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<NuxtLink to="/discover/mods">
|
||||
{{ formatMessage(messages.discoverMods) }}
|
||||
<RightArrowIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<ButtonStyled v-else color="brand">
|
||||
<NuxtLink to="/auth/sign-in" class="!w-full">
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
<RightArrowIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<h1>{{ formatMessage(failedVerificationMessages.title) }}</h1>
|
||||
<h1 class="m-0 mx-auto text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(failedVerificationMessages.title) }}
|
||||
</h1>
|
||||
|
||||
<section class="auth-form">
|
||||
<p>
|
||||
<template v-if="auth.user">
|
||||
{{ formatMessage(failedVerificationMessages.loggedInDescription) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ formatMessage(failedVerificationMessages.description) }}
|
||||
</template>
|
||||
</p>
|
||||
<Admonition v-if="auth.user" type="warning">
|
||||
{{ formatMessage(failedVerificationMessages.loggedInDescription) }}
|
||||
</Admonition>
|
||||
<Admonition v-else type="warning">
|
||||
{{ formatMessage(failedVerificationMessages.description) }}
|
||||
</Admonition>
|
||||
|
||||
<ButtonStyled v-if="auth.user" color="brand">
|
||||
<button @click="handleResendEmailVerification">
|
||||
{{ formatMessage(failedVerificationMessages.action) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled v-else color="brand">
|
||||
<NuxtLink to="/auth/sign-in">
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
<RightArrowIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
<ButtonStyled v-if="auth.user" color="brand">
|
||||
<button class="!w-full" @click="handleResendEmailVerification">
|
||||
{{ formatMessage(failedVerificationMessages.action) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="brand">
|
||||
<NuxtLink to="/auth/sign-in" class="!w-full">
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
<RightArrowIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { RightArrowIcon, SettingsIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { LocationQueryValue } from 'vue-router'
|
||||
|
||||
interface ApiErrorShape {
|
||||
data?: {
|
||||
description?: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
const getQueryString = (
|
||||
value: LocationQueryValue | LocationQueryValue[] | null | undefined,
|
||||
): string => {
|
||||
const firstValue = Array.isArray(value) ? value[0] : value
|
||||
return typeof firstValue === 'string' ? firstValue : ''
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
const apiError = error as ApiErrorShape
|
||||
if (typeof apiError?.data?.description === 'string') {
|
||||
return apiError.data.description
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -86,6 +134,22 @@ const messages = defineMessages({
|
||||
id: 'auth.verify-email.action.account-settings',
|
||||
defaultMessage: 'Account settings',
|
||||
},
|
||||
discoverMods: {
|
||||
id: 'auth.verify-email.action.discover-mods',
|
||||
defaultMessage: 'Discover mods',
|
||||
},
|
||||
emailSentNotificationTitle: {
|
||||
id: 'auth.verify-email.notification.email-sent.title',
|
||||
defaultMessage: 'Email sent',
|
||||
},
|
||||
emailSentNotificationDescription: {
|
||||
id: 'auth.verify-email.notification.email-sent.description',
|
||||
defaultMessage: 'An email with a link to verify your account has been sent to {email}.',
|
||||
},
|
||||
errorOccurredTitle: {
|
||||
id: 'auth.verify-email.notification.error-occurred.title',
|
||||
defaultMessage: 'An error occurred',
|
||||
},
|
||||
})
|
||||
|
||||
const alreadyVerifiedMessages = defineMessages({
|
||||
@@ -142,13 +206,13 @@ const route = useNativeRoute()
|
||||
|
||||
if (route.query.flow) {
|
||||
try {
|
||||
const emailVerified = useState('emailVerified', () => null)
|
||||
const emailVerified = useState<boolean | null>('emailVerified', () => null)
|
||||
|
||||
if (emailVerified.value === null) {
|
||||
await useBaseFetch('auth/email/verify', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
flow: route.query.flow,
|
||||
flow: getQueryString(route.query.flow),
|
||||
},
|
||||
})
|
||||
emailVerified.value = true
|
||||
@@ -171,14 +235,16 @@ async function handleResendEmailVerification() {
|
||||
try {
|
||||
await resendVerifyEmail()
|
||||
addNotification({
|
||||
title: 'Email sent',
|
||||
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
|
||||
title: formatMessage(messages.emailSentNotificationTitle),
|
||||
text: formatMessage(messages.emailSentNotificationDescription, {
|
||||
email: auth.value.user?.email ?? '',
|
||||
}),
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
title: formatMessage(messages.errorOccurredTitle),
|
||||
text: getErrorMessage(err),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
<template>
|
||||
<div class="welcome-box has-bot">
|
||||
<img :src="WavingRinthbot" alt="Waving Modrinth Bot" class="welcome-box__waving-bot" />
|
||||
<div class="welcome-box__top-glow" />
|
||||
<div class="welcome-box__body">
|
||||
<h1 class="welcome-box__title">
|
||||
{{ formatMessage(messages.welcomeLongTitle) }}
|
||||
</h1>
|
||||
|
||||
<p class="welcome-box__subtitle">
|
||||
<IntlFormatted :message-id="messages.welcomeDescription">
|
||||
<template #bold="{ children }">
|
||||
<strong>
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</strong>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<Checkbox
|
||||
v-model="subscribe"
|
||||
class="subscribe-btn"
|
||||
:label="formatMessage(messages.subscribeCheckbox)"
|
||||
:description="formatMessage(messages.subscribeCheckbox)"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button class="centered-btn" @click="continueSignUp">
|
||||
{{ formatMessage(commonMessages.continueButton) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<p class="tos-text">
|
||||
<IntlFormatted :message-id="messages.tosLabel">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/legal/terms" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #privacy-policy-link="{ children }">
|
||||
<NuxtLink to="/legal/privacy" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RightArrowIcon, WavingRinthbot } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
normalizeChildren,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
subscribeCheckbox: {
|
||||
id: 'auth.welcome.checkbox.subscribe',
|
||||
defaultMessage: 'Subscribe to updates about Modrinth',
|
||||
},
|
||||
tosLabel: {
|
||||
id: 'auth.welcome.label.tos',
|
||||
defaultMessage:
|
||||
"By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
|
||||
},
|
||||
welcomeDescription: {
|
||||
id: 'auth.welcome.description',
|
||||
defaultMessage:
|
||||
'You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with amazing mods.',
|
||||
},
|
||||
welcomeLongTitle: {
|
||||
id: 'auth.welcome.long-title',
|
||||
defaultMessage: 'Welcome to Modrinth!',
|
||||
},
|
||||
welcomeTitle: {
|
||||
id: 'auth.welcome.title',
|
||||
defaultMessage: 'Welcome',
|
||||
},
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: () => `${formatMessage(messages.welcomeTitle)} - Modrinth`,
|
||||
})
|
||||
|
||||
const subscribe = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
await useAuth(route.query.authToken)
|
||||
await useUser()
|
||||
queryClient.clear()
|
||||
})
|
||||
|
||||
async function continueSignUp() {
|
||||
if (subscribe.value) {
|
||||
try {
|
||||
await useBaseFetch('auth/email/subscribe', {
|
||||
method: 'POST',
|
||||
})
|
||||
} catch {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
if (route.query.redirect) {
|
||||
await navigateTo(route.query.redirect)
|
||||
} else {
|
||||
await navigateTo('/dashboard')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome-box {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-lg);
|
||||
padding: 1.75rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
box-shadow: var(--shadow-card);
|
||||
position: relative;
|
||||
|
||||
&.has-bot {
|
||||
margin-block: 120px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
a {
|
||||
color: var(--color-brand);
|
||||
font-weight: var(--weight-bold);
|
||||
&:hover,
|
||||
&:focus {
|
||||
filter: brightness(1.125);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__waving-bot {
|
||||
--bot-height: 112px;
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--bot-height));
|
||||
right: 5rem;
|
||||
height: var(--bot-height);
|
||||
width: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
--bot-height: 70px;
|
||||
right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__top-glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
opacity: 0.4;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 2rem,
|
||||
var(--color-green) calc(100% - 13rem),
|
||||
var(--color-green) calc(100% - 5rem),
|
||||
transparent calc(100% - 2rem)
|
||||
);
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: var(--text-32);
|
||||
font-weight: var(--weight-extrabold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: var(--text-18);
|
||||
}
|
||||
|
||||
.tos-text {
|
||||
font-size: var(--text-14);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -57,7 +57,6 @@ import { Avatar, ButtonStyled, defineMessages, injectModrinthClient, useVIntl }
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
|
||||
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
|
||||
import { useAuth } from '~/composables/auth.js'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
|
||||
@@ -9,24 +9,31 @@
|
||||
:has-to-type="true"
|
||||
@proceed="deleteAccount"
|
||||
/>
|
||||
<Modal
|
||||
<NewModal
|
||||
ref="changeEmailModal"
|
||||
width="500px"
|
||||
:header="`${auth.user.email ? formatMessage(messages.changeEmailHeaderChange) : formatMessage(messages.changeEmailHeaderAdd)}`"
|
||||
>
|
||||
<div class="universal-modal">
|
||||
<p>{{ formatMessage(messages.emailNotPublicNotice) }}</p>
|
||||
<label for="email-input">
|
||||
<span class="label__title">{{ formatMessage(messages.emailAddressLabel) }}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="email-input"
|
||||
v-model="email"
|
||||
:maxlength="2048"
|
||||
type="email"
|
||||
:placeholder="formatMessage(messages.emailAddressPlaceholder)"
|
||||
@keyup.enter="saveEmail()"
|
||||
/>
|
||||
<div class="input-group push-right">
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition type="info">
|
||||
{{ formatMessage(messages.emailNotPublicNotice) }}
|
||||
</Admonition>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="email-input">
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.emailAddressLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="email-input"
|
||||
v-model="email"
|
||||
:maxlength="2048"
|
||||
type="email"
|
||||
:placeholder="formatMessage(messages.emailAddressPlaceholder)"
|
||||
@keyup.enter="saveEmail()"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.changeEmailModal.hide()">
|
||||
<XIcon />
|
||||
@@ -41,9 +48,10 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
</NewModal>
|
||||
<NewModal
|
||||
ref="managePasswordModal"
|
||||
width="600px"
|
||||
:header="`${
|
||||
removePasswordMode
|
||||
? formatMessage(messages.passwordHeaderRemove)
|
||||
@@ -52,61 +60,75 @@
|
||||
: formatMessage(messages.passwordHeaderAdd)
|
||||
}`"
|
||||
>
|
||||
<div class="universal-modal">
|
||||
<ul
|
||||
v-if="newPassword !== confirmNewPassword && confirmNewPassword.length > 0"
|
||||
class="known-errors"
|
||||
>
|
||||
<li>{{ formatMessage(messages.passwordsDoNotMatchError) }}</li>
|
||||
</ul>
|
||||
<label v-if="removePasswordMode" for="old-password">
|
||||
<span class="label__title">{{ formatMessage(messages.confirmPasswordLabel) }}</span>
|
||||
<span class="label__description">{{
|
||||
formatMessage(messages.confirmPasswordDescription)
|
||||
}}</span>
|
||||
</label>
|
||||
<label v-else-if="auth.user.has_password" for="old-password">
|
||||
<span class="label__title">{{ formatMessage(messages.oldPasswordLabel) }}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
v-if="auth.user.has_password"
|
||||
id="old-password"
|
||||
v-model="oldPassword"
|
||||
:maxlength="2048"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
:placeholder="
|
||||
removePasswordMode
|
||||
? formatMessage(messages.confirmPasswordPlaceholder)
|
||||
: formatMessage(messages.oldPasswordPlaceholder)
|
||||
"
|
||||
/>
|
||||
<template v-if="!removePasswordMode">
|
||||
<label for="new-password"
|
||||
><span class="label__title">{{ formatMessage(messages.newPasswordLabel) }}</span></label
|
||||
>
|
||||
<StyledInput
|
||||
id="new-password"
|
||||
v-model="newPassword"
|
||||
:maxlength="2048"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(messages.newPasswordPlaceholder)"
|
||||
/>
|
||||
<label for="confirm-new-password">
|
||||
<span class="label__title">{{ formatMessage(messages.confirmNewPasswordLabel) }}</span>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div v-if="auth.user.has_password" class="flex flex-col gap-2.5">
|
||||
<label v-if="removePasswordMode" for="old-password">
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.confirmPasswordLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<label v-else for="old-password">
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.oldPasswordLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="confirm-new-password"
|
||||
v-model="confirmNewPassword"
|
||||
id="old-password"
|
||||
v-model="oldPassword"
|
||||
:maxlength="2048"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(messages.confirmNewPasswordPlaceholder)"
|
||||
autocomplete="current-password"
|
||||
:placeholder="
|
||||
removePasswordMode
|
||||
? formatMessage(messages.confirmPasswordPlaceholder)
|
||||
: formatMessage(messages.oldPasswordPlaceholder)
|
||||
"
|
||||
/>
|
||||
<span v-if="removePasswordMode" class="label__description">{{
|
||||
formatMessage(messages.confirmPasswordDescription)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="!removePasswordMode">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="new-password"
|
||||
><span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.newPasswordLabel)
|
||||
}}</span></label
|
||||
>
|
||||
<StyledInput
|
||||
id="new-password"
|
||||
v-model="newPassword"
|
||||
:maxlength="2048"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(messages.newPasswordPlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="confirm-new-password">
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.confirmNewPasswordLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="confirm-new-password"
|
||||
v-model="confirmNewPassword"
|
||||
:maxlength="2048"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="formatMessage(messages.confirmNewPasswordPlaceholder)"
|
||||
/>
|
||||
<div
|
||||
v-if="newPassword !== confirmNewPassword && confirmNewPassword.length > 0"
|
||||
class="known-errors"
|
||||
>
|
||||
{{ formatMessage(messages.passwordsDoNotMatchError) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p></p>
|
||||
<div class="input-group push-right">
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.managePasswordModal.hide()">
|
||||
<XIcon />
|
||||
@@ -147,9 +169,10 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
</NewModal>
|
||||
<NewModal
|
||||
ref="manageTwoFactorModal"
|
||||
width="500px"
|
||||
:header="
|
||||
formatMessage(
|
||||
auth.user.has_totp && twoFactorStep === 0
|
||||
@@ -158,10 +181,12 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="universal-modal">
|
||||
<div class="flex flex-col gap-6">
|
||||
<template v-if="auth.user.has_totp && twoFactorStep === 0">
|
||||
<label for="two-factor-code">
|
||||
<span class="label__title">{{ formatMessage(messages.twoFactorEnterCodeLabel) }}</span>
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.twoFactorEnterCodeLabel)
|
||||
}}</span>
|
||||
<span class="label__description">{{
|
||||
formatMessage(messages.twoFactorEnterCodeDescription)
|
||||
}}</span>
|
||||
@@ -173,10 +198,10 @@
|
||||
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
|
||||
@keyup.enter="removeTwoFactor()"
|
||||
/>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors">
|
||||
<p v-if="twoFactorIncorrect" class="known-errors m-0">
|
||||
{{ formatMessage(messages.twoFactorIncorrectError) }}
|
||||
</p>
|
||||
<div class="input-group push-right">
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.manageTwoFactorModal.hide()">
|
||||
<XIcon />
|
||||
@@ -193,8 +218,8 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="twoFactorStep === 0">
|
||||
<p>{{ formatMessage(messages.twoFactorSetupIntro) }}</p>
|
||||
<p>
|
||||
<p class="m-0">{{ formatMessage(messages.twoFactorSetupIntro) }}</p>
|
||||
<p class="m-0">
|
||||
<IntlFormatted :message-id="messages.twoFactorSetupScan">
|
||||
<template #authy-link="{ children }">
|
||||
<a href="https://authy.com/" target="_blank" rel="noreferrer">
|
||||
@@ -221,14 +246,14 @@
|
||||
:margin="2"
|
||||
level="H"
|
||||
/>
|
||||
<p>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.twoFactorManualSecretPrefix) }}
|
||||
<strong>{{ twoFactorSecret }}</strong>
|
||||
</p>
|
||||
</template>
|
||||
<template v-if="twoFactorStep === 1">
|
||||
<label for="verify-code">
|
||||
<span class="label__title">{{
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.twoFactorVerifyCodeLabel)
|
||||
}}</span>
|
||||
<span class="label__description">{{
|
||||
@@ -243,18 +268,18 @@
|
||||
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
|
||||
@keyup.enter="verifyTwoFactorCode()"
|
||||
/>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors">
|
||||
<p v-if="twoFactorIncorrect" class="known-errors m-0">
|
||||
{{ formatMessage(messages.twoFactorIncorrectError) }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-if="twoFactorStep === 2">
|
||||
<p>{{ formatMessage(messages.twoFactorBackupCodesIntro) }}</p>
|
||||
<p>{{ formatMessage(messages.twoFactorBackupCodesSingleUse) }}</p>
|
||||
<p class="m-0">{{ formatMessage(messages.twoFactorBackupCodesIntro) }}</p>
|
||||
<p class="m-0">{{ formatMessage(messages.twoFactorBackupCodesSingleUse) }}</p>
|
||||
<ul>
|
||||
<li v-for="code in backupCodes" :key="code">{{ code }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div class="input-group push-right">
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled v-if="twoFactorStep === 1">
|
||||
<button @click="twoFactorStep = 0">
|
||||
<LeftArrowIcon />
|
||||
@@ -282,38 +307,34 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal ref="manageProvidersModal" :header="formatMessage(messages.manageProvidersModalHeader)">
|
||||
<div class="universal-modal">
|
||||
<div class="table">
|
||||
<div class="table-head table-row">
|
||||
<div class="table-text table-cell">
|
||||
{{ formatMessage(messages.providersTableProvider) }}
|
||||
</div>
|
||||
<div class="table-text table-cell">
|
||||
{{ formatMessage(messages.providersTableActions) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="provider in authProviders" :key="provider.id" class="table-row">
|
||||
<div class="table-text table-cell">
|
||||
<span><component :is="provider.icon" /> {{ provider.display }}</span>
|
||||
</div>
|
||||
<div class="table-text manage table-cell">
|
||||
<ButtonStyled v-if="auth.user.auth_providers.includes(provider.id)">
|
||||
<button @click="handleRemoveAuthProvider(provider.id)">
|
||||
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<a :href="`${getAuthUrl(provider.id, '/settings/account')}&token=${auth.token}`">
|
||||
<ExternalIcon /> {{ formatMessage(messages.providerAddButton) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="input-group push-right">
|
||||
</NewModal>
|
||||
<NewModal
|
||||
ref="manageProvidersModal"
|
||||
width="500px"
|
||||
:header="formatMessage(messages.manageProvidersModalHeader)"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Table :columns="manageProvidersColumns" :data="authProviders" row-key="id">
|
||||
<template #cell-provider="{ row }">
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<component :is="row.icon" class="size-5" />
|
||||
{{ row.display }}
|
||||
</span>
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<ButtonStyled v-if="auth.user.auth_providers.includes(row.id)">
|
||||
<button class="!w-full" @click="handleRemoveAuthProvider(row.id)">
|
||||
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<a :href="`${getAuthUrl(row.id, '/settings/account')}&token=${auth.token}`">
|
||||
<ExternalIcon /> {{ formatMessage(messages.providerAddButton) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="self-end">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.manageProvidersModal.hide()">
|
||||
<XIcon />
|
||||
@@ -322,7 +343,7 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</NewModal>
|
||||
<section class="universal-card">
|
||||
<h2 class="text-2xl">{{ formatMessage(messages.accountSecurityTitle) }}</h2>
|
||||
|
||||
@@ -419,6 +440,7 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<PasskeySettings />
|
||||
</section>
|
||||
|
||||
<section id="data-export" class="universal-card">
|
||||
@@ -471,13 +493,16 @@ import {
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
ConfirmModal,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
Table,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import KeyIcon from 'assets/icons/auth/key.svg'
|
||||
@@ -489,8 +514,8 @@ import MicrosoftIcon from 'assets/icons/auth/sso-microsoft.svg'
|
||||
import SteamIcon from 'assets/icons/auth/sso-steam.svg'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.js'
|
||||
import PasskeySettings from '~/components/ui/auth/PasskeySettings.vue'
|
||||
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.ts'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
@@ -957,6 +982,18 @@ const authProviders = [
|
||||
icon: GoogleIcon,
|
||||
},
|
||||
]
|
||||
const manageProvidersColumns = computed(() => [
|
||||
{
|
||||
key: 'provider',
|
||||
label: formatMessage(messages.providersTableProvider),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: formatMessage(messages.providersTableActions),
|
||||
align: 'right',
|
||||
width: '10rem',
|
||||
},
|
||||
])
|
||||
|
||||
async function deleteAccount() {
|
||||
startLoading()
|
||||
@@ -1010,20 +1047,4 @@ canvas {
|
||||
margin: 0 auto;
|
||||
border-radius: var(--size-rounded-card);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
grid-template-columns: 1fr 10rem;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: auto 0;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -561,7 +561,7 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
|
||||
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
||||
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { projectUserSorting } from '~/utils/projects.ts'
|
||||
import { reportUser } from '~/utils/report-helpers.ts'
|
||||
import { hasActiveMidas, hasPride26Badge } from '~/utils/user-membership.ts'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { promotePendingSignInOAuthProvider } from '@/composables/auth.ts'
|
||||
|
||||
export default defineNuxtPlugin(async () => {
|
||||
const auth = await useAuth()
|
||||
if (auth.value.user) {
|
||||
promotePendingSignInOAuthProvider()
|
||||
}
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import { type AuthProvider, type AuthUser, provideAuth } from '@modrinth/ui'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
import { getSignInRedirectPath } from '~/composables/auth.js'
|
||||
import { getSignInRedirectPath } from '~/composables/auth.ts'
|
||||
|
||||
export function setupAuthProvider(auth: Awaited<ReturnType<typeof useAuth>>) {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -173,3 +173,6 @@ MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
|
||||
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
|
||||
SERVER_PING_TIMEOUT=10000
|
||||
SERVER_PING_RETRIES=3
|
||||
|
||||
# Display name for Webauthn Authenticators
|
||||
WEBAUTHN_RP_NAME=Modrinth
|
||||
|
||||
@@ -195,3 +195,6 @@ DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
|
||||
SERVER_PING_TIMEOUT=10000
|
||||
SERVER_PING_RETRIES=3
|
||||
SERVER_PING_MIN_INTERVAL_SEC=1800
|
||||
|
||||
# Display name for Webauthn Authenticators
|
||||
WEBAUTHN_RP_NAME=Modrinth
|
||||
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Ask2AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/labrinth.iml" filepath="$PROJECT_DIR$/.idea/labrinth.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json<Passkey>\",\n last_used, created_at\n FROM user_passkeys\n WHERE credential_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "credential_id",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "passkey: sqlx::types::Json<Passkey>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_used",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c"
|
||||
}
|
||||
Generated
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4"
|
||||
}
|
||||
Generated
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_passkeys SET name = $1\n WHERE id = $2 AND user_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129"
|
||||
}
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sessions WHERE user_id = $1 RETURNING id, session\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "session",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n (\n SELECT MAX(campaign_donations.donated_at)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_last_donated_at,\n (\n SELECT SUM(campaign_donations.amount_usd)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_total_amount_donated_usd,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
||||
"query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n (\n SELECT MAX(campaign_donations.donated_at)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_last_donated_at,\n (\n SELECT SUM(campaign_donations.amount_usd)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_total_amount_donated_usd,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,\n eligibility_verified_at\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -137,6 +137,11 @@
|
||||
"ordinal": 26,
|
||||
"name": "is_subscribed_to_newsletter",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "eligibility_verified_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -172,8 +177,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5"
|
||||
"hash": "3ca51012492b969bb6a474ee22c9e53e57b6da3a1145f0d86a2bab36be436eb6"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM user_passkeys\n WHERE id = $1 AND user_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_passkeys\n SET passkey = $1, last_used = NOW()\n WHERE id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850"
|
||||
}
|
||||
Generated
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Numeric",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO user_passkeys (\n id, user_id, name, credential_id, passkey, created_at, last_used\n )\n VALUES (\n $1, $2 ,$3, $4, $5, $6, $7\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Bytea",
|
||||
"Jsonb",
|
||||
"Timestamptz",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9"
|
||||
}
|
||||
Generated
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json<Passkey>\",\n last_used, created_at\n FROM user_passkeys\n WHERE user_id = $1\n ORDER BY created_at DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "credential_id",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "passkey: sqlx::types::Json<Passkey>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_used",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM user_passkeys WHERE id=$1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21, $22\n )\n ",
|
||||
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,\n eligibility_verified_at\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21, $22,\n $23\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -26,10 +26,11 @@
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool"
|
||||
"Bool",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "010c69fa61e1329156020b251e75d46bc09344c1846b3098accce5801e571e5e"
|
||||
"hash": "a335ff4338fa4b3c1aaa9f62c61aad3c3e8bef0e68035a024995c62cfe69c568"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
|
||||
}
|
||||
Generated
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
|
||||
}
|
||||
Generated
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM user_passkeys\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41"
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
- `Modrinth-Admin: feedbeef` as admin key
|
||||
- If some steps require you to create a project/mod or version for testing, ask the user to go into the web frontend and manually create a project/version
|
||||
- When using `sqlx::query` etc. always use the macro form like `sqlx::query!` or `sqlx::query_scalar!` - never the plain function form. Avoid using `query_as!`.
|
||||
- Do not use `()` as an error type for operations, unless you have a very good reason. Either make a new error type, or use `eyre::Report`.
|
||||
- Do not run `cargo test`, even for a single specific test, unless explicitly prompted to by the user, since it takes a long time to run.
|
||||
- You can force a search reindex by:
|
||||
- Running `cd apps/labrinth && cargo run -p labrinth -- --run-background-task index-search` (prefer this if backend is running locally)
|
||||
|
||||
@@ -131,6 +131,11 @@ utoipa-actix-web = { workspace = true }
|
||||
utoipa-scalar = { workspace = true, features = ["actix-web"] }
|
||||
uuid = { workspace = true, features = ["fast-rng", "serde", "v4", "v7"] }
|
||||
validator = { workspace = true, features = ["derive"] }
|
||||
webauthn-rs = { workspace = true, features = [
|
||||
"conditional-ui",
|
||||
"danger-allow-state-serialisation"
|
||||
] }
|
||||
webauthn-rs-proto = { workspace = true }
|
||||
webp = { workspace = true }
|
||||
woothee = { workspace = true }
|
||||
yaserde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN eligibility_verified_at TIMESTAMPTZ;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE user_passkeys (
|
||||
id BIGINT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
credential_id BYTEA NOT NULL UNIQUE,
|
||||
passkey JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX user_passkeys_user_id ON user_passkeys (user_id);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user