1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Add user registration

This commit is contained in:
NG (Graham)
2025-05-12 20:56:24 -04:00
parent d3bd795a01
commit 8affa33ace
14 changed files with 774 additions and 45 deletions

231
Cargo.lock generated
View File

@@ -216,6 +216,8 @@ dependencies = [
"log", "log",
"rc_core", "rc_core",
"rocket", "rocket",
"rocket_dyn_templates",
"serde",
"steamworks", "steamworks",
"uuid", "uuid",
] ]
@@ -562,6 +564,15 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.12" version = "0.3.12"
@@ -809,6 +820,18 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.35" version = "1.0.35"
@@ -851,6 +874,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"
@@ -1049,6 +1081,20 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "handlebars"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
dependencies = [
"log",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@@ -1491,6 +1537,26 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
@@ -1554,6 +1620,26 @@ dependencies = [
"simple_asn1", "simple_asn1",
] ]
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -1606,6 +1692,17 @@ version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.8.0",
"libc",
"redox_syscall",
]
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.30.1" version = "0.30.1"
@@ -1706,6 +1803,18 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.3" version = "1.0.3"
@@ -1736,6 +1845,34 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "normpath"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.8.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@@ -2031,6 +2168,51 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.11",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "pest_meta"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]] [[package]]
name = "pgvector" name = "pgvector"
version = "0.4.0" version = "0.4.0"
@@ -2765,6 +2947,19 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "rocket_dyn_templates"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bbab919c9e67df3f7ac6624a32ef897df4cd61c0969f4d66f3ced0534660d7a"
dependencies = [
"handlebars",
"normpath",
"notify",
"rocket",
"walkdir",
]
[[package]] [[package]]
name = "rocket_http" name = "rocket_http"
version = "0.5.1" version = "0.5.1"
@@ -2909,6 +3104,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.1" version = "1.0.1"
@@ -3734,7 +3938,7 @@ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"libc", "libc",
"mio", "mio 1.0.3",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
@@ -3942,6 +4146,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]] [[package]]
name = "uncased" name = "uncased"
version = "0.9.10" version = "0.9.10"
@@ -4067,6 +4277,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -4223,6 +4443,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View File

