diff --git a/.gitignore b/.gitignore index ea8c4bf..0cf3a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +migration/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0e76801 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "yggdrasil-clone" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.24.2", features = ["full"] } +sea-orm = { version = "0.10", features = [ "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] } +axum = "0.6.2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = "1" +chrono = "0.4" diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 0000000..2b29730 --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +async-std = { version = "^1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "0.10" +features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 0000000..b3ea53e --- /dev/null +++ b/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- migrate generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 0000000..2c605af --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_create_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220101_000001_create_table::Migration)] + } +} diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs new file mode 100644 index 0000000..f68ff56 --- /dev/null +++ b/migration/src/m20220101_000001_create_table.rs @@ -0,0 +1,44 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(User::Table) + .if_not_exists() + .col( + ColumnDef::new(User::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(User::Name).string().not_null()) + .col(ColumnDef::new(User::Password).string().not_null()) + .col(ColumnDef::new(User::Token).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(User::Table).to_owned()) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum User { + Table, + Id, + Name, + Password, + Token, +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 0000000..c6b6e48 --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs new file mode 100644 index 0000000..62ff289 --- /dev/null +++ b/src/entities/mod.rs @@ -0,0 +1,5 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2 + +pub mod prelude; + +pub mod user; diff --git a/src/entities/prelude.rs b/src/entities/prelude.rs new file mode 100644 index 0000000..c373f02 --- /dev/null +++ b/src/entities/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2 + +pub use super::user::Entity as User; diff --git a/src/entities/user.rs b/src/entities/user.rs new file mode 100644 index 0000000..547e978 --- /dev/null +++ b/src/entities/user.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "user")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub password: String, + pub token: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2e7ac34 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,104 @@ +mod proto; +mod entities; + +use std::{net::SocketAddr, collections::HashMap, sync::Arc}; + +use chrono::{DateTime, Utc}; +use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::{State, Query}}; +use proto::{Error, AuthenticateRequest, AuthenticateResponse, JoinRequest, JoinResponse}; +use tokio::sync::Mutex; +use uuid::Uuid; + +#[derive(Clone)] +struct JoinAttempt { + server: String, + time: DateTime, +} + +impl JoinAttempt { + pub fn new(server: String) -> JoinAttempt { + JoinAttempt { server, time: Utc::now() } + } +} + +#[derive(Clone)] +struct AppState { + store: Arc>>, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let store = Arc::new(Mutex::new(HashMap::new())); + let app = Router::new() + .route("/", get(root)) + // .route("/authenticate", post(auth)) + // .route("/refresh", post(refresh)) + // .route("/validate", post(validate)) + // .route("/signout", post(signout)) + // .route("/invalidate", post(invalidate)) + .with_state(AppState { store }); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await?; + + Ok(()) +} + +async fn root(State(state): State) -> impl IntoResponse { + let error = Error { + error: "No route specified".into(), + errorMessage: "No route specified".into(), + cause: "No route specified".into(), + }; + + (StatusCode::OK, Json(error)) +} + +async fn authenticate(State(state): State, Json(payload): Json) -> impl IntoResponse { + let response = AuthenticateResponse { + accessToken: "".into(), + user: proto::User { id: "".parse().unwrap(), username: "".into(), properties: Some(vec![]) }, + clientToken: "".into(), + availableProfiles: "".into(), + selectedProfile: "".into(), + }; + + (StatusCode::OK, Json(response)) +} + +async fn join(State(state): State, Json(payload): Json) -> impl IntoResponse { + // 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, Query(query): Query>) -> 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 + } +} diff --git a/src/proto.rs b/src/proto.rs new file mode 100644 index 0000000..dd17b1b --- /dev/null +++ b/src/proto.rs @@ -0,0 +1,104 @@ +use uuid::Uuid; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct Error { + pub error: String, + pub errorMessage: String, + pub cause: String, +} + +impl Error { + pub fn simple(msg: String) -> Error { + Error { error: msg.clone(), errorMessage: msg.clone(), cause: msg } + } +} + +#[derive(Serialize, Deserialize)] +pub struct Agent { + pub name: String, + pub version: u32, +} + +#[derive(Serialize, Deserialize)] +pub struct Property { + pub name: String, + pub value: String, + pub signature: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct User { + pub id: Uuid, + pub username: String, + pub properties: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct Profile { + pub name: String, + pub id: Uuid, +} + + +#[derive(Serialize, Deserialize)] +pub struct AuthenticateRequest { + pub agent: Agent, + pub username: String, + pub password: String, + pub clientToken: Option, + pub requestUser: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct AuthenticateResponse { + pub user: User, + pub clientToken: String, + pub accessToken: String, + pub availableProfiles: Vec, + pub selectedProfile: Profile, +} + +#[derive(Serialize, Deserialize)] +pub struct RefreshRequest { + pub accessToken: String, + pub clientToken: String, + pub selectedProfile: Option, + pub requestUser: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct RefreshResponse { + pub accessToken: String, + pub clientToken: String, + pub selectedProfile: Profile, + pub user: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct ValidateRequest { + pub accessToken: String, + pub clientToken: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct SignoutRequest { + pub username: String, + pub password: String, +} + + + +#[derive(Serialize, Deserialize)] +pub struct JoinRequest { + pub accessToken: String, + pub selectedProfile: Uuid, + pub serverId: String, +} + +#[derive(Serialize, Deserialize)] +pub struct JoinResponse { + pub id: Uuid, + pub name: String, + pub properties: Vec, +}