Add crates for desktop, mobile and simple ui

This commit is contained in:
fluo10 2025-06-13 08:32:35 +09:00
parent 18de94b3c9
commit 1bdaa45f52
43 changed files with 3013 additions and 1352 deletions

3016
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@ async-graphql = { version = "7.0", features = ["chrono"] }
chrono = {version = "0.4", features = ["serde"]} chrono = {version = "0.4", features = ["serde"]}
chrono-tz = {version = "0.10.3", features = ["serde"]} chrono-tz = {version = "0.10.3", features = ["serde"]}
clap = {version = "4.5", features = ["derive"]} clap = {version = "4.5", features = ["derive"]}
dioxus = { version = "0.6.0" }
dirs = "6.0.0" dirs = "6.0.0"
dotenv = "0.15.0" dotenv = "0.15.0"
lazy-supplements-core.path = "lazy-supplements/lazy-supplements-core" lazy-supplements-core.path = "lazy-supplements/lazy-supplements-core"
@ -24,8 +25,7 @@ progress-pile-client = { path = "progress-pile-client", default-features = false
progress-pile-core = { path = "progress-pile-core", default-features = false } progress-pile-core = { path = "progress-pile-core", default-features = false }
progress-pile-migration-client.path = "progress-pile-migration-client" progress-pile-migration-client.path = "progress-pile-migration-client"
progress-pile-migration-core.path = "progress-pile-migration-core" progress-pile-migration-core.path = "progress-pile-migration-core"
progress-pile-migration-server.path = "progress-pile-migration-server" progress-pile-simple-ui.path = "progress-pile-simple-ui"
progress-pile-server.path = "progress-pile-server"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tempfile = "3.20.0" tempfile = "3.20.0"
thiserror = "2.0.12" thiserror = "2.0.12"
@ -52,3 +52,15 @@ features = [
"sqlx-sqlite", "sqlx-sqlite",
] ]
[profile]
[profile.wasm-dev]
inherits = "dev"
opt-level = 1
[profile.server-dev]
inherits = "dev"
[profile.android-dev]
inherits = "dev"

View file

View file

