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

Create society service with basic functionality and auth #118

This commit is contained in:
NG (Graham)
2026-05-02 11:03:49 -04:00
parent e09aeceed3
commit e3183eb238
51 changed files with 1424 additions and 77 deletions

185
Cargo.lock generated
View File

@@ -52,7 +52,7 @@ dependencies = [
"actix-rt",
"actix-service",
"actix-utils",
"base64",
"base64 0.22.1",
"bitflags",
"brotli",
"bytes",
@@ -80,6 +80,22 @@ dependencies = [
"zstd",
]
[[package]]
name = "actix-identity"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "810f47733f956175bd5b2ae17ae5237fa92bd1b6a4a65f646a7240dbe9ff2728"
dependencies = [
"actix-service",
"actix-session",
"actix-utils",
"actix-web",
"derive_more",
"futures-core",
"serde",
"tracing",
]
[[package]]
name = "actix-macros"
version = "0.2.4"
@@ -141,6 +157,23 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "actix-session"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "400c27fd4cdbe0082b7bbd29ac44a3070cbda1b2114138dc106ba39fe2f90dff"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"anyhow",
"derive_more",
"rand 0.9.2",
"serde",
"serde_json",
"tracing",
]
[[package]]
name = "actix-utils"
version = "3.0.1"
@@ -169,6 +202,7 @@ dependencies = [
"bytes",
"bytestring",
"cfg-if",
"cookie",
"derive_more",
"encoding_rs",
"foldhash",
@@ -224,6 +258,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
@@ -235,6 +279,20 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -341,6 +399,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "approx"
version = "0.1.1"
@@ -467,6 +531,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "base64"
version = "0.22.1"
@@ -800,6 +870,24 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"aes-gcm",
"base64 0.20.0",
"hkdf",
"hmac",
"percent-encoding",
"rand 0.8.5",
"sha2",
"subtle",
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -879,9 +967,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"typenum",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@@ -1422,6 +1520,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "git-version"
version = "0.3.9"
@@ -1678,7 +1786,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
@@ -1955,7 +2063,7 @@ version = "10.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1"
dependencies = [
"base64",
"base64 0.22.1",
"ed25519-dalek",
"getrandom 0.2.17",
"hmac",
@@ -2006,7 +2114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68fff2e457fb18346f6de760a0bbfe2de69febc5ab1782c5ff470db14de2241d"
dependencies = [
"async-trait",
"base64",
"base64 0.22.1",
"cgmath 0.18.0",
"chrono",
"genmesh",
@@ -2522,7 +2630,7 @@ version = "1.2.2"
dependencies = [
"argon2",
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"futures",
"hex",
@@ -2563,7 +2671,7 @@ name = "oj_rc_factory"
version = "1.2.2"
dependencies = [
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"hex",
"libfj",
@@ -2579,7 +2687,7 @@ version = "1.2.2"
dependencies = [
"actix-files",
"actix-web",
"base64",
"base64 0.22.1",
"clap",
"env_logger",
"git-version",
@@ -2772,6 +2880,29 @@ dependencies = [
"tokio",
]
[[package]]
name = "oj_rc_society"
version = "1.2.2"
dependencies = [
"actix-files",
"actix-identity",
"actix-session",
"actix-web",
"actix-ws",
"chrono",
"clap",
"cookie",
"env_logger",
"git-version",
"handlebars",
"libfj",
"log",
"oj_rc_core",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "oj_serdes"
version = "0.3.0"
@@ -2794,6 +2925,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "ordered-float"
version = "4.6.0"
@@ -2907,7 +3044,7 @@ version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64",
"base64 0.22.1",
"serde_core",
]
@@ -3038,6 +3175,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -3429,7 +3578,7 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
@@ -4074,7 +4223,7 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"base64 0.22.1",
"bigdecimal",
"bytes",
"chrono",
@@ -4154,7 +4303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"base64 0.22.1",
"bigdecimal",
"bitflags",
"byteorder",
@@ -4201,7 +4350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"base64 0.22.1",
"bigdecimal",
"bitflags",
"byteorder",
@@ -4748,6 +4897,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -4760,7 +4919,7 @@ version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64",
"base64 0.22.1",
"flate2",
"log",
"once_cell",

View File

@@ -26,6 +26,7 @@ members = [
"rc_factory", "rc_factory_web",
"rc_multiplayer",
"rc_plugins",
"rc_society",
]
[workspace.dependencies]

View File

@@ -0,0 +1,273 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<title>Dashboard - Openjam</title>
<meta name="title" content="Dashboard - Openjam" />
<meta name="description" content="User dashboard for third-party Robocraft servers" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Dashboard - Openjam" />
<meta property="og:description" content="Create an account for third-party Robocraft servers" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Dashboard - Openjam" />
<meta property="twitter:description" content="Create an account for third-party Robocraft servers" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<style>
body {
background-color: lightgrey;
padding: 0;
margin: 0;
}
.form-stripe {
position: relative;
min-width: 300px;
max-width: 90%;
height: auto;
min-height: 100%;
margin: 0 auto;
background-color: grey;
padding: 0 0 25px 0;
box-shadow: 0px 0px 50px 5px grey;
}
.form-header-right {
position: relative;
width: 25%;
text-align: right;
margin: 0 0 0 auto;
box-sizing: border-box;
padding: 0 25px;
}
.form-banner {
position: relative;
width: 100%;
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;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: orange;
}
.form-error-message {
padding: 25px;
margin: 0 auto;
width: 95%;
}
.form-text-block {
width: 80%;
margin: 0 auto;
}
.access-grid {
width: 80%;
display: grid;
margin: 0 auto;
padding: 5px;
grid-template-columns: auto auto auto auto;
gap: 5px;
}
.access-box {
background-color: dimgrey;
color: black;
padding: 5px 0;
text-align: center;
}
.access-box-title {
padding: 5px;
margin: 0 auto;
}
.access-box-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: black;
color: lightgrey;
padding: 5px;
}
.box-details {
text-align: left;
display: grid;
grid-template-columns: auto auto;
gap: 2px;
padding: 5px;
}
.box-details-key {
padding: 0 5px;
background-color: black;
color: lightgrey;
text-align: left;
}
.box-details-value {
padding: 0 5px;
background-color: lightgrey;
color: black;
text-align: left;
}
footer {
position: relative;
width: 100%
margin: 0 auto;
background-color: black;
}
.footer {
width: 95%;
color: lightgrey;
box-sizing: border-box;
padding: 25px;
}
</style>
</head>
<body>
<div class="form-stripe">
<div class="form-header-right">
Welcome {{form.display_name}}! |
<a href="/">Home</a>
</div>
<h1 class="form-banner">{{form.display_name}} Dashboard</h1>
{{#if error}}
<div class="form-error">
<p class="form-error-message">{{error}}</p>
</div>
{{/if}}
<div class="access-grid">
<div class="access-box">
<h4 class="access-box-title">Details</h4>
<div class="box-details">
<div class="box-details-key">Name</div>
<div class="box-details-value">{{form.display_name}}</div>
<div class="box-details-key">Public ID</div>
<div class="box-details-value">{{form.public_id}}</div>
<div class="box-details-key">Database ID</div>
<div class="box-details-value">{{form.debug.user_id}}</div>
<div class="box-details-key">Last Seen</div> <!-- TODO -->
<div class="box-details-value"><time datetime="{{form.debug.creation_time_iso}}">{{form.debug.creation_time_iso}}</time></div>
<div class="box-details-key">Creation</div>
<div class="box-details-value"><time datetime="{{form.debug.creation_time_iso}}">{{form.debug.creation_time_iso}}</time></div>
</div>
</div>
<div class="access-box">
<h4 class="access-box-title">Permissions</h4>
<div class="box-details">
<div class="box-details-key">Moderator</div>
<div class="box-details-value">{{form.perms.mod}}</div>
<div class="box-details-key">Administrator</div>
<div class="box-details-value">{{form.perms.admin}}</div>
<div class="box-details-key">Developer</div>
<div class="box-details-value">{{form.perms.dev}}</div>
<div class="box-details-key">Royal</div>
<div class="box-details-value">{{form.perms.royal}}</div>
<div class="box-details-key">Banned</div>
<div class="box-details-value">{{form.perms.banned}}</div>
</div>
</div>
<div class="access-box">
<h4 class="access-box-title">Garages</h4>
<div class="box-details">
<div class="box-details-key">Total</div> <!-- TODO -->
<div class="box-details-value">{{form.debug.user_id}}/{{form.debug.user_id}}</div>
</div>
<a href="#"> <!-- TODO -->
<div class="access-box-banner">
View My Garages
</div>
</a>
</div>
<div class="access-box">
<h4 class="access-box-title">Factory</h4>
<div class="box-details">
<div class="box-details-key">Total</div> <!-- TODO -->
<div class="box-details-value">{{form.debug.user_id}}/{{form.debug.user_id}}</div>
</div>
<a href="#"> <!-- TODO -->
<div class="access-box-banner">
View My Uploads
</div>
</a>
</div>
<div class="access-box">
<h4 class="access-box-title">Account</h4>
<a href="#"> <!-- TODO -->
<div class="access-box-banner">
Manage My Account
</div>
</a>
</div>
<div class="access-box">
<h4 class="access-box-title">Sanctions</h4>
<div class="box-details">
<div class="box-details-key">Total</div> <!-- TODO -->
<div class="box-details-value">{{form.debug.user_id}}</div>
</div>
<a href="#"> <!-- TODO -->
<div class="access-box-banner">
View My Sanctions
</div>
</a>
</div>
<div class="access-box">
<h4 class="access-box-title">Social</h4>
<div class="box-details">
<div class="box-details-key">Clan</div> <!-- TODO -->
<div class="box-details-value">{{form.display_name}}</div>
<div class="box-details-key">Friends</div> <!-- TODO -->
<div class="box-details-value">{{form.debug.user_id}}</div>
</div>
</div>
<div class="access-box">
<h4 class="access-box-title">Federation</h4>
<div class="box-details">
<div class="box-details-key">Enabled</div> <!-- TODO -->
<div class="box-details-value">{{form.perms.royal}}</div>
</div>
<a href="#"> <!-- TODO -->
<div class="access-box-banner">
Manage My Federation
</div>
</a>
</div>
</div>
<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,233 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<title>Home - Openjam</title>
<meta name="title" content="Home - Openjam" />
<meta name="description" content="OpenJam third-party Robocraft servers federation" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Home - Openjam" />
<meta property="og:description" content="OpenJam third-party Robocraft servers federation" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Home - Openjam" />
<meta property="twitter:description" content="OpenJam third-party Robocraft servers federation" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<style>
body {
background-color: lightgrey;
padding: 0;
margin: 0;
}
.form-stripe {
position: relative;
min-width: 300px;
max-width: 90%;
height: auto;
min-height: 100%;
margin: 0 auto;
background-color: grey;
padding: 0 0 25px 0;
box-shadow: 0px 0px 50px 5px grey;
}
.form-header-right {
position: relative;
width: 25%;
text-align: right;
margin: 0 0 0 auto;
box-sizing: border-box;
padding: 0 25px;
}
.form-banner {
position: relative;
width: 100%;
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;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: orange;
}
.form-error-message {
padding: 25px;
margin: 0 auto;
width: 95%;
}
.form-text-block {
width: 80%;
margin: 0 auto;
}
.access-grid {
width: 80%;
display: grid;
margin: 0 auto;
padding: 5px;
grid-template-columns: auto auto auto auto;
gap: 5px;
}
.access-box {
background-color: dimgrey;
color: black;
padding: 5px;
text-align: center;
}
.access-box-title {
padding: 5px;
margin: 0 auto;
}
.access-box-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: black;
color: lightgrey;
padding: 5px;
}
.box-details {
text-align: left;
display: grid;
grid-template-columns: auto auto;
gap: 2px;
padding: 5px;
}
.box-details-key {
padding: 0 5px;
background-color: black;
color: lightgrey;
text-align: left;
}
.box-details-value {
padding: 0 5px;
background-color: lightgrey;
color: black;
text-align: left;
}
footer {
position: relative;
width: 100%
margin: 0 auto;
background-color: black;
}
.footer {
width: 95%;
color: lightgrey;
box-sizing: border-box;
padding: 25px;
}
</style>
</head>
<body>
<div class="form-stripe">
<div class="form-header-right">
{{#if form.is_logged_in}}
Welcome {{form.display_name}}! |
<a href="/dashboard">Dashboard</a>
{{else}}
<a href="/login">Login</a>
{{/if}}
</div>
<h1 class="form-banner">Welcome to <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}}
<div class="form-text-block">
<p>
This service allows you to manage your account settings, import and export your garage bays, view the server config, and will eventually (not yet) federate with other OpenJam rc-servers instances.
You can view other known instances <a href="/federation">here</a>.
This service is intended to have stable public APIs for expanding existing functionality.
If you do not want any of this functionality in your instance, simply do not run this service with your instance.
</p>
<p>
Interested in running your own server instance? Check out the <a href="https://git.ngram.ca/OpenJam/rc-servers/wiki/Server-Quickstart">quickstart guide</a> in the wiki.
</p>
</div>
<div class="access-grid">
<div class="access-box">
<h4 class="access-box-title">Server Details</h4>
<div class="box-details">
<div class="box-details-key">Domain</div>
<div class="box-details-value">{{form.server.domain}}</div>
<div class="box-details-key">CDN</div>
<div class="box-details-value">{{form.server.cdn}}</div>
<div class="box-details-key">Auth</div>
<div class="box-details-value">{{form.server.auth}}</div>
<div class="box-details-key">Client Version</div>
<div class="box-details-value">>={{form.server.min_version}}</div>
<div class="box-details-key">Server Version</div>
<div class="box-details-value">{{form.server.server_version}}</div>
<div class="box-details-key">Start Time</div>
<div class="box-details-value"><time datetime="{{form.server.start_time_iso}}">{{form.server.start_time_iso}}</time></div>
</div>
</div>
<div class="access-box">
Grid item 2
</div>
<div class="access-box">
Grid item 3
</div>
<div class="access-box">
Grid item 4
</div>
<div class="access-box">
Grid item 5
</div>
<div class="access-box">
Grid item 6
</div>
<div class="access-box">
Grid item 7
</div>
<div class="access-box">
Grid item 8
</div>
</div>
<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,197 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<title>Login - Openjam</title>
<meta name="title" content="Login - Openjam" />
<meta name="description" content="Connect to a third-party Robocraft servers account" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Login - Openjam" />
<meta property="og:description" content="Connect to a third-party Robocraft servers account" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Login - Openjam" />
<meta property="twitter:description" content="Connect to a third-party Robocraft servers account" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<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: "*required";
color: red;
font-size: 12px;
}
input {
width: 100%;
height: 40px;
border: thin solid white;
background-color: lightgrey;
font-size: 20px;
}
.input-required-hint::after {
color: red;
/*content: "*required";*/
font-size: 12px;
}
.form-disclaimer {
margin: 0 auto;
width: 95%;
}
.form-submit {
margin: 0 auto;
}
.form-submit-button {
background-color: black;
color: lightgrey;
}
.fake-button {
padding-top: 25px;
padding-left: 25px;
padding-right: 25px;
margin: 0 auto;
text-align: center;
font-size: 20px;
}
.fake-button-anchor {
width: 100%;
background-color: black;
color: lightgrey;
border: 1px solid white;
display: inline-block;
padding-top: 10px;
padding-bottom: 10px;
}
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">Login to <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="/login" method="post">
<div class="form-input">
<div class="form-input-label">
<label for="username">Username</label>
</div>
<input class="form-input-box" type="text" id="username" name="username" value="{{form.username}}" placeholder="username" required/>
<span class="input-required-hint"></span>
</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/>
<span class="input-required-hint"></span>
</div>
<div class="form-disclaimer">
<p>By submitting, you agree to the <a href="https://git.ngram.ca/OpenJam/rc-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

@@ -81,16 +81,12 @@ impl AccountProvider {
db: self.db.clone(),
})
}*/
}
#[async_trait::async_trait]
impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
async fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
//let new_root = self.root.join(&token.uuid);
async fn auth_internal(&self, token: &str) -> Result<UserData, super::AuthError> {
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_required_spec_claims::<&str>(&[]);
let token_data = jsonwebtoken::decode::<crate::auth::Token>(&token.token, &secret, &validation).map_err(|e| super::AuthError {
let token_data = jsonwebtoken::decode::<crate::auth::Token>(&token, &secret, &validation).map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
})?;
@@ -119,8 +115,7 @@ impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
};
#[cfg(debug_assertions)]
log::info!("Authenticated user {} with flags {:?}", display_name, token_data.claims.client_details.flags.as_slice());
//let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
Ok(Box::new(UserData {
Ok(UserData {
account: user_info,
perms: user_perms,
cubes: self.cubes.clone(),
@@ -133,7 +128,14 @@ impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
http_client: std::sync::Arc::new(reqwest::Client::new()),
db: self.db.clone(),
secret: self.secret.clone(),
}))
})
}
}
#[async_trait::async_trait]
impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
async fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
Ok(Box::new(self.auth_internal(&token.token).await?))
}
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
@@ -174,6 +176,10 @@ impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
secret: self.secret.clone(),
}))
}
async fn web_authenticate(&self, token: String) -> Result<Box<dyn super::WebUser>, super::AuthError> {
Ok(Box::new(self.auth_internal(&token).await?))
}
}
#[async_trait::async_trait]
@@ -349,6 +355,18 @@ impl super::UserAuthenticator for AccountProvider {
async fn register(&self, info: super::RegistrationInfo) -> Result<i32, String> {
super::register_new_user(&info, &self.db).await.map_err(|e| e.to_string())
}
async fn verify(&self, token: String) -> Result<crate::auth::Token, super::AuthError> {
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_required_spec_claims::<&str>(&[]);
jsonwebtoken::decode::<crate::auth::Token>(&token, &secret, &validation)
.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
})
.map(|decoded| decoded.claims)
}
}
pub(super) struct UserData {

View File

@@ -14,6 +14,14 @@ impl super::CommonUser for UserData {
&self.account.public_id
}
fn display_name(&self) -> &'_ str {
&self.account.display_name
}
fn creation(&self) -> i64 {
self.account.creation_time
}
fn is_mod(&self) -> bool {
self.perms.moderator
}

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::{UnlockedParts, UnlockOverride};
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser};
pub mod intercom;
pub use intercom::generate_token as generate_intercom_token;
@@ -26,6 +26,7 @@ mod factory;
mod userless;
mod team;
pub use team::{TeamChooser, StandardTeamChooser};
mod web;
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -106,18 +106,6 @@ impl UserData {
#[async_trait::async_trait]
impl super::MultiplayerUser for UserData {
fn user_id(&self) -> i32 {
self.account.id
}
fn user_name(&self) -> &'_ str {
&self.account.public_id
}
fn display_name(&self) -> &'_ str {
&self.account.display_name
}
async fn current_game(&self) -> Result<Option<super::GameDescriptor>, super::MultiplayerError> {
Ok(self.db.game_by_user_id_and_completion(self.account.id, false).await
.map_err(|e| {

View File

@@ -63,6 +63,8 @@ pub trait UserProvider<C> {
async fn authenticate(&self, user: UserToken) -> Result<Box<dyn User<C> + Send + Sync>, AuthError>;
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn User<C> + Send + Sync>, AuthError>;
async fn web_authenticate(&self, token: String) -> Result<Box<dyn WebUser>, AuthError>;
}
#[async_trait::async_trait]
@@ -70,6 +72,7 @@ pub trait UserAuthenticator {
async fn login(&self, info: UserAuthInfo) -> Result<UserLoginInfo, AuthError>;
async fn user_exists(&self, user: UserId) -> Result<bool, String>;
async fn register(&self, info: RegistrationInfo) -> Result<i32, String>;
async fn verify(&self, token: String) -> Result<crate::auth::Token, AuthError>;
}
#[async_trait::async_trait]
@@ -385,9 +388,6 @@ pub enum MultiplayerErrorCode {
#[async_trait::async_trait]
pub trait MultiplayerUser: IntercomUser + CommonUser {
fn user_id(&self) -> i32;
fn user_name(&self) -> &'_ str;
fn display_name(&self) -> &'_ str;
async fn current_game(&self) -> Result<Option<GameDescriptor>, MultiplayerError>;
async fn game_players(&self, guid: &str) -> Result<Vec<PlayerDescriptor>, MultiplayerError>;
async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>;
@@ -455,6 +455,9 @@ pub trait CommonUser: Send + Sync {
fn account_id(&self) -> i32;
async fn resolve_config_vehicle(&self, vehicle: &crate::persist::config::VehicleInfo, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result<ResolvedVehicle, polariton_server::operations::SimpleOpError>;
fn public_id(&self) -> &'_ str;
fn display_name(&self) -> &'_ str;
/// Seconds since Unix epoch
fn creation(&self) -> i64;
fn is_mod(&self) -> bool;
fn is_admin(&self) -> bool;
fn is_dev(&self) -> bool;
@@ -671,6 +674,11 @@ pub trait FactoryUser {
async fn rate_vehicle(&self, slot: i32, combat: i32, cosmetic: i32) -> Result<Option<i32>, polariton_server::operations::SimpleOpError>;
}
#[async_trait::async_trait]
pub trait WebUser: CommonUser {
}
#[async_trait::async_trait]
pub trait Userless: Send + Sync {
async fn lobby_state_listener(&self) -> Result<super::IntercomListener<super::intercom::IntercomLobbyStateMessage>, reqwest_websocket::Error>;

View File

@@ -0,0 +1,4 @@
#[async_trait::async_trait]
impl super::WebUser for super::account_json::UserData {
// TODO
}

View File

@@ -19,7 +19,7 @@ impl crate::DisconnectHandler for ClientDisconnecter {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData) {
if let Some(user_info) = user.user().await {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::EndConnection {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
is_unregister: false,
}).await);
} else {

View File

@@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for RequestLoadingSync {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RequestLoadingSync {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
}).await);
} else {
log::error!("Failed to handle sync loading request for unknown user");

View File

@@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for RequestAllLoadingProgress {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RequestLoadingProgress {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
}).await);
} else {
log::error!("Failed to broadcast loading progress for unknown user");

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for AssistBonus {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::AssistBonus {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
shootee: data.requester_player_id as u8,
shooters: data.player_ids.into_iter().map(|x| x.player).collect(),
}).await);

View File

@@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for ClientUnregisterer {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::EndConnection {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
is_unregister: true,
}).await);
} else {

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for DamageBonus {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::DestroyCubesBonus {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
info: data,
}).await);
}

View File

@@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for RectifierStart {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::FlippingStarted {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
}).await);
} else {
log::error!("Failed to handle sync loading request for unknown user");

View File

@@ -23,7 +23,7 @@ impl crate::handlers::RlnlEventCodeHandler for HealAssistBonus {
if let Some(user_info) = user.user().await {
//log::info!("Heal assist for player {}", data.healing_player_id);
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::HealAssistBonus {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
healer: data.healing_player_id,
healee: data.healed_player_id,
}).await);

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for HealBonus {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::HealCubesBonus {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
info: data,
}).await);
}

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for KillEnemyBonus {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::KillBonus {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
shootee: data.killee_player_id,
shooter: data.killer_player_id,
}).await);

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for KillEnemy {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::DestroyVehicle {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
remote_player: data.killee_player_id,
killer_player: data.killer_player_id,
}).await);