@@ -0,0 +1,171 @@
<html>
<head>
<title>OpenJam Registration</title>
<style>
body {
background-color: lightgrey;
padding: 0;
margin: 0;
}
.form-stripe {
position: relative;
min-width: 300px;
max-width: 500px;
height: auto;
min-height: 100%;
margin: 0 auto;
background-color: grey;
padding: 25px 0;
box-shadow: 0px 0px 50px 5px grey;
}
.form-banner {
position: relative;
min-width: 300px;
max-width: 500px;
margin: 0 auto;
box-sizing: border-box;
background-color: dimgrey;
color: black;
padding: 25px;
}
.form-padded {
padding: 0 25px;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
a { text-decoration-line: none; }
a:link { color: darkgrey; }
a:hover { color: white; }
a:visited { color: lightgrey; }
a:active { color: white; }
form {
padding-top: 25px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 250px;
}
.form-error {
position: relative;
min-width: 300px;
max-width: 500px;
margin: 0 auto;
box-sizing: border-box;
background-color: orange;
}
.form-error-message {
padding: 25px;
margin: 0 auto;
width: 95%;
}
.form-input {
padding-top: 5px;
}
.form-input-label { display: none; }
input:required:invalid.form-input-box {
border-color: red;
}
input:required:invalid.form-input-box::after {
content: "*";
color: red;
}
input {
width: 100%;
height: 40px;
border: thin solid white;
background-color: lightgrey;
font-size: 20px;
}
.form-disclaimer {
margin: 0 auto;
width: 95%;
}
.form-submit {
margin: 0 auto;
}
.form-submit-button {
background-color: black;
color: lightgrey;
}
footer {
position: relative;
min-width: 300px;
max-width: 500px;
margin: 0 auto;
background-color: black;
}
.footer {
width: 95%;
color: lightgrey;
box-sizing: border-box;
padding: 25px;
}
</style>
</head>
<body>
<div class="form-stripe">
<h1 class="form-banner">Create an account for <a href="https://git.ngram.ca/OpenJam">OpenJam</a> servers</h1>
<h3 class="form-padded">FOSSifying proprietary games since 2025</h3>
{{#if error}}
<div class="form-error">
<p class="form-error-message">{{error}}</p>
</div>
{{/if}}
<form action="/register" method="post">
<div class="form-input">
<div class="form-input-label">
<label for="display_name">Username</label>
</div>
<input class="form-input-box" type="text" id="display_name" name="display_name" value="{{form.display_name}}" placeholder="username" required/>
</div>
<div class="form-input">
<div class="form-input-label">
<label for="password">Password</label>
</div>
<input class="form-input-box" type="password" id="password" name="password" value="{{form.password}}" placeholder="password" required/>
</div>
<div class="form-input">
<div class="form-input-label">
<label for="password_c">Password (again)</label>
</div>
<input class="form-input-box" type="password" id="password_c" name="password_c" value="{{form.password_c}}" placeholder="password (again)" required/>
</div>
<div class="form-input">
<div class="form-input-label">
<label for="email">Email</label>
</div>
<input class="form-input-box" type="email" id="email" name="email" value="{{form.email}}" placeholder="email@domain.tld"/>
</div>
<div class="form-input">
<div class="form-input-label">
<label for="steam_id">SteamID</label>
</div>
<input class="form-input-box" type="text" id="steam_id" name="steam_id" value="{{form.steam_id}}" placeholder="7656119XXXXXXXXXX"/>
</div>
<div class="form-disclaimer">
<p>By submitting, you agree to the <a href="https://git.ngram.ca/OpenJam/servers#privacy">privacy policy</a>.</p>
</div>
<div class="form-submit">
<input class="form-submit-button" type="submit" value="Submit">
</div>
<div class="form-disclaimer">
<p>This project is not affiliated with FreeJam, Cardlife or Robocraft. We're just fixing their """sunset""".</p>
</div>
</form>
<footer>
<div class="footer">
{{version}}
| <a href="{{source_url}}#readme">About</a>
| <a href="https://liberapay.com/NGram">Support Me</a>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,100 @@
<html>
<head>
<title>OpenJam Success</title>
<style>
body {
background-color: lightgrey;
padding: 0;
margin: 0;
}
.form-stripe {
position: relative;
min-width: 300px;
max-width: 500px;
height: auto;
min-height: 100%;
margin: 0 auto;
background-color: grey;
padding: 25px 0;
box-shadow: 0px 0px 50px 5px grey;
}
.form-banner {
position: relative;
min-width: 300px;
max-width: 500px;
margin: 0 auto;
box-sizing: border-box;
background-color: dimgrey;
color: black;
padding: 25px;
}
.form-padded {
padding: 0 25px;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
a { text-decoration-line: none; }
a:link { color: darkgrey; }
a:hover { color: white; }
a:visited { color: lightgrey; }
a:active { color: white; }
footer {
position: relative;
min-width: 300px;
max-width: 500px;
margin: 0 auto;
background-color: black;
}
.footer {
width: 95%;
color: lightgrey;
box-sizing: border-box;
padding: 25px;
}
.display-name { display: inline-block; }
.display-name:hover {
/* Place silly things in here */
animation: rotate-clk 1s linear infinite;
-webkit-animation: rotate-clk 1s linear infinite;
}
@keyframes rotate-clk {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.display-id { display: inline-block; }
.display-id:hover {
/* Place silly things in here */
animation: rotate-cclk .1s linear infinite;
-webkit-animation: rotate-cclk .1s linear infinite;
}
@keyframes rotate-cclk {
0% {
transform: rotate(0);
}
100% {
transform: rotate(-360deg);
}
}
</style>
</head>
<body>
<div class="form-stripe">
<h1 class="form-padded">Account '<span class="display-name">{{display_name}}</span>' created!</h1>
<h3 class="form-padded">Welcome user #<span class="display-id">{{id}}</span> to Robocraft!</h3>
<h5 class="form-padded" style="padding-bottom:250px;">Let the FOSS flow through you...</h5>
<footer>
<div class="footer">
{{version}}
| <a href="{{source_url}}#readme">About</a>
| <a href="https://liberapay.com/NGram">Support Me</a>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -15,6 +15,7 @@ default = ["robocraft", "cardlife"]
[dependencies] [dependencies]
rocket.workspace = true rocket.workspace = true
rocket_dyn_templates = { version = "0.2", features = [ "handlebars" ] }
libfj.workspace = true libfj.workspace = true
log.workspace = true log.workspace = true
env_logger.workspace = true env_logger.workspace = true
@@ -24,3 +25,4 @@ hex = "0.4"
jsonwebtoken = "9" jsonwebtoken = "9"
clap.workspace = true clap.workspace = true
rc_core = { version = "*", optional = true, path = "../rc_core" } rc_core = { version = "*", optional = true, path = "../rc_core" }
serde.workspace = true

View File

@@ -2,6 +2,7 @@
[default] [default]
address = "127.0.0.1" address = "127.0.0.1"
port = 8001 port = 8001
template_dir = "../assets/templates/"
## set only when compiled in debug mode, i.e, `cargo build` ## set only when compiled in debug mode, i.e, `cargo build`
[debug] [debug]

View File

@@ -2,6 +2,7 @@ mod debug;
mod username; mod username;
mod steam; mod steam;
mod email; mod email;
mod registration;
pub fn stage() -> rocket::fairing::AdHoc { pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("robocraft", |rocket| async { rocket::fairing::AdHoc::on_ignite("robocraft", |rocket| async {
@@ -9,6 +10,7 @@ pub fn stage() -> rocket::fairing::AdHoc {
.attach(email::stage()) .attach(email::stage())
.attach(steam::stage()) .attach(steam::stage())
.attach(debug::stage()) .attach(debug::stage())
.attach(registration::stage())
.register("/", rocket::catchers![unauthorized]) .register("/", rocket::catchers![unauthorized])
}) })
} }

View File

@@ -0,0 +1,189 @@
use rc_core::UserAuthenticator;
use rocket::{post, get, form::{Form, FromForm}, routes, http::Status, State};
use rocket_dyn_templates::{Template, context};
use serde::Serialize;
const FORM_NAME: &str = "rc_register";
const FORM_NAME_SUCCESS: &str = "rc_register_success";
const VALID_CHARS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'_',
];
#[derive(FromForm, Serialize)]
struct RegisterForm {
display_name: String,
password: String,
password_c: String,
email: Option<String>,
steam_id: Option<String>,
}
#[derive(Serialize)]
struct Context {
form: RegisterForm,
error: Option<String>,
version: String,
source_url: String,
}
fn version_string() -> String {
let name = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
//let license = env!("CARGO_PKG_LICENSE");
//let repo = env!("CARGO_PKG_REPOSITORY");
format!("OpenJam {} {}", name, version)
}
fn all_valid_chars(s: &str) -> bool {
for c in s.chars() {
if !VALID_CHARS.contains(&c) {
return false;
}
}
true
}
fn registration_ok(form: RegisterForm) -> Template {
Template::render(FORM_NAME, Context {
form,
error: None,
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
})
}
fn registration_err(form: RegisterForm, error: String) -> Template {
Template::render(FORM_NAME, Context {
form,
error: Some(error),
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
})
}
#[post("/register", data = "<form>")]
async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli::Config>) -> Result<Template, Status> {
// password confirmation validation
if form.password != form.password_c {
return Ok(registration_err(form.into_inner(), "Passwords do not match".to_owned()));
}
if form.password.len() < 8 {
return Ok(registration_err(form.into_inner(), "Password too short (minimum 8 characters)".to_owned()));
}
if form.password.len() > 128 {
return Ok(registration_err(form.into_inner(), "Password too long (maximum 128 characters)".to_owned()));
}
// email validation
let actual_email: Option<String>;
if let Some(email) = &form.email {
if email.is_empty() {
actual_email = None;
} else {
if !email.contains('@') {
return Ok(registration_err(form.into_inner(), "Email must contain @".to_owned()));
}
let email_exists = config.robocraft.account_provider.user_exists(rc_core::persist::user::UserId::Email(email.to_owned()))
.await
.map_err(|e| {
log::error!("Failed to check if user email {} exists: {}", email, e);
Status { code: 500 }
})?;
if email_exists {
return Ok(registration_err(form.into_inner(), "Email already registered".to_owned()));
}
actual_email = Some(email.to_owned());
}
} else {
actual_email = None;
}
// steam id validation
let actual_steam_id: Option<u64>;
if let Some(steam_id) = &form.steam_id {
if steam_id.is_empty() {
actual_steam_id = None;
} else {
let steam_id = match steam_id.parse() {
Ok(id) => id,
Err(_e) => return Ok(registration_err(form.into_inner(), "Invalid SteamID (not an integer)".to_owned())),
};
if steam_id >= 7656120_0000000000 || steam_id < 7656119_0000000000 {
return Ok(registration_err(form.into_inner(), "Invalid SteamID (should be like 7656119XXXXXXXXXX)".to_owned()));
}
let steam_exists = config.robocraft.account_provider.user_exists(rc_core::persist::user::UserId::SteamId(steam_id))
.await
.map_err(|e| {
log::error!("Failed to check if user steam id {} exists: {}", steam_id, e);
Status { code: 500 }
})?;
if steam_exists {
return Ok(registration_err(form.into_inner(), "SteamID already registered".to_owned()));
}
actual_steam_id = Some(steam_id);
}
} else {
actual_steam_id = None;
}
// username validation
if form.display_name.len() < 4 {
return Ok(registration_err(form.into_inner(), "Username too short (minimum 4 characters)".to_owned()));
}
if form.display_name.len() > 32 {
return Ok(registration_err(form.into_inner(), "Username too long (maximum 32 characters)".to_owned()));
}
if !all_valid_chars(&form.display_name.to_lowercase()) {
return Ok(registration_err(form.into_inner(), "Invalid username (only alphanumerics and _ allowed)".to_owned()));
}
let username_exists = config.robocraft.account_provider.user_exists(rc_core::persist::user::UserId::Username(form.display_name.to_owned()))
.await
.map_err(|e| {
log::error!("Failed to check if user name {} exists: {}", form.display_name, e);
Status { code: 500 }
})?;
if username_exists {
return Ok(registration_err(form.into_inner(), "Username already registered".to_owned()));
}
let user_id = match config.robocraft.account_provider.register(rc_core::persist::user::RegistrationInfo {
display_name: form.display_name.clone(),
password: form.password.clone(),
email: actual_email,
steam_id: actual_steam_id,
}).await {
Ok(id) => id,
Err(e) => {
return Ok(registration_err(form.into_inner(), format!("Registration failed: {}", e)));
}
};
Ok(Template::render(FORM_NAME_SUCCESS, context! {
display_name: form.display_name.clone(),
id: user_id,
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
}))
}
#[get("/register")]
async fn form_load() -> Template {
registration_ok(RegisterForm {
display_name: "".to_owned(),
password: "".to_owned(),
password_c: "".to_owned(),
email: None,
steam_id: None,
})
}
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("Robocraft Steam", |rocket| async {
rocket.mount("/", routes![form_load, form_submit])
.attach(Template::fairing())
})
}

View File

@@ -259,6 +259,7 @@ impl <C: Clone> super::ConfigProvider<C> for CubeConfig {
fn server_config(&self) -> super::ServerConfig { fn server_config(&self) -> super::ServerConfig {
super::ServerConfig { super::ServerConfig {
database: self.settings.server.database.clone(), database: self.settings.server.database.clone(),
auto_signup: self.settings.server.auto_signup,
} }
} }

View File

@@ -88,6 +88,7 @@ pub struct TypedDevMessage<C> {
pub struct ServerConfig { pub struct ServerConfig {
pub database: String, pub database: String,
pub auto_signup: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@@ -72,6 +72,8 @@ fn default_slot_upgrades() -> Vec<GarageSlotUpgrade> {
pub struct ServerSettings { pub struct ServerSettings {
#[serde(default = "default_db_conn")] #[serde(default = "default_db_conn")]
pub database: String, pub database: String,
#[serde(default)]
pub auto_signup: bool,
} }
fn default_db_conn() -> String { fn default_db_conn() -> String {
@@ -81,5 +83,6 @@ fn default_db_conn() -> String {
fn default_server_conf() -> ServerSettings { fn default_server_conf() -> ServerSettings {
ServerSettings { ServerSettings {
database: default_db_conn(), database: default_db_conn(),
auto_signup: false,
} }
} }

View File

@@ -5,6 +5,7 @@ use crate::persist::config::ConfigProvider;
pub struct AccountProvider { pub struct AccountProvider {
cubes: std::sync::Arc<Vec<u32>>, cubes: std::sync::Arc<Vec<u32>>,
garage_upgrades: std::sync::Arc<crate::persist::config::GarageUpgrades>, garage_upgrades: std::sync::Arc<crate::persist::config::GarageUpgrades>,
auto_signups: bool,
secret: Vec<u8>, secret: Vec<u8>,
db: std::sync::Arc<rc_database::Database>, db: std::sync::Arc<rc_database::Database>,
} }
@@ -12,13 +13,15 @@ pub struct AccountProvider {
impl AccountProvider { impl AccountProvider {
pub async fn load(root: impl AsRef<std::path::Path>, conf: &crate::persist::config::ConfigImpl) -> std::io::Result<Self> { pub async fn load(root: impl AsRef<std::path::Path>, conf: &crate::persist::config::ConfigImpl) -> std::io::Result<Self> {
let token_path = root.as_ref().join(super::TOKEN_SECRET_FILENAME); let token_path = root.as_ref().join(super::TOKEN_SECRET_FILENAME);
let database_uri = <crate::persist::config::ConfigImpl as ConfigProvider<()>>::server_config(conf).database; let server_settings = <crate::persist::config::ConfigImpl as ConfigProvider<()>>::server_config(conf);
let database_uri = server_settings.database;
log::debug!("Connecting to user database URI: {}", database_uri); log::debug!("Connecting to user database URI: {}", database_uri);
let db = rc_database::Database::init(&database_uri).await let db = rc_database::Database::init(&database_uri).await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotConnected, e))?; .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotConnected, e))?;
Ok(Self { Ok(Self {
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(conf)), cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(conf)),
garage_upgrades: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::garage_upgrades(conf)), garage_upgrades: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::garage_upgrades(conf)),
auto_signups: server_settings.auto_signup,
secret: std::fs::read(&token_path)?, secret: std::fs::read(&token_path)?,
db: std::sync::Arc::new(db), db: std::sync::Arc::new(db),
}) })
@@ -72,9 +75,14 @@ impl super::UserAuthenticator for AccountProvider {
user_info user_info
} else { } else {
is_new_user = true; is_new_user = true;
log::info!("New user {}", info.payload.public_id); if self.auto_signups {
super::setup_new_user(&info, &self.db).await.map_err(|e| e.to_string())?; log::info!("New user {}", info.payload.public_id);
self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| e.to_string())?.unwrap() super::setup_new_user(&info, &self.db).await.map_err(|e| e.to_string())?;
self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| e.to_string())?.unwrap()
} else {
log::info!("Rejecting user sign-in for `{}` (set settings.server.auto_signup=true to disable this behaviour)", info.payload.public_id);
return Err(format!("User does not exist"));
}
}; };
let override_password = user_info.password.is_empty() && user_info.steam_id.is_none(); let override_password = user_info.password.is_empty() && user_info.steam_id.is_none();
match info.extra { match info.extra {
@@ -128,6 +136,24 @@ impl super::UserAuthenticator for AccountProvider {
is_new: is_new_user, is_new: is_new_user,
}) })
} }
async fn user_exists(&self, user: super::UserId) -> Result<bool, String> {
Ok(match user {
super::UserId::SteamId(steam_id) => {
self.db.user_by_steam_id(steam_id).await.map_err(|e| e.to_string())?.is_some()
},
super::UserId::Email(email) => {
self.db.user_by_email(email).await.map_err(|e| e.to_string())?.is_some()
},
super::UserId::Username(display_name) => {
self.db.user_by_display_name(display_name).await.map_err(|e| e.to_string())?.is_some()
},
})
}
async fn register(&self, info: super::RegistrationInfo) -> Result<u32, String> {
super::register_new_user(&info, &self.db).await.map_err(|e| e.to_string())
}
} }
#[allow(dead_code)] #[allow(dead_code)]