7
progress-pile-desktop/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target
.DS_Store
# These are backup files generated by rustfmt
**/*.rs.bk

View file

@ -1,10 +1,17 @@
[package] [package]
name = "progress-pile-desktop" name = "progress-pile-desktop"
version.workspace = true version = "0.1.0"
edition.workspace = true authors = ["fluo10 <fluo10.dev@fireturtle.net>"]
description.workspace = true edition = "2021"
license.workspace = true
repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
lazy-supplements-desktop.workspace = true dioxus.workspace = true
progress-pile-simple-ui.workspace = true
[features]
default = ["desktop"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]

View file

@ -0,0 +1,21 @@
[application]
[web.app]
# HTML title tag content
title = "progress-pile-desktop"
# include `assets` in web platform
[web.resource]
# Additional CSS style files
style = []
# Additional JavaScript files
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []

View file

@ -0,0 +1,25 @@
# Development
Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets.
```
project/
├─ assets/ # Any assets that are used by the app should be placed here
├─ src/
│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
```
### Serving Your App
Run the following command in the root of your project to start developing with the default platform:
```bash
dx serve
```
To run for a different platform, use the `--platform platform` flag. E.g.
```bash
dx serve --platform desktop
```

View file

@ -0,0 +1,8 @@
await-holding-invalid-types = [
"generational_box::GenerationalRef",
{ path = "generational_box::GenerationalRef", reason = "Reads should not be held over an await point. This will cause any writes to fail while the await is pending since the read borrow is still active." },
"generational_box::GenerationalRefMut",
{ path = "generational_box::GenerationalRefMut", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
"dioxus_signals::Write",
{ path = "dioxus_signals::Write", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
]

View file

@ -1,3 +1,3 @@
fn main() { fn main() {
println!("Hello, world!"); dioxus::launch(progress_pile_simple_ui::App);
} }

View file

@ -1,19 +0,0 @@
[package]
name = "progress-pile-migration-server"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
progress-pile-migration-core.workspace = true
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
# "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
]

View file

@ -1,41 +0,0 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- 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
```

View file

@ -1,12 +0,0 @@
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<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}

View file

@ -1,289 +0,0 @@
use progress_pile_migration_core::m20220101_000001_create_table::{
TableMigration,
ProgressCategory as DefaultProgressCategory,
ProgressEntry as DefaultProgressEntry,
PK_PROGRESS_CATEGORY,
PK_PROGRESS_ENTITY,
};
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
User::up(manager).await?;
AccessToken::up(manager).await?;
ProgressCategory::up(manager).await?;
ProgressEntry::up(manager).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
ProgressEntry::down(manager).await?;
ProgressCategory::down(manager).await?;
AccessToken::down(manager).await?;
User::down(manager).await
}
}
#[derive(DeriveIden)]
pub enum User {
Table,
Id,
CreatedAt,
UpdatedAt,
DeletedAt,
LoginName,
PasswordHash,
}
static IDX_USER_LOGIN_NAME: &str = "idx_user_login_name";
static IDX_USER_CREATED_AT: &str = "idx_user_created_at";
static IDX_USER_UPDATED_AT: &str = "idx_user_updated_at";
static IDX_USER_DELETED_AT: &str = "idx_user_deleted_at";
impl TableMigration for User {
fn table_create_statement() -> TableCreateStatement{
Table::create()
.table(Self::Table)
.if_not_exists()
.col(pk_auto(Self::Id))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::DeletedAt))
.col(string_uniq(Self::LoginName))
.col(string(Self::PasswordHash))
.to_owned()
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_USER_LOGIN_NAME)
.table(Self::Table)
.col(Self::LoginName)
.to_owned(),
Index::create().name(IDX_USER_CREATED_AT)
.table(Self::Table)
.col((Self::CreatedAt, IndexOrder::Desc))
.to_owned(),
Index::create().name(IDX_USER_UPDATED_AT)
.table(Self::Table)
.col((Self::UpdatedAt, IndexOrder::Desc))
.to_owned(),
Index::create().name(IDX_USER_DELETED_AT)
.table(Self::Table)
.col((Self::DeletedAt, IndexOrder::Desc))
.to_owned(),
]
}
fn table_drop_statement() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[derive(DeriveIden)]
pub enum AccessToken{
Table,
Id,
UserId,
CreatedAt,
UpdatedAt,
ExpiredAt,
TokenValue,
Note,
}
static IDX_ACCESS_TOKEN_TOKEN_VALUE: &str = "idx_access_token_token_value";
static IDX_ACCESS_TOKEN_CREATED_AT: &str = "idx_access_token_created_at";
static IDX_ACCESS_TOKEN_UPDATED_AT: &str = "idx_access_token_updated_at";
static IDX_ACCESS_TOKEN_EXPIRED_AT: &str = "idx_access_token_expired_at";
static IDX_ACCESS_TOKEN_USER_ID_CREATED_AT: &str = "idx_access_token_user_id_created_at";
static IDX_ACCESS_TOKEN_USER_ID_UPDATED_AT: &str = "idx_access_token_user_id_updated_at";
static IDX_ACCESS_TOKEN_USER_ID_EXPIRED_AT: &str = "idx_access_token_user_id_expired_at";
static FK_ACCESS_TOKEN_USER: &str = "fk_access_token_user";
impl TableMigration for AccessToken {
fn table_create_statement() -> TableCreateStatement {
Table::create()
.table(Self::Table)
.if_not_exists()
.col(pk_auto(Self::Id))
.col(integer(Self::UserId))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::ExpiredAt))
.col(string(Self::TokenValue))
.col(string(Self::Note))
.foreign_key(ForeignKey::create()
.name(FK_ACCESS_TOKEN_USER)
.from(Self::Table, Self::UserId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
)
.to_owned()
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_ACCESS_TOKEN_CREATED_AT)
.table(Self::Table)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_EXPIRED_AT)
.table(Self::Table)
.col(Self::ExpiredAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_TOKEN_VALUE)
.table(Self::Table)
.col(Self::TokenValue)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_UPDATED_AT)
.table(Self::Table)
.col(Self::UpdatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_EXPIRED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::ExpiredAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::UpdatedAt)
.to_owned(),
]
}
fn table_drop_statement() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[derive(DeriveIden)]
pub enum ProgressCategory {
Table,
UserId,
}
static IDX_PROGRESS_CATEGORY_USER_ID_NAME: &str = "idx_progress_category_user_id_name";
static IDX_PROGRESS_CATEGORY_USER_ID_CREATED_AT: &str = "idx_progress_category_user_id_created_at";
static IDX_PROGRESS_CATEGORY_USER_ID_UPDATED_AT: &str = "idx_progress_category_user_id_updated_at";
static IDX_PROGRESS_CATEGORY_USER_ID_DELETED_AT: &str = "idx_progress_category_user_id_deleted_at";
static FK_PROGRESS_CATEGORY_USER: &str = "fk_progress_category_user";
impl TableMigration for ProgressCategory {
fn table_create_statement() -> TableCreateStatement{
let mut tcs = DefaultProgressCategory::table_create_statement();
tcs.col(integer(Self::UserId));
tcs.foreign_key(ForeignKey::create().name(FK_PROGRESS_CATEGORY_USER).from(Self::Table, Self::UserId)
.to(User::Table, User::Id));
tcs.primary_key(Index::create().name(PK_PROGRESS_CATEGORY).col(Self::UserId).col(DefaultProgressCategory::Id));
tcs
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
[DefaultProgressCategory::index_create_statements(), vec![
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_DELETED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_NAME)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::Name)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::UpdatedAt)
.to_owned(),
]].concat()
}
fn table_drop_statement() -> TableDropStatement {
DefaultProgressCategory::table_drop_statement()
}
}
#[derive(DeriveIden)]
pub enum ProgressEntry {
Table,
UserId,
}
static IDX_PROGRESS_ENTITY_USER_ID_CREATED_AT: &str = "idx_progress_entity_user_id_created_at";
static IDX_PROGRESS_ENTITY_USER_ID_UPDATED_AT: &str = "idx_progress_entity_user_id_updated_at";
static IDX_PROGRESS_ENTITY_USER_ID_DELETED_AT: &str = "idx_progress_entity_user_id_deleted_at";
static IDX_PROGRESS_ENTITY_USER_ID_PROGRESSED_AT: &str = "idx_progress_entity_user_id_progressed_at";
static FK_PROGRESS_ENTITY_PROGRESS_CATEGORY: &str = "fk_progress_entity_progress_category";
static FK_PROGRESS_ENTITY_USER: &str = "fk_progress_entity_user";
impl TableMigration for ProgressEntry {
fn table_create_statement() -> TableCreateStatement{
let mut tcs: TableCreateStatement = DefaultProgressEntry::table_create_statement();
tcs.col(integer(Self::UserId));
tcs.foreign_key(ForeignKey::create()
.name(FK_PROGRESS_ENTITY_USER)
.from(Self::Table, Self::UserId)
.to(User::Table, User::Id)
);
tcs.primary_key(Index::create().name(PK_PROGRESS_ENTITY).col(Self::UserId).col(DefaultProgressEntry::Id));
tcs
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
let mut default = DefaultProgressEntry::index_create_statements();
default.append(&mut vec![
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_DELETED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_PROGRESSED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::ProgressedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::UpdatedAt)
.to_owned(),
]);
default
}
fn table_drop_statement() -> TableDropStatement {
DefaultProgressEntry::table_drop_statement()
}
}

View file

@ -1,6 +0,0 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(progress_pile_migration_server::Migrator).await;
}

7
progress-pile-mobile/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target
.DS_Store
# These are backup files generated by rustfmt
**/*.rs.bk

