added tokens table and db queries, implemented most routes
This commit is contained in:
parent
0f95fe2536
commit
611bf35f89
9 changed files with 290 additions and 60 deletions
|
@ -6,10 +6,12 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
tokio = { version = "1.24.2", features = ["full"] }
|
tokio = { version = "1.24.2", features = ["full"] }
|
||||||
sea-orm = { version = "0.10", features = [ "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
|
sea-orm = { version = "0.10", features = [ "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
|
||||||
axum = "0.6.2"
|
axum = "0.6.2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
uuid = "1"
|
uuid = "1"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
|
8
README.md
Normal file
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# HOW TO USE
|
||||||
|
Add to java args (on client/server)
|
||||||
|
```
|
||||||
|
-Dminecraft.api.auth.host=http://localhost:8081/auth
|
||||||
|
-Dminecraft.api.account.host=http://localhost:8081/account
|
||||||
|
-Dminecraft.api.session.host=http://localhost:8081/session
|
||||||
|
-Dminecraft.api.services.host=http://localhost:8081/services
|
||||||
|
```
|
|
@ -18,27 +18,72 @@ impl MigrationTrait for Migration {
|
||||||
.auto_increment()
|
.auto_increment()
|
||||||
.primary_key(),
|
.primary_key(),
|
||||||
)
|
)
|
||||||
|
.col(ColumnDef::new(User::Uuid).uuid().not_null())
|
||||||
.col(ColumnDef::new(User::Name).string().not_null())
|
.col(ColumnDef::new(User::Name).string().not_null())
|
||||||
.col(ColumnDef::new(User::Password).string().not_null())
|
.col(ColumnDef::new(User::Password).string().not_null())
|
||||||
.col(ColumnDef::new(User::Token).string().not_null())
|
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Token::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Token::Id)
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.auto_increment()
|
||||||
|
.primary_key(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Token::UserId)
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk-user-id")
|
||||||
|
.from(Token::Table, Token::UserId)
|
||||||
|
.to(User::Table, User::Id)
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Token::AccessToken).string().not_null())
|
||||||
|
.col(ColumnDef::new(Token::CreatedAt).date_time().not_null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
manager
|
manager
|
||||||
.drop_table(Table::drop().table(User::Table).to_owned())
|
.drop_table(Table::drop().table(User::Table).to_owned())
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Token::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Learn more at https://docs.rs/sea-query#iden
|
|
||||||
#[derive(Iden)]
|
#[derive(Iden)]
|
||||||
enum User {
|
enum User {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
|
Uuid,
|
||||||
Password,
|
Password,
|
||||||
Token,
|
}
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
enum Token {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
UserId,
|
||||||
|
AccessToken,
|
||||||
|
CreatedAt,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
|
||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod token;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
|
||||||
|
|
||||||
|
pub use super::token::Entity as Token;
|
||||||
pub use super::user::Entity as User;
|
pub use super::user::Entity as User;
|
||||||
|
|
29
src/entities/token.rs
Normal file
29
src/entities/token.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "token")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub access_token: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "NoAction"
|
||||||
|
)]
|
||||||
|
SelfRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,4 +1,4 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ use sea_orm::entity::prelude::*;
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
pub uuid: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub token: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
235
src/main.rs
235
src/main.rs
|
@ -3,12 +3,35 @@ mod entities;
|
||||||
|
|
||||||
use std::{net::SocketAddr, collections::HashMap, sync::Arc};
|
use std::{net::SocketAddr, collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc, Duration};
|
||||||
|
use clap::Parser;
|
||||||
use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::{State, Query}};
|
use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::{State, Query}};
|
||||||
use proto::{Error, AuthenticateRequest, AuthenticateResponse, JoinRequest, JoinResponse};
|
use proto::{Error, AuthenticateRequest, AuthenticateResponse, JoinRequest, JoinResponse, ValidateRequest, RefreshRequest, RefreshResponse, RegisterRequest};
|
||||||
|
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, DatabaseConnection, Database, ActiveValue::NotSet, Set};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Reimplementation of legacy auth server for minecraft
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct ConfigArgs {
|
||||||
|
/// Connection string for database
|
||||||
|
database: String,
|
||||||
|
|
||||||
|
// /// Listen address
|
||||||
|
// #[arg(short, long, default_value_t = )]
|
||||||
|
// listen_addr: String,
|
||||||
|
|
||||||
|
/// Listen port
|
||||||
|
#[arg(short, long, default_value_t = 25556)]
|
||||||
|
listen_port: u16,
|
||||||
|
|
||||||
|
/// Valid time for join requests, in seconds
|
||||||
|
#[arg(short, long, default_value_t = 10)]
|
||||||
|
time_window: u32,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct JoinAttempt {
|
struct JoinAttempt {
|
||||||
server: String,
|
server: String,
|
||||||
|
@ -24,21 +47,32 @@ impl JoinAttempt {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
store: Arc<Mutex<HashMap<Uuid, JoinAttempt>>>,
|
store: Arc<Mutex<HashMap<Uuid, JoinAttempt>>>,
|
||||||
|
db: DatabaseConnection,
|
||||||
|
cfg: ConfigArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let cfg = ConfigArgs::parse();
|
||||||
|
|
||||||
|
let db = Database::connect("sqlite://./test.db").await?;
|
||||||
let store = Arc::new(Mutex::new(HashMap::new()));
|
let store = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
// .route("/authenticate", post(auth))
|
.route("/authenticate", post(authenticate))
|
||||||
// .route("/refresh", post(refresh))
|
.route("/validate", post(validate))
|
||||||
// .route("/validate", post(validate))
|
.route("/refresh", post(refresh))
|
||||||
// .route("/signout", post(signout))
|
// .route("/signout", post(signout))
|
||||||
// .route("/invalidate", post(invalidate))
|
// .route("/invalidate", post(invalidate))
|
||||||
.with_state(AppState { store });
|
.route("/join", post(join))
|
||||||
|
.route("/hasJoined", get(has_joined))
|
||||||
|
.route("/register", post(register_tmp))
|
||||||
|
.with_state(AppState { store, db, cfg });
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // TODO parse strings from CLI
|
||||||
|
|
||||||
axum::Server::bind(&addr)
|
axum::Server::bind(&addr)
|
||||||
.serve(app.into_make_service())
|
.serve(app.into_make_service())
|
||||||
|
@ -47,7 +81,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn root(State(state): State<AppState>) -> impl IntoResponse {
|
async fn root() -> impl IntoResponse {
|
||||||
let error = Error {
|
let error = Error {
|
||||||
error: "No route specified".into(),
|
error: "No route specified".into(),
|
||||||
errorMessage: "No route specified".into(),
|
errorMessage: "No route specified".into(),
|
||||||
|
@ -57,48 +91,149 @@ async fn root(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
(StatusCode::OK, Json(error))
|
(StatusCode::OK, Json(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn authenticate(State(state): State<AppState>, Json(payload): Json<AuthenticateRequest>) -> impl IntoResponse {
|
async fn validate(State(state): State<AppState>, Json(payload): Json<ValidateRequest>) -> StatusCode {
|
||||||
let response = AuthenticateResponse {
|
let token = entities::token::Entity::find().filter(
|
||||||
accessToken: "".into(),
|
entities::token::Column::AccessToken.eq(payload.accessToken)
|
||||||
user: proto::User { id: "".parse().unwrap(), username: "".into(), properties: Some(vec![]) },
|
).one(&state.db).await.unwrap();
|
||||||
clientToken: "".into(),
|
|
||||||
availableProfiles: "".into(),
|
|
||||||
selectedProfile: "".into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
(StatusCode::OK, Json(response))
|
if let Some(_t) = token {
|
||||||
}
|
StatusCode::NO_CONTENT
|
||||||
|
} else {
|
||||||
async fn join(State(state): State<AppState>, Json(payload): Json<JoinRequest>) -> impl IntoResponse {
|
StatusCode::UNAUTHORIZED
|
||||||
// TODO make sure that accessToken is valid
|
|
||||||
|
|
||||||
state.store.lock().await.insert(payload.selectedProfile, JoinAttempt::new(payload.serverId));
|
|
||||||
|
|
||||||
(StatusCode::OK, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn hasJoined(State(state): State<AppState>, Query(query): Query<HashMap<String, String>>) -> impl IntoResponse {
|
|
||||||
let username = query.get("username").unwrap();
|
|
||||||
let serverId = query.get("serverId").unwrap();
|
|
||||||
|
|
||||||
// TODO find UUID from username to match it with JoinAttempt
|
|
||||||
// entities::user::Entity::find().where();
|
|
||||||
let uid = uuid::uuid!("1234");
|
|
||||||
|
|
||||||
match state.store.lock().await.get(&uid) {
|
|
||||||
Some(join) => {
|
|
||||||
if join.server.to_lowercase() == serverId.to_lowercase() {
|
|
||||||
let response = JoinResponse {
|
|
||||||
id: uid,
|
|
||||||
name: username.clone(),
|
|
||||||
properties: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
(StatusCode::OK, Json(response))
|
|
||||||
} else {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => todo!(), // return 404? idk error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn refresh(State(state): State<AppState>, Json(payload): Json<RefreshRequest>) -> Result<Json<RefreshResponse>, StatusCode> {
|
||||||
|
let token = entities::token::Entity::find().filter(
|
||||||
|
entities::token::Column::AccessToken.eq(payload.accessToken.clone())
|
||||||
|
).one(&state.db).await.unwrap();
|
||||||
|
|
||||||
|
if let Some(t) = token {
|
||||||
|
// TODO if user requests profile, fetch it and include it
|
||||||
|
let user = entities::user::Entity::find_by_id(t.user_id).one(&state.db).await.unwrap().unwrap();
|
||||||
|
entities::token::Entity::delete(
|
||||||
|
entities::token::ActiveModel{
|
||||||
|
id: NotSet,
|
||||||
|
access_token: Set(payload.accessToken.clone()),
|
||||||
|
created_at: NotSet,
|
||||||
|
user_id: NotSet,
|
||||||
|
}
|
||||||
|
).exec(&state.db).await.unwrap();
|
||||||
|
let new_access_token = Uuid::new_v4(); // TODO same as with authenticate
|
||||||
|
entities::token::Entity::insert(
|
||||||
|
entities::token::ActiveModel{
|
||||||
|
id: NotSet,
|
||||||
|
access_token: Set(payload.accessToken.clone()),
|
||||||
|
created_at: NotSet,
|
||||||
|
user_id: NotSet,
|
||||||
|
}
|
||||||
|
).exec(&state.db).await.unwrap();
|
||||||
|
Ok(Json(
|
||||||
|
RefreshResponse {
|
||||||
|
accessToken: new_access_token.to_string(),
|
||||||
|
clientToken: "idc".into(),
|
||||||
|
selectedProfile: proto::Profile { id: user.uuid, name: user.name },
|
||||||
|
user: None,
|
||||||
|
}
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(StatusCode::UNAUTHORIZED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticate(State(state): State<AppState>, Json(payload): Json<AuthenticateRequest>) -> Result<Json<AuthenticateResponse>, StatusCode> {
|
||||||
|
let user = entities::user::Entity::find().filter(
|
||||||
|
entities::user::Column::Name.eq(payload.username)
|
||||||
|
).one(&state.db).await.unwrap();
|
||||||
|
|
||||||
|
if let Some(u) = user {
|
||||||
|
if payload.password == u.password {
|
||||||
|
// make new token
|
||||||
|
let access_token = Uuid::new_v4().to_string(); // TODO maybe use a JWT?
|
||||||
|
entities::token::Entity::insert(entities::token::ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
user_id: Set(u.id),
|
||||||
|
access_token: Set(access_token.clone()),
|
||||||
|
created_at: Set(Utc::now()),
|
||||||
|
}).exec(&state.db).await.unwrap();
|
||||||
|
let profile = proto::Profile {
|
||||||
|
name: u.name.clone(),
|
||||||
|
id: u.uuid,
|
||||||
|
};
|
||||||
|
Ok(Json(
|
||||||
|
AuthenticateResponse {
|
||||||
|
accessToken: access_token,
|
||||||
|
user: proto::User { id: u.uuid, username: u.name, properties: Some(vec![]) },
|
||||||
|
clientToken: Uuid::new_v4().to_string(),
|
||||||
|
availableProfiles: vec![profile.clone()],
|
||||||
|
selectedProfile: profile,
|
||||||
|
}
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(StatusCode::UNAUTHORIZED)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn join(State(state): State<AppState>, Json(payload): Json<JoinRequest>) -> StatusCode {
|
||||||
|
let user = entities::user::Entity::find().filter(
|
||||||
|
entities::user::Column::Uuid.eq(payload.selectedProfile)
|
||||||
|
).one(&state.db).await.unwrap().unwrap();
|
||||||
|
|
||||||
|
let tokens = entities::token::Entity::find().filter(
|
||||||
|
entities::token::Column::UserId.eq(user.id)
|
||||||
|
).all(&state.db).await.unwrap();
|
||||||
|
|
||||||
|
if tokens.iter().any(|x| x.access_token == payload.accessToken) {
|
||||||
|
state.store.lock().await.insert(payload.selectedProfile, JoinAttempt::new(payload.serverId));
|
||||||
|
StatusCode::OK
|
||||||
|
} else {
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn has_joined(State(state): State<AppState>, Query(query): Query<HashMap<String, String>>) -> Result<Json<JoinResponse>, StatusCode> {
|
||||||
|
let username = query.get("username").unwrap().clone();
|
||||||
|
let server_id = query.get("serverId").unwrap();
|
||||||
|
|
||||||
|
let user = entities::user::Entity::find().filter(
|
||||||
|
entities::user::Column::Name.eq(username.clone())
|
||||||
|
).one(&state.db).await.unwrap();
|
||||||
|
|
||||||
|
match user {
|
||||||
|
Some(user) => {
|
||||||
|
match state.store.lock().await.get(&user.uuid) {
|
||||||
|
Some(join) => {
|
||||||
|
if Utc::now() - join.time < Duration::seconds(state.cfg.time_window as i64)
|
||||||
|
&& join.server.to_lowercase() == server_id.to_lowercase() {
|
||||||
|
let response = JoinResponse {
|
||||||
|
id: user.uuid,
|
||||||
|
name: username.clone(),
|
||||||
|
properties: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
} else {
|
||||||
|
Err(StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Err(StatusCode::NOT_FOUND),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Err(StatusCode::NOT_FOUND),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_tmp(State(state): State<AppState>, Json(payload): Json<RegisterRequest>) -> StatusCode {
|
||||||
|
let user = entities::user::ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
name: Set(payload.user),
|
||||||
|
password: Set(payload.password),
|
||||||
|
uuid: Set(Uuid::new_v4()),
|
||||||
|
};
|
||||||
|
entities::user::Entity::insert(user).exec(&state.db).await.unwrap();
|
||||||
|
|
||||||
|
StatusCode::OK
|
||||||
|
}
|
||||||
|
|
11
src/proto.rs
11
src/proto.rs
|
@ -34,7 +34,7 @@ pub struct User {
|
||||||
pub properties: Option<Vec<Property>>,
|
pub properties: Option<Vec<Property>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -102,3 +102,12 @@ pub struct JoinResponse {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub properties: Vec<Property>,
|
pub properties: Vec<Property>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
pub user: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue