Merge branch 'feat/email' into 'main'

Add email verification

See merge request perditum/account-rs!1
This commit is contained in:
andrea 2025-04-26 11:48:02 +00:00
commit 9f6fd3f17d
26 changed files with 1109 additions and 19 deletions

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
target
.idea
.env
.env
.DS_Store

View file

@ -0,0 +1,27 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users (\n pid,\n username,\n password,\n birthdate,\n timezone,\n email,\n country,\n language,\n marketing_allowed,\n off_device_allowed,\n region,\n gender,\n mii_data,\n verification_code\n ) VALUES (\n $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Varchar",
"Varchar",
"Date",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Int4",
"Bpchar",
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "02d51edf65163f311dfa215da26acb2bc8c02735e2f79782c612d5fe0cf01042"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 ) as exists",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "08c4a5721982ecd267ebc515b866c61aaef9395170430a58dcd7ac78f8cc75b2"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET email_verified_since = $1 WHERE pid = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamp",
"Int4"
]
},
"nullable": []
},
"hash": "248fc3dbfadb793f1f380486d9c1c95230d4d5a7ee3cb66d9382b7f0522c5e82"
}

View file

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT nextval('pid_counter') as pid",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "3c9b1695f8ae49e4308c048de98c1c262351465b6f102e98912f67442a1f54d9"
}

View file

@ -0,0 +1,66 @@
{
"db_name": "PostgreSQL",
"query": "select * from tokens where pid = $1 and token_id = $2 and random = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "token_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "assigned_ip",
"type_info": "Inet"
},
{
"ordinal": 3,
"name": "random",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "token_type",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "creation_time",
"type_info": "Timestamp"
},
{
"ordinal": 6,
"name": "expires",
"type_info": "Timestamp"
},
{
"ordinal": 7,
"name": "title_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int4",
"Int8",
"Int4"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
false,
true
]
},
"hash": "48710e0b87742cc3fef816b3c95604095f71324011e7093ec37af15da8c158f4"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "select pid, username from users where username = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "566221b869f293e6c721e2e8bbf3087943e5816a98cdce151302055c58bd1183"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "select address, port from nex_servers where game_server_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "address",
"type_info": "Inet"
},
{
"ordinal": 1,
"name": "port",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "5bd26d4c9e701bde77ce598fea2ce0b98ea2c7de03e71ac704c3ba047162c0b2"
}

View file

@ -0,0 +1,130 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM users WHERE username = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "password",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "birthdate",
"type_info": "Date"
},
{
"ordinal": 4,
"name": "timezone",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email_verified_since",
"type_info": "Timestamp"
},
{
"ordinal": 7,
"name": "country",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "language",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "gender",
"type_info": "Bpchar"
},
{
"ordinal": 10,
"name": "marketing_allowed",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "off_device_allowed",
"type_info": "Bool"
},
{
"ordinal": 12,
"name": "region",
"type_info": "Int4"
},
{
"ordinal": 13,
"name": "mii_data",
"type_info": "Varchar"
},
{
"ordinal": 14,
"name": "account_level",
"type_info": "Int4"
},
{
"ordinal": 15,
"name": "creation_date",
"type_info": "Timestamp"
},
{
"ordinal": 16,
"name": "updated",
"type_info": "Timestamp"
},
{
"ordinal": 17,
"name": "nex_password",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "verification_code",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "606364c79e0990deb07dfbe6c32b3d302d083ec5333f3a5ce04113c38a041100"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "select nex_password from users where pid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "nex_password",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false
]
},
"hash": "6c1df0b05553305ba847f571a5859bf11353f28c25e4f81268e9379b5b2cb375"
}

View file

@ -0,0 +1,130 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM users WHERE pid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "password",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "birthdate",
"type_info": "Date"
},
{
"ordinal": 4,
"name": "timezone",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email_verified_since",
"type_info": "Timestamp"
},
{
"ordinal": 7,
"name": "country",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "language",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "gender",
"type_info": "Bpchar"
},
{
"ordinal": 10,
"name": "marketing_allowed",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "off_device_allowed",
"type_info": "Bool"
},
{
"ordinal": 12,
"name": "region",
"type_info": "Int4"
},
{
"ordinal": 13,
"name": "mii_data",
"type_info": "Varchar"
},
{
"ordinal": 14,
"name": "account_level",
"type_info": "Int4"
},
{
"ordinal": 15,
"name": "creation_date",
"type_info": "Timestamp"
},
{
"ordinal": 16,
"name": "updated",
"type_info": "Timestamp"
},
{
"ordinal": 17,
"name": "nex_password",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "verification_code",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "93960465bbf8f670891d49b95fc52257c0ef596eee2d9a0e6a7d0aad03de4421"
}

View file

