Add Auth
This commit is contained in:
parent
d38f8891f5
commit
73791a0760
0
backend/.gitignore → .gitignore
vendored
0
backend/.gitignore → .gitignore
vendored
2394
Cargo.lock
generated
Normal file
2394
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
Cargo.toml
Normal file
4
Cargo.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["backend"]
|
||||
default-members = ["backend"]
|
||||
75
backend/Cargo.lock
generated
75
backend/Cargo.lock
generated
@ -104,6 +104,7 @@ dependencies = [
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"bytes",
|
||||
"prost",
|
||||
"prost-build",
|
||||
@ -113,6 +114,25 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blowfish",
|
||||
"getrandom 0.2.16",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binascii"
|
||||
version = "0.1.4"
|
||||
@ -125,6 +145,16 @@ version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@ -167,6 +197,16 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.54"
|
||||
@ -193,6 +233,16 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuckoofilter"
|
||||
version = "0.5.0"
|
||||
@ -423,6 +473,16 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
@ -618,6 +678,15 @@ version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intrusive-collections"
|
||||
version = "0.9.7"
|
||||
@ -1894,6 +1963,12 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "ubyte"
|
||||
version = "0.10.4"
|
||||
|
||||
@ -10,6 +10,7 @@ rocket = { git = "https://github.com/rwf2/Rocket", rev = "504efef179622df82ba1db
|
||||
bytes = "1"
|
||||
rocket_prost_responder_derive = { path = "rocket_prost_responder_derive" }
|
||||
uuid = { version = "1.10.0", features = ["v4"] }
|
||||
bcrypt = "0.17.1"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.14.1"
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use crate::items;
|
||||
use crate::proto_utils::Proto;
|
||||
use rocket::State;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
use crate::items;
|
||||
use crate::proto_utils::Proto;
|
||||
|
||||
pub struct AuthState {
|
||||
// Map token -> username
|
||||
@ -21,26 +21,29 @@ impl AuthState {
|
||||
#[post("/login", data = "<request>")]
|
||||
pub fn login(
|
||||
state: &State<AuthState>,
|
||||
user_list: &State<Vec<crate::User>>,
|
||||
request: Proto<items::LoginRequest>,
|
||||
) -> items::LoginResponse {
|
||||
let req = request.into_inner();
|
||||
// Simple mock authentication: allow any non-empty username/password
|
||||
if !req.username.is_empty() && !req.password.is_empty() {
|
||||
|
||||
if let Some(user) = user_list.iter().find(|u| u.name == req.username)
|
||||
&& bcrypt::verify(&req.password, &user.password_hash).unwrap_or(false)
|
||||
{
|
||||
let token = Uuid::new_v4().to_string();
|
||||
let mut tokens = state.tokens.lock().unwrap();
|
||||
tokens.insert(token.clone(), req.username);
|
||||
|
||||
items::LoginResponse {
|
||||
return items::LoginResponse {
|
||||
token,
|
||||
success: true,
|
||||
message: "Login successful".to_string(),
|
||||
}
|
||||
} else {
|
||||
items::LoginResponse {
|
||||
token: "".to_string(),
|
||||
success: false,
|
||||
message: "Invalid credentials".to_string(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
items::LoginResponse {
|
||||
token: "".to_string(),
|
||||
success: false,
|
||||
message: "Invalid credentials".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,21 +10,38 @@ pub mod items {
|
||||
mod auth;
|
||||
mod proto_utils;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct User {
|
||||
pub person: items::Person,
|
||||
pub password_hash: String,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for User {
|
||||
type Target = items::Person;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.person
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/<name>")]
|
||||
fn get_user(user_list: &rocket::State<Vec<items::Person>>, name: String) -> Option<items::Person> {
|
||||
user_list.iter().find(|user| user.name == name).cloned()
|
||||
fn get_user(user_list: &rocket::State<Vec<User>>, name: String) -> Option<items::Person> {
|
||||
user_list
|
||||
.iter()
|
||||
.find(|user| user.person.name == name)
|
||||
.map(|u| u.person.clone())
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn get_users(user_list: &rocket::State<Vec<items::Person>>) -> items::PersonList {
|
||||
fn get_users(user_list: &rocket::State<Vec<User>>) -> items::PersonList {
|
||||
items::PersonList {
|
||||
person: user_list
|
||||
.inner()
|
||||
.to_vec()
|
||||
.iter_mut()
|
||||
.map(|x| {
|
||||
x.opinion.clear();
|
||||
x.clone()
|
||||
.iter()
|
||||
.map(|u| {
|
||||
let mut p = u.person.clone();
|
||||
p.opinion.clear();
|
||||
p
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
@ -44,22 +61,25 @@ async fn index_fallback() -> Option<rocket::fs::NamedFile> {
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
let mut user_list: Vec<items::Person> = Vec::new();
|
||||
let mut user_list: Vec<User> = Vec::new();
|
||||
|
||||
user_list.push(items::Person {
|
||||
name: "John".to_string(),
|
||||
opinion: vec![items::Opinion {
|
||||
game: Some(items::Game {
|
||||
title: "Naramo Nuclear Plant V2".to_string(),
|
||||
source: items::Source::Roblox.into(),
|
||||
multiplayer: true,
|
||||
min_players: 1,
|
||||
max_players: 90,
|
||||
price: 0,
|
||||
remote_id: 0,
|
||||
}),
|
||||
would_play: true,
|
||||
}],
|
||||
user_list.push(User {
|
||||
person: items::Person {
|
||||
name: "John".to_string(),
|
||||
opinion: vec![items::Opinion {
|
||||
game: Some(items::Game {
|
||||
title: "Naramo Nuclear Plant V2".to_string(),
|
||||
source: items::Source::Roblox.into(),
|
||||
multiplayer: true,
|
||||
min_players: 1,
|
||||
max_players: 90,
|
||||
price: 0,
|
||||
remote_id: 0,
|
||||
}),
|
||||
would_play: true,
|
||||
}],
|
||||
},
|
||||
password_hash: bcrypt::hash("password123", bcrypt::DEFAULT_COST).unwrap(),
|
||||
});
|
||||
|
||||
rocket::build()
|
||||
@ -71,5 +91,5 @@ fn rocket() -> _ {
|
||||
routes![auth::login, auth::logout, auth::get_auth_status],
|
||||
)
|
||||
.mount("/", routes![index_fallback])
|
||||
.mount("/", FileServer::new("../frontend/dist"))
|
||||
.mount("/", FileServer::new("frontend/dist"))
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use rocket::data::{Data, FromData, Outcome, ToByteUnit};
|
||||
use rocket::http::{Status, ContentType};
|
||||
use rocket::Request;
|
||||
use prost::Message;
|
||||
use rocket::Request;
|
||||
use rocket::data::{Data, FromData, Outcome, ToByteUnit};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub struct Proto<T>(pub T);
|
||||
@ -31,8 +31,10 @@ impl<'r, T: Message + Default> FromData<'r> for Proto<T> {
|
||||
type Error = String;
|
||||
|
||||
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
|
||||
if req.content_type() != Some(&ContentType::new("application", "protobuf")) {
|
||||
return Outcome::Forward((data, Status::NotFound));
|
||||
if req.content_type() != Some(&ContentType::new("application", "protobuf"))
|
||||
&& req.content_type() != Some(&ContentType::new("application", "octet-stream"))
|
||||
{
|
||||
return Outcome::Forward((data, Status::NotFound));
|
||||
}
|
||||
|
||||
let limit = req.limits().get("protobuf").unwrap_or(1.mebibytes());
|
||||
|
||||
@ -59,12 +59,44 @@ export interface Game {
|
||||
minPlayers: number;
|
||||
maxPlayers: number;
|
||||
price: number;
|
||||
remoteId: number;
|
||||
}
|
||||
|
||||
export interface PersonList {
|
||||
person: Person[];
|
||||
}
|
||||
|
||||
/** Authentication messages */
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface LogoutRequest {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface LogoutResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AuthStatusRequest {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface AuthStatusResponse {
|
||||
authenticated: boolean;
|
||||
username: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function createBasePerson(): Person {
|
||||
return { name: "", opinion: [] };
|
||||
}
|
||||
@ -218,7 +250,7 @@ export const Opinion: MessageFns<Opinion> = {
|
||||
};
|
||||
|
||||
function createBaseGame(): Game {
|
||||
return { title: "", source: 0, multiplayer: false, minPlayers: 0, maxPlayers: 0, price: 0 };
|
||||
return { title: "", source: 0, multiplayer: false, minPlayers: 0, maxPlayers: 0, price: 0, remoteId: 0 };
|
||||
}
|
||||
|
||||
export const Game: MessageFns<Game> = {
|
||||
@ -241,6 +273,9 @@ export const Game: MessageFns<Game> = {
|
||||
if (message.price !== 0) {
|
||||
writer.uint32(48).uint32(message.price);
|
||||
}
|
||||
if (message.remoteId !== 0) {
|
||||
writer.uint32(56).uint64(message.remoteId);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
@ -299,6 +334,14 @@ export const Game: MessageFns<Game> = {
|
||||
message.price = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 56) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.remoteId = longToNumber(reader.uint64());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
@ -316,6 +359,7 @@ export const Game: MessageFns<Game> = {
|
||||
minPlayers: isSet(object.minPlayers) ? globalThis.Number(object.minPlayers) : 0,
|
||||
maxPlayers: isSet(object.maxPlayers) ? globalThis.Number(object.maxPlayers) : 0,
|
||||
price: isSet(object.price) ? globalThis.Number(object.price) : 0,
|
||||
remoteId: isSet(object.remoteId) ? globalThis.Number(object.remoteId) : 0,
|
||||
};
|
||||
},
|
||||
|
||||
@ -339,6 +383,9 @@ export const Game: MessageFns<Game> = {
|
||||
if (message.price !== 0) {
|
||||
obj.price = Math.round(message.price);
|
||||
}
|
||||
if (message.remoteId !== 0) {
|
||||
obj.remoteId = Math.round(message.remoteId);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
@ -353,6 +400,7 @@ export const Game: MessageFns<Game> = {
|
||||
message.minPlayers = object.minPlayers ?? 0;
|
||||
message.maxPlayers = object.maxPlayers ?? 0;
|
||||
message.price = object.price ?? 0;
|
||||
message.remoteId = object.remoteId ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
@ -417,6 +465,499 @@ export const PersonList: MessageFns<PersonList> = {
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseLoginRequest(): LoginRequest {
|
||||
return { username: "", password: "" };
|
||||
}
|
||||
|
||||
export const LoginRequest: MessageFns<LoginRequest> = {
|
||||
encode(message: LoginRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.username !== "") {
|
||||
writer.uint32(10).string(message.username);
|
||||
}
|
||||
if (message.password !== "") {
|
||||
writer.uint32(18).string(message.password);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): LoginRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseLoginRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.username = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.password = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): LoginRequest {
|
||||
return {
|
||||
username: isSet(object.username) ? globalThis.String(object.username) : "",
|
||||
password: isSet(object.password) ? globalThis.String(object.password) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: LoginRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.username !== "") {
|
||||
obj.username = message.username;
|
||||
}
|
||||
if (message.password !== "") {
|
||||
obj.password = message.password;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<LoginRequest>, I>>(base?: I): LoginRequest {
|
||||
return LoginRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<LoginRequest>, I>>(object: I): LoginRequest {
|
||||
const message = createBaseLoginRequest();
|
||||
message.username = object.username ?? "";
|
||||
message.password = object.password ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseLoginResponse(): LoginResponse {
|
||||
return { token: "", success: false, message: "" };
|
||||
}
|
||||
|
||||
export const LoginResponse: MessageFns<LoginResponse> = {
|
||||
encode(message: LoginResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.token !== "") {
|
||||
writer.uint32(10).string(message.token);
|
||||
}
|
||||
if (message.success !== false) {
|
||||
writer.uint32(16).bool(message.success);
|
||||
}
|
||||
if (message.message !== "") {
|
||||
writer.uint32(26).string(message.message);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): LoginResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseLoginResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.token = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.success = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.message = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): LoginResponse {
|
||||
return {
|
||||
token: isSet(object.token) ? globalThis.String(object.token) : "",
|
||||
success: isSet(object.success) ? globalThis.Boolean(object.success) : false,
|
||||
message: isSet(object.message) ? globalThis.String(object.message) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: LoginResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.token !== "") {
|
||||
obj.token = message.token;
|
||||
}
|
||||
if (message.success !== false) {
|
||||
obj.success = message.success;
|
||||
}
|
||||
if (message.message !== "") {
|
||||
obj.message = message.message;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<LoginResponse>, I>>(base?: I): LoginResponse {
|
||||
return LoginResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<LoginResponse>, I>>(object: I): LoginResponse {
|
||||
const message = createBaseLoginResponse();
|
||||
message.token = object.token ?? "";
|
||||
message.success = object.success ?? false;
|
||||
message.message = object.message ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseLogoutRequest(): LogoutRequest {
|
||||
return { token: "" };
|
||||
}
|
||||
|
||||
export const LogoutRequest: MessageFns<LogoutRequest> = {
|
||||
encode(message: LogoutRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.token !== "") {
|
||||
writer.uint32(10).string(message.token);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): LogoutRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseLogoutRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.token = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): LogoutRequest {
|
||||
return { token: isSet(object.token) ? globalThis.String(object.token) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: LogoutRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.token !== "") {
|
||||
obj.token = message.token;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<LogoutRequest>, I>>(base?: I): LogoutRequest {
|
||||
return LogoutRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<LogoutRequest>, I>>(object: I): LogoutRequest {
|
||||
const message = createBaseLogoutRequest();
|
||||
message.token = object.token ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseLogoutResponse(): LogoutResponse {
|
||||
return { success: false, message: "" };
|
||||
}
|
||||
|
||||
export const LogoutResponse: MessageFns<LogoutResponse> = {
|
||||
encode(message: LogoutResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.success !== false) {
|
||||
writer.uint32(8).bool(message.success);
|
||||
}
|
||||
if (message.message !== "") {
|
||||
writer.uint32(18).string(message.message);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): LogoutResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseLogoutResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.success = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.message = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): LogoutResponse {
|
||||
return {
|
||||
success: isSet(object.success) ? globalThis.Boolean(object.success) : false,
|
||||
message: isSet(object.message) ? globalThis.String(object.message) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: LogoutResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.success !== false) {
|
||||
obj.success = message.success;
|
||||
}
|
||||
if (message.message !== "") {
|
||||
obj.message = message.message;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<LogoutResponse>, I>>(base?: I): LogoutResponse {
|
||||
return LogoutResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<LogoutResponse>, I>>(object: I): LogoutResponse {
|
||||
const message = createBaseLogoutResponse();
|
||||
message.success = object.success ?? false;
|
||||
message.message = object.message ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAuthStatusRequest(): AuthStatusRequest {
|
||||
return { token: "" };
|
||||
}
|
||||
|
||||
export const AuthStatusRequest: MessageFns<AuthStatusRequest> = {
|
||||
encode(message: AuthStatusRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.token !== "") {
|
||||
writer.uint32(10).string(message.token);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AuthStatusRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAuthStatusRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.token = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): AuthStatusRequest {
|
||||
return { token: isSet(object.token) ? globalThis.String(object.token) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: AuthStatusRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.token !== "") {
|
||||
obj.token = message.token;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AuthStatusRequest>, I>>(base?: I): AuthStatusRequest {
|
||||
return AuthStatusRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AuthStatusRequest>, I>>(object: I): AuthStatusRequest {
|
||||
const message = createBaseAuthStatusRequest();
|
||||
message.token = object.token ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAuthStatusResponse(): AuthStatusResponse {
|
||||
return { authenticated: false, username: "", message: "" };
|
||||
}
|
||||
|
||||
export const AuthStatusResponse: MessageFns<AuthStatusResponse> = {
|
||||
encode(message: AuthStatusResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.authenticated !== false) {
|
||||
writer.uint32(8).bool(message.authenticated);
|
||||
}
|
||||
if (message.username !== "") {
|
||||
writer.uint32(18).string(message.username);
|
||||
}
|
||||
if (message.message !== "") {
|
||||
writer.uint32(26).string(message.message);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AuthStatusResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAuthStatusResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.authenticated = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.username = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.message = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): AuthStatusResponse {
|
||||
return {
|
||||
authenticated: isSet(object.authenticated) ? globalThis.Boolean(object.authenticated) : false,
|
||||
username: isSet(object.username) ? globalThis.String(object.username) : "",
|
||||
message: isSet(object.message) ? globalThis.String(object.message) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: AuthStatusResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.authenticated !== false) {
|
||||
obj.authenticated = message.authenticated;
|
||||
}
|
||||
if (message.username !== "") {
|
||||
obj.username = message.username;
|
||||
}
|
||||
if (message.message !== "") {
|
||||
obj.message = message.message;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AuthStatusResponse>, I>>(base?: I): AuthStatusResponse {
|
||||
return AuthStatusResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AuthStatusResponse>, I>>(object: I): AuthStatusResponse {
|
||||
const message = createBaseAuthStatusResponse();
|
||||
message.authenticated = object.authenticated ?? false;
|
||||
message.username = object.username ?? "";
|
||||
message.message = object.message ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
/** Authentication service */
|
||||
export interface AuthService {
|
||||
Login(request: LoginRequest): Promise<LoginResponse>;
|
||||
Logout(request: LogoutRequest): Promise<LogoutResponse>;
|
||||
GetAuthStatus(request: AuthStatusRequest): Promise<AuthStatusResponse>;
|
||||
}
|
||||
|
||||
export const AuthServiceServiceName = "items.AuthService";
|
||||
export class AuthServiceClientImpl implements AuthService {
|
||||
private readonly rpc: Rpc;
|
||||
private readonly service: string;
|
||||
constructor(rpc: Rpc, opts?: { service?: string }) {
|
||||
this.service = opts?.service || AuthServiceServiceName;
|
||||
this.rpc = rpc;
|
||||
this.Login = this.Login.bind(this);
|
||||
this.Logout = this.Logout.bind(this);
|
||||
this.GetAuthStatus = this.GetAuthStatus.bind(this);
|
||||
}
|
||||
Login(request: LoginRequest): Promise<LoginResponse> {
|
||||
const data = LoginRequest.encode(request).finish();
|
||||
const promise = this.rpc.request(this.service, "Login", data);
|
||||
return promise.then((data) => LoginResponse.decode(new BinaryReader(data)));
|
||||
}
|
||||
|
||||
Logout(request: LogoutRequest): Promise<LogoutResponse> {
|
||||
const data = LogoutRequest.encode(request).finish();
|
||||
const promise = this.rpc.request(this.service, "Logout", data);
|
||||
return promise.then((data) => LogoutResponse.decode(new BinaryReader(data)));
|
||||
}
|
||||
|
||||
GetAuthStatus(request: AuthStatusRequest): Promise<AuthStatusResponse> {
|
||||
const data = AuthStatusRequest.encode(request).finish();
|
||||
const promise = this.rpc.request(this.service, "GetAuthStatus", data);
|
||||
return promise.then((data) => AuthStatusResponse.decode(new BinaryReader(data)));
|
||||
}
|
||||
}
|
||||
|
||||
interface Rpc {
|
||||
request(service: string, method: string, data: Uint8Array): Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
@ -429,6 +970,17 @@ type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function longToNumber(int64: { toString(): string }): number {
|
||||
const num = globalThis.Number(int64.toString());
|
||||
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
@ -1,31 +1,63 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Person, PersonList } from '../items'
|
||||
import './App.css'
|
||||
import { useState, useEffect } from "react";
|
||||
import { Person, PersonList } from "../items";
|
||||
import { Login } from "./Login";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [people, setPeople] = useState<Person[]>([])
|
||||
const [people, setPeople] = useState<Person[]>([]);
|
||||
const [token, setToken] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api')
|
||||
if (!token) return;
|
||||
|
||||
fetch("/api")
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then((buffer) => {
|
||||
const list = PersonList.decode(new Uint8Array(buffer))
|
||||
setPeople(list.person)
|
||||
const list = PersonList.decode(new Uint8Array(buffer));
|
||||
setPeople(list.person);
|
||||
})
|
||||
.catch((err) => console.error('Failed to fetch people:', err))
|
||||
}, [])
|
||||
.catch((err) => console.error("Failed to fetch people:", err));
|
||||
}, [token]);
|
||||
|
||||
const handleLogout = () => {
|
||||
setToken("");
|
||||
setPeople([]);
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return <Login onLogin={setToken} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card">
|
||||
<h2>People List</h2>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "1rem",
|
||||
}}
|
||||
>
|
||||
<h2>People List</h2>
|
||||
<button onClick={handleLogout}>Logout</button>
|
||||
</div>
|
||||
{people.map((person, index) => (
|
||||
<div key={index}>
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
marginBottom: "1rem",
|
||||
padding: "1rem",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<h3>{person.name}</h3>
|
||||
<ul>
|
||||
{person.opinion.map((op, i) => (
|
||||
<li key={i}>
|
||||
{op.game?.title} - {op.wouldPlay ? 'Would Play' : 'Would Not Play'}
|
||||
{op.game?.title} -{" "}
|
||||
{op.wouldPlay ? "Would Play" : "Would Not Play"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -33,7 +65,7 @@ function App() {
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
77
frontend/src/Login.tsx
Normal file
77
frontend/src/Login.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { useState } from "react";
|
||||
import { LoginRequest, LoginResponse } from "../items";
|
||||
|
||||
interface LoginProps {
|
||||
onLogin: (token: string) => void;
|
||||
}
|
||||
|
||||
export function Login({ onLogin }: LoginProps) {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const req = LoginRequest.create({ username, password });
|
||||
const body = LoginRequest.encode(req).finish();
|
||||
|
||||
const res = await fetch("/auth/login", {
|
||||
method: "POST",
|
||||
body,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
const response = LoginResponse.decode(new Uint8Array(buffer));
|
||||
|
||||
if (response.success) {
|
||||
onLogin(response.token);
|
||||
} else {
|
||||
setError(response.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
setError("Failed to login");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>Login</h2>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
||||
>
|
||||
<div>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||
Username:{" "}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={{ padding: "0.5rem" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||
Password:{" "}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{ padding: "0.5rem" }}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{error && <p style={{ color: "red", marginTop: "1rem" }}>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -40,18 +40,14 @@ message LoginResponse {
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
message LogoutRequest {
|
||||
string token = 1;
|
||||
}
|
||||
message LogoutRequest { string token = 1; }
|
||||
|
||||
message LogoutResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message AuthStatusRequest {
|
||||
string token = 1;
|
||||
}
|
||||
message AuthStatusRequest { string token = 1; }
|
||||
|
||||
message AuthStatusResponse {
|
||||
bool authenticated = 1;
|
||||
@ -61,7 +57,7 @@ message AuthStatusResponse {
|
||||
|
||||
// Authentication service
|
||||
service AuthService {
|
||||
rpc Login (LoginRequest) returns (LoginResponse);
|
||||
rpc Logout (LogoutRequest) returns (LogoutResponse);
|
||||
rpc GetAuthStatus (AuthStatusRequest) returns (AuthStatusResponse);
|
||||
rpc Login(LoginRequest) returns (LoginResponse);
|
||||
rpc Logout(LogoutRequest) returns (LogoutResponse);
|
||||
rpc GetAuthStatus(AuthStatusRequest) returns (AuthStatusResponse);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user