View file

@ -0,0 +1,17 @@
[package]
name = "progress-pile-mobile"
version = "0.1.0"
authors = ["fluo10 <fluo10.dev@fireturtle.net>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus.workspace = true
progress-pile-simple-ui.workspace = true
[features]
default = ["mobile"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]

View file

@ -0,0 +1,21 @@
[application]
[web.app]
# HTML title tag content
title = "progress-pile-mobile"
# include `assets` in web platform
[web.resource]
# Additional CSS style files
style = []
# Additional JavaScript files
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []

View file

@ -0,0 +1,25 @@
# Development
Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets.
```
project/
├─ assets/ # Any assets that are used by the app should be placed here
├─ src/
│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
```
### Serving Your App
Run the following command in the root of your project to start developing with the default platform:
```bash
dx serve
```
To run for a different platform, use the `--platform platform` flag. E.g.
```bash
dx serve --platform desktop
```

View file

@ -0,0 +1,8 @@
await-holding-invalid-types = [
"generational_box::GenerationalRef",
{ path = "generational_box::GenerationalRef", reason = "Reads should not be held over an await point. This will cause any writes to fail while the await is pending since the read borrow is still active." },
"generational_box::GenerationalRefMut",
{ path = "generational_box::GenerationalRefMut", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
"dioxus_signals::Write",
{ path = "dioxus_signals::Write", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
]

View file

@ -0,0 +1,5 @@
use dioxus::prelude::*;
fn main() {
dioxus::launch(progress_pile_simple_ui::App);
}

View file

@ -1,26 +0,0 @@
[package]
name = "progress-pile-server"
version.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
argon2 = "0.5.3"
async-graphql.workspace = true
axum = "0.8.4"
clap = {workspace = true, features = ["derive"]}
progress-pile-core = {workspace = true, features = ["desktop"]}
progress-pile-migration-server.workspace = true
chrono = {workspace = true}
sea-orm = { workspace = true, features = ["sqlx-postgres"] }
serde.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true
uuid.workspace = true
rand = "0.9.1"
async-graphql-axum = "7.0.16"
[dev-dependencies]
tempfile.workspace = true

View file

@ -1,45 +0,0 @@
use argon2::{
Argon2,
PasswordHasher,
PasswordVerifier,
password_hash::{
Salt,
SaltString,
PasswordHash,
rand_core::OsRng,
},
};
use chrono::format::parse;
use crate::error::Error;
use tokio::sync::OnceCell;
pub fn try_hash_password(password: &str) -> Result<String, Error> {
let mut rng = OsRng::default();
let salt_string= SaltString::generate(&mut rng);
let salt = salt_string.as_salt();
let hash = Argon2::default().hash_password(password.as_bytes(), salt).or(Err(Error::PasswordHash("Hashing password with salt".to_string())))?;
Ok(hash.to_string())
}
pub fn try_verify_password(password: &str, password_hash: &str) -> Result<bool, Error> {
let parsed_hash = PasswordHash::new(password_hash).or(Err(Error::PasswordHash("Failed to parse password hash string".to_string())))?;
match Argon2::default().verify_password(password.as_bytes(), &parsed_hash) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
#[cfg(test)]
mod tests {
use argon2::password_hash::rand_core::OsRng;
use super::*;
#[test]
fn test_password() {
let valid_password = "valid";
let invalid_password = "invalid";
let hash = try_hash_password(valid_password).unwrap();
assert!(try_verify_password(valid_password, &hash).unwrap());
assert!(!try_verify_password(invalid_password, &hash).unwrap());
}
}

View file

@ -1,17 +0,0 @@
use crate::config::PartialServerConfig;
use clap::Parser;
use std::{
net::IpAddr,
path::PathBuf,
};
use crate::config::ServerConfig;
#[derive(Clone, Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[arg(short, long)]
pub config_file: Option<PathBuf>,
}

View file

@ -1,89 +0,0 @@
use std::time::Duration;
use clap::Args;
use sea_orm::ConnectOptions;
use serde::Deserialize;
use crate::error::Error;
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: Option<u32>,
pub min_connections: Option<u32>,
pub connect_timeout: Option<Duration>,
pub acquire_timeout: Option<Duration>,
pub idle_timeout: Option<Duration>,
pub max_lifetime: Option<Duration>,
pub sqlx_logging: bool,
}
impl Into<ConnectOptions> for &DatabaseConfig {
fn into(self) -> ConnectOptions {
let mut opt = ConnectOptions::new(&self.url);
if let Some(x) = self.max_connections {
opt.max_connections(x);
}
if let Some(x) = self.min_connections {
opt.min_connections(x);
}
if let Some(x) = self.connect_timeout {
opt.connect_timeout(x);
}
if let Some(x) = self.acquire_timeout {
opt.acquire_timeout(x);
}
if let Some(x) = self.idle_timeout {
opt.idle_timeout(x);
}
if let Some(x) = self.max_lifetime {
opt.max_lifetime(x);
}
opt.sqlx_logging(self.sqlx_logging);
opt
}
}
impl TryFrom<PartialDatabaseConfig> for DatabaseConfig{
type Error = Error;
fn try_from(p: PartialDatabaseConfig) -> Result<DatabaseConfig, Self::Error> {
Ok(DatabaseConfig{
url: p.url.ok_or(Error::MissingConfig("url".to_string()))?,
max_connections: p.max_connections,
min_connections: p.min_connections,
connect_timeout: p.connect_timeout,
acquire_timeout: p.acquire_timeout,
idle_timeout: p.idle_timeout,
max_lifetime: p.max_lifetime,
sqlx_logging: p.sqlx_logging.ok_or(Error::MissingConfig("sqlx_logging".to_string()))?
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[derive(Args)]
pub struct PartialDatabaseConfig {
#[arg(long)]
pub url: Option<String>,
#[arg(long)]
pub max_connections: Option<u32>,
#[arg(long)]
pub min_connections: Option<u32>,
#[arg(long, value_parser = parse_duration )]
pub connect_timeout: Option<Duration>,
#[arg(long, value_parser = parse_duration )]
pub acquire_timeout: Option<Duration>,
#[arg(long, value_parser = parse_duration )]
pub idle_timeout: Option<Duration>,
#[arg(long, value_parser = parse_duration )]
pub max_lifetime: Option<Duration>,
#[arg(long)]
pub sqlx_logging: Option<bool>
}
fn parse_duration(arg: &str) -> Result<std::time::Duration, Error> {
let seconds = arg.parse()?;
Ok(std::time::Duration::from_secs(seconds))
}

View file

@ -1,10 +0,0 @@
mod database;
pub use database::{
DatabaseConfig,
PartialDatabaseConfig
};
pub struct ServerConfig {}
pub struct PartialServerConfig {}

View file

@ -1,55 +0,0 @@
use core::time;
use async_graphql::*;
use chrono::Local;
use sea_orm::entity::{
Set,
prelude::*
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel,)]
#[sea_orm(table_name = "access_token")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(indexed)]
pub user_id: i32,
#[sea_orm(indexed)]
pub token_value: String,
pub note: String,
#[sea_orm(indexed)]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub expired_at: Option<DateTimeWithTimeZone>,
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModel {
pub fn new() -> Self {
let timestamp = Local::now().fixed_offset();
Self{
note: Set("".to_string()),
created_at: Set(timestamp),
updated_at: Set(timestamp),
..Default::default()
}
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,91 +0,0 @@
mod access_token;
mod progress_category;
mod progress_entry;
mod user;
pub use access_token::{
ActiveModel as AccessTokenActiveModel,
Column as AccessTokenColumn,
Entity as AccessTokenEntity,
Model as AccessTokenModel,
};
pub use progress_category::{
ActiveModel as ProgressCategoryActiveModel,
Column as ProgressCategoryColumn,
Entity as ProgressCategoryEntity,
Model as ProgressCategoryModel,
};
pub use progress_entry::{
ActiveModel as ProgressEntryActiveModel,
Column as ProgressEntryColumn,
Entity as ProgressEntryEntity,
Model as ProgressEntryModel,
};
pub use user::{
ActiveModel as UserActiveModel,
Column as UserColumn,
Entity as UserEntity,
Model as UserModel,
};
pub use progress_pile_core::entity::*;
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use chrono::{offset, FixedOffset, Local, TimeZone};
use sea_orm::{entity::*, query::*, ConnectOptions, Database};
use crate::{entity::*, global::GLOBAL};
#[tokio::test]
async fn check_database_connection() {
let db = GLOBAL.get_or_init_temporary_database().await;
assert!(db.ping().await.is_ok());
}
#[tokio::test]
async fn check_insert_entity() {
let db = GLOBAL.get_or_init_temporary_database().await;
let timestamp = Local::now().fixed_offset();
let user = UserActiveModel{
login_name: Set("admin".to_owned()),
password_hash: Set("admin".to_owned()),
created_at: Set(timestamp),
updated_at: Set(timestamp),
..UserActiveModel::new()
}.insert(db).await.unwrap();
let access_token = AccessTokenActiveModel{
user_id: Set(user.id),
..Default::default()
}.insert(db).await.unwrap();
let progress_category = ProgressCategoryActiveModel{
user_id: Set(user.id),
name: Set("test_category".to_string()),
..Default::default()
}.insert(db)
.await.unwrap();
ProgressEntryActiveModel {
user_id: Set(user.id),
progress_category_id: Set(progress_category.id),
..Default::default()
}.insert(db)
.await.unwrap();
}
}

View file

@ -1,58 +0,0 @@
use async_graphql::*;
use chrono::Local;
use sea_orm::{entity::prelude::*, ActiveValue::Set};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel,)]
#[sea_orm(table_name = "progress_category")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
#[sea_orm(primary_key, auto_increment = false)]
pub user_id: i32,
#[sea_orm(indexed)]
pub name: String,
#[sea_orm(indexed)]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub deleted_at: Option<DateTimeWithTimeZone>,
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(has_many = "super::progress_entry::Entity")]
ProgressEntry,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::progress_entry::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProgressEntry.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {
pub fn new() -> Self {
let timestamp: DateTimeWithTimeZone = Local::now().fixed_offset();
Self{
id: Set(Uuid::new_v4()),
created_at: Set(timestamp),
updated_at: Set(timestamp),
..Default::default()
}
}
}

View file

@ -1,71 +0,0 @@
use async_graphql::*;
use chrono::Local;
use sea_orm::{entity::prelude::*, ActiveValue::Set};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "progress_entry")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
#[sea_orm(primary_key, auto_increment = false)]
pub user_id: i32,
#[sea_orm(indexed)]
pub progress_category_id: Uuid,
#[sea_orm(indexed)]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub deleted_at: Option<DateTimeWithTimeZone>,
#[sea_orm(indexed)]
pub progressed_at: DateTimeWithTimeZone,
pub quantity: i32,
pub note: String,
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::progress_category::Entity",
from = "Column::ProgressCategoryId",
to = "super::progress_category::Column::Id"
)]
ProgressCategory,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::progress_category::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProgressCategory.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {
pub fn new() -> Self {
let timestamp: DateTimeWithTimeZone = Local::now().fixed_offset();
Self{
id: Set(Uuid::new_v4()),
created_at: Set(timestamp),
updated_at: Set(timestamp),
progressed_at: Set(timestamp),
quantity: Set(1),
note: Set("".to_string()),
..Default::default()
}
}
}