View File

@@ -5,17 +5,17 @@ fn current_unix_time() -> i64 {
} }
async fn build_new_account_data(user: &super::UserInfo, db: &rc_database::Database) -> Result<(), rc_database::sea_orm::DbErr> { async fn build_new_account_data(user: &super::UserInfo, db: &rc_database::Database) -> Result<(), rc_database::sea_orm::DbErr> {
//std::fs::create_dir(&root)?; let reg_info = super::RegistrationInfo {
//let garage_dir = root.as_ref().join(super::GARAGE_DIR); display_name: user.payload.display_name.clone(),
//std::fs::create_dir(&garage_dir)?; password: if let super::ExtraUserInfo::Email { password } | super::ExtraUserInfo::Username { password } = &user.extra {
let user_data = db.insert_user(default_user_data(user)).await?; password.to_owned()
db.insert_perms(default_user_perms(user_data.id)).await?; } else {
db.insert_user_aux(default_user_aux_data(user_data.id)).await?; "".to_owned()
db.insert_garages(default_garage_slots(user_data.id)).await?; },
/*for slot in default_garage_slots() { email: None,
let filepath = garage_dir.join(format!("{}.json", slot.slot)); steam_id: if let super::ExtraUserInfo::Steam { id } = &user.extra { Some(*id) } else { None },
slot.save(filepath)?; };
}*/ register_new_user(&reg_info, db).await?;
Ok(()) Ok(())
} }
@@ -31,23 +31,28 @@ pub async fn setup_new_user(user: &super::UserInfo, db: &rc_database::Database)
Ok(()) Ok(())
} }
fn default_user_data(user: &super::UserInfo) -> rc_database::schema::user::ActiveModel { pub async fn register_new_user(info: &super::RegistrationInfo, db: &rc_database::Database) -> Result<u32, rc_database::sea_orm::DbErr> {
let password = if let super::ExtraUserInfo::Email { password } | super::ExtraUserInfo::Username { password } = &user.extra { let user_data = db.insert_user(default_user_data(info)).await?;
//password.to_owned() db.insert_perms(default_user_perms(user_data.id)).await?;
db.insert_user_aux(default_user_aux_data(user_data.id)).await?;
db.insert_garages(default_garage_slots(user_data.id)).await?;
Ok(user_data.id)
}
fn default_user_data(info: &super::RegistrationInfo) -> rc_database::schema::user::ActiveModel {
let password = {
use argon2::password_hash::PasswordHasher; use argon2::password_hash::PasswordHasher;
let argon2_algo = argon2::Argon2::default(); let argon2_algo = argon2::Argon2::default();
let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng); let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
match argon2_algo.hash_password(password.as_bytes(), &salt) { match argon2_algo.hash_password(info.password.as_bytes(), &salt) {
Err(e) => { Err(e) => {
log::error!("Failed to hash password for user {}/{}: {}", user.payload.public_id, user.payload.display_name, e); log::error!("Failed to hash password for user {}: {}", info.display_name, e);
"".to_owned() "".to_owned()
}, },
Ok(password) => password.to_string(), Ok(password) => password.to_string(),
} }
} else {
Default::default()
}; };
let steam_id = if let super::ExtraUserInfo::Steam { id } = &user.extra { let steam_id = if let Some(id) = info.steam_id {
Some(id.to_string()) Some(id.to_string())
} else { } else {
None None
@@ -55,28 +60,12 @@ fn default_user_data(user: &super::UserInfo) -> rc_database::schema::user::Activ
rc_database::schema::user::ActiveModel { rc_database::schema::user::ActiveModel {
id: Default::default(), id: Default::default(),
creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()), creation_time: rc_database::sea_orm::ActiveValue::Set(current_unix_time()),
public_id: rc_database::sea_orm::ActiveValue::Set(user.payload.public_id.clone()), public_id: rc_database::sea_orm::ActiveValue::Set(info.display_name.clone()),
display_name: rc_database::sea_orm::ActiveValue::Set(user.payload.display_name.clone()), display_name: rc_database::sea_orm::ActiveValue::Set(info.display_name.clone()),
password: rc_database::sea_orm::ActiveValue::Set(password), password: rc_database::sea_orm::ActiveValue::Set(password),
email: rc_database::sea_orm::ActiveValue::Set("//TODO".to_owned()), email: rc_database::sea_orm::ActiveValue::Set(info.email.clone().unwrap_or_else(|| "".to_owned())),
steam_id: rc_database::sea_orm::ActiveValue::Set(steam_id), steam_id: rc_database::sea_orm::ActiveValue::Set(steam_id),
} }
/*super::AccountInfo {
is_mod: false,
is_admin: false,
is_dev: false,
steam_id: None,
password: None,
inventory: super::UnlockedParts {
unlocked: vec![],
override_: super::inventory::UnlockOverride::UnlockAll,
},
garage: super::SelectedGarage {
uuid: (0, 0),
slot: 0,
},
}*/
} }
fn default_user_aux_data(user_id: u32) -> Vec<rc_database::schema::user_aux::ActiveModel> { fn default_user_aux_data(user_id: u32) -> Vec<rc_database::schema::user_aux::ActiveModel> {

View File

@@ -5,13 +5,13 @@ mod garage_data;
pub use garage_data::SelectedGarage; pub use garage_data::SelectedGarage;
mod initial_data; mod initial_data;
pub use initial_data::setup_new_user; pub use initial_data::{setup_new_user, register_new_user};
mod inventory; mod inventory;
pub use inventory::UnlockedParts; pub use inventory::UnlockedParts;
mod traits; mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData}; pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo};
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -23,11 +23,24 @@ pub enum ExtraUserInfo {
} }
} }
pub enum UserId {
SteamId(u64),
Email(String),
Username(String),
}
pub struct UserLoginInfo { pub struct UserLoginInfo {
pub response: libfj::robocraft::AuthenticationResponseInfo, pub response: libfj::robocraft::AuthenticationResponseInfo,
pub is_new: bool, pub is_new: bool,
} }
pub struct RegistrationInfo {
pub display_name: String,
pub password: String,
pub email: Option<String>,
pub steam_id: Option<u64>,
}
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait UserProvider<C> { pub trait UserProvider<C> {
async fn authenticate(&self, user: UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn User<C> + Send + Sync>, String>; async fn authenticate(&self, user: UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn User<C> + Send + Sync>, String>;
@@ -36,6 +49,8 @@ pub trait UserProvider<C> {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait UserAuthenticator { pub trait UserAuthenticator {
async fn login(&self, info: UserInfo) -> Result<UserLoginInfo, String>; async fn login(&self, info: UserInfo) -> Result<UserLoginInfo, String>;
async fn user_exists(&self, user: UserId) -> Result<bool, String>;
async fn register(&self, info: RegistrationInfo) -> Result<u32, String>;
} }
#[async_trait::async_trait] #[async_trait::async_trait]