View File

@@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for LoadComplete {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::LoadComplete {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
}).await);
} else {
log::error!("Failed to handle sync loading request for unknown user");

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for GameLoadingProgress {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::LoadingProgress {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
user_name: data.user_name.0,
progress: data.progress,
}).await);

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for PingMap {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::MapPing {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
ping: data,
}).await);
}

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for PlayerInput {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::PlayerInputChanged {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
data,
}).await);
} else {

View File

@@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for PlayerQuit {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RequestLeave {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
}).await);
} else {
log::error!("Failed to handle sync loading request for unknown user");

View File

@@ -21,7 +21,7 @@ impl crate::handlers::DatalessEventCodeHandler for SelfDestructElim {
async fn handle(&self, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::SelfDestruct {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
is_classic: true,
}).await);
} else {

View File

@@ -22,7 +22,7 @@ impl crate::handlers::RlnlEventCodeHandler for SpotEnemy {
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::SpotVehicle {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
remote_player: data.player_id,
}).await);
}

View File

@@ -64,7 +64,7 @@ impl crate::handlers::RlnlEventCodeHandler for AuthUserGame {
}
},
Ok(None) => {
log::warn!("Cannot validate game guid for user {} with no ongoing game [disconnecting...]", user_info.user_id());
log::warn!("Cannot validate game guid for user {} with no ongoing game [disconnecting...]", user_info.account_id());
super::log_lnl_send_failure(crate::handlers::RlnlSender::new(sender)
.send_data(&rlnl::types::StringCode {
ty: rlnl::types::GameServerErrorCodes::StrErrIncorrectGameGuid,
@@ -76,7 +76,7 @@ impl crate::handlers::RlnlEventCodeHandler for AuthUserGame {
peer.disconnect();
},
Err(e) => {
log::error!("Failed to get current game for user {}: {} [disconnecting...]", user_info.user_id(), e.message);
log::error!("Failed to get current game for user {}: {} [disconnecting...]", user_info.account_id(), e.message);
super::log_lnl_send_failure(crate::handlers::RlnlSender::new(sender)
.send_data(&rlnl::types::StringCode {
ty: core_to_rlnl_mp_error_code(e.code),

View File

@@ -24,7 +24,7 @@ impl crate::handlers::RlnlEventCodeHandler for WeaponSelect {
if let Some(category) = oj_rc_core::data::weapon_list::ItemCategory::from_smaller(data.item_category as _) {
if let Some(tier) = oj_rc_core::data::cube_list::ItemTier::from_u32(data.item_size as _) {
super::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::WeaponSelect {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
machine_id: data.machine_id,
category,
size: tier,

View File

@@ -79,7 +79,7 @@ impl literustlib_server::EventHandler for LnlEventHandler {
async fn on_disconnect(&self, peer: &std::sync::Arc<literustlib_server::Connection<Self::PacketData>>, user: &Self::UserData) {
self.disconnect_handler.handle(peer, user).await;
if let Some(user_info) = user.user().await {
log::info!("Disconnect from user {} ({})", user_info.user_id(), peer.id());
log::info!("Disconnect from user {} ({})", user_info.account_id(), peer.id());
} else {
log::debug!("Disconnect from connection {}", peer.id());
}

View File

@@ -29,7 +29,7 @@ impl <const EVENT: i16, const PROPERTY: u8, InOut: byteserde::des_slice::ByteDes
async fn handle(&self, data: Self::In, _peer: &std::sync::Arc<literustlib_server::Connection<crate::PacketData>>, user: &crate::UserData, _sender: &std::sync::Arc<literustlib_server::DataSender<crate::PacketData>>) {
if let Some(user_info) = user.user().await {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::CustomLogicRlnl {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
event: self.event,
property: self.property,
data: Box::new(data),

View File

@@ -30,7 +30,7 @@ impl <const EXCLUDE_SENDER: bool, const CODE_IN: i16, const CODE_OUT: i16, const
if EXCLUDE_SENDER {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RebroadcastRlnl {
skip_user_id: user_info.user_id(),
skip_user_id: user_info.account_id(),
event: self.code_out,
event_in: Self::CODE,
property: self.property,
@@ -38,7 +38,7 @@ impl <const EXCLUDE_SENDER: bool, const CODE_IN: i16, const CODE_OUT: i16, const
}).await);
} else {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::BroadcastRlnl {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
event: self.code_out,
event_in: Self::CODE,
property: self.property,

View File

@@ -27,7 +27,7 @@ impl <const EXCLUDE_SENDER: bool, const CODE_IN: i16, const CODE_OUT: i16, const
if let Some(user_info) = user.user().await {
if EXCLUDE_SENDER {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RebroadcastRlnl {
skip_user_id: user_info.user_id(),
skip_user_id: user_info.account_id(),
event: self.code_out,
event_in: Self::CODE,
property: self.property,
@@ -35,7 +35,7 @@ impl <const EXCLUDE_SENDER: bool, const CODE_IN: i16, const CODE_OUT: i16, const
}).await);
} else {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::BroadcastRlnl {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
event: self.code_out,
event_in: Self::CODE,
property: self.property,

View File

@@ -260,7 +260,7 @@ impl GameMatches {
}
};
self.matches.insert(game_guid.clone(), tx.clone());
self.routing.insert(user.user_id(), game_guid.clone());
self.routing.insert(user.account_id(), game_guid.clone());
if !self.is_crystal_regen_running.swap(true, std::sync::atomic::Ordering::SeqCst) {
tokio::task::spawn(Self::regenerate_crystal_order_task(
self.ba_sorted_crystals.clone(),
@@ -304,7 +304,7 @@ impl GameMatches {
self.do_game_cleanup(&game_guid);
self.create_new_game(user.clone(), game_guid, connection, response, sender).await;
} else {
self.routing.insert(user.user_id(), game_guid.clone());
self.routing.insert(user.account_id(), game_guid.clone());
if tx.send(super::GameMessage::NewConnection { user: user.clone(), game_guid, connection, response, sender }).await.is_err() {
log::error!("Failed to send NewConnection game message to existing match");
}

View File

@@ -370,7 +370,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
pub(super) async fn rebroadcast<T: byteserde::ser_heap::ByteSerializeHeap + ?Sized>(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T, in_game: bool) {
for (player_id, conn) in self.users.read().await.iter() {
if user_id == conn.user.user_id() { continue; }
if user_id == conn.user.account_id() { continue; }
if conn.aliases.contains(player_id) { continue; }
if in_game {
let mode = ConnectionMode::from_u8(self.user_descriptor(*player_id).unwrap().state.mode.load(std::sync::atomic::Ordering::Relaxed));
@@ -388,7 +388,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
pub(super) async fn rebroadcast_dataless(&self, user_id: i32, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, in_game: bool) {
for (player_id, conn) in self.users.read().await.iter() {
if user_id == conn.user.user_id() { continue; }
if user_id == conn.user.account_id() { continue; }
if conn.aliases.contains(player_id) { continue; }
if in_game {
let mode = ConnectionMode::from_u8(self.user_descriptor(*player_id).unwrap().state.mode.load(std::sync::atomic::Ordering::Relaxed));
@@ -586,7 +586,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
} else {
//tokio::time::sleep(std::time::Duration::from_secs(1)).await;
//let id = users.len() as u8;
let user_id = user.user_id();
let user_id = user.account_id();
let player_info_opt = self.descriptors.values().find(|p| p.descriptor.user_id == Some(user_id));
if player_info_opt.is_none() {
log::warn!("User {} tried to connect to match {} which they are not in", user_id, self.game_guid());
@@ -617,7 +617,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
literustlib::packet::Property::ReliableOrdered,
&new_user.connection.connection
).await);
log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid);
log::debug!("User {} is validated to play game {}", new_user.user.account_id(), game_guid);
let new_user = std::sync::Arc::new(new_user);
if let Err(e) = new_user.user.save_player_connected_status(self.game_guid(), true).await {
log::error!("Failed to mark player {} (user {}) as connected to game {}: {}", id, user_id, self.game_guid(), e);
@@ -757,7 +757,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
};
for (player_id, conn) in self.users.read().await.iter() {
let user_desc = self.user_descriptor(*player_id).unwrap();
if user_id == conn.user.user_id() {
if user_id == conn.user.account_id() {
let progress_percent = ((progress * 100.0).ceil() as u8).clamp(0, 100);
log::debug!("User {} is loaded {}% into game {}", user_id, progress_percent, self.game_guid());
user_desc.state.progress.store(progress_percent, std::sync::atomic::Ordering::Relaxed);
@@ -894,7 +894,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
for (player_id, user) in self.users.read().await.iter() {
let user_desc = self.user_descriptor(*player_id).unwrap();
if user.aliases.contains(player_id) { continue; }
if user.user.user_id() == user_id {
if user.user.account_id() == user_id {
if !matches!(ConnectionMode::from_u8(user_desc.state.mode.load(std::sync::atomic::Ordering::Relaxed)), ConnectionMode::Loading | ConnectionMode::Disconnected) {
log::warn!("Got RequestLoadingSync after user {} was already in/past WaitingForSync stage", user_id);
continue;
@@ -1361,7 +1361,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
fn spawn_send_loading_events(&self, user: &UserConnection, player_id: u8, players: Vec<std::sync::Arc<oj_rc_core::persist::user::PlayerDescriptor>>, client_ais: Vec<u8>) {
let connection = user.connection.clone();
let user_id = user.user.user_id();
let user_id = user.user.account_id();
tokio::spawn(Self::send_loading_events_wrapper(connection, player_id, user_id, players, client_ais));
}

View File

@@ -120,7 +120,7 @@ impl GameMessage {
pub fn user_id(&self) -> i32 {
match self {
Self::NewConnection { user, .. } => {
user.user_id()
user.account_id()
}
Self::EndConnection { user_id, .. } => *user_id,
Self::RequestLeave { user_id, .. } => *user_id,

View File

@@ -411,7 +411,7 @@ impl CustomGameLogic for PitLogic {
}
}
if single_client.is_some() {
let user_id = read_lock.values().next().unwrap().user.user_id();
let user_id = read_lock.values().next().unwrap().user.account_id();
let player_id = generic.user_key_by_user_id(user_id).unwrap();
let user_info = generic.user_descriptor(player_id).unwrap();
WinTracker::do_win(generic, self, user_info.descriptor.team as u8).await;

View File

@@ -24,7 +24,7 @@ impl crate::RobotMotionHandler for VehicleMotionHandler {
match rlnl::machine_motion::MachineMotion::byte_deserialize(&mut des) {
Ok(motion_data) => {
crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::Motion {
user_id: user_info.user_id(),
user_id: user_info.account_id(),
motion: motion_data,
}).await);
},

28
rc_society/Cargo.toml Normal file
View File

@@ -0,0 +1,28 @@
[package]
name = "oj_rc_society"
version.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
[dependencies]
actix-web = { workspace = true, features = [ "secure-cookies" ] }
actix-session = { version = "0.11", features = [ "cookie-session" ], default-features = false }
actix-identity = "0.9"
cookie = "0.16"
actix-files.workspace = true
actix-ws = "0.3"
log.workspace = true
env_logger.workspace = true
tokio = { version = "1.43", features = [ "rt-multi-thread" ] }
#futures.workspace = true
clap.workspace = true
handlebars = { version = "6", features = ["dir_source"] }
oj_rc_core = { version = "*", path = "../rc_core" }
libfj.workspace = true
git-version.workspace = true
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true

0
rc_society/README.md Normal file
View File

3
rc_society/run_debug.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
RUST_LOG=debug cargo run

View File

58
rc_society/src/cli.rs Normal file
View File

@@ -0,0 +1,58 @@
use clap::Parser;
#[derive(Parser, Debug, Clone)]
#[command(version, about, long_about = None)]
pub struct CliArgs {
/// TCP port on which to accept connections
#[arg(short, long, default_value_t = 8002)]
pub port: u16,
/// IP Address on which to accept connections
#[arg(long, default_value_t = {"127.0.0.1".to_string()})]
pub ip: String,
/// Assets root
#[arg(long, default_value_t = {"../assets/robocraft".to_string()})]
pub assets_robocraft: String,
/// User data root
#[arg(long, default_value_t = {"../data/robocraft".to_string()})]
pub data_robocraft: String,
}
impl CliArgs {
pub fn get() -> Self {
Self::parse()
}
pub fn loaded(&self) -> LoadedArgs {
let assets_path = std::path::PathBuf::from(&self.assets_robocraft);
let data_path = std::path::PathBuf::from(&self.data_robocraft);
let token_path = data_path.join(oj_rc_core::persist::user::TOKEN_SECRET_FILENAME);
let secret = std::fs::read(&token_path).expect("Bad token");
let cookie_key = if secret.len() < 32 {
log::warn!("{} should be >= 32 bytes (extending with zeroes)", token_path.display());
let mut secret_ext = secret.clone();
while secret_ext.len() < 32 {
secret_ext.push(0);
}
actix_web::cookie::Key::derive_from(&secret_ext)
} else {
actix_web::cookie::Key::derive_from(&secret)
};
LoadedArgs {
secret: std::sync::Arc::new(secret.clone()),
cookie_key,
assets: assets_path,
data: data_path,
}
}
}
#[allow(dead_code)]
pub struct LoadedArgs {
pub secret: std::sync::Arc<Vec<u8>>,
pub cookie_key: actix_web::cookie::Key,
pub assets: std::path::PathBuf,
pub data: std::path::PathBuf,
}

78
rc_society/src/main.rs Normal file
View File

@@ -0,0 +1,78 @@
#![forbid(unsafe_code)]
mod cli;
mod api;
mod web;
use actix_web::{App, HttpServer, Responder};
pub static START_TIME: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1);
#[actix_web::get("/version")]
async fn version_info() -> impl Responder {
let name = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]);
let authors = env!("CARGO_PKG_AUTHORS");
let license = env!("CARGO_PKG_LICENSE");
let repo = env!("CARGO_PKG_REPOSITORY");
format!("{} {}:{} by [{}]\n{}\n{}", name, version, git_version, authors, license, repo)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let cli_args = cli::CliArgs::get();
let config = oj_rc_core::ConfigImpl::load(&cli_args.assets_robocraft)?;
let server_settings = actix_web::web::Data::new(<oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::server_config(&config));
let users = oj_rc_core::UserImpl::load(&cli_args.data_robocraft, &config).await.expect("Bad user data");
let auth_ref = actix_web::web::Data::new(Box::new(users));
let cli_args2 = actix_web::web::Data::new(cli_args.clone());
let loadeds_args = actix_web::web::Data::new(cli_args.loaded());
let mut handlebars_conf = handlebars::Handlebars::new();
let mut dir_conf = handlebars::DirectorySourceOptions::default();
dir_conf.tpl_extension = ".html.hbs".to_owned();
dir_conf.hidden = false;
dir_conf.temporary = false;
handlebars_conf
.register_templates_directory(
std::path::PathBuf::from(&cli_args.assets_robocraft).parent().expect("Bad robocraft asset path").join("templates/rc_society"),
dir_conf,
)
.unwrap();
let handlebars_ref = actix_web::web::Data::new(handlebars_conf);
START_TIME.store(chrono::Utc::now().timestamp(), std::sync::atomic::Ordering::SeqCst);
HttpServer::new(move || {
App::new()
.wrap_fn(|req, srv| {
use actix_web::dev::Service;
log::trace!("Request {} {}", req.method(), req.path());
srv.call(req)
})
.wrap(actix_identity::IdentityMiddleware::default())
.wrap(actix_session::SessionMiddleware::new(
actix_session::storage::CookieSessionStore::default(),
loadeds_args.cookie_key.clone(),
))
.app_data(cli_args2.clone())
.app_data(loadeds_args.clone())
.app_data(handlebars_ref.clone())
.app_data(server_settings.clone())
.app_data(auth_ref.clone())
.service(version_info)
.service(web::login::form_submit)
.service(web::login::form_load)
.service(web::favicon::favicon_standard)
.service(web::dashboard::get)
.service(web::index::get)
})
.bind((cli_args.ip, cli_args.port))?
.run()
.await
}

View File

@@ -0,0 +1,65 @@
use actix_web::{get, web::Data, Responder, HttpRequest};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
const FORM_NAME: &str = "dashboard";
#[derive(Serialize, Deserialize)]
struct RenderData {
// TODO
display_name: String,
public_id: String,
debug: DebugData,
perms: PermissionData,
}
#[derive(Serialize, Deserialize)]
struct DebugData {
user_id: i32,
creation_time_unix: i64,
creation_time_iso: String,
}
#[derive(Serialize, Deserialize)]
struct PermissionData {
r#mod: bool,
admin: bool,
dev: bool,
royal: bool,
banned: bool,
}
#[get("/dashboard")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match super::try_auth_user(user_opt, auth.as_ref(), &req).await? {
super::LoginReturn::AuthFail(resp) => Ok(resp),
super::LoginReturn::Success(user) => {
// TODO
let creation_time = user.creation();
let creation_time_chrono = chrono::DateTime::<chrono::Utc>::from_timestamp_secs(creation_time).unwrap_or_default();
Ok(super::render_ok(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
debug: DebugData {
user_id: user.account_id(),
creation_time_unix: creation_time,
creation_time_iso: creation_time_chrono.to_rfc3339(),
},
perms: PermissionData {
r#mod: user.is_mod(),
admin: user.is_admin(),
dev: user.is_dev(),
royal: user.is_royal(),
banned: user.is_banned(),
}
},
handlebars_ref.as_ref(),
FORM_NAME,
)
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}

View File

@@ -0,0 +1,11 @@
use actix_web::{get, web::Data, Responder};
async fn favicon_impl(cli_args: Data<crate::cli::LoadedArgs>) -> impl Responder {
let path = std::path::PathBuf::from(&cli_args.assets).join("favicon.jpg");
actix_files::NamedFile::open_async(path).await
}
#[get("/favicon.ico")]
pub async fn favicon_standard(cli_args: Data<crate::cli::LoadedArgs>) -> impl Responder {
favicon_impl(cli_args).await
}

View File

@@ -0,0 +1,77 @@
use actix_web::{get, web::Data, Responder, HttpRequest};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
pub const FORM_NAME: &str = "index";
#[derive(Serialize, Deserialize)]
struct RenderData {
is_logged_in: bool,
display_name: Option<String>,
server: ServerDetails,
}
#[derive(Serialize, Deserialize)]
struct ServerDetails {
domain: String,
cdn: String,
auth: String,
min_version: i32,
server_version: String,
start_time_iso: String,
start_time_unix: i64,
}
fn server_details(conf: &oj_rc_core::persist::config::ServerConfig) -> ServerDetails {
let start_time_unix = crate::START_TIME.load(std::sync::atomic::Ordering::Relaxed);
let start_time_chrono = chrono::DateTime::<chrono::Utc>::from_timestamp_secs(start_time_unix).unwrap_or_default();
let version = env!("CARGO_PKG_VERSION");
let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]);
let server_version = format!("{}:{}", version, git_version);
ServerDetails {
domain: conf.domain.clone(),
cdn: conf.cdn_url.clone(),
auth: conf.auth_url.clone(),
min_version: conf.minimum_version,
server_version,
start_time_unix,
start_time_iso: start_time_chrono.to_rfc3339(),
}
}
#[get("/")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, server_config: Data<oj_rc_core::persist::config::ServerConfig>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
let server_info = server_details(server_config.as_ref());
if let Some(user) = user_opt {
match super::try_auth_user(Some(user), auth.as_ref(), &req).await? {
super::LoginReturn::AuthFail(resp) => Ok(resp),
super::LoginReturn::Success(user) => {
Ok(super::render_ok(
RenderData {
is_logged_in: true,
display_name: Some(user.display_name().to_owned()),
server: server_info,
},
handlebars_ref.as_ref(),
FORM_NAME,
)
.respond_to(&req)
.map_into_boxed_body()
)
}
}
} else {
Ok(super::render_ok(
RenderData {
is_logged_in: false,
display_name: None,
server: server_info,
},
handlebars_ref.as_ref(),
FORM_NAME,
)
.respond_to(&req)
.map_into_boxed_body()
)
}
}

View File

@@ -0,0 +1,66 @@
use actix_web::{get, post, web::{Data, Form, Redirect}, Responder, HttpRequest, HttpMessage};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
const FORM_NAME: &str = "login";
#[derive(Serialize, Deserialize, Default)]
struct LoginForm {
username: String,
password: String,
}
#[post("/login")]
pub async fn form_submit(form: Form<LoginForm>, auth: Data<Box<oj_rc_core::UserImpl>>, handlebars_ref: Data<handlebars::Handlebars<'_>>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
use oj_rc_core::UserAuthenticator;
let auth_result = auth.login(oj_rc_core::persist::user::UserAuthInfo::Username {
username: form.username.to_owned(),
password: form.password.to_owned(),
}).await;
match auth_result {
Ok(user) => {
let resp = Redirect::to("/dashboard")
.respond_to(&req)
.map_into_boxed_body();
Identity::login(&req.extensions(), user.response.token)?;
Ok(resp)
},
Err(e) => {
Ok(super::render_err(form.0, e.message, handlebars_ref.as_ref(), FORM_NAME)
.respond_to(&req)
.map_into_boxed_body())
}
}
}
#[get("/login")]
pub async fn form_load(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
if let Some(user) = user_opt {
let user_id = user.id()?;
use oj_rc_core::UserAuthenticator;
if auth.verify(user_id).await.is_ok() {
Ok(Redirect::to("/dashboard")
.respond_to(&req)
.map_into_boxed_body())
} else {
Ok(super::render_err(
LoginForm::default(),
"Invalid login token".to_owned(),
handlebars_ref.as_ref(),
FORM_NAME,
)
.respond_to(&req)
.map_into_boxed_body()
)
}
} else {
Ok(super::render_ok(
LoginForm::default(),
handlebars_ref.as_ref(),
FORM_NAME,
)
.respond_to(&req)
.map_into_boxed_body()
)
}
}

71
rc_society/src/web/mod.rs Normal file
View File

@@ -0,0 +1,71 @@
pub mod dashboard;
pub mod login;
pub mod favicon;
pub mod index;
use serde::Serialize;
use actix_web::{web::{Html, Redirect}, Responder};
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)
}
#[derive(Serialize)]
struct Context<T: Serialize> {
form: T,
error: Option<String>,
version: String,
source_url: String,
}
fn render_ok<T: Serialize>(form: T, renderer: &handlebars::Handlebars<'_>, form_name: &str) -> Html {
let rendered = renderer.render(form_name, &Context {
form,
error: None,
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
}).unwrap();
Html::new(rendered)
}
fn render_err<T: Serialize>(form: T, error: String , renderer: &handlebars::Handlebars<'_>, form_name: &str) -> Html {
let rendered = renderer.render(form_name, &Context {
form,
error: Some(error),
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
}).unwrap();
Html::new(rendered)
}
enum LoginReturn {
Success(Box<dyn oj_rc_core::persist::user::WebUser>),
AuthFail(actix_web::HttpResponse<actix_web::body::BoxBody>),
}
async fn try_auth_user(user_opt: Option<actix_identity::Identity>, auth: &oj_rc_core::UserImpl, req: &actix_web::HttpRequest) -> Result<LoginReturn, actix_web::Error> {
if let Some(user) = user_opt {
let user_id = user.id()?;
match <oj_rc_core::UserImpl as oj_rc_core::UserProvider<()>>::web_authenticate(auth, user_id.clone()).await {
Ok(user) => Ok(LoginReturn::Success(user)),
Err(e) => {
log::warn!("Failed to login with token {}: {} ({:?})", user_id, e.message, e.code);
Ok(LoginReturn::AuthFail(
Redirect::to("/login")
.respond_to(req)
.map_into_boxed_body()
))
}
}
} else {
Ok(LoginReturn::AuthFail(
Redirect::to("/login")
.respond_to(req)
.map_into_boxed_body()
))
}
}