View file

@ -1,63 +0,0 @@
use async_graphql::SimpleObject;
use chrono::{DateTime, FixedOffset, Local,};
use sea_orm::{entity::prelude::*, ActiveValue::Set};
use serde::{Deserialize, Serialize};
use crate::error::Error;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel,)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique, indexed)]
pub login_name: String,
pub password_hash: String,
#[sea_orm(indexed)]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub deleted_at: Option<DateTimeWithTimeZone>,
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(has_many = "super::access_token::Entity")]
AccessToken,
#[sea_orm(has_many = "super::progress_category::Entity")]
ProgressCategory,
#[sea_orm(has_many = "super::progress_entry::Entity")]
ProgressEntry,
}
impl Related<super::access_token::Entity> for Model {
fn to() -> RelationDef {
Relation::AccessToken.def()
}
}
impl Related<super::progress_category::Entity> for Model {
fn to() -> RelationDef {
Relation::ProgressCategory.def()
}
}
impl Related<super::progress_entry::Entity> for Model {
fn to() -> RelationDef {
Relation::ProgressEntry.def()
}
}
impl ActiveModel {
pub fn new() -> Self {
let timestamp = Local::now().fixed_offset();
Self {
created_at: Set(timestamp),
updated_at: Set(timestamp),
..Default::default()
}
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,57 +0,0 @@
use progress_pile_migration_server::{Migrator, MigratorTrait};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use crate::error::Error;
use tokio::sync::OnceCell;
use progress_pile_core::global::GlobalDatabase;
use super::Global;
impl GlobalDatabase for Global {
fn get_database(&self) -> Option<&DatabaseConnection> {
self.database.get()
}
async fn get_or_try_init_database(&self) -> Result<&DatabaseConnection, Error> {
todo!()
}
async fn get_or_try_init_database_with_connect_options<T>(&self, options: T) -> Result<&DatabaseConnection, Error> where
T: Into<ConnectOptions>,
{
Ok(self.database.get_or_try_init(|| async {
let db = Database::connect(options).await?;
Migrator::up(&db, None).await?;
Ok::<DatabaseConnection, Error>(db)
}).await?)
}
}
#[cfg(test)]
pub mod tests {
use std::sync::LazyLock;
use crate::global::GLOBAL;
use super::*;
pub static TEST_DATABASE_URL: LazyLock<String> = LazyLock::new(|| {
let mut temp_path = tempfile::NamedTempFile::new().unwrap().into_temp_path();
temp_path.disable_cleanup(true);
let url = "sqlite://".to_string() + temp_path.as_os_str().to_str().unwrap() + "?mode=rwc";
println!("{}", &url);
url
});
impl Global {
pub async fn get_or_init_temporary_database(&self) -> &DatabaseConnection {
self.get_or_try_init_database_with_connect_options(&*TEST_DATABASE_URL).await.unwrap()
}
}
#[tokio::test]
async fn connect_database () {
let db = GLOBAL.get_or_init_temporary_database().await;
assert!(db.ping().await.is_ok());
}
}

View file

@ -1,15 +0,0 @@
use crate::config::ServerConfig;
use sea_orm::DatabaseConnection;
use tokio::sync::OnceCell;
mod database;
pub static GLOBAL: Global = Global{
config: OnceCell::const_new(),
database: OnceCell::const_new(),
};
pub struct Global {
config: OnceCell<ServerConfig>,
database: OnceCell<DatabaseConnection>,
}

View file

@ -1,20 +0,0 @@
mod mutation;
mod query;
pub use progress_pile_core::graphql::*;
pub use mutation::Mutation;
pub use query::Query;
use async_graphql::{EmptySubscription, Schema};
use async_graphql_axum::GraphQL;
use axum::{routing::get, Router};
pub fn build_schema() -> Schema<Query, Mutation, EmptySubscription>{
Schema::build(Query, Mutation, EmptySubscription).finish()
}
pub fn build_service() -> GraphQL<Schema<Query, Mutation, EmptySubscription>> {
GraphQL::new(build_schema())
}

View file

@ -1,15 +0,0 @@
use async_graphql::*;
use crate::{auth::try_hash_password, entity::UserActiveModel};
pub struct Mutation;
#[Object]
impl Mutation {
async fn login(&self, username:String, password: String) -> Result<String> {
todo!()
}
async fn create_user(&self, username:String, password: String) -> Result<String> {
todo!()
}
}

View file

@ -1,22 +0,0 @@
use async_graphql::{
*,
http::GraphiQLSource,
};
use axum::{
response::{Html, IntoResponse},
routing::get,
Router,
};
use crate::{entity::{UserEntity, UserModel},};
pub struct Query;
#[Object]
impl Query {
pub async fn user(&self, user_name: String) -> Result<Option<String>> {
todo!()
}
pub async fn users(&self) -> Result<Vec<String>> {
todo!()
}
}

View file

@ -1,23 +0,0 @@
pub mod cli;
pub mod auth;
pub mod config;
pub mod entity;
pub mod global;
pub mod graphql;
pub use progress_pile_core::error;
pub use cli::Cli;
use async_graphql::{EmptySubscription, Schema};
use async_graphql_axum::{
GraphQL,
};
use axum::{routing::get, Router};
use crate::graphql::build_service;
pub fn build_app() -> Router {
let router = Router::new()
.route_service("/graphql", build_service());
router
}

View file

@ -1,18 +0,0 @@
use async_graphql::{http::{playground_source, GraphQLPlaygroundConfig}, *};
use async_graphql_axum::GraphQL;
use progress_pile_server::{build_app, cli::Cli};
use axum::{response::{Html, IntoResponse}, routing::get, Router};
use clap::Parser;
#[tokio::main]
async fn main() {
axum::serve(
tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(),
crate::build_app()
).await.unwrap()
}

View file

@ -0,0 +1,7 @@
[package]
name = "progress-pile-simple-ui"
version = "0.1.0"
edition = "2024"
[dependencies]
dioxus.workspace = true

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,46 @@
/* App-wide styling */
body {
background-color: #0f1116;
color: #ffffff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
}
#hero {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#links {
width: 400px;
text-align: left;
font-size: x-large;
color: white;
display: flex;
flex-direction: column;
}
#links a {
color: white;
text-decoration: none;
margin-top: 20px;
margin: 10px 0px;
border: white 1px solid;
border-radius: 5px;
padding: 10px;
}
#links a:hover {
background-color: #1f1f1f;
cursor: pointer;
}
#header {
max-width: 1200px;
}

View file

@ -0,0 +1,33 @@
use dioxus::prelude::*;
const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/main.css");
const HEADER_SVG: Asset = asset!("/assets/header.svg");
#[component]
pub fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
Hero {}
}
}
#[component]
pub fn Hero() -> Element {
rsx! {
div {
id: "hero",
img { src: HEADER_SVG, id: "header" }
div { id: "links",
a { href: "https://dioxuslabs.com/learn/0.6/", "📚 Learn Dioxus" }
a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" }
a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" }
a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" }
a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", "💫 VSCode Extension" }
a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" }
}
}
}
}