@ -0,0 +1,66 @@
{
"db_name": "PostgreSQL",
"query": "select * from tokens where pid = $1 and token_id = $2 and random =$3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "token_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "assigned_ip",
"type_info": "Inet"
},
{
"ordinal": 3,
"name": "random",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "token_type",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "creation_time",
"type_info": "Timestamp"
},
{
"ordinal": 6,
"name": "expires",
"type_info": "Timestamp"
},
{
"ordinal": 7,
"name": "title_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int4",
"Int8",
"Int4"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
false,
true
]
},
"hash": "9d3cee43a86cead9a6d078abc1266fc2a97ac6e25a9733d1d20faf555c67abe1"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT verification_code FROM users WHERE pid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "verification_code",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
true
]
},
"hash": "b16ba4b6c1b7d1c207e94515268c3bbd90d4bcf92d20203cba5afb2664f4bb8a"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(select 1 from users where pid = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
null
]
},
"hash": "c8d50662530cac49c4261fb321cd15f9e4bafdfca12d2130a873d44a88dd435b"
}

View file

@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "insert into tokens (token_type, pid, title_id)\n values ($1, $2, $3) returning token_id, random",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "random",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4",
"Int4",
"Varchar"
]
},
"nullable": [
false,
false
]
},
"hash": "e5a2f7f28c3d7b9524d3dce48a9e47d6180ff634ebf59f3a1efd92b797170ac2"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "select pid, username from users where pid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pid",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false,
false
]
},
"hash": "eafa97669e8ec04f1dc0a8e05d417f12ffeeb26a5eabca323abfed0e1d7bcbea"
}

123
Cargo.lock generated
View file

