diff --git a/server/Cargo.lock b/server/Cargo.lock index bbc48e8..72cddec 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -47,6 +47,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "argon2" version = "0.5.3" @@ -266,10 +316,57 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] +[[package]] +name = "clap" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -994,6 +1091,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.14.0" @@ -2195,6 +2298,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.12.0" @@ -2540,6 +2649,7 @@ dependencies = [ "axum-extra", "bitflags", "chrono", + "clap", "csv", "dotenvy", "itertools", diff --git a/server/Cargo.toml b/server/Cargo.toml index ac97d19..f192d12 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,12 +17,13 @@ dotenvy = "0.15.7" validator = { version = "0.19.0", features = [ "derive" ] } argon2 = "0.5" bitflags = { version = "2.8", features = [ "serde" ] } +clap = { version = "4.5.31", features = ["derive"] } # Tertiary crates tracing = "0.1" tracing-subscriber = "0.3" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.12", features = ["v4", "fast-rng", "serde"] } serde_json = "1.0.137" rand = "0.9" diff --git a/server/migrations/001_create_members.sql b/server/migrations/001_create_members.sql index 53104c5..74af784 100644 --- a/server/migrations/001_create_members.sql +++ b/server/migrations/001_create_members.sql @@ -1,4 +1,4 @@ -CREATE TABLE "members" ( +CREATE TABLE IF NOT EXISTS "members" ( member_id varchar(7) NOT NULL PRIMARY KEY, first_name text NOT NULL, full_name text NOT NULL, diff --git a/server/migrations/002_create_users.sql b/server/migrations/002_create_users.sql index faffd57..70bb23e 100644 --- a/server/migrations/002_create_users.sql +++ b/server/migrations/002_create_users.sql @@ -1,4 +1,4 @@ -CREATE TABLE "users" ( +CREATE TABLE IF NOT EXISTS "users" ( user_id uuid NOT NULL PRIMARY KEY, email text NOT NULL UNIQUE, password text NOT NULL, diff --git a/server/migrations/003_create_sessions.sql b/server/migrations/003_create_sessions.sql index e1d3565..4a27778 100644 --- a/server/migrations/003_create_sessions.sql +++ b/server/migrations/003_create_sessions.sql @@ -1,4 +1,4 @@ -CREATE TABLE "sessions" ( +CREATE TABLE IF NOT EXISTS "sessions" ( session_id uuid NOT NULL PRIMARY KEY, user_id uuid NOT NULL REFERENCES users (user_id) ON UPDATE cascade ON DELETE cascade, token text NOT NULL UNIQUE, diff --git a/server/migrations/005_create_news.sql b/server/migrations/005_create_news.sql index c686631..27f17e7 100644 --- a/server/migrations/005_create_news.sql +++ b/server/migrations/005_create_news.sql @@ -1,6 +1,6 @@ CREATE TYPE message_status AS ENUM ('pending', 'sent', 'canceled'); -CREATE TABLE messages ( +CREATE TABLE IF NOT EXISTS messages ( message_id uuid NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL, scheduled_at timestamptz, @@ -11,7 +11,7 @@ CREATE TABLE messages ( thumbnail_url text ); -CREATE TABLE messages_users ( +CREATE TABLE IF NOT EXISTS messages_users ( message_id uuid NOT NULL REFERENCES users (user_id) ON UPDATE cascade ON DELETE cascade, user_id uuid NOT NULL REFERENCES users (user_id) ON UPDATE cascade ON DELETE cascade, is_read boolean NOT NULL, diff --git a/server/migrations/006_alter_messages.sql b/server/migrations/006_alter_messages.sql index b431b49..2be81ca 100644 --- a/server/migrations/006_alter_messages.sql +++ b/server/migrations/006_alter_messages.sql @@ -1,3 +1,3 @@ ALTER TABLE messages - ADD COLUMN member_groups bigint, - ADD COLUMN member_roles bigint; + ADD COLUMN member_groups bigint NOT NULL, + ADD COLUMN member_roles bigint NOT NULL; diff --git a/server/src/database/model/message.rs b/server/src/database/model/message.rs index ac77ef5..75eb6b3 100644 --- a/server/src/database/model/message.rs +++ b/server/src/database/model/message.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use sqlx::Postgres; +use sqlx::{PgPool, Postgres}; use crate::model::{ member::{Groups, Roles}, @@ -61,4 +61,20 @@ impl Message { Ok(()) } + + pub async fn get(pool: &PgPool, channel: Channel) -> Result, sqlx::Error> { + let messages = sqlx::query_as!( + Self, + " + SELECT message_id, created_at, scheduled_at, status as \"status:MessageStatus\", title, content, channel, member_groups, member_roles, thumbnail_url FROM messages + WHERE status = 'sent' + AND channel & $1 > 0; + ", + channel.bits() as i64 + ) + .fetch_all(pool) + .await?; + + Ok(messages) + } } diff --git a/server/src/main.rs b/server/src/main.rs index 0322d08..aa595e6 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,13 +1,7 @@ -use std::sync::Arc; - -use axum::Router; -use tokio::{net::TcpListener, sync::Mutex}; use tracing::Level; use tracing_subscriber::FmtSubscriber; -use wrbapp_server::routes::member::migrate::MigrationStore; -use wrbapp_server::routes::routes; -use wrbapp_server::{database, AppState}; +use wrbapp_server::database; #[tokio::main] async fn main() { @@ -30,23 +24,5 @@ async fn main() { .await .expect("Database connection failed"); - let migration_store = Arc::new(Mutex::new(MigrationStore::default())); - - let app_state = AppState { - pool, - migration_store, - }; - - // Serve app - let app = Router::new().nest("/v1", routes()).with_state(app_state); - - let listener = TcpListener::bind("127.0.0.1:3000") - .await - .expect("Error while initializing listener"); - - tracing::info!("Listening on {}", listener.local_addr().unwrap()); - - axum::serve(listener, app) - .await - .expect("Error while serving axum application"); + wrbapp_server::util::cli::parse(pool).await; } diff --git a/server/src/model/message.rs b/server/src/model/message.rs index 3e503f9..a75c523 100644 --- a/server/src/model/message.rs +++ b/server/src/model/message.rs @@ -1,7 +1,8 @@ use bitflags::bitflags; use chrono::{DateTime, Utc}; +use serde::Serialize; -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct Message { pub message_id: uuid::Uuid, pub created_at: DateTime, @@ -15,7 +16,7 @@ pub struct Message { pub thumbnail_url: Option, } -#[derive(Debug, Clone, Copy, sqlx::Type)] +#[derive(Debug, Clone, Copy, sqlx::Type, Serialize)] #[sqlx(type_name = "message_status", rename_all = "lowercase")] pub enum MessageStatus { Pending, @@ -24,7 +25,7 @@ pub enum MessageStatus { } bitflags! { - #[derive(Clone, Copy, Debug)] + #[derive(Clone, Copy, Debug, Serialize)] pub struct Channel: u16 { const ALGEMEEN = 1 << 0; const BELANGRIJK = 1 << 1; diff --git a/server/src/routes/user.rs b/server/src/routes/user.rs index d1a073d..abb8a66 100644 --- a/server/src/routes/user.rs +++ b/server/src/routes/user.rs @@ -7,8 +7,8 @@ use axum::{ use crate::{ auth::get_user_from_header, - database::model::{Member as DbMember, UserMember as DbUserMember}, - model::{member::Roles, Member, User}, + database::model::{Member as DbMember, Message as DbMessage, UserMember as DbUserMember}, + model::{member::Roles, message::Channel, Member, Message, User}, util::convert_vec, AppState, }; @@ -18,6 +18,7 @@ pub fn routes() -> Router { .route("/user", get(get_current_user)) .route("/user/{user_id}/members", post(members_insert)) .route("/user/{user_id}/members", delete(members_remove)) + .route("/user/{user_id}/messages", get(get_messages)) } pub async fn get_current_user( @@ -73,3 +74,16 @@ pub async fn members_remove( Ok(()) } + +pub async fn get_messages( + State(state): State, + Path(user_id): Path, + headers: HeaderMap, +) -> Result>, crate::Error> { + let user = get_user_from_header(&state.pool, &headers).await?; + user.authorize(&state.pool, None, Some(user_id)).await?; + + let messages = DbMessage::get(&state.pool, Channel::ALGEMEEN).await?; + + Ok(Json(convert_vec(messages))) +} diff --git a/server/src/util.rs b/server/src/util.rs index d277126..1a46f91 100644 --- a/server/src/util.rs +++ b/server/src/util.rs @@ -1,5 +1,7 @@ mod bitflags; +pub mod cli; pub mod error; mod helpers; +pub mod serve; pub use helpers::convert_vec; diff --git a/server/src/util/cli.rs b/server/src/util/cli.rs new file mode 100644 index 0000000..d356a9c --- /dev/null +++ b/server/src/util/cli.rs @@ -0,0 +1,56 @@ +use clap::{Parser, Subcommand}; +use sqlx::{Acquire, PgPool}; + +use crate::model::{ + member::{Groups, Roles}, + Member, +}; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + Serve, + CreateAdminMember, +} + +pub async fn parse(pool: PgPool) { + let cli = Cli::parse(); + + match &cli.command { + Some(Commands::Serve) => { + crate::util::serve::serve(pool).await; + } + Some(Commands::CreateAdminMember) => { + create_admin_member(&pool).await.unwrap(); + } + None => {} + } +} + +pub async fn create_admin_member(pool: &PgPool) -> Result<(), sqlx::Error> { + use crate::database::model::Member as DbMember; + + let member = DbMember { + member_id: "D000000".to_string(), + first_name: "Admin".to_string(), + full_name: "Admin Admin".to_string(), + registration_token: None, + diploma: None, + groups: Groups::empty(), + roles: Roles::ADMIN, + }; + + let mut transaction = pool.begin().await?; + + DbMember::insert_many(&mut transaction, vec![member]).await?; + + transaction.commit().await?; + + Ok(()) +} diff --git a/server/src/util/serve.rs b/server/src/util/serve.rs new file mode 100644 index 0000000..3816d11 --- /dev/null +++ b/server/src/util/serve.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use axum::Router; +use sqlx::PgPool; +use tokio::{net::TcpListener, sync::Mutex}; + +use crate::routes::member::migrate::MigrationStore; +use crate::routes::routes; +use crate::AppState; + +pub async fn serve(pool: PgPool) { + let migration_store = Arc::new(Mutex::new(MigrationStore::default())); + + let app_state = AppState { + pool, + migration_store, + }; + + // Serve app + let app = Router::new().nest("/v1", routes()).with_state(app_state); + + let listener = TcpListener::bind("127.0.0.1:3000") + .await + .expect("Error while initializing listener"); + + tracing::info!("Listening on {}", listener.local_addr().unwrap()); + + axum::serve(listener, app) + .await + .expect("Error while serving axum application"); +}