forked from spacebar/account
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82b5bb36c5 | |||
| 124916e62c | |||
| fa063d78fb | |||
| 672674f7ad | |||
| acb04bd782 | |||
| b0221e55d5 | |||
| d7c226b5a1 | |||
| 8c01e2d5fd | |||
| 73ff739c81 | |||
| e00913e7a3 | |||
| fedb6f67d4 | |||
| b934df8531 | |||
| 5546583b46 | |||
| b0a2476cc0 | |||
| b1c247961d | |||
| 622f0e989a | |||
| 282b1c34cf | |||
| b814887461 | |||
| e3489f4eaa | |||
| 66e8e689af | |||
| 7af0739a69 | |||
| d76175fbea | |||
| a055cbefd2 | |||
| 9951c60327 | |||
| 8f1d0bf0e7 | |||
| db665374aa | |||
| 7df36c1301 | |||
| 7fa5d6a4d0 | |||
| 68b5f2251d | |||
| e7c94f30cd | |||
| 579ba418c1 | |||
| 02a4b2b504 | |||
| fdcdd1ef20 | |||
| 3a9c856e03 | |||
| 426dbf3f1b | |||
| 762214f81a | |||
| a0a48a3508 | |||
| 487f06e311 | |||
| fae51441cd | |||
| 9b7d7928a3 | |||
| 08a789bd64 | |||
| 44b380e6c1 | |||
| 08146a3604 | |||
| f2a5997dab | |||
| 4621679bf5 | |||
| 9037439d9e | |||
| 5160206147 | |||
| 19505b4348 | |||
| 9bf06fffd6 | |||
| f32e69be99 | |||
| c47a3fb5fc | |||
| 4970f09db5 | |||
| 437ab17e81 | |||
| a19d45a4ac | |||
| 35a3e27591 | |||
| ace476b66b | |||
| 1576062160 | |||
| 2c7767124e | |||
| 3c3b8bf91c | |||
| 18da6e247b | |||
| f7d6b3ebf0 | |||
| ea5f9d1c13 | |||
| cfd4bc21e3 | |||
| 2dd404a957 | |||
| 2f1cb63e1e | |||
| 381610c2ec | |||
| cec6414bc1 | |||
| 52f044a51f | |||
| 250cb9a509 | |||
| d310f81376 | |||
| ccd10c62a9 |
44 changed files with 3464 additions and 1606 deletions
|
|
@ -1,2 +1,5 @@
|
|||
[target.'cfg(target_arch = "x86_64")']
|
||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||
|
||||
[registries]
|
||||
spbr = { index = "sparse+https://crates.spbr.net/api/v1/crates/" }
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Cache container storage
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
/var/lib/containers/storage
|
||||
|
|
|
|||
16
.sqlx/query-0edf3f60bfb92912f73b6c61962e4a98b37c62d596cb0c8c3d1cc1c0199dcb2d.json
generated
Normal file
16
.sqlx/query-0edf3f60bfb92912f73b6c61962e4a98b37c62d596cb0c8c3d1cc1c0199dcb2d.json
generated
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users SET\n email = $1,\n email_verified_since = NULL,\n verification_code = $2\n WHERE pid = $3\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0edf3f60bfb92912f73b6c61962e4a98b37c62d596cb0c8c3d1cc1c0199dcb2d"
|
||||
}
|
||||
28
.sqlx/query-0ef5f08a84d6864291cb6125610d9d3e1961530ae963f9ab5442482d81228590.json
generated
Normal file
28
.sqlx/query-0ef5f08a84d6864291cb6125610d9d3e1961530ae963f9ab5442482d81228590.json
generated
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "select serial from certificates where hash = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "serial",
|
||||
"type_info": "Varchar",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "certificates",
|
||||
"name": "serial"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0ef5f08a84d6864291cb6125610d9d3e1961530ae963f9ab5442482d81228590"
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT nextval('pid_counter') as pid",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "pid",
|
||||
"type_info": "Int8",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3c9b1695f8ae49e4308c048de98c1c262351465b6f102e98912f67442a1f54d9"
|
||||
}
|
||||
|
|
@ -68,6 +68,17 @@
|
|||
"name": "expires"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "cert_hash",
|
||||
"type_info": "Bytea",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "tokens",
|
||||
"name": "cert_hash"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
|
@ -83,7 +94,8 @@
|
|||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "48710e0b87742cc3fef816b3c95604095f71324011e7093ec37af15da8c158f4"
|
||||
|
|
|
|||
40
.sqlx/query-48fd98db66a68865babe4ac6a453de747bba2ce1da198dda4b6f3b78eacfebf3.json
generated
Normal file
40
.sqlx/query-48fd98db66a68865babe4ac6a453de747bba2ce1da198dda4b6f3b78eacfebf3.json
generated
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT username, email FROM users WHERE pid = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "users",
|
||||
"name": "username"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"type_info": "Varchar",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "users",
|
||||
"name": "email"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "48fd98db66a68865babe4ac6a453de747bba2ce1da198dda4b6f3b78eacfebf3"
|
||||
}
|
||||
|
|
@ -211,6 +211,17 @@
|
|||
"name": "verification_code"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "eula_version",
|
||||
"type_info": "Int4",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "users",
|
||||
"name": "eula_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
|
@ -237,6 +248,7 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
|
|
|
|||
15
.sqlx/query-6c276038b92bef06b30da32dab42256be431b361cfd36754aa41b6da220618cc.json
generated
Normal file
15
.sqlx/query-6c276038b92bef06b30da32dab42256be431b361cfd36754aa41b6da220618cc.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET password = $1 WHERE pid = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6c276038b92bef06b30da32dab42256be431b361cfd36754aa41b6da220618cc"
|
||||
}
|
||||
15
.sqlx/query-7d5d57a9c0065301382767c85de4f25f0636ec0e7c0beed4cdb627c419e407c4.json
generated
Normal file
15
.sqlx/query-7d5d57a9c0065301382767c85de4f25f0636ec0e7c0beed4cdb627c419e407c4.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO device_parental_controls (serial_number, email, updated_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (serial_number)\n DO UPDATE SET email = EXCLUDED.email, updated_at = NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7d5d57a9c0065301382767c85de4f25f0636ec0e7c0beed4cdb627c419e407c4"
|
||||
}
|
||||
15
.sqlx/query-7e74c28ce6daa8155e268756411f73f1d281a736aa5a838ad3e72fce0e9a109e.json
generated
Normal file
15
.sqlx/query-7e74c28ce6daa8155e268756411f73f1d281a736aa5a838ad3e72fce0e9a109e.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET eula_version = $1 WHERE pid = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7e74c28ce6daa8155e268756411f73f1d281a736aa5a838ad3e72fce0e9a109e"
|
||||
}
|
||||
22
.sqlx/query-8cd49b3defe144fe73bdeee1ce000b9bb8716320c6b421395a04f12bd826a469.json
generated
Normal file
22
.sqlx/query-8cd49b3defe144fe73bdeee1ce000b9bb8716320c6b421395a04f12bd826a469.json
generated
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users SET\n gender = COALESCE($1, gender),\n region = COALESCE($2, region),\n country = COALESCE($3, country),\n language = COALESCE($4, language),\n timezone = COALESCE($5, timezone),\n marketing_allowed = COALESCE($6, marketing_allowed),\n off_device_allowed = COALESCE($7, off_device_allowed),\n password = COALESCE($8, password)\n WHERE pid = $9\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bpchar",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8cd49b3defe144fe73bdeee1ce000b9bb8716320c6b421395a04f12bd826a469"
|
||||
}
|
||||
|
|
@ -211,6 +211,17 @@
|
|||
"name": "verification_code"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "eula_version",
|
||||
"type_info": "Int4",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "users",
|
||||
"name": "eula_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
|
@ -237,6 +248,7 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,6 +68,17 @@
|
|||
"name": "expires"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "cert_hash",
|
||||
"type_info": "Bytea",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "tokens",
|
||||
"name": "cert_hash"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
|
@ -83,7 +94,8 @@
|
|||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9d3cee43a86cead9a6d078abc1266fc2a97ac6e25a9733d1d20faf555c67abe1"
|
||||
|
|
|
|||
15
.sqlx/query-9d41e04076235e9a8c857c58c6f5fe7b26cba4da50ce8527ba32128152a31ce6.json
generated
Normal file
15
.sqlx/query-9d41e04076235e9a8c857c58c6f5fe7b26cba4da50ce8527ba32128152a31ce6.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET account_level = $1 WHERE username = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9d41e04076235e9a8c857c58c6f5fe7b26cba4da50ce8527ba32128152a31ce6"
|
||||
}
|
||||
15
.sqlx/query-beeeea07993ba8daac1e43c985e8c840f66eec4e5458d3709c0e0f09afe51377.json
generated
Normal file
15
.sqlx/query-beeeea07993ba8daac1e43c985e8c840f66eec4e5458d3709c0e0f09afe51377.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "update certificates set serial = $1 where hash = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "beeeea07993ba8daac1e43c985e8c840f66eec4e5458d3709c0e0f09afe51377"
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(select 1 from users where pid = $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c8d50662530cac49c4261fb321cd15f9e4bafdfca12d2130a873d44a88dd435b"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "insert into tokens (token_type, pid, title_id)\n values ($1, $2, $3) returning token_id, random",
|
||||
"query": "insert into tokens (token_type, pid, title_id, cert_hash)\n values ($1, $2, $3, $4) returning token_id, random",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -30,7 +30,8 @@
|
|||
"Left": [
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Varchar"
|
||||
"Varchar",
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
|
|
@ -38,5 +39,5 @@
|
|||
false
|
||||
]
|
||||
},
|
||||
"hash": "e5a2f7f28c3d7b9524d3dce48a9e47d6180ff634ebf59f3a1efd92b797170ac2"
|
||||
"hash": "d8298703381f0e220bdbe122dbdc4dc5cd7ae99fac34ae16023f9a6a701857c9"
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT game_server_id, maintenance_mode, address, port FROM nex_servers WHERE title_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "game_server_id",
|
||||
"type_info": "Varchar",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "nex_servers",
|
||||
"name": "game_server_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "maintenance_mode",
|
||||
"type_info": "Bool",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "nex_servers",
|
||||
"name": "maintenance_mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "address",
|
||||
"type_info": "Inet",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "nex_servers",
|
||||
"name": "address"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "port",
|
||||
"type_info": "Int4",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "nex_servers",
|
||||
"name": "port"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e3349c0e5ab82bbef359cf573caf454f23588f3cfa27299e58863dc9397d55de"
|
||||
}
|
||||
14
.sqlx/query-ffef185a4db4b101477f7a78444192a7dfa8af98c00394978a9627b5ecdae9b5.json
generated
Normal file
14
.sqlx/query-ffef185a4db4b101477f7a78444192a7dfa8af98c00394978a9627b5ecdae9b5.json
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM users WHERE pid = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ffef185a4db4b101477f7a78444192a7dfa8af98c00394978a9627b5ecdae9b5"
|
||||
}
|
||||
2423
Cargo.lock
generated
2423
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
|
@ -16,7 +16,7 @@ incremental = false
|
|||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
log = "0.4.26"
|
||||
quick-xml = { version = "0.40.0", features = ["serialize"] }
|
||||
quick-xml = { version = "0.41.0", features = ["serialize"] }
|
||||
tokio = "1.43.0"
|
||||
dotenvy = "0.15.7"
|
||||
once_cell = "1.20.3"
|
||||
|
|
@ -25,7 +25,7 @@ chrono = { version = "0.4.39", features = ["serde"] }
|
|||
argon2 = "0.5.3"
|
||||
sha2 = "0.11.0"
|
||||
bytemuck = { version = "1.21.0", features = ["derive"] }
|
||||
base64 = "0.22.1"
|
||||
base64 = "0.23.0"
|
||||
hex = "0.4.3"
|
||||
thiserror = "2.0.11"
|
||||
bcrypt = "0.19.0"
|
||||
|
|
@ -36,7 +36,7 @@ md-5 = "0.11.0"
|
|||
cbc = "0.1.2"
|
||||
crc32fast = "1.4.2"
|
||||
gxhash = "3.4.1"
|
||||
sentry = "0.48.0"
|
||||
sentry = "0.49.0"
|
||||
rocket_cors = "0.6.0"
|
||||
|
||||
juniper = { version = "0.17.0", features = ["chrono"] }
|
||||
|
|
@ -47,11 +47,12 @@ lettre = "0.11.15"
|
|||
rand = { version = "0.10.0", features = ["std"] }
|
||||
reqwest = "0.13.0"
|
||||
binrw = "0.15.1"
|
||||
ecdsa = { version = "0.16.9", features = ["pem", "std", "verifying"] }
|
||||
ecdsa = { version = "0.17.0", features = ["pem", "std", "algorithm", "digest"] }
|
||||
sha256 = "1.6.0"
|
||||
p256 = "0.13.2"
|
||||
k256 = "0.13.4"
|
||||
dsa = "0.6.3"
|
||||
p256 = "0.14.0"
|
||||
k256 = "0.14.0"
|
||||
dsa = "0.7.0"
|
||||
openssl = {version = "0.10.78", features = ["vendored"]}
|
||||
time = "0.3.47"
|
||||
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
|
||||
nex-account = { version = "0.2.4", registry = "spbr" }
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<?xml version="1.0"?><agreements><agreement><country>US</country><language>en</language><language_name>English</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Splatfestival Network Services Agreement]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[Welcome to the Splatfestival Network! If you are seeing this, you have correctly installed the environment! Please note that we have rules to follow. You only have one warning. The rules are the following: Do not harrass people. Do not advertise your own servers and such. Do not use ANY hacks. That includes Silverlight, you will be banned. Do not try and stress test the server. Do not DDoS the server. If we detect a pirated copy of Splatoon, you will be banned without appeal. Do not impersonate staff members. If you have any questions, please contact TV/Maple(username: djt.v.) on discord.]]></main_text><sub_title><![CDATA[SPFN Privacy Policy]]></sub_title><sub_text index="1"><![CDATA[Please note that we will store the following: Email Address, IP Address, birthdate and timezone. These are required for the following purposes: Email is required to validate you as a real person. It will only be stored for the purpose of sending you a validation email. Your IP address is required to make sure you do not bypass any bans and store your current connection to the server so that you cannot connect twice. Your birthdate is required to make sure you are old enough to access our services and your timezone is required to have a valid created date for your account.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement><agreement><country>US</country><language>en</language><language_name>Español</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Acuerdo de servicios de red de Splatfestival]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[¡Bienvenido a la red Splatfestival! Si ves esto, ¡has instalado el entorno correctamente! Ten en cuenta que tenemos reglas que seguir. Solo tienes una advertencia. Las reglas son las siguientes: No acoses a la gente. No hagas publicidad de tus propios servidores ni nada parecido. No uses NINGÚN hack. Eso incluye Silverlight, serás baneado. No intentes poner a prueba el servidor. No hagas DDoS en el servidor. Si detectamos una copia pirateada de Splatoon, serás baneado sin posibilidad de apelación. No te hagas pasar por miembros del personal. Si tienes alguna pregunta, ponte en contacto con djt.v. en discord.]]></main_text><sub_title><![CDATA[Política de Privacidad]]></sub_title><sub_text index="1"><![CDATA[Tenga en cuenta que almacenaremos lo siguiente: d* Connection #0 to host account.spfn.cc left intactirección de correo electrónico, dirección IP, fecha de nacimiento y zona horaria. Estos son necesarios para los siguientes fines: el correo electrónico es necesario para validarlo como una persona real. Solo se almacenará con el fin de enviarle un correo electrónico de validación. Su dirección IP es necesaria para asegurarnos de que no eluda ninguna prohibición y para almacenar su conexión actual al servidor para que no pueda conectarse dos veces. Su fecha de nacimiento es necesaria para asegurarnos de que tiene la edad suficiente para acceder a nuestros servicios y su zona horaria es necesaria para tener una fecha de creación válida para su cuenta.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement></agreements>
|
||||
<?xml version="1.0"?><agreements><agreement><country>US</country><language>en</language><language_name>English</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Splatfestival Network Services Agreement]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[Welcome to the Splatfestival Network! If you are seeing this, you have correctly installed the environment! Please note that we have rules to follow. You only have one warning. The rules are the following: Do not harrass people. Do not advertise your own servers and such. Do not use ANY hacks. That includes Silverlight, you will be banned. Do not try and stress test the server. Do not DDoS the server. If we detect a pirated copy of Splatoon, you will be banned without appeal. Do not impersonate staff members. If you have any questions, please contact TV/Maple(username: djt.v.) on discord.]]></main_text><sub_title><![CDATA[SPFN Privacy Policy]]></sub_title><sub_text index="1"><![CDATA[Please note that we will store the following: Email Address, IP Address, birthdate and timezone. These are required for the following purposes: Email is required to validate you as a real person. It will only be stored for the purpose of sending you a validation email. Your IP address is required to make sure you do not bypass any bans and store your current connection to the server so that you cannot connect twice. Your birthdate is required to make sure you are old enough to access our services and your timezone is required to have a valid created date for your account.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement><agreement><country>US</country><language>en</language><language_name>Español</language_name><publish_date>2014-09-29T20:07:35</publish_date><texts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="chunkedStoredAgreementText"><main_title><![CDATA[Acuerdo de servicios de red de Splatfestival]]></main_title><agree_text><![CDATA[I Accept]]></agree_text><non_agree_text><![CDATA[I Decline]]></non_agree_text><main_text index="1"><![CDATA[¡Bienvenido a la red Splatfestival! Si ves esto, ¡has instalado el entorno correctamente! Ten en cuenta que tenemos reglas que seguir. Solo tienes una advertencia. Las reglas son las siguientes: No acoses a la gente. No hagas publicidad de tus propios servidores ni nada parecido. No uses NINGÚN hack. Eso incluye Silverlight, serás baneado. No intentes poner a prueba el servidor. No hagas DDoS en el servidor. Si detectamos una copia pirateada de Splatoon, serás baneado sin posibilidad de apelación. No te hagas pasar por miembros del personal. Si tienes alguna pregunta, ponte en contacto con djt.v. en discord.]]></main_text><sub_title><![CDATA[Política de Privacidad]]></sub_title><sub_text index="1"><![CDATA[Tenga en cuenta que almacenaremos lo siguiente: dirección de correo electrónico, dirección IP, fecha de nacimiento y zona horaria. Estos son necesarios para los siguientes fines: el correo electrónico es necesario para validarlo como una persona real. Solo se almacenará con el fin de enviarle un correo electrónico de validación. Su dirección IP es necesaria para asegurarnos de que no eluda ninguna prohibición y para almacenar su conexión actual al servidor para que no pueda conectarse dos veces. Su fecha de nacimiento es necesaria para asegurarnos de que tiene la edad suficiente para acceder a nuestros servicios y su zona horaria es necesaria para tener una fecha de creación válida para su cuenta.]]></sub_text></texts><type>NINTENDO-NETWORK-EULA</type><version>0300</version></agreement></agreements>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
color: #B60000 !important;
|
||||
}
|
||||
img.logo {
|
||||
content: url("https://spfn.net/res/img/spfn.png") !important;
|
||||
content: url("https://spfn.spbr.net/res/img/spfn.png") !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
@ -91,8 +91,8 @@
|
|||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://spfn.net">
|
||||
<img class="logo" width="auto" height="48px" src="https://spfn.net/res/img/spfn.png" alt="SPFN">
|
||||
<a href="https://spfn.spbr.net">
|
||||
<img class="logo" width="auto" height="48px" src="https://spfn.spbr.net/res/img/spfn.png" alt="SPFN">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -122,19 +122,6 @@
|
|||
Your Splatfestival Network ID activation is almost complete.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="16px" style="line-height: 16px;"> </td>
|
||||
</tr>
|
||||
<!-- <tr>-->
|
||||
<!-- <td class="confirm-link" bgcolor="#673db6" style="font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center">-->
|
||||
<!-- <a href="{{confirmation-href}}" style="text-decoration: none; color: #ffffff; " width="100%">-->
|
||||
<!-- Confirm email address-->
|
||||
<!-- </a>-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
<tr>
|
||||
<td width="100%" height="48px" style="line-height: 48px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Enter the following 6-digit code on your console:
|
||||
|
|
|
|||
190
res/email/masterKeyTemplate.html
Normal file
190
res/email/masterKeyTemplate.html
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" lang="en">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
supported-color-schemes:light dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
body.email-body,
|
||||
table.centerer,
|
||||
table.wrapper {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
table.card {
|
||||
background-color: #B60000 !important;
|
||||
}
|
||||
span.shoutout {
|
||||
color: #FFB3B3 !important;
|
||||
}
|
||||
td.confirm-link {
|
||||
background-color: #FF4D4D !important;
|
||||
}
|
||||
td.confirm-code {
|
||||
background-color: #FFB3B3 !important;
|
||||
color: #660000 !important;
|
||||
}
|
||||
td.notice {
|
||||
color: #FF4D4D !important;
|
||||
}
|
||||
td.notice a {
|
||||
color: #B60000 !important;
|
||||
}
|
||||
img.logo {
|
||||
content: url("https://spfn.spbr.net/res/img/spfn.png") !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.email-body,
|
||||
table.centerer,
|
||||
table.wrapper {
|
||||
background-color: #3B1B1B !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
table.card {
|
||||
background-color: #4A2323 !important;
|
||||
}
|
||||
span.shoutout {
|
||||
color: #FF9999 !important;
|
||||
}
|
||||
td.confirm-link {
|
||||
background-color: #B60000 !important;
|
||||
}
|
||||
td.confirm-code {
|
||||
background-color: #652323 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
td.notice {
|
||||
color: #C18989 !important;
|
||||
}
|
||||
td.notice a {
|
||||
color: #F5C1C1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="email-body" bgcolor="#1B1F3B" style="margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; font-family: Poppins, Arial, Helvetica, sans-serif;">
|
||||
<div style="display:none;">Hello. You have requested a reset of parental controls on your console.</div>
|
||||
<table class="centerer" bgcolor="#1B1F3B" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="wrapper" bgcolor="#1B1F3B" style="font-family: Poppins, Arial, Helvetica, sans-serif;" border="0" cellpadding="0" cellspacing="0" height="100%" width="420px">
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td width="32px"> </td>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td height="36px" style="line-height: 36px;" width="100%"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://spfn.spbr.net">
|
||||
<img class="logo" width="auto" height="48px" src="https://spfn.spbr.net/res/img/spfn.png" alt="SPFN">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="36px" style="line-height: 36px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="card" bgcolor="#23274a" style="color: #ffffff; border-radius: 10px;" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td width="24px" height="100%"> </td>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr width="100%" height="48px" style="line-height: 48px;">
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr style="font-size: 24px; font-weight: 700;">
|
||||
<td>
|
||||
Hello.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color: #ffffff; ">
|
||||
Your email was linked to a Wii U console with serial number {{serial}} for parental controls.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
You have requested a master key to reset your parental controls PIN. The master key you must enter on your Wii U is:
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="confirm-code" bgcolor="#373c65" style="color: #ffffff; font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center">
|
||||
{{mkey}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="36px" style="line-height: 36px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Please make sure to change your PIN as soon as possible.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
The SPFN team
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="24px" height="100%"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="18px" style="line-height: 18px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="notice" style="color: #8990c1; font-size: 12px;">
|
||||
Note: this email message was auto-generated, please do not respond. For further assistance, please join our <a href="https://discord.gg/grMSxZf" style="text-decoration: none; color: #ffffff; ">Discord server</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="48px" style="line-height: 48px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="32px"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
177
res/email/parentalControlsTemplate.html
Normal file
177
res/email/parentalControlsTemplate.html
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" lang="en">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
supported-color-schemes:light dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
body.email-body,
|
||||
table.centerer,
|
||||
table.wrapper {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
table.card {
|
||||
background-color: #B60000 !important;
|
||||
}
|
||||
span.shoutout {
|
||||
color: #FFB3B3 !important;
|
||||
}
|
||||
td.confirm-link {
|
||||
background-color: #FF4D4D !important;
|
||||
}
|
||||
td.confirm-code {
|
||||
background-color: #FFB3B3 !important;
|
||||
color: #660000 !important;
|
||||
}
|
||||
td.notice {
|
||||
color: #FF4D4D !important;
|
||||
}
|
||||
td.notice a {
|
||||
color: #B60000 !important;
|
||||
}
|
||||
img.logo {
|
||||
content: url("https://spfn.spbr.net/res/img/spfn.png") !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.email-body,
|
||||
table.centerer,
|
||||
table.wrapper {
|
||||
background-color: #3B1B1B !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
table.card {
|
||||
background-color: #4A2323 !important;
|
||||
}
|
||||
span.shoutout {
|
||||
color: #FF9999 !important;
|
||||
}
|
||||
td.confirm-link {
|
||||
background-color: #B60000 !important;
|
||||
}
|
||||
td.confirm-code {
|
||||
background-color: #652323 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
td.notice {
|
||||
color: #C18989 !important;
|
||||
}
|
||||
td.notice a {
|
||||
color: #F5C1C1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="email-body" bgcolor="#1B1F3B" style="margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; font-family: Poppins, Arial, Helvetica, sans-serif;">
|
||||
<div style="display:none;">Hello. The console with the serial number {{serial}} has been linked to your email for parental controls.</div>
|
||||
<table class="centerer" bgcolor="#1B1F3B" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="wrapper" bgcolor="#1B1F3B" style="font-family: Poppins, Arial, Helvetica, sans-serif;" border="0" cellpadding="0" cellspacing="0" height="100%" width="420px">
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td width="32px"> </td>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td height="36px" style="line-height: 36px;" width="100%"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://spfn.spbr.net">
|
||||
<img class="logo" width="auto" height="48px" src="https://spfn.spbr.net/res/img/spfn.png" alt="SPFN">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="36px" style="line-height: 36px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="card" bgcolor="#23274a" style="color: #ffffff; border-radius: 10px;" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td width="24px" height="100%"> </td>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr width="100%" height="48px" style="line-height: 48px;">
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr style="font-size: 24px; font-weight: 700;">
|
||||
<td>
|
||||
Hello.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color: #ffffff; ">
|
||||
Your email was linked to a Wii U console with serial number {{serial}} for parental controls.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
If you did not authorize this, please contact our team at support@spbr.net.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="36px" style="line-height: 36px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
The SPFN team
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="24px" height="100%"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="18px" style="line-height: 18px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="notice" style="color: #8990c1; font-size: 12px;">
|
||||
Note: this email message was auto-generated, please do not respond. For further assistance, please join our <a href="https://discord.gg/grMSxZf" style="text-decoration: none; color: #ffffff; ">Discord server</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="48px" style="line-height: 48px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="32px"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
190
res/email/resetTemplate.html
Normal file
190
res/email/resetTemplate.html
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" lang="en">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
supported-color-schemes:light dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
body.email-body,
|
||||
table.centerer,
|
||||
table.wrapper {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
table.card {
|
||||
background-color: #B60000 !important;
|
||||
}
|
||||
span.shoutout {
|
||||
color: #FFB3B3 !important;
|
||||
}
|
||||
td.confirm-link {
|
||||
background-color: #FF4D4D !important;
|
||||
}
|
||||
td.confirm-code {
|
||||
background-color: #FFB3B3 !important;
|
||||
color: #660000 !important;
|
||||
}
|
||||
td.notice {
|
||||
color: #FF4D4D !important;
|
||||
}
|
||||
td.notice a {
|
||||
color: #B60000 !important;
|
||||
}
|
||||
img.logo {
|
||||
content: url("https://spfn.spbr.net/res/img/spfn.png") !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.email-body,
|
||||
table.centerer,
|
||||
table.wrapper {
|
||||
background-color: #3B1B1B !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
table.card {
|
||||
background-color: #4A2323 !important;
|
||||
}
|
||||
span.shoutout {
|
||||
color: #FF9999 !important;
|
||||
}
|
||||
td.confirm-link {
|
||||
background-color: #B60000 !important;
|
||||
}
|
||||
td.confirm-code {
|
||||
background-color: #652323 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
td.notice {
|
||||
color: #C18989 !important;
|
||||
}
|
||||
td.notice a {
|
||||
color: #F5C1C1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="email-body" bgcolor="#1B1F3B" style="margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; font-family: Poppins, Arial, Helvetica, sans-serif;">
|
||||
<div style="display:none;">Hello {{username}}. Your Splatfestival Network ID password reset has been processed. You may use the temporary password below to log into your account.</div>
|
||||
<table class="centerer" bgcolor="#1B1F3B" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="wrapper" bgcolor="#1B1F3B" style="font-family: Poppins, Arial, Helvetica, sans-serif;" border="0" cellpadding="0" cellspacing="0" height="100%" width="420px">
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td width="32px"> </td>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td height="36px" style="line-height: 36px;" width="100%"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://spfn.spbr.net">
|
||||
<img class="logo" width="auto" height="48px" src="https://spfn.spbr.net/res/img/spfn.png" alt="SPFN">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="36px" style="line-height: 36px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="card" bgcolor="#23274a" style="color: #ffffff; border-radius: 10px;" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td width="24px" height="100%"> </td>
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr width="100%" height="48px" style="line-height: 48px;">
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr style="font-size: 24px; font-weight: 700;">
|
||||
<td>
|
||||
Hello <span class="shoutout" style="color: #cab1fb;">{{username}}</span>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color: #ffffff; ">
|
||||
Your Splatfestival Network ID password reset has been processed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
You may use the following temporary password to login:
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="16px" style="line-height: 16px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="confirm-code" bgcolor="#373c65" style="color: #ffffff; font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center">
|
||||
{{password}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="48px" style="line-height: 48px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Please change your password as soon as you can on your Wii U. (Click your mii on the top-left of the Wii U Menu and scroll to "Change Password")
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="36px" style="line-height: 36px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
The SPFN team
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="24px" style="line-height: 24px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="24px" height="100%"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="18px" style="line-height: 18px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="notice" style="color: #8990c1; font-size: 12px;">
|
||||
Note: this email message was auto-generated, please do not respond. For further assistance, please join our <a href="https://discord.gg/grMSxZf" style="text-decoration: none; color: #ffffff; ">Discord server</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="100%" height="48px" style="line-height: 48px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="32px"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -64,6 +64,7 @@ pub struct User {
|
|||
pub updated: NaiveDateTime,
|
||||
pub nex_password: String,
|
||||
pub verification_code: Option<i32>,
|
||||
pub eula_version: i32,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
|
|
@ -117,7 +118,10 @@ pub fn generate_password(pid: i32, cleartext_password: &str) -> Option<String> {
|
|||
bcrypt::hash(password, 10).ok()
|
||||
}
|
||||
|
||||
pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<User> {
|
||||
pub async fn read_basic_auth_token(
|
||||
connection: &Pool,
|
||||
token: &str,
|
||||
) -> Option<(User, Option<[u8; 32]>)> {
|
||||
let data = match BASE64_STANDARD.decode(&token) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
|
|
@ -161,13 +165,69 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<Use
|
|||
let password_valid = user.verify_cleartext_password(&login_password);
|
||||
|
||||
if password_valid == Some(true) {
|
||||
Some(user)
|
||||
Some((user, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
|
||||
pub async fn read_hashed_basic_auth_token(
|
||||
connection: &Pool,
|
||||
token: &str,
|
||||
) -> Option<(User, Option<[u8; 32]>)> {
|
||||
let data = match BASE64_STANDARD.decode(token) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
println!("Failed to decode base64: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let decoded_token = match String::from_utf8(data) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
println!("Failed to convert decoded bytes to UTF-8 string: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let (login_username, login_hash) = match decoded_token.split_once(' ') {
|
||||
Some(parts) => parts,
|
||||
None => {
|
||||
println!("Failed to split hashed basic token into username and hash");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let user_result = sqlx::query_as!(
|
||||
User,
|
||||
"SELECT * FROM users WHERE username = $1",
|
||||
login_username
|
||||
)
|
||||
.fetch_one(connection)
|
||||
.await;
|
||||
|
||||
let user = match user_result {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
println!("Failed to fetch user from database: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let password_valid = user.verify_hashed_password(login_hash);
|
||||
|
||||
if password_valid == Some(true) {
|
||||
Some((user, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_bearer_auth_token(
|
||||
connection: &Pool,
|
||||
token: &str,
|
||||
) -> Option<(User, Option<[u8; 32]>)> {
|
||||
let data = TokenData::decode(token)?;
|
||||
|
||||
let token_info = sqlx::query!(
|
||||
|
|
@ -189,11 +249,11 @@ pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<Us
|
|||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(user)
|
||||
Some((user, token_info.cert_hash.and_then(|v| v.try_into().ok())))
|
||||
}
|
||||
|
||||
pub fn generate_nex_password() -> String {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand::rng();
|
||||
let mut output = String::with_capacity(16);
|
||||
|
||||
while output.len() < 16 {
|
||||
|
|
@ -214,6 +274,7 @@ pub fn generate_nex_password() -> String {
|
|||
|
||||
pub struct Auth<const FORCE_BEARER_AUTH: bool = true, const USE_CERT: bool = FORCE_BEARER_AUTH>(
|
||||
pub User,
|
||||
pub Option<[u8; 32]>,
|
||||
);
|
||||
|
||||
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> AsRef<User>
|
||||
|
|
@ -278,6 +339,7 @@ pub async fn link_certificate_to_pid(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// todo: make this more consistent by requiring a cert to be asociated to the token if USE_CERT is set
|
||||
#[async_trait]
|
||||
impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
|
|
@ -297,13 +359,14 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
|||
|
||||
let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS));
|
||||
|
||||
let user = match auth_type {
|
||||
let data = match auth_type {
|
||||
"Basic" if !FORCE_BEARER_AUTH => read_basic_auth_token(pool, token).await,
|
||||
"HashedBasic" if !FORCE_BEARER_AUTH => read_hashed_basic_auth_token(pool, token).await,
|
||||
"Bearer" => read_bearer_auth_token(pool, token).await,
|
||||
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
|
||||
};
|
||||
|
||||
let Some(user) = user else {
|
||||
let Some((user, cert)) = data else {
|
||||
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
|
||||
};
|
||||
|
||||
|
|
@ -316,7 +379,7 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
|||
// ..user
|
||||
// };
|
||||
|
||||
Outcome::Success(Self(user))
|
||||
Outcome::Success(Self(user, cert))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
101
src/email.rs
101
src/email.rs
|
|
@ -36,3 +36,104 @@ pub async fn send_verification_email(to: &str, code: i32, username: &str) -> Res
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_reset_email(to: &str, pwd: &str, username: &str) -> Result<(), String> {
|
||||
let smtp_user = env::var("SMTP_USER").map_err(|_| "SMTP_USER not set".to_string())?;
|
||||
let smtp_pass = env::var("SMTP_PASS").map_err(|_| "SMTP_PASS not set".to_string())?;
|
||||
let smtp_server = env::var("SMTP_SERVER").map_err(|_| "SMTP_SERVER not set".to_string())?;
|
||||
|
||||
// Load template
|
||||
let template = fs::read_to_string("res/email/resetTemplate.html")
|
||||
.map_err(|e| format!("Failed to read email template: {}", e))?;
|
||||
|
||||
// Replace placeholders
|
||||
let body = template
|
||||
.replace("{{username}}", username)
|
||||
.replace("{{password}}", pwd);
|
||||
|
||||
let email = Message::builder()
|
||||
.from(smtp_user.parse().unwrap())
|
||||
.to(to.parse().unwrap())
|
||||
.subject("Password Reset for SPFN")
|
||||
.header(lettre::message::header::ContentType::TEXT_HTML)
|
||||
.body(body)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let creds = Credentials::new(smtp_user, smtp_pass);
|
||||
|
||||
let mailer = SmtpTransport::relay(&smtp_server)
|
||||
.map_err(|e| e.to_string())?
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
mailer.send(&email).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_pc_email(to: &str, serial: &str) -> Result<(), String> {
|
||||
let smtp_user = env::var("SMTP_USER").map_err(|_| "SMTP_USER not set".to_string())?;
|
||||
let smtp_pass = env::var("SMTP_PASS").map_err(|_| "SMTP_PASS not set".to_string())?;
|
||||
let smtp_server = env::var("SMTP_SERVER").map_err(|_| "SMTP_SERVER not set".to_string())?;
|
||||
|
||||
// Load template
|
||||
let template = fs::read_to_string("res/email/parentalControlsTemplate.html")
|
||||
.map_err(|e| format!("Failed to read email template: {}", e))?;
|
||||
|
||||
// Replace placeholders
|
||||
let body = template
|
||||
.replace("{{serial}}", serial);
|
||||
|
||||
let email = Message::builder()
|
||||
.from(smtp_user.parse().unwrap())
|
||||
.to(to.parse().unwrap())
|
||||
.subject("Parental controls confirmation for SPFN")
|
||||
.header(lettre::message::header::ContentType::TEXT_HTML)
|
||||
.body(body)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let creds = Credentials::new(smtp_user, smtp_pass);
|
||||
|
||||
let mailer = SmtpTransport::relay(&smtp_server)
|
||||
.map_err(|e| e.to_string())?
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
mailer.send(&email).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_reset_pin_email(to: &str, serial: &str, master_key: &str) -> Result<(), String> {
|
||||
let smtp_user = env::var("SMTP_USER").map_err(|_| "SMTP_USER not set".to_string())?;
|
||||
let smtp_pass = env::var("SMTP_PASS").map_err(|_| "SMTP_PASS not set".to_string())?;
|
||||
let smtp_server = env::var("SMTP_SERVER").map_err(|_| "SMTP_SERVER not set".to_string())?;
|
||||
|
||||
// Load template
|
||||
let template = fs::read_to_string("res/email/masterKeyTemplate.html")
|
||||
.map_err(|e| format!("Failed to read email template: {}", e))?;
|
||||
|
||||
// Replace placeholders
|
||||
let body = template
|
||||
.replace("{{serial}}", serial)
|
||||
.replace("{{mkey}}", master_key);
|
||||
|
||||
let email = Message::builder()
|
||||
.from(smtp_user.parse().unwrap())
|
||||
.to(to.parse().unwrap())
|
||||
.subject("Parental controls PIN reset for SPFN")
|
||||
.header(lettre::message::header::ContentType::TEXT_HTML)
|
||||
.body(body)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let creds = Credentials::new(smtp_user, smtp_pass);
|
||||
|
||||
let mailer = SmtpTransport::relay(&smtp_server)
|
||||
.map_err(|e| e.to_string())?
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
mailer.send(&email).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
35
src/json_api/admin/bans.rs
Normal file
35
src/json_api/admin/bans.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use rocket::{post, FromForm, State};
|
||||
use rocket::form::Form;
|
||||
use rocket::http::Status;
|
||||
use crate::account::account::Auth;
|
||||
use crate::Pool;
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct AdminRequest<'r> {
|
||||
pub username: &'r str,
|
||||
}
|
||||
|
||||
#[post("/api/v2/admin/ban", data = "<request>")]
|
||||
pub async fn ban_user(pool: &State<Pool>, auth: Auth<true>, request: Form<AdminRequest<'_>>) -> Result<(), Status> {
|
||||
if auth.account_level < 2 {
|
||||
return Err(Status::Forbidden);
|
||||
};
|
||||
|
||||
log::info!("banning user {:?} from moderator {:?}", request.username, auth.username);
|
||||
|
||||
let _row = sqlx::query!(
|
||||
"UPDATE users SET account_level = $1 WHERE username = $2",
|
||||
-1,
|
||||
request.username
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("failed to execute query: {:?}", e);
|
||||
return Err::<(), rocket::http::Status>(Status::InternalServerError);
|
||||
});
|
||||
|
||||
log::info!("banned user {:?}", request.username);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
1
src/json_api/admin/mod.rs
Normal file
1
src/json_api/admin/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod bans;
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod oauth;
|
||||
pub mod users;
|
||||
pub mod users;
|
||||
pub mod admin;
|
||||
|
|
@ -65,12 +65,13 @@ pub async fn authorize_submit(
|
|||
let expires_at = Utc::now().naive_utc() + Duration::minutes(5);
|
||||
|
||||
if let Err(db_err) = sqlx::query(
|
||||
"INSERT INTO oauth_auth_codes (code, pid, redirect_uri, expires_at) VALUES ($1, $2, $3, $4)"
|
||||
"INSERT INTO oauth_auth_codes (code, pid, redirect_uri, expires_at, client_id) VALUES ($1, $2, $3, $4, $5)"
|
||||
)
|
||||
.bind(&secure_auth_code)
|
||||
.bind(pid)
|
||||
.bind(form_data.redirect_uri)
|
||||
.bind(expires_at)
|
||||
.bind(form_data.client_id)
|
||||
.execute(pool.inner())
|
||||
.await {
|
||||
eprintln!("failed to save auth code: {:?}", db_err);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
use crate::Pool;
|
||||
use serde::Serialize;
|
||||
use rocket::FromForm;
|
||||
use sha2::{Sha256, Digest};
|
||||
use bytemuck::bytes_of;
|
||||
use rocket::{post, State, form::Form, http::Status, serde::json::Json};
|
||||
use sqlx::Row;
|
||||
use std::env;
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::nnid::oauth::generate_token::create_token;
|
||||
use crate::nnid::oauth::generate_token::token_type::AUTH_TOKEN;
|
||||
use bytemuck::bytes_of;
|
||||
use chrono::Utc;
|
||||
|
||||
pub static CLIENT_SECRET: Lazy<String> = Lazy::new(|| {
|
||||
env::var("OAUTH_CLIENT_SECRET").expect("OAUTH_CLIENT_SECRET not set")
|
||||
});
|
||||
use rocket::FromForm;
|
||||
use rocket::{State, form::Form, http::Status, post, serde::json::Json};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::Row;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OAuthTokenResponse {
|
||||
|
|
@ -34,20 +28,14 @@ pub struct TokenRequest<'r> {
|
|||
}
|
||||
|
||||
pub fn verify_nintendo_password(pid: i32, text_password: &str, db_bcrypt_hash: &str) -> bool {
|
||||
// maple: binder, there's already a function for this, why duplicate code?
|
||||
// binder: dear maple, with this function i can just hand it a pid, the password from the user and the db_bcrypt_hash and it gives me a true/false value.
|
||||
let mut sha = Sha256::new();
|
||||
sha.update(bytes_of(&pid));
|
||||
sha.update(&[0x02, 0x65, 0x43, 0x46]);
|
||||
sha.update(text_password.as_bytes());
|
||||
let hashed_password_hex = hex::encode(sha.finalize());
|
||||
match bcrypt::verify(hashed_password_hex, db_bcrypt_hash) {
|
||||
Ok(valid) => valid,
|
||||
Err(_) => false,
|
||||
}
|
||||
bcrypt::verify(hashed_password_hex, db_bcrypt_hash).unwrap_or(false)
|
||||
}
|
||||
|
||||
// dummy error responses
|
||||
#[derive(Serialize)]
|
||||
pub struct OAuthErrorResponse {
|
||||
pub error: String,
|
||||
|
|
@ -55,47 +43,92 @@ pub struct OAuthErrorResponse {
|
|||
|
||||
#[post("/api/v2/oauth2/generate_token", data = "<form_data>")]
|
||||
pub async fn generate_token(
|
||||
pool: &State<Pool>,
|
||||
form_data: Form<TokenRequest<'_>>
|
||||
pool: &State<Pool>,
|
||||
form_data: Form<TokenRequest<'_>>,
|
||||
) -> Result<Json<OAuthTokenResponse>, (Status, Json<OAuthErrorResponse>)> {
|
||||
|
||||
if form_data.client_id != Some("account") {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
|
||||
));
|
||||
}
|
||||
println!("redirect URI is: {:?}", form_data.redirect_uri);
|
||||
|
||||
let cl_secret: String = CLIENT_SECRET.clone();
|
||||
if form_data.client_secret != Some(&cl_secret) {
|
||||
return Err((
|
||||
let client_id = form_data.client_id.ok_or((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_client".to_string(),
|
||||
}),
|
||||
))?;
|
||||
|
||||
let client_row = match sqlx::query(
|
||||
"SELECT client_secret, is_confidential FROM oauth_clients WHERE client_id = $1",
|
||||
)
|
||||
.bind(client_id)
|
||||
.fetch_optional(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
return Err((
|
||||
Status::Unauthorized,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_client".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let db_client_secret: Option<String> = client_row.get("client_secret");
|
||||
let is_confidential: bool = client_row.get("is_confidential");
|
||||
|
||||
if is_confidential {
|
||||
let provided_secret = form_data.client_secret.ok_or((
|
||||
Status::Unauthorized,
|
||||
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
|
||||
));
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_client".to_string(),
|
||||
}),
|
||||
))?;
|
||||
|
||||
let matches = match db_client_secret {
|
||||
Some(secret) => secret == provided_secret,
|
||||
None => false,
|
||||
};
|
||||
|
||||
if !matches {
|
||||
return Err((
|
||||
Status::Unauthorized,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_client".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// i'm only supporting the password grant incase someone feels lazy.
|
||||
// i'm only supporting the password grant for the id server.
|
||||
let pid: i32 = match form_data.grant_type {
|
||||
"password" => {
|
||||
let username = form_data.username.ok_or((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_request".to_string(),
|
||||
}),
|
||||
))?;
|
||||
let password = form_data.password.ok_or((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_request".to_string(),
|
||||
}),
|
||||
))?;
|
||||
|
||||
let user_row = match sqlx::query("SELECT pid, password FROM users WHERE username = $1")
|
||||
.bind(username)
|
||||
.fetch_optional(pool.inner())
|
||||
.await
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
)),
|
||||
_ => {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let db_pid: i32 = user_row.get("pid");
|
||||
|
|
@ -104,31 +137,39 @@ pub async fn generate_token(
|
|||
if !verify_nintendo_password(db_pid, password, &db_bcrypt_hash) {
|
||||
return Err((
|
||||
Status::Unauthorized,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
db_pid
|
||||
},
|
||||
}
|
||||
|
||||
"authorization_code" => {
|
||||
let incoming_code = form_data.code.ok_or((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_request".to_string(),
|
||||
}),
|
||||
))?;
|
||||
|
||||
|
||||
let code_row = match sqlx::query(
|
||||
"SELECT pid, expires_at, used FROM oauth_auth_codes WHERE code = $1"
|
||||
"SELECT pid, expires_at, used FROM oauth_auth_codes WHERE code = $1",
|
||||
)
|
||||
.bind(incoming_code)
|
||||
.fetch_optional(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
)),
|
||||
_ => {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let is_used: bool = code_row.get("used");
|
||||
|
|
@ -137,8 +178,10 @@ pub async fn generate_token(
|
|||
|
||||
if is_used || expires_at < Utc::now().naive_utc() {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -148,15 +191,19 @@ pub async fn generate_token(
|
|||
.await;
|
||||
|
||||
target_pid
|
||||
},
|
||||
}
|
||||
|
||||
_ => return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "unsupported_grant_type".to_string() })
|
||||
))
|
||||
_ => {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse {
|
||||
error: "unsupported_grant_type".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let token = create_token(pool.inner(), pid, AUTH_TOKEN, None).await;
|
||||
let token = create_token(pool.inner(), pid, AUTH_TOKEN, None, None).await;
|
||||
|
||||
Ok(Json(OAuthTokenResponse {
|
||||
access_token: token,
|
||||
|
|
|
|||
40
src/json_api/users/console.rs
Normal file
40
src/json_api/users/console.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use rocket::serde::json::Json;
|
||||
use rocket::{get, State};
|
||||
use rocket::http::Status;
|
||||
use crate::account::account::Auth;
|
||||
use serde::Serialize;
|
||||
use crate::Pool;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ConsoleData {
|
||||
pub serial: String,
|
||||
pub cert_hash: String,
|
||||
}
|
||||
|
||||
#[get("/api/v2/users/@me/console")]
|
||||
pub async fn get_own_console(pool: &State<Pool>, auth: Auth<true>) -> Result<Json<ConsoleData>, (Status, &'static str)> {
|
||||
let hash = auth.1.ok_or((Status::BadRequest, "token needs cert hash"))?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
"select serial from certificates where hash = $1",
|
||||
&hash[..]
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("failed to execute query: {:?}", e);
|
||||
(Status::InternalServerError, "error in database query")
|
||||
})?;
|
||||
|
||||
let hex_hash = hex::encode(hash);
|
||||
let serial = row.serial.ok_or((Status::BadRequest, "we had no serial"))?;
|
||||
|
||||
Ok(
|
||||
Json (
|
||||
ConsoleData {
|
||||
serial,
|
||||
cert_hash: hex_hash
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod profile;
|
||||
pub mod mii;
|
||||
pub mod delete;
|
||||
pub mod delete;
|
||||
pub mod console;
|
||||
12
src/main.rs
12
src/main.rs
|
|
@ -101,11 +101,21 @@ async fn launch() -> _ {
|
|||
nnid::support::validate,
|
||||
nnid::support::verify_email,
|
||||
nnid::support::resend_email,
|
||||
nnid::support::forgotten_password,
|
||||
nnid::support::send_parental_controls_pin,
|
||||
nnid::support::send_forgotten_pin,
|
||||
nnid::people::create_account,
|
||||
nnid::people::get_own_profile,
|
||||
nnid::people::get_device_owner,
|
||||
nnid::people::get_own_device,
|
||||
nnid::people::change_mii,
|
||||
nnid::people::update_account,
|
||||
nnid::people::delete_account,
|
||||
nnid::people::get_user_devices,
|
||||
nnid::people::get_device_status,
|
||||
nnid::people::inactivate_current_device,
|
||||
nnid::people::get_own_emails,
|
||||
nnid::people::update_primary_email,
|
||||
nnid::miis::get_miis,
|
||||
nnid::oauth::generate_token::generate_token,
|
||||
nnid::provider::get_nex_token,
|
||||
|
|
@ -118,6 +128,8 @@ async fn launch() -> _ {
|
|||
json_api::users::delete::delete_account,
|
||||
json_api::oauth::authorize::authorize_page,
|
||||
json_api::oauth::authorize::authorize_submit,
|
||||
json_api::admin::bans::ban_user,
|
||||
json_api::users::console::get_own_console,
|
||||
nnid::people::thing,
|
||||
// graphql::graphiql,
|
||||
// graphql::playground,
|
||||
|
|
|
|||
|
|
@ -1,37 +1,64 @@
|
|||
use std::{env, io};
|
||||
use std::collections::HashSet;
|
||||
use std::{env, io, fs};
|
||||
use serde::Deserialize;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::{get, Request};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use rocket::get;
|
||||
use rocket::response::content::RawXml;
|
||||
use tokio::fs::try_exists;
|
||||
use tokio::sync::RwLock;
|
||||
use rocket::async_trait;
|
||||
use crate::dsresponse::Ds;
|
||||
|
||||
pub static EVIL_AGREEMENT_THING: Lazy<RwLock<HashSet<String>>> = Lazy::new(|| Default::default());
|
||||
// pub static EVIL_AGREEMENT_THING: Lazy<RwLock<HashSet<String>>> = Lazy::new(|| Default::default());
|
||||
|
||||
pub struct CFIP(pub String);
|
||||
// pub struct CFIP(pub String);
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for CFIP{
|
||||
type Error = ();
|
||||
// #[async_trait]
|
||||
// impl<'r> FromRequest<'r> for CFIP{
|
||||
// type Error = ();
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
match request.headers().get("CF-Connecting-IP").next(){
|
||||
Some(v) => Outcome::Success(Self(v.to_owned())),
|
||||
None => Outcome::Error((Status::ImATeapot, ()))
|
||||
}
|
||||
}
|
||||
// async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
// match request.headers().get("CF-Connecting-IP").next(){
|
||||
// Some(v) => Outcome::Success(Self(v.to_owned())),
|
||||
// None => Outcome::Error((Status::ImATeapot, ()))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Agreement {
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Agreements {
|
||||
#[serde(rename = "agreement")]
|
||||
agreements: Vec<Agreement>,
|
||||
}
|
||||
|
||||
pub fn get_latest_eula_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let mut path = std::env::current_dir()?;
|
||||
path.push("res");
|
||||
path.push("agreement");
|
||||
path.push("DEFAULT.xml");
|
||||
|
||||
let content = fs::read_to_string(path)?;
|
||||
let parsed: Agreements = quick_xml::de::from_str(&content)?;
|
||||
|
||||
parsed
|
||||
.agreements
|
||||
.first()
|
||||
.map(|a| a.version.clone())
|
||||
.ok_or_else(|| "No agreement found in XML".into())
|
||||
}
|
||||
|
||||
pub static LATEST_EULA_VERSION: Lazy<i32> = Lazy::new(|| {
|
||||
get_latest_eula_version()
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(300)
|
||||
});
|
||||
|
||||
#[get("/v1/api/content/agreements/Nintendo-Network-EULA/<lang>/@latest")]
|
||||
pub async fn get_agreement(lang: &str, ip: CFIP) -> io::Result<Ds<RawXml<NamedFile>>>{
|
||||
|
||||
|
||||
pub async fn get_agreement(lang: &str) -> io::Result<Ds<RawXml<NamedFile>>>{
|
||||
let base_path = {
|
||||
// if this crashes then something is wrong with the server setup so crashing here is fine imo
|
||||
let mut path = env::current_dir().unwrap();
|
||||
|
|
@ -42,38 +69,26 @@ pub async fn get_agreement(lang: &str, ip: CFIP) -> io::Result<Ds<RawXml<NamedFi
|
|||
path
|
||||
};
|
||||
|
||||
if EVIL_AGREEMENT_THING.read().await.contains(&ip.0) {
|
||||
let path = {
|
||||
let requested_file_path = {
|
||||
let mut path = base_path.clone();
|
||||
|
||||
path.push(format!("{}.xml", lang));
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
|
||||
if try_exists(&requested_file_path).await.is_ok_and(|v| v == true) {
|
||||
Ok(Ds(RawXml(NamedFile::open(&requested_file_path).await?)))
|
||||
} else {
|
||||
let fallback_path = {
|
||||
let mut path = base_path;
|
||||
|
||||
path.push("EVIL.xml");
|
||||
path.push("DEFAULT.xml");
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
Ok(Ds(RawXml(NamedFile::open(&path).await?)))
|
||||
} else {
|
||||
let requested_file_path = {
|
||||
let mut path = base_path.clone();
|
||||
|
||||
path.push(format!("{}.xml", lang));
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
|
||||
if try_exists(&requested_file_path).await.is_ok_and(|v| v == true) {
|
||||
Ok(Ds(RawXml(NamedFile::open(&requested_file_path).await?)))
|
||||
} else {
|
||||
let fallback_path = {
|
||||
let mut path = base_path;
|
||||
|
||||
path.push("DEFAULT.xml");
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
Ok(Ds(RawXml(NamedFile::open(&fallback_path).await?)))
|
||||
}
|
||||
Ok(Ds(RawXml(NamedFile::open(&fallback_path).await?)))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +1,50 @@
|
|||
#![allow(unused)]
|
||||
use rocket::{post, FromForm, State};
|
||||
use rocket::form::Form;
|
||||
use serde::{Serialize};
|
||||
use crate::Pool;
|
||||
use crate::account::account::{Auth, DeviceCert, User, link_certificate_to_pid};
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::nnid::agreements::{CFIP, EVIL_AGREEMENT_THING};
|
||||
use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN};
|
||||
use crate::nnid::oauth::TokenData;
|
||||
use crate::Pool;
|
||||
use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN};
|
||||
use crate::nnid::agreements::LATEST_EULA_VERSION;
|
||||
use crate::xml::Xml;
|
||||
use rocket::form::Form;
|
||||
use rocket::{FromForm, State, post};
|
||||
use serde::Serialize;
|
||||
|
||||
pub mod token_type{
|
||||
pub mod token_type {
|
||||
pub const AUTH_REFRESH_TOKEN: i32 = 1;
|
||||
pub const AUTH_TOKEN: i32 = 0;
|
||||
pub const NEX_TOKEN: i32 = 2;
|
||||
}
|
||||
|
||||
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "0106",
|
||||
message: "Invalid account ID or password"
|
||||
}
|
||||
]
|
||||
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "0106",
|
||||
message: "Invalid account ID or password",
|
||||
}],
|
||||
};
|
||||
|
||||
const ACCOUNT_BANNED_ERRORS: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "0122",
|
||||
message: "Device has been banned by game server"
|
||||
}
|
||||
]
|
||||
const ACCOUNT_BANNED_ERRORS: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "0108",
|
||||
message: "Account banned from server",
|
||||
}],
|
||||
};
|
||||
|
||||
const ACCOUNT_TEMPBANNED_ERRORS: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "0132",
|
||||
message: "Account temporarily banned from server",
|
||||
}],
|
||||
};
|
||||
|
||||
const REREAD_EULA_EXTRABANNED_ERRORS: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "0109",
|
||||
message: "REREAD THE EULA LOL"
|
||||
}
|
||||
]
|
||||
const REREAD_EULA_ERRORS: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "0109",
|
||||
message: "The EULA has been updated",
|
||||
}],
|
||||
};
|
||||
#[derive(FromForm)]
|
||||
pub struct TokenRequestData<'a>{
|
||||
pub struct TokenRequestData<'a> {
|
||||
grant_type: &'a str,
|
||||
user_id: &'a str,
|
||||
password: &'a str,
|
||||
|
|
@ -55,17 +55,27 @@ pub struct TokenRequestData<'a>{
|
|||
pub struct TokenReturnData {
|
||||
token: String,
|
||||
refresh_token: String,
|
||||
expires_in: i32
|
||||
expires_in: i32,
|
||||
}
|
||||
|
||||
pub async fn create_token(pool: &Pool, pid: i32, token_type: i32, title_id: Option<&str>) -> String{
|
||||
pub async fn create_token(
|
||||
pool: &Pool,
|
||||
pid: i32,
|
||||
token_type: i32,
|
||||
title_id: Option<&str>,
|
||||
cert_hash: Option<&[u8]>,
|
||||
) -> String {
|
||||
let data = sqlx::query!(
|
||||
"insert into tokens (token_type, pid, title_id)
|
||||
values ($1, $2, $3) returning token_id, random",
|
||||
token_type, pid, title_id
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await.unwrap();
|
||||
"insert into tokens (token_type, pid, title_id, cert_hash)
|
||||
values ($1, $2, $3, $4) returning token_id, random",
|
||||
token_type,
|
||||
pid,
|
||||
title_id,
|
||||
cert_hash
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_id = data.token_id;
|
||||
let random = data.random;
|
||||
|
|
@ -73,61 +83,77 @@ pub async fn create_token(pool: &Pool, pid: i32, token_type: i32, title_id: Opti
|
|||
let token = TokenData {
|
||||
token_id,
|
||||
random,
|
||||
pid
|
||||
pid,
|
||||
};
|
||||
|
||||
token.encode().to_string()
|
||||
}
|
||||
|
||||
|
||||
impl TokenReturnData {
|
||||
async fn new(pid: i32, pool: &Pool) -> Self{
|
||||
let token = create_token(pool, pid, AUTH_TOKEN, None).await;
|
||||
async fn new(pid: i32, pool: &Pool) -> Self {
|
||||
let token = create_token(pool, pid, AUTH_TOKEN, None, None).await;
|
||||
|
||||
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None).await;
|
||||
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None, None).await;
|
||||
|
||||
Self{
|
||||
Self {
|
||||
token,
|
||||
refresh_token,
|
||||
expires_in: 3600
|
||||
expires_in: 3600,
|
||||
}
|
||||
}
|
||||
async fn new_with_cert(pid: i32, cert: Option<&[u8]>, pool: &Pool) -> Self {
|
||||
let token = create_token(pool, pid, AUTH_TOKEN, None, cert).await;
|
||||
|
||||
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None, cert).await;
|
||||
|
||||
Self {
|
||||
token,
|
||||
refresh_token,
|
||||
expires_in: 3600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename="OAuth20")]
|
||||
pub struct TokenRequestReturnData{
|
||||
access_token: TokenReturnData
|
||||
#[serde(rename = "OAuth20")]
|
||||
pub struct TokenRequestReturnData {
|
||||
access_token: TokenReturnData,
|
||||
}
|
||||
|
||||
#[post("/v1/api/oauth20/access_token/generate", data="<data>")]
|
||||
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>, ip: CFIP, cert: DeviceCert) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>>{
|
||||
#[post("/v1/api/oauth20/access_token/generate", data = "<data>")]
|
||||
pub async fn generate_token(
|
||||
pool: &State<Pool>,
|
||||
data: Form<TokenRequestData<'_>>,
|
||||
cert: DeviceCert,
|
||||
) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>> {
|
||||
let pool = pool.inner();
|
||||
|
||||
let user = User::get_by_username(data.user_id, pool).await
|
||||
let user = User::get_by_username(data.user_id, pool)
|
||||
.await
|
||||
.ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||
|
||||
if !user.verify_hashed_password(&data.password).is_some_and(|v| v){
|
||||
if !user
|
||||
.verify_hashed_password(&data.password)
|
||||
.is_some_and(|v| v)
|
||||
{
|
||||
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
|
||||
}
|
||||
|
||||
if user.account_level < 0{
|
||||
if user.account_level == -2 {
|
||||
return Err(Some(REREAD_EULA_EXTRABANNED_ERRORS));
|
||||
}
|
||||
if user.account_level == -3{
|
||||
EVIL_AGREEMENT_THING.write().await.insert(ip.0);
|
||||
if user.eula_version != *LATEST_EULA_VERSION {
|
||||
return Err(Some(REREAD_EULA_ERRORS));
|
||||
}
|
||||
|
||||
return Err(Some(REREAD_EULA_EXTRABANNED_ERRORS));
|
||||
if user.account_level < 0 {
|
||||
if user.account_level == -2 {
|
||||
return Err(Some(ACCOUNT_TEMPBANNED_ERRORS));
|
||||
}
|
||||
return Err(Some(ACCOUNT_BANNED_ERRORS));
|
||||
}
|
||||
|
||||
|
||||
link_certificate_to_pid(&pool, &cert.0, user.pid).await?;
|
||||
|
||||
let access_token = TokenReturnData::new(user.pid, pool).await;
|
||||
let access_token =
|
||||
TokenReturnData::new_with_cert(user.pid, Some(&cert.0.hash()[..]), pool).await;
|
||||
|
||||
Ok(Xml(TokenRequestReturnData{
|
||||
access_token
|
||||
}))
|
||||
}
|
||||
Ok(Xml(TokenRequestReturnData { access_token }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
#![allow(unused)]
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use gxhash::{gxhash32, gxhash64};
|
||||
use rocket::{get, post, put, State};
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use crate::Pool;
|
||||
use crate::account::account::{Auth, User, generate_nex_password, generate_password};
|
||||
use crate::dsresponse::Ds;
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::nnid::pid_distribution::next_pid;
|
||||
use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE};
|
||||
use crate::Pool;
|
||||
use crate::xml::{Xml, YesNoVal};
|
||||
use crate::email::send_verification_email;
|
||||
use rand::prelude::*;
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::mii_util::get_mii_img_url;
|
||||
use crate::nnid::timezones::OFFSET_FROM_TIMEZONE;
|
||||
use crate::xml::{Xml, YesNoVal};
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use gxhash::{gxhash32, gxhash64};
|
||||
use nex_account::grpc::{ActStageInfo, ActStageReturn};
|
||||
use nex_account::grpc_client;
|
||||
use rand::prelude::*;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::request::{FromRequest, Outcome, Request};
|
||||
use rocket::{State, get, post, put};
|
||||
|
||||
const DATABASE_ERROR: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "9999",
|
||||
message: "Internal server error"
|
||||
}
|
||||
]
|
||||
const DATABASE_ERROR: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "2001",
|
||||
message: "Internal server error",
|
||||
}],
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Email{
|
||||
address: Box<str>
|
||||
pub struct Email {
|
||||
address: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateMiiData {
|
||||
_name: Box<str>,
|
||||
_primary: crate::xml::YesNoVal,
|
||||
name: Box<str>,
|
||||
primary: crate::xml::YesNoVal,
|
||||
data: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Mii{
|
||||
pub struct Mii {
|
||||
name: Box<str>,
|
||||
primary: YesNoVal,
|
||||
data: Box<str>,
|
||||
|
|
@ -44,7 +44,7 @@ pub struct Mii{
|
|||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct AccountCreationData{
|
||||
pub struct AccountCreationData {
|
||||
birth_date: NaiveDate,
|
||||
user_id: Box<str>,
|
||||
password: Box<str>,
|
||||
|
|
@ -56,24 +56,99 @@ pub struct AccountCreationData{
|
|||
gender: Box<str>,
|
||||
marketing_flag: YesNoVal,
|
||||
off_device_flag: YesNoVal,
|
||||
region: i32
|
||||
region: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct AccountCreationResponseData{
|
||||
pid: i32
|
||||
pub struct AccountCreationResponseData {
|
||||
pid: i32,
|
||||
}
|
||||
|
||||
#[post("/v1/api/people", data="<data>")]
|
||||
pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Option<Errors<'_>>>{
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename = "device")]
|
||||
pub struct DeviceInfo {
|
||||
pub device_id: String,
|
||||
pub language: String,
|
||||
pub updated: NaiveDateTime,
|
||||
pub pid: i32,
|
||||
pub platform_id: String,
|
||||
pub region: String,
|
||||
pub serial_number: String,
|
||||
pub status: String,
|
||||
pub system_version: String,
|
||||
pub r#type: String,
|
||||
pub updated_by: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DevicesWrapper {
|
||||
#[serde(rename = "device")]
|
||||
pub devices: Vec<DeviceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EmptyDeviceResponse {
|
||||
pub device: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename = "person")]
|
||||
pub struct UpdateAccountData {
|
||||
pub gender: Option<Box<str>>,
|
||||
pub region: Option<i32>,
|
||||
pub country: Option<Box<str>>,
|
||||
pub language: Option<Box<str>>,
|
||||
pub tz_name: Option<Box<str>>,
|
||||
pub marketing_flag: Option<YesNoVal>,
|
||||
pub off_device_flag: Option<YesNoVal>,
|
||||
pub password: Option<Box<str>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EmailWrapper {
|
||||
pub email: EmailInfoOwnProfileData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EmailsWrapper {
|
||||
#[serde(rename = "email")]
|
||||
pub emails: Vec<EmailInfoOwnProfileData>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateEmailData {
|
||||
pub address: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateEmailRequest {
|
||||
pub email: UpdateEmailData,
|
||||
}
|
||||
|
||||
#[post("/v1/api/people", data = "<data>")]
|
||||
pub async fn create_account(
|
||||
database: &State<Pool>,
|
||||
data: Xml<AccountCreationData>,
|
||||
) -> Result<Xml<AccountCreationResponseData>, Option<Errors<'_>>> {
|
||||
let database = database.inner();
|
||||
let nex_password = generate_nex_password();
|
||||
|
||||
let mut client = grpc_client().await.expect("unable to connect to grpc");
|
||||
|
||||
let Ok(ret) = client
|
||||
.stage_new_account(ActStageInfo {
|
||||
password: nex_password.clone().into_bytes(),
|
||||
})
|
||||
.await
|
||||
else {
|
||||
return Err(Some(DATABASE_ERROR));
|
||||
};
|
||||
let ActStageReturn { pid, .. } = ret.into_inner();
|
||||
|
||||
// its fine to crash here if we cant get the next pid as that is in my opinion a dead state
|
||||
// anyways as noone can register anymore, EVER
|
||||
|
||||
let pid = next_pid(database).await;
|
||||
|
||||
let verification_code: i32 = rand::rng().random_range(100_000..1_000_000);
|
||||
|
||||
let AccountCreationData {
|
||||
|
|
@ -82,14 +157,8 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
|||
birth_date,
|
||||
tz_name,
|
||||
language,
|
||||
email: Email{
|
||||
address
|
||||
},
|
||||
mii: Mii{
|
||||
name,
|
||||
data,
|
||||
..
|
||||
},
|
||||
email: Email { address },
|
||||
mii: Mii { name, data, .. },
|
||||
marketing_flag,
|
||||
gender,
|
||||
region,
|
||||
|
|
@ -98,17 +167,16 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
|||
..
|
||||
} = data.0;
|
||||
|
||||
let account_level = if user_id.to_lowercase().contains("omey"){
|
||||
let account_level = if user_id.to_lowercase().contains("omey") {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
0
|
||||
};
|
||||
|
||||
|
||||
let password = generate_password(pid, &password).ok_or(None)?;
|
||||
let nex_password = generate_nex_password();
|
||||
|
||||
sqlx::query!("
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO users (
|
||||
pid,
|
||||
username,
|
||||
|
|
@ -146,19 +214,20 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
|||
verification_code,
|
||||
account_level,
|
||||
nex_password
|
||||
).execute(database).await.unwrap();
|
||||
)
|
||||
.execute(database)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//generate_s3_images(pid, &data).await;
|
||||
|
||||
if let Err(e) = send_verification_email(address.as_ref(), verification_code, user_id.as_ref()).await {
|
||||
if let Err(e) =
|
||||
send_verification_email(address.as_ref(), verification_code, user_id.as_ref()).await
|
||||
{
|
||||
println!("Failed to send verification email: {e}");
|
||||
}
|
||||
|
||||
Ok(
|
||||
Xml(AccountCreationResponseData{
|
||||
pid
|
||||
})
|
||||
)
|
||||
Ok(Xml(AccountCreationResponseData { pid }))
|
||||
}
|
||||
|
||||
// #[derive(Serialize)]
|
||||
|
|
@ -167,21 +236,21 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
|
|||
// }
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EmailInfoOwnProfileData{
|
||||
address: String,
|
||||
id: u32,
|
||||
parent: YesNoVal,
|
||||
primary: YesNoVal,
|
||||
reachable: YesNoVal,
|
||||
pub struct EmailInfoOwnProfileData {
|
||||
pub address: String,
|
||||
pub id: u32,
|
||||
pub parent: YesNoVal,
|
||||
pub primary: YesNoVal,
|
||||
pub reachable: YesNoVal,
|
||||
#[serde(rename = "type")]
|
||||
email_type: String,
|
||||
updated_by: String,
|
||||
validated: YesNoVal,
|
||||
validated_date: Option<NaiveDateTime>
|
||||
pub email_type: String,
|
||||
pub updated_by: String,
|
||||
pub validated: YesNoVal,
|
||||
pub validated_date: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EmailInfoOwnOAuthProfileData{
|
||||
struct EmailInfoOwnOAuthProfileData {
|
||||
address: String,
|
||||
id: u32,
|
||||
parent: bool,
|
||||
|
|
@ -191,40 +260,37 @@ struct EmailInfoOwnOAuthProfileData{
|
|||
email_type: String,
|
||||
updated_by: String,
|
||||
validated: bool,
|
||||
validated_date: Option<NaiveDateTime>
|
||||
validated_date: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MiiImage{
|
||||
struct MiiImage {
|
||||
cached_url: String,
|
||||
id: u32,
|
||||
url: String,
|
||||
#[serde(rename = "type")]
|
||||
image_type: String
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MiiImages{
|
||||
mii_image: MiiImage
|
||||
image_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MiiDataOwnProfileData{
|
||||
struct MiiImages {
|
||||
mii_image: MiiImage,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MiiDataOwnProfileData {
|
||||
status: String,
|
||||
data: String,
|
||||
id: u32,
|
||||
mii_hash: String,
|
||||
mii_images: MiiImages,
|
||||
name: String,
|
||||
primary: YesNoVal
|
||||
primary: YesNoVal,
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct GetOwnProfileData{
|
||||
pub struct GetOwnProfileData {
|
||||
active_flag: YesNoVal,
|
||||
birth_date: NaiveDate,
|
||||
country: String,
|
||||
|
|
@ -246,7 +312,7 @@ pub struct GetOwnProfileData{
|
|||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
pub struct GetOwnOAuthProfileData{
|
||||
pub struct GetOwnOAuthProfileData {
|
||||
active_flag: bool,
|
||||
birth_date: NaiveDate,
|
||||
country: String,
|
||||
|
|
@ -270,17 +336,17 @@ pub struct GetOwnOAuthProfileData{
|
|||
}
|
||||
|
||||
#[get("/v1/api/people/@me/profile")]
|
||||
pub fn get_own_profile(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>>{
|
||||
pub fn get_own_profile(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>> {
|
||||
Ds(Xml(build_profile(user.into())))
|
||||
}
|
||||
|
||||
#[get("/v1/api/people/@me/devices/owner")]
|
||||
pub fn get_device_owner(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>>{
|
||||
pub fn get_device_owner(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>> {
|
||||
Ds(Xml(build_profile(user.into())))
|
||||
}
|
||||
|
||||
#[post("/v1/api/people/@me/devices")]
|
||||
pub fn get_own_device(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>>{
|
||||
pub fn get_own_device(user: Auth<false>) -> Ds<Xml<GetOwnProfileData>> {
|
||||
Ds(Xml(build_profile(user.into())))
|
||||
}
|
||||
|
||||
|
|
@ -314,59 +380,58 @@ pub fn build_profile(user: User) -> GetOwnProfileData {
|
|||
.replace("\r", "")
|
||||
.replace(" ", "");
|
||||
|
||||
|
||||
GetOwnProfileData {
|
||||
active_flag: YesNoVal(true),
|
||||
pid,
|
||||
user_id: username,
|
||||
gender,
|
||||
birth_date: birthdate,
|
||||
country,
|
||||
create_date: creation_date,
|
||||
tz_name: timezone,
|
||||
language,
|
||||
updated,
|
||||
marketing_flag: YesNoVal(marketing_allowed),
|
||||
email: EmailInfoOwnProfileData {
|
||||
id: gxhash32(email.as_bytes(), 0),
|
||||
address: email,
|
||||
validated: YesNoVal(email_verified_since.is_some()),
|
||||
validated_date: email_verified_since,
|
||||
email_type: "DEFAULT".to_string(),
|
||||
updated_by: "USER".to_string(),
|
||||
reachable: YesNoVal(true),
|
||||
primary: YesNoVal(true),
|
||||
parent: YesNoVal(false),
|
||||
},
|
||||
mii: MiiDataOwnProfileData {
|
||||
id: gxhash32(mii_data.as_bytes(), 0),
|
||||
mii_hash: hex::encode(bytemuck::bytes_of(
|
||||
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000))
|
||||
)),
|
||||
name: crate::mii_util::MiiData::read(&mii_data)
|
||||
GetOwnProfileData {
|
||||
active_flag: YesNoVal(true),
|
||||
pid,
|
||||
user_id: username,
|
||||
gender,
|
||||
birth_date: birthdate,
|
||||
country,
|
||||
create_date: creation_date,
|
||||
tz_name: timezone,
|
||||
language,
|
||||
updated,
|
||||
marketing_flag: YesNoVal(marketing_allowed),
|
||||
email: EmailInfoOwnProfileData {
|
||||
id: gxhash32(email.as_bytes(), 0),
|
||||
address: email,
|
||||
validated: YesNoVal(email_verified_since.is_some()),
|
||||
validated_date: email_verified_since,
|
||||
email_type: "DEFAULT".to_string(),
|
||||
updated_by: "USER".to_string(),
|
||||
reachable: YesNoVal(true),
|
||||
primary: YesNoVal(true),
|
||||
parent: YesNoVal(false),
|
||||
},
|
||||
mii: MiiDataOwnProfileData {
|
||||
id: gxhash32(mii_data.as_bytes(), 0),
|
||||
mii_hash: hex::encode(bytemuck::bytes_of(
|
||||
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)),
|
||||
)),
|
||||
name: crate::mii_util::MiiData::read(&mii_data)
|
||||
.map(|v| v.name)
|
||||
.unwrap_or_else(|| "INVALID".to_string()),
|
||||
primary: YesNoVal(true),
|
||||
data: mii_data,
|
||||
status: "COMPLETED".to_string(),
|
||||
mii_images: MiiImages {
|
||||
mii_image: {
|
||||
let image_url = get_mii_img_url(pid, "tga");
|
||||
let url_hash = gxhash32(image_url.as_bytes(), 0);
|
||||
MiiImage {
|
||||
image_type: "standard".to_string(),
|
||||
id: url_hash,
|
||||
url: image_url.clone(),
|
||||
cached_url: image_url,
|
||||
}
|
||||
primary: YesNoVal(true),
|
||||
data: mii_data,
|
||||
status: "COMPLETED".to_string(),
|
||||
mii_images: MiiImages {
|
||||
mii_image: {
|
||||
let image_url = get_mii_img_url(pid, "tga");
|
||||
let url_hash = gxhash32(image_url.as_bytes(), 0);
|
||||
MiiImage {
|
||||
image_type: "standard".to_string(),
|
||||
id: url_hash,
|
||||
url: image_url.clone(),
|
||||
cached_url: image_url,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
off_device_flag: YesNoVal(off_device_allowed),
|
||||
region,
|
||||
utc_offset: timezone_offset,
|
||||
account_level,
|
||||
}
|
||||
},
|
||||
off_device_flag: YesNoVal(off_device_allowed),
|
||||
region,
|
||||
utc_offset: timezone_offset,
|
||||
account_level,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_oauth_profile(user: User) -> GetOwnOAuthProfileData {
|
||||
|
|
@ -404,62 +469,61 @@ pub fn build_oauth_profile(user: User) -> GetOwnOAuthProfileData {
|
|||
.replace("\r", "")
|
||||
.replace(" ", "");
|
||||
|
||||
|
||||
GetOwnOAuthProfileData {
|
||||
id,
|
||||
sub,
|
||||
uid,
|
||||
active_flag: true,
|
||||
pid,
|
||||
user_id: username,
|
||||
gender,
|
||||
birth_date: birthdate,
|
||||
country,
|
||||
create_date: creation_date,
|
||||
tz_name: timezone,
|
||||
language,
|
||||
updated,
|
||||
marketing_flag: marketing_allowed,
|
||||
email: EmailInfoOwnOAuthProfileData {
|
||||
id: gxhash32(email.as_bytes(), 0),
|
||||
address: email,
|
||||
validated: email_verified_since.is_some(),
|
||||
validated_date: email_verified_since,
|
||||
email_type: "DEFAULT".to_string(),
|
||||
updated_by: "USER".to_string(),
|
||||
reachable: true,
|
||||
primary: true,
|
||||
parent: false,
|
||||
},
|
||||
mii: MiiDataOwnProfileData {
|
||||
id: gxhash32(mii_data.as_bytes(), 0),
|
||||
mii_hash: hex::encode(bytemuck::bytes_of(
|
||||
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000))
|
||||
)),
|
||||
name: crate::mii_util::MiiData::read(&mii_data)
|
||||
GetOwnOAuthProfileData {
|
||||
id,
|
||||
sub,
|
||||
uid,
|
||||
active_flag: true,
|
||||
pid,
|
||||
user_id: username,
|
||||
gender,
|
||||
birth_date: birthdate,
|
||||
country,
|
||||
create_date: creation_date,
|
||||
tz_name: timezone,
|
||||
language,
|
||||
updated,
|
||||
marketing_flag: marketing_allowed,
|
||||
email: EmailInfoOwnOAuthProfileData {
|
||||
id: gxhash32(email.as_bytes(), 0),
|
||||
address: email,
|
||||
validated: email_verified_since.is_some(),
|
||||
validated_date: email_verified_since,
|
||||
email_type: "DEFAULT".to_string(),
|
||||
updated_by: "USER".to_string(),
|
||||
reachable: true,
|
||||
primary: true,
|
||||
parent: false,
|
||||
},
|
||||
mii: MiiDataOwnProfileData {
|
||||
id: gxhash32(mii_data.as_bytes(), 0),
|
||||
mii_hash: hex::encode(bytemuck::bytes_of(
|
||||
&(gxhash64(mii_data.as_bytes(), 1) & !(0x1000000000000000)),
|
||||
)),
|
||||
name: crate::mii_util::MiiData::read(&mii_data)
|
||||
.map(|v| v.name)
|
||||
.unwrap_or_else(|| "INVALID".to_string()),
|
||||
primary: YesNoVal(true),
|
||||
data: mii_data,
|
||||
status: "COMPLETED".to_string(),
|
||||
mii_images: MiiImages {
|
||||
mii_image: {
|
||||
let image_url = get_mii_img_url(pid, "png");
|
||||
let url_hash = gxhash32(image_url.as_bytes(), 0);
|
||||
MiiImage {
|
||||
image_type: "standard".to_string(),
|
||||
id: url_hash,
|
||||
url: image_url.clone(),
|
||||
cached_url: image_url,
|
||||
}
|
||||
primary: YesNoVal(true),
|
||||
data: mii_data,
|
||||
status: "COMPLETED".to_string(),
|
||||
mii_images: MiiImages {
|
||||
mii_image: {
|
||||
let image_url = get_mii_img_url(pid, "png");
|
||||
let url_hash = gxhash32(image_url.as_bytes(), 0);
|
||||
MiiImage {
|
||||
image_type: "standard".to_string(),
|
||||
id: url_hash,
|
||||
url: image_url.clone(),
|
||||
cached_url: image_url,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
off_device_flag: off_device_allowed,
|
||||
region,
|
||||
utc_offset: timezone_offset,
|
||||
account_level,
|
||||
}
|
||||
},
|
||||
off_device_flag: off_device_allowed,
|
||||
region,
|
||||
utc_offset: timezone_offset,
|
||||
account_level,
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/v1/api/people/@me/miis/@primary", data = "<data>")]
|
||||
|
|
@ -479,8 +543,8 @@ pub async fn change_mii(
|
|||
mii_data,
|
||||
pid
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
println!("Failed to update Mii data for PID {}: {:?}", pid, e);
|
||||
|
|
@ -492,8 +556,245 @@ pub async fn change_mii(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AgreedEulaData {
|
||||
pub agreement_date: NaiveDateTime,
|
||||
pub country: Box<str>,
|
||||
pub location: Box<str>,
|
||||
pub version: i32
|
||||
}
|
||||
|
||||
#[post("/v1/api/people/@me/agreements")]
|
||||
pub async fn thing(){
|
||||
#[post("/v1/api/people/@me/agreements", data = "<data>")]
|
||||
pub async fn thing(
|
||||
database: &State<Pool>,
|
||||
auth: Auth<false>,
|
||||
data: Xml<AgreedEulaData>,
|
||||
) -> Result<(), Option<Errors<'static>>> {
|
||||
let db = database.inner();
|
||||
let version = data.version;
|
||||
let pid = auth.pid;
|
||||
println!("eula data: {:?}", data.version);
|
||||
|
||||
let result = sqlx::query!(
|
||||
"UPDATE users SET eula_version = $1 WHERE pid = $2",
|
||||
version,
|
||||
pid
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
println!("Failed to update EULA version for PID {}: {:?}", pid, e);
|
||||
return Err(Some(DATABASE_ERROR));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct DeviceHeaders {
|
||||
pub device_id: String,
|
||||
pub accept_language: String,
|
||||
pub platform_id: String,
|
||||
pub region: String,
|
||||
pub serial_number: String,
|
||||
pub system_version: String,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for DeviceHeaders {
|
||||
type Error = Errors<'static>;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = req.headers();
|
||||
|
||||
let get_h = |key: &str| headers.get_one(key).map(|s| s.to_string());
|
||||
|
||||
match (
|
||||
get_h("x-nintendo-device-id"),
|
||||
get_h("accept-language"),
|
||||
get_h("x-nintendo-platform-id"),
|
||||
get_h("x-nintendo-region"),
|
||||
get_h("x-nintendo-serial-number"),
|
||||
get_h("x-nintendo-system-version"),
|
||||
) {
|
||||
(
|
||||
Some(device_id),
|
||||
Some(accept_language),
|
||||
Some(platform_id),
|
||||
Some(region),
|
||||
Some(serial_number),
|
||||
Some(system_version),
|
||||
) => Outcome::Success(DeviceHeaders {
|
||||
device_id,
|
||||
accept_language,
|
||||
platform_id,
|
||||
region,
|
||||
serial_number,
|
||||
system_version,
|
||||
}),
|
||||
_ => Outcome::Error((
|
||||
rocket::http::Status::BadRequest,
|
||||
Errors {
|
||||
error: &[Error {
|
||||
code: "1600",
|
||||
message: "Unable to process request",
|
||||
}],
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/api/people/@me/devices")]
|
||||
pub fn get_user_devices(
|
||||
auth: Auth<true>,
|
||||
headers: DeviceHeaders,
|
||||
) -> Xml<DevicesWrapper> {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
|
||||
Xml(DevicesWrapper {
|
||||
devices: vec![DeviceInfo {
|
||||
device_id: headers.device_id,
|
||||
language: headers.accept_language,
|
||||
updated: now,
|
||||
pid: auth.pid,
|
||||
platform_id: headers.platform_id,
|
||||
region: headers.region,
|
||||
serial_number: headers.serial_number,
|
||||
status: "ACTIVE".to_string(),
|
||||
system_version: headers.system_version,
|
||||
r#type: "RETAIL".to_string(),
|
||||
updated_by: "USER".to_string(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/v1/api/people/@me/devices/status")]
|
||||
pub fn get_device_status(_auth: Auth<true>) -> Xml<EmptyDeviceResponse> {
|
||||
Xml(EmptyDeviceResponse {
|
||||
device: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[put("/v1/api/people/@me/devices/@current/inactivate")]
|
||||
pub fn inactivate_current_device(_auth: Auth<true>) -> () {
|
||||
// just 200
|
||||
}
|
||||
|
||||
#[post("/v1/api/people/@me/deletion")]
|
||||
pub async fn delete_account(
|
||||
database: &State<Pool>,
|
||||
auth: Auth<true>,
|
||||
) -> Result<(), Option<Errors<'static>>> {
|
||||
let db = database.inner();
|
||||
|
||||
let result = sqlx::query!(
|
||||
"DELETE FROM users WHERE pid = $1",
|
||||
auth.pid
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
println!("failed to delete PID {}: {:?}", auth.pid, e);
|
||||
return Err(Some(DATABASE_ERROR));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[put("/v1/api/people/@me", data = "<data>")]
|
||||
pub async fn update_account(
|
||||
database: &State<Pool>,
|
||||
auth: Auth<true>,
|
||||
data: Xml<UpdateAccountData>,
|
||||
) -> Result<(), Option<Errors<'static>>> {
|
||||
let db = database.inner();
|
||||
let pid = auth.pid;
|
||||
let data = data.0;
|
||||
|
||||
let updated_password = if let Some(ref new_pass) = data.password {
|
||||
generate_password(pid, new_pass)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
UPDATE users SET
|
||||
gender = COALESCE($1, gender),
|
||||
region = COALESCE($2, region),
|
||||
country = COALESCE($3, country),
|
||||
language = COALESCE($4, language),
|
||||
timezone = COALESCE($5, timezone),
|
||||
marketing_allowed = COALESCE($6, marketing_allowed),
|
||||
off_device_allowed = COALESCE($7, off_device_allowed),
|
||||
password = COALESCE($8, password)
|
||||
WHERE pid = $9
|
||||
",
|
||||
data.gender.as_deref(),
|
||||
data.region,
|
||||
data.country.as_deref(),
|
||||
data.language.as_deref(),
|
||||
data.tz_name.as_deref(),
|
||||
data.marketing_flag.map(|v| v.0),
|
||||
data.off_device_flag.map(|v| v.0),
|
||||
updated_password,
|
||||
pid
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
println!("failed to update account for PID {}: {:?}", pid, e);
|
||||
return Err(Some(DATABASE_ERROR));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/v1/api/people/@me/emails")]
|
||||
pub fn get_own_emails(user: Auth<false>) -> Xml<EmailsWrapper> {
|
||||
let profile = build_profile(user.into());
|
||||
Xml(EmailsWrapper {
|
||||
emails: vec![profile.email],
|
||||
})
|
||||
}
|
||||
|
||||
#[put("/v1/api/people/@me/emails/@primary", data = "<data>")]
|
||||
pub async fn update_primary_email(
|
||||
database: &State<Pool>,
|
||||
auth: Auth<true>,
|
||||
data: Xml<UpdateEmailRequest>,
|
||||
) -> Result<(), Option<Errors<'static>>> {
|
||||
let db = database.inner();
|
||||
let pid = auth.pid;
|
||||
let new_address = data.0.email.address.to_lowercase();
|
||||
let verification_code: i32 = rand::rng().random_range(100_000..1_000_000);
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
UPDATE users SET
|
||||
email = $1,
|
||||
email_verified_since = NULL,
|
||||
verification_code = $2
|
||||
WHERE pid = $3
|
||||
",
|
||||
new_address,
|
||||
verification_code,
|
||||
pid
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
println!("failed to update email for PID {}: {:?}", pid, e);
|
||||
return Err(Some(DATABASE_ERROR));
|
||||
}
|
||||
|
||||
if let Err(e) = send_verification_email(&new_address, verification_code, &auth.username).await {
|
||||
println!("failed to send verification email: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#![deprecated = "handled by nex-account now"]
|
||||
/*
|
||||
use crate::Pool;
|
||||
|
||||
pub async fn next_pid(pool: &Pool) -> i32{
|
||||
loop {
|
||||
let next_pid = sqlx::query!("SELECT nextval('pid_counter') as pid")
|
||||
|
|
@ -26,4 +27,5 @@ pub async fn next_pid(pool: &Pool) -> i32{
|
|||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,69 +1,112 @@
|
|||
use std::net::Ipv4Addr;
|
||||
use rocket::{get, State};
|
||||
use serde::Serialize;
|
||||
use sqlx::types::ipnetwork::IpNetwork::V4;
|
||||
use crate::Pool;
|
||||
use crate::account::account::Auth;
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::nnid::oauth::generate_token::{create_token};
|
||||
use crate::nnid::oauth::generate_token::create_token;
|
||||
use crate::nnid::oauth::generate_token::token_type::NEX_TOKEN;
|
||||
use crate::Pool;
|
||||
use crate::xml::Xml;
|
||||
use log::{info, warn};
|
||||
use nex_account::grpc::Pid;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome, Request};
|
||||
use rocket::{State, async_trait, get};
|
||||
use serde::Serialize;
|
||||
use sqlx::query;
|
||||
use sqlx::types::ipnetwork::IpNetwork::V4;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
const NO_IPV4_ERROR: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "1022",
|
||||
message: "Server is not a valid IPv4 address"
|
||||
}
|
||||
]
|
||||
const NO_IPV4_ERROR: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "1022",
|
||||
message: "Server is not a valid IPv4 address",
|
||||
}],
|
||||
};
|
||||
|
||||
const SERVER_ERROR: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "9999",
|
||||
message: "Internal Server Error"
|
||||
}
|
||||
]
|
||||
const SERVER_ERROR: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "9999",
|
||||
message: "Internal Server Error",
|
||||
}],
|
||||
};
|
||||
|
||||
const NO_SERVER_ERROR: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "1021",
|
||||
message: "The requested game server was not found"
|
||||
}
|
||||
]
|
||||
const NO_SERVER_ERROR: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "1021",
|
||||
message: "The requested game server was not found",
|
||||
}],
|
||||
};
|
||||
|
||||
const MAINTENANCE_ERROR: Errors = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "2002",
|
||||
message: "The requested game server is under maintenance"
|
||||
}
|
||||
]
|
||||
const MAINTENANCE_ERROR: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "2002",
|
||||
message: "The requested game server is under maintenance",
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename = "nex_token")]
|
||||
pub struct NexToken{
|
||||
pub struct NexToken {
|
||||
host: Ipv4Addr,
|
||||
nex_password: String,
|
||||
pid: i32,
|
||||
port: u16,
|
||||
token: String
|
||||
token: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename = "service_token")]
|
||||
pub struct ServiceToken{
|
||||
token: String
|
||||
pub struct ServiceToken {
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn store_or_check_serial(pool: &Pool, serial: &str, cert_hash: [u8; 32]) -> bool {
|
||||
let Ok(res) = query!(
|
||||
"select serial from certificates where hash = $1",
|
||||
&cert_hash[..]
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
else {
|
||||
warn!(
|
||||
"user tried to access a route which is locked behind a console asociated token without a console token"
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(stored_serial) = res.serial else {
|
||||
let _row = query!(
|
||||
"update certificates set serial = $1 where hash = $2",
|
||||
serial,
|
||||
&cert_hash[..]
|
||||
).execute(pool)
|
||||
.await
|
||||
.ok();
|
||||
return true;
|
||||
};
|
||||
|
||||
serial == stored_serial
|
||||
}
|
||||
|
||||
pub struct Serial(pub String);
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for Serial {
|
||||
type Error = Errors<'static>;
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let Some(header) = request.headers().get("X-Nintendo-Serial-Number").next() else {
|
||||
warn!("serial number wasnt sent on request which expected a serial number");
|
||||
return Outcome::Error((Status::BadRequest, SERVER_ERROR));
|
||||
};
|
||||
|
||||
Outcome::Success(Self(header.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/api/provider/service_token/@me")]
|
||||
pub async fn get_service_token(pool: &State<Pool>, auth: Auth<true, false>) -> Result<Xml<ServiceToken>, Option<Errors<'static>>>{
|
||||
pub async fn get_service_token(
|
||||
pool: &State<Pool>,
|
||||
auth: Auth<true, false>,
|
||||
serial: Serial,
|
||||
) -> Result<Xml<ServiceToken>, Option<Errors<'static>>> {
|
||||
// just gonna put this here as a side note for the future:
|
||||
// we could also be using key derivation to derive the nex token as if it were a key
|
||||
// that way we could reduce the data the database needs to store and also reduce the transfer
|
||||
|
|
@ -73,19 +116,27 @@ pub async fn get_service_token(pool: &State<Pool>, auth: Auth<true, false>) -> R
|
|||
|
||||
let pool = pool.inner();
|
||||
|
||||
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
|
||||
let Some(cert_hash) = auth.1 else {
|
||||
info!("attempt to generate service token using non wii u/certificate token");
|
||||
return Err(Some(SERVER_ERROR));
|
||||
};
|
||||
|
||||
Ok(
|
||||
Xml(
|
||||
ServiceToken{
|
||||
token
|
||||
}
|
||||
)
|
||||
)
|
||||
if !store_or_check_serial(pool, &serial.0, cert_hash).await {
|
||||
info!("serial mismatched with certificate");
|
||||
return Err(Some(SERVER_ERROR));
|
||||
}
|
||||
|
||||
let token = create_token(pool, auth.pid, NEX_TOKEN, None, Some(&cert_hash[..])).await;
|
||||
|
||||
Ok(Xml(ServiceToken { token }))
|
||||
}
|
||||
|
||||
#[get("/v1/api/provider/nex_token/@me?<game_server_id>")]
|
||||
pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true, false>, game_server_id: &str) -> Result<Xml<NexToken>, Option<Errors<'static>>>{
|
||||
pub async fn get_nex_token(
|
||||
pool: &State<Pool>,
|
||||
auth: Auth<true, false>,
|
||||
game_server_id: &str,
|
||||
) -> Result<Xml<NexToken>, Option<Errors<'static>>> {
|
||||
// just gonna put this here as a side note for the future:
|
||||
// we could also be using key derivation to derive the nex token as if it were a key
|
||||
// that way we could reduce the data the database needs to store and also reduce the transfer
|
||||
|
|
@ -108,12 +159,10 @@ pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true, false>, game_ser
|
|||
Some(row) => row,
|
||||
None => return Err(Some(NO_SERVER_ERROR)),
|
||||
}; // only crash on db failure (not missing row)
|
||||
|
||||
if server.maintenance_mode {
|
||||
return Err(Some(MAINTENANCE_ERROR))
|
||||
}
|
||||
|
||||
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
|
||||
if server.maintenance_mode {
|
||||
return Err(Some(MAINTENANCE_ERROR));
|
||||
}
|
||||
|
||||
let V4(host) = server.address else {
|
||||
return Err(Some(NO_IPV4_ERROR));
|
||||
|
|
@ -121,15 +170,29 @@ pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true, false>, game_ser
|
|||
|
||||
let host = host.ip();
|
||||
|
||||
Ok(
|
||||
Xml(
|
||||
NexToken{
|
||||
host,
|
||||
port: server.port as u16,
|
||||
nex_password: auth.nex_password.clone(),
|
||||
pid: auth.pid,
|
||||
token
|
||||
}
|
||||
)
|
||||
let mut client = nex_account::grpc_client().await.unwrap();
|
||||
let Ok(key) = client
|
||||
.get_nex_key_by_pid_maybe_staged(Pid { pid: auth.pid })
|
||||
.await
|
||||
else {
|
||||
println!("account does not exist on nex-account server");
|
||||
return Err(Some(SERVER_ERROR));
|
||||
};
|
||||
|
||||
let token = nex_account::gen_nexact_token(
|
||||
auth.pid,
|
||||
key.into_inner()
|
||||
.key
|
||||
.try_into()
|
||||
.map_err(|_| Some(SERVER_ERROR))?,
|
||||
)
|
||||
.expect("NEX_ACCOUNT_KEYPAIR not set");
|
||||
|
||||
Ok(Xml(NexToken {
|
||||
host,
|
||||
port: server.port as u16,
|
||||
nex_password: auth.nex_password.clone(),
|
||||
pid: auth.pid,
|
||||
token,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use rocket::form::Form;
|
|||
use rocket::{FromForm, State, post, put, get, Request};
|
||||
use rocket::request::{self, FromRequest};
|
||||
use rocket::http::Status;
|
||||
use rand::RngExt;
|
||||
use rand::distr::Alphanumeric;
|
||||
|
||||
const BAD_CODE_ERROR: Errors = Errors {
|
||||
error: &[Error {
|
||||
|
|
@ -14,6 +16,13 @@ const BAD_CODE_ERROR: Errors = Errors {
|
|||
}],
|
||||
};
|
||||
|
||||
const UNAUTHORIZED_DEVICE_ERROR: Errors = Errors {
|
||||
error: &[Error {
|
||||
code: "0113",
|
||||
message: "Unauthorized device",
|
||||
}],
|
||||
};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct ValidateEmailInput {
|
||||
email: String,
|
||||
|
|
@ -45,7 +54,6 @@ pub async fn validate(
|
|||
|
||||
let email = data.email.trim();
|
||||
|
||||
// 1. Validate presence + basic format
|
||||
if email.is_empty() || !email.contains('@') {
|
||||
return Err(Errors {
|
||||
error: &[Error {
|
||||
|
|
@ -55,7 +63,6 @@ pub async fn validate(
|
|||
});
|
||||
}
|
||||
|
||||
// 2. Extract domain safely
|
||||
let domain = match email.split('@').nth(1) {
|
||||
Some(d) if !d.is_empty() => d,
|
||||
_ => {
|
||||
|
|
@ -68,7 +75,7 @@ pub async fn validate(
|
|||
}
|
||||
};
|
||||
|
||||
// 3. DNS resolver
|
||||
// This shouldn't ever fail unless there's something wrong with the server
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf()
|
||||
.map_err(|_| Errors {
|
||||
error: &[Error {
|
||||
|
|
@ -77,7 +84,6 @@ pub async fn validate(
|
|||
}],
|
||||
})?;
|
||||
|
||||
// 4. MX lookup
|
||||
match resolver.mx_lookup(domain).await {
|
||||
Ok(mx) if mx.iter().next().is_some() => Ok(()),
|
||||
|
||||
|
|
@ -159,5 +165,165 @@ pub async fn resend_email(
|
|||
BAD_CODE_ERROR
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/v1/api/support/forgotten_password/<pid>")]
|
||||
pub async fn forgotten_password(
|
||||
database: &State<Pool>,
|
||||
pid: i32,
|
||||
) -> Result<(), Errors<'static>> {
|
||||
let db = database.inner();
|
||||
|
||||
let user_data = sqlx::query!(
|
||||
"SELECT username, email FROM users WHERE pid = $1",
|
||||
pid
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("database lookup error: {e}");
|
||||
UNAUTHORIZED_DEVICE_ERROR
|
||||
})?;
|
||||
|
||||
let user = match user_data {
|
||||
Some(u) => u,
|
||||
None => return Err(UNAUTHORIZED_DEVICE_ERROR),
|
||||
};
|
||||
|
||||
let cleartext_password: String = rand::rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(10)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let nintendo_hash = crate::account::account::generate_password(pid, &cleartext_password)
|
||||
.ok_or(UNAUTHORIZED_DEVICE_ERROR)?;
|
||||
|
||||
let update_result = sqlx::query!(
|
||||
"UPDATE users SET password = $1 WHERE pid = $2",
|
||||
nintendo_hash,
|
||||
pid
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = update_result {
|
||||
eprintln!("failed to update password for PID {pid}: {e}");
|
||||
return Err(UNAUTHORIZED_DEVICE_ERROR);
|
||||
}
|
||||
|
||||
crate::email::send_reset_email(&user.email, &cleartext_password, &user.username)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("failed to send reset email: {e}");
|
||||
UNAUTHORIZED_DEVICE_ERROR
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct DeviceSerial(pub String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for DeviceSerial {
|
||||
type Error = Errors<'static>;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
match req.headers().get_one("x-nintendo-serial-number") {
|
||||
Some(val) if !val.trim().is_empty() => {
|
||||
request::Outcome::Success(DeviceSerial(val.to_string()))
|
||||
}
|
||||
_ => request::Outcome::Error((
|
||||
Status::BadRequest,
|
||||
Errors {
|
||||
error: &[Error {
|
||||
code: "0130",
|
||||
message: "Serial not found",
|
||||
}],
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/api/support/send_confirmation/pin/<email>")]
|
||||
pub async fn send_parental_controls_pin(
|
||||
database: &State<Pool>,
|
||||
serial: DeviceSerial,
|
||||
email: &str,
|
||||
) -> Result<(), Errors<'static>> {
|
||||
let db = database.inner();
|
||||
let serial_number = serial.0;
|
||||
let email = email.trim().to_lowercase();
|
||||
|
||||
if email.is_empty() || !email.contains('@') {
|
||||
return Err(Errors {
|
||||
error: &[Error {
|
||||
code: "0103",
|
||||
message: "Email format is invalid",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
let db_result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO device_parental_controls (serial_number, email, updated_at)
|
||||
VALUES ($1, $2, NOW())
|
||||
ON CONFLICT (serial_number)
|
||||
DO UPDATE SET email = EXCLUDED.email, updated_at = NOW()
|
||||
",
|
||||
serial_number,
|
||||
email
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = db_result {
|
||||
eprintln!("Failed to associate serial {serial_number} with email {email}: {e}");
|
||||
return Err(Errors {
|
||||
error: &[Error {
|
||||
code: "0130",
|
||||
message: "PID has not been registered yet",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
crate::email::send_pc_email(&email, &serial_number)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Failed to send parental controls email to {email}: {e}");
|
||||
Errors {
|
||||
error: &[Error {
|
||||
code: "0130",
|
||||
message: "PID has not been registered yet",
|
||||
}],
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/v1/api/support/send_forgotten/pin/<email>/<master_key>")]
|
||||
pub async fn send_forgotten_pin(
|
||||
serial: DeviceSerial,
|
||||
email: &str,
|
||||
master_key: &str,
|
||||
) -> Result<(), Errors<'static>> {
|
||||
let serial_number = serial.0;
|
||||
let email = email.trim().to_lowercase();
|
||||
|
||||
crate::email::send_reset_pin_email(&email, &serial_number, &master_key)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Failed to send parental controls email to {email}: {e}");
|
||||
Errors {
|
||||
error: &[Error {
|
||||
code: "0130",
|
||||
message: "PID has not been registered yet",
|
||||
}],
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue