summaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/src')
-rw-r--r--server/src/api_model.rs47
-rw-r--r--server/src/auth.rs80
-rw-r--r--server/src/main.rs55
3 files changed, 155 insertions, 27 deletions
diff --git a/server/src/api_model.rs b/server/src/api_model.rs
index 286e11f..a7c8e88 100644
--- a/server/src/api_model.rs
+++ b/server/src/api_model.rs
@@ -1,6 +1,7 @@
use rocket::serde::Serialize;
+use utoipa::ToSchema;
-#[derive(Serialize, Copy, Clone)]
+#[derive(Serialize, Copy, Clone, ToSchema)]
pub enum ReviewState {
Draft,
Open,
@@ -8,70 +9,96 @@ pub enum ReviewState {
Closed,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct User {
+ #[schema(example = 1337u64)]
pub id: u64,
+ #[schema(example = "jsmith")]
pub username: String,
+ #[schema(example = "John Smith")]
pub name: String,
+ #[schema(example = true)]
pub active: bool,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct Review {
+ #[schema(example = 1000u64)]
pub id: u64,
+ #[schema(example = "FAKE-512: Add more features")]
pub title: String,
+ #[schema(example = "We're adding more features because features are what we want.")]
pub description: String,
pub owner: User,
pub reviewers: Vec<User>,
pub watchers: Vec<User>,
+ #[schema(example = ReviewState::Open)]
pub state: ReviewState,
+ #[schema(example = 37.5)]
pub progress: f32,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct ReviewEntry {
+ #[schema(example = 1000u64)]
pub id: u64,
+ #[schema(example = "FAKE-512: Add more features")]
pub title: String,
pub owner: User,
+ #[schema(example = ReviewState::Open)]
pub state: ReviewState,
+ #[schema(example = 37.5)]
pub progress: f32,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct Reviews {
+ #[schema(example = 0u32)]
pub offset: u32,
+ #[schema(example = 10u32)]
pub limit: u32,
+ #[schema(example = 42u32)]
pub total_count: u32,
+ #[schema(example = true)]
pub more: bool,
pub reviews: Vec<ReviewEntry>,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct Project {
+ #[schema(example = 1u64)]
pub id: u64,
+ #[schema(example = "FAKE: Features All Kids Erase")]
pub title: String,
+ #[schema(example = "Example project")]
pub description: String,
pub members: Vec<User>,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct ProjectEntry {
+ #[schema(example = 1u64)]
pub id: u64,
+ #[schema(example = "FAKE: Features All Kids Erase")]
pub title: String,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct Projects {
+ #[schema(example = 0u32)]
pub offset: u32,
+ #[schema(example = 10u32)]
pub limit: u32,
+ #[schema(example = 1u32)]
pub total_count: u32,
+ #[schema(example = false)]
pub more: bool,
pub projects: Vec<ProjectEntry>,
}
-#[derive(Serialize)]
+#[derive(Serialize, ToSchema)]
pub struct StatusResponse {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
- pub error: Option<String>,
+ pub error: Option<&'static str>,
}
diff --git a/server/src/auth.rs b/server/src/auth.rs
index 31e18a0..4e66448 100644
--- a/server/src/auth.rs
+++ b/server/src/auth.rs
@@ -12,10 +12,31 @@ use std::collections::BTreeMap;
use std::sync::Mutex;
use std::time::Instant;
use time::Duration;
+use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
+use utoipa::{Modify, OpenApi, ToSchema};
use crate::api_model;
-#[derive(FromForm)]
+#[derive(OpenApi)]
+#[openapi(
+ paths(login, logout, status,),
+ modifiers(&AuthApiAddon),
+)]
+pub struct AuthApi;
+
+pub struct AuthApiAddon;
+
+impl Modify for AuthApiAddon {
+ fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
+ let components = openapi.components.as_mut().unwrap();
+ components.add_security_scheme(
+ "session",
+ SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(SESSION_COOKIE))),
+ )
+ }
+}
+
+#[derive(FromForm, ToSchema)]
struct Login<'r> {
username: &'r str,
password: &'r str,
@@ -49,6 +70,14 @@ pub enum SessionError {
}
const SESSION_COOKIE: &str = "s";
+const STATUS_OK: api_model::StatusResponse = api_model::StatusResponse {
+ ok: true,
+ error: None,
+};
+const STATUS_UNAUTHORIZED: api_model::StatusResponse = api_model::StatusResponse {
+ ok: false,
+ error: Some("Unauthorized"),
+};
fn validate(sessions: &State<Sessions>, session: &Session, request: &Request<'_>) -> bool {
match request.client_ip() {
@@ -123,6 +152,17 @@ fn new_session(
}
}
+#[utoipa::path(
+ responses(
+ (status = 200, description = "Login successful", body = api_model::StatusResponse,
+ example = json!(STATUS_OK)),
+ (status = 401, description = "Login failed", body = api_model::StatusResponse,
+ example = json!(STATUS_UNAUTHORIZED)),
+ ),
+ security(
+ (),
+ ),
+)]
#[post("/login", data = "<login>")]
fn login(
auth_config: &State<AuthConfig>,
@@ -142,15 +182,20 @@ fn login(
.build();
cookies.add_private(cookie);
- Ok(Json(api_model::StatusResponse {
- ok: true,
- error: None,
- }))
+ Ok(Json(STATUS_OK))
} else {
Err(Unauthorized("Unknown username or password"))
}
}
+#[utoipa::path(
+ responses(
+ (status = 200, description = "Logout successful", body = api_model::StatusResponse, example = json!(STATUS_OK)),
+ ),
+ security(
+ ("session" = []),
+ ),
+)]
#[get("/logout")]
fn logout(
session: Session,
@@ -169,26 +214,27 @@ fn logout(
cookies.remove_private(cookie);
- Json(api_model::StatusResponse {
- ok: true,
- error: None,
- })
+ Json(STATUS_OK)
}
+#[utoipa::path(
+ responses(
+ (status = 200, description = "Current status", body = api_model::StatusResponse, example = json!(STATUS_OK)),
+ (status = 401, description = "Not authorized", body = api_model::StatusResponse, example = json!(STATUS_UNAUTHORIZED)),
+ ),
+ security(
+ (),
+ ("session" = []),
+ ),
+)]
#[get("/status")]
fn status(_session: Session) -> Json<api_model::StatusResponse> {
- Json(api_model::StatusResponse {
- ok: true,
- error: None,
- })
+ Json(STATUS_OK)
}
#[catch(401)]
fn unauthorized() -> Json<api_model::StatusResponse> {
- Json(api_model::StatusResponse {
- ok: false,
- error: Some("Unauthorized".to_string()),
- })
+ Json(STATUS_UNAUTHORIZED)
}
pub fn stage(basepath: String) -> AdHoc {
diff --git a/server/src/main.rs b/server/src/main.rs
index 223d861..124d914 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -8,14 +8,25 @@ use rocket::response::status::NotFound;
use rocket::serde::json::Json;
use rocket::{futures, Build, Rocket};
use rocket_db_pools::{sqlx, Connection, Database};
+use utoipa::OpenApi;
+use utoipa_swagger_ui::SwaggerUi;
mod api_model;
mod auth;
+use auth::AuthApiAddon;
+
#[derive(Database)]
#[database("eyeballs")]
struct Db(sqlx::MySqlPool);
+#[derive(OpenApi)]
+#[openapi(
+ paths(projects, project, reviews, review,),
+ modifiers(&AuthApiAddon),
+)]
+pub struct MainApi;
+
enum Role {
Reviewer,
Watcher,
@@ -52,6 +63,14 @@ impl TryFrom<u8> for api_model::ReviewState {
}
}
+#[utoipa::path(
+ responses(
+ (status = 200, description = "Get all projects", body = api_model::Projects),
+ ),
+ security(
+ ("session" = []),
+ ),
+)]
#[get("/projects?<limit>&<offset>")]
async fn projects<'r>(
mut db: Connection<Db>,
@@ -93,6 +112,15 @@ async fn projects<'r>(
})
}
+#[utoipa::path(
+ responses(
+ (status = 200, description = "Get project", body = api_model::Project),
+ (status = 404, description = "No such project"),
+ ),
+ security(
+ ("session" = []),
+ ),
+)]
#[get("/project/<projectid>")]
async fn project<'r>(
mut db: Connection<Db>,
@@ -130,6 +158,14 @@ async fn project<'r>(
Ok(Json(project))
}
+#[utoipa::path(
+ responses(
+ (status = 200, description = "Get all reviews for project", body = api_model::Reviews),
+ ),
+ security(
+ ("session" = []),
+ ),
+)]
#[get("/project/<projectid>/reviews?<limit>&<offset>")]
async fn reviews<'r>(
mut db: Connection<Db>,
@@ -180,6 +216,15 @@ async fn reviews<'r>(
})
}
+#[utoipa::path(
+ responses(
+ (status = 200, description = "Get review", body = api_model::Review),
+ (status = 404, description = "No such review"),
+ ),
+ security(
+ ("session" = []),
+ ),
+)]
#[get("/review/<reviewid>")]
async fn review<'r>(
mut db: Connection<Db>,
@@ -258,10 +303,20 @@ async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
async fn main() -> Result<(), rocket::Error> {
let basepath = "/api/v1";
+ let mut api = MainApi::openapi();
+ api.merge(auth::AuthApi::openapi());
+ api.servers = Some(vec![utoipa::openapi::ServerBuilder::new()
+ .url(basepath)
+ .build()]);
+
let _rocket = rocket::build()
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Database Migrations", run_migrations))
.mount(basepath, routes![projects, project, reviews, review])
+ .mount(
+ "/",
+ SwaggerUi::new("/openapi/ui/<_..>").url("/openapi/openapi.json", api),
+ )
.attach(auth::stage(basepath.to_string()))
.launch()
.await?;