@ -20,6 +20,7 @@ dependencies = [
"hmac",
"juniper",
"juniper_rocket",
"lettre",
"log",
"md-5",
"mii",
@ -27,6 +28,7 @@ dependencies = [
"once_cell",
"prost",
"quick-xml",
"rand",
"rocket",
"serde",
"serde_json",
@ -64,6 +66,18 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -470,6 +484,16 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "chumsky"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
dependencies = [
"hashbrown 0.14.5",
"stacker",
]
[[package]]
name = "cipher"
version = "0.4.4"
@ -713,6 +737,22 @@ dependencies = [
"serde",
]
[[package]]
name = "email-encoding"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
dependencies = [
"base64",
"memchr",
]
[[package]]
name = "email_address"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -1072,6 +1112,10 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashbrown"
@ -1144,6 +1188,17 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "hostname"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
dependencies = [
"cfg-if",
"libc",
"windows-link",
]
[[package]]
name = "http"
version = "0.2.12"
@ -1639,6 +1694,31 @@ dependencies = [
"spin",
]
[[package]]
name = "lettre"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759bc2b8eabb6a30b235d6f716f7f36479f4b38cbe65b8747aefee51f89e8437"
dependencies = [
"base64",
"chumsky",
"email-encoding",
"email_address",
"fastrand",
"futures-util",
"hostname",
"httpdate",
"idna",
"mime",
"native-tls",
"nom",
"percent-encoding",
"quoted_printable",
"socket2",
"tokio",
"url",
]
[[package]]
name = "libc"
version = "0.2.170"
@ -1861,6 +1941,15 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -2256,6 +2345,15 @@ dependencies = [
"prost",
]
[[package]]
name = "psm"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
dependencies = [
"cc",
]
[[package]]
name = "quick-xml"
version = "0.37.2"
@ -2275,6 +2373,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
[[package]]
name = "rand"
version = "0.8.5"
@ -3029,6 +3133,19 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacker"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys 0.59.0",
]
[[package]]
name = "state"
version = "0.6.0"
@ -3781,6 +3898,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-registry"
version = "0.2.0"

View file

@ -44,8 +44,10 @@ juniper_rocket = "0.9.0"
tonic = "0.12.3"
prost = "0.13.4"
lettre = "0.11.15"
rand = "0.8.5"
[build-dependencies]
tonic-build = "0.12.3"
tonic-build = "0.12.3"

View file

@ -0,0 +1,203 @@
<!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://cdn.abmanagement.al/perditumgames.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 activation is almost complete. Please click the link in this email to confirm your e-mail address and complete the activation process.</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">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td height="36px" style="line-height: 36px;" width="100%">&nbsp;</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td>
<a href="https://spfn.cc">
<img class="logo" width="auto" height="48px" src="https://cdn.abmanagement.al/SPFN.png" alt="SPFN">
</a>
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</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%">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr width="100%" height="48px" style="line-height: 48px;">
<td>&nbsp;</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;">&nbsp;</td>
</tr>
<tr>
<td style="color: #ffffff; ">
Your Splatfestival Network ID activation is almost complete.
</td>
</tr>
<tr>
<td width="100%" height="16px" style="line-height: 16px;">&nbsp;</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;">&nbsp;</td>
</tr>
<tr>
<td>
Enter the following 6-digit code on your console:
</td>
</tr>
<tr>
<td width="100%" height="16px" style="line-height: 16px;">&nbsp;</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">
{{confirmation-code}}
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
<tr>
<td>
We hope you have fun using our services!
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
</tr>
<tr>
<td align="right">
The SPFN team
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
</table>
</td>
<td width="24px" height="100%">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<tr>
<td width="100%" height="18px" style="line-height: 18px;">&nbsp;</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/splatfestival" style="text-decoration: none; color: #ffffff; ">Discord server</a> or make a post on our <a href="https://forum.perditum.com" style="text-decoration: none; color: #ffffff; ">Forum</a>.
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
<td width="32px">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -54,7 +54,8 @@ pub struct User {
pub mii_data: String,
pub creation_date: NaiveDateTime,
pub updated: NaiveDateTime,
pub nex_password: String
pub nex_password: String,
pub verification_code: Option<i32>
}
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{

38
src/email.rs Normal file
View file

@ -0,0 +1,38 @@
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
use std::env;
use std::fs;
pub async fn send_verification_email(to: &str, code: i32, 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/confirmationTemplate.html")
.map_err(|e| format!("Failed to read email template: {}", e))?;
// Replace placeholders
let body = template
.replace("{{username}}", username)
.replace("{{confirmation-code}}", &format!("{:06}", code));
let email = Message::builder()
.from(smtp_user.parse().unwrap())
.to(to.parse().unwrap())
.subject("Your Verification Code")
.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(())
}

View file

@ -26,6 +26,7 @@ mod data_wrapper;
#[deprecated]
mod grpc;
mod graphql;
mod email;
type Pool = sqlx::Pool<Postgres>;
@ -109,7 +110,8 @@ async fn launch() -> _ {
nnid::agreements::get_agreement,
nnid::timezones::get_timezone,
nnid::person_exists::person_exists,
nnid::email::validate,
nnid::support::validate,
nnid::support::verify_email,
nnid::people::create_account,
nnid::people::get_own_profile,
nnid::oauth::generate_token::generate_token,

View file

@ -1,11 +0,0 @@
use rocket::{post, FromForm};
use rocket::form::Form;
#[derive(FromForm)]
struct ValidateEmailInput{
email: String,
}
#[post("/v1/api/support/validate/email", data="<data>")]
pub fn validate(data: Form<ValidateEmailInput>){
}

View file

@ -2,9 +2,9 @@ pub mod devices;
pub mod agreements;
pub mod timezones;
pub mod person_exists;
pub mod email;
pub mod oauth;
mod pid_distribution;
pub mod people;
pub mod provider;
pub mod mapped_ids;
pub mod support;

View file

@ -18,6 +18,8 @@ use crate::nnid::pid_distribution::next_pid;
use crate::nnid::timezones::{OFFSET_FROM_TIMEZONE, ZONE_TO_TIMEZONES};
use crate::Pool;
use crate::xml::{Xml, YesNoVal};
use crate::email::send_verification_email;
use rand::Rng;
static S3_URL_STRING: Lazy<Box<str>> = Lazy::new(||
env::var("S3_URL").expect("S3_URL not specified").into_boxed_str()
@ -114,6 +116,8 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
let pid = next_pid(database).await;
let verification_code: i32 = rand::thread_rng().gen_range(100_000..1_000_000);
let AccountCreationData {
user_id,
password,
@ -152,9 +156,10 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
off_device_allowed,
region,
gender,
mii_data
mii_data,
verification_code
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14
)
",
pid,
@ -169,11 +174,16 @@ pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationDat
off_device_flag.0,
region,
gender.as_ref(),
data.as_ref()
data.as_ref(),
verification_code,
).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 {
println!("Failed to send verification email: {e}");
}
Ok(
Xml(AccountCreationResponseData{
pid

65
src/nnid/support.rs Normal file
View file

@ -0,0 +1,65 @@
use rocket::{get, State, post, FromForm};
use rocket::http::Status;
use crate::Pool;
use rocket::form::Form;
use crate::email::send_verification_email;
use crate::error::{Error, Errors};
use chrono::Utc;
const BAD_CODE_ERROR: Errors = Errors{
error: &[
Error{
code: "0116",
message: "Missing or invalid verification code"
}
]
};
#[derive(FromForm)]
struct ValidateEmailInput{
email: String,
}
#[post("/v1/api/support/validate/email", data="<data>")]
pub async fn validate(data: Form<ValidateEmailInput>){
if let Err(e) = send_verification_email(&*data.email, 123456, "Andrea Test Username").await {
println!("Failed to send verification email: {e}");
}
}
#[get("/v1/api/support/email_confirmation/<pid>/<code>")]
pub async fn verify_email(database: &State<Pool>, pid: i32, code: i32) -> Result<(), Errors<'static>> {
let db = database.inner();
let result = sqlx::query!(
"SELECT verification_code FROM users WHERE pid = $1",
pid
)
.fetch_optional(db)
.await;
let Ok(Some(record)) = result else {
return Err(BAD_CODE_ERROR);
};
if let Some(stored_code) = record.verification_code {
if stored_code == code {
// Set email_verified_since to NOW
let now = Utc::now().naive_utc();
let update_result = sqlx::query!(
"UPDATE users SET email_verified_since = $1 WHERE pid = $2",
now,
pid
)
.execute(db)
.await;
if update_result.is_err() {
return Err(BAD_CODE_ERROR); // fallback in case the update fails
}
return Ok(()); // Success
}
}
Err(BAD_CODE_ERROR)
}