Compare commits

9 Commits

Author SHA1 Message Date
Lukas Wölfer
f91303b92c chore: bump version to v0.1.2
All checks were successful
Release / build_release (push) Successful in 3m31s
Rust / build_and_test (push) Successful in 24s
2026-04-11 15:07:55 +02:00
Lukas Wölfer
aff3367623 chore: update git cliff version building 2026-04-11 15:07:49 +02:00
Lukas Wölfer
1736aeb122 chore: bump version to 0.1.1
All checks were successful
Rust / build_and_test (push) Successful in 25s
2026-04-11 15:02:11 +02:00
Lukas Wölfer
37d0c33633 chore: ignore test files
All checks were successful
Rust / build_and_test (push) Successful in 24s
2026-04-11 15:01:26 +02:00
Lukas Wölfer
3bfe779425 chore: bump version to 0.1.0
Some checks failed
Rust / build_and_test (push) Failing after 25s
2026-04-11 14:55:08 +02:00
Lukas Wölfer
395099d0ee chore: add static folder to build artifacts 2026-04-11 14:52:52 +02:00
Lukas Wölfer
b922a610e6 feat: usability 2026-04-11 14:48:43 +02:00
Lukas Wölfer
7010aee5b2 feat: automatically run migrations on database file
Some checks failed
Rust / build_and_test (push) Failing after 1m44s
2026-04-11 14:36:42 +02:00
Lukas Wölfer
614f044160 fix: database creation 2026-04-11 14:33:36 +02:00
11 changed files with 123 additions and 71 deletions

View File

@@ -3,7 +3,7 @@ name: Release
on:
push:
tags:
- 'v*.*.*'
- "v*.*.*"
jobs:
build_release:
@@ -22,6 +22,11 @@ jobs:
- name: Build release
run: |
cargo build --release
- name: Package frontend
run: |
zip -r static.zip static
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
id: git-cliff
@@ -31,8 +36,10 @@ jobs:
github_token: ""
env:
OUTPUT: CHANGELOG.md
- uses: akkuman/gitea-release-action@v1
with:
files: |-
target/release/weight_tracker
body: Release build for weight_tracker
static.zip
body: ${{ steps.git-cliff.outputs.content }}

2
Cargo.lock generated
View File

@@ -3259,7 +3259,7 @@ dependencies = [
[[package]]
name = "weight_tracker"
version = "0.1.0"
version = "0.1.2"
dependencies = [
"anyhow",
"askama",

View File

@@ -1,6 +1,6 @@
[package]
name = "weight_tracker"
version = "0.1.0"
version = "0.1.2"
edition = "2024"
[dependencies]

View File

@@ -2,8 +2,14 @@
set -euo pipefail
VERSION="$(git cliff --bumped-version)"
VERSION_CLEAN="${VERSION#v}"
VERSION_CLEAN="$(git cliff --bumped-version)"
if [[ ! $VERSION_CLEAN =~ ^[0-9] ]]; then
echo "Error: VERSION_CLEAN does not start with a number"
exit 1
fi
VERSION="v${VERSION_CLEAN}"
sed -i "s/^version = \".*\"/version = \"${VERSION_CLEAN}\"/" Cargo.toml
cargo check
@@ -14,7 +20,7 @@ if [ "${CONFIRM}" != "Y" ]; then
exit 1
fi
git commit -am "chore: bump version to ${VERSION}"
git commit --allow-empty -am "chore: bump version to ${VERSION}"
git tag -am "Version ${VERSION}" "${VERSION}"
echo Press Y to push commit and tag

View File

@@ -1,6 +1,9 @@
use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl, basic::BasicClient};
use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::OidcClient;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub oidc: OidcConfig,
@@ -18,6 +21,24 @@ pub struct OidcConfig {
pub redirect_url: String,
}
impl OidcConfig {
pub fn to_client(
&self,
) -> OidcClient {
let client_id = ClientId::new(self.client_id.clone());
let client_secret = ClientSecret::new(self.client_secret.clone());
let auth_url = AuthUrl::new(self.auth_url.clone()).unwrap();
let token_url = TokenUrl::new(self.token_url.clone()).unwrap();
let redirect_url = RedirectUrl::new(self.redirect_url.clone()).unwrap();
BasicClient::new(client_id)
.set_client_secret(client_secret)
.set_auth_uri(auth_url)
.set_token_uri(token_url)
.set_redirect_uri(redirect_url)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,

View File

@@ -3,8 +3,11 @@ use axum::response::{Html, Redirect};
use axum::{Form, extract::Query, extract::State};
use axum_session::Session;
use axum_session_sqlx::SessionSqlitePool;
use oauth2::{AuthorizationCode, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse, HttpRequest, HttpResponse};
use oauth2::http;
use oauth2::{
AuthorizationCode, CsrfToken, HttpRequest, HttpResponse, PkceCodeChallenge, PkceCodeVerifier,
Scope, TokenResponse,
};
use reqwest::Client;
use serde::Deserialize;
@@ -17,6 +20,7 @@ pub struct IndexTemplate {
#[derive(Deserialize)]
pub struct WeightForm {
date: String,
time: String,
weight: f64,
}
@@ -28,7 +32,10 @@ pub async fn index(State(state): State<crate::AppState>) -> Html<String> {
Html(template.render().unwrap())
}
pub async fn login(State(state): State<crate::AppState>, session: Session<SessionSqlitePool>) -> Redirect {
pub async fn login(
State(state): State<crate::AppState>,
session: Session<SessionSqlitePool>,
) -> Redirect {
// Generate PKCE challenge
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
@@ -102,9 +109,7 @@ pub async fn callback(
}
// Async HTTP client for oauth2
async fn async_http_client(
req: HttpRequest,
) -> Result<HttpResponse, reqwest::Error> {
async fn async_http_client(req: HttpRequest) -> Result<HttpResponse, reqwest::Error> {
let client = Client::new();
// Convert http::Method to reqwest::Method
@@ -130,8 +135,7 @@ async fn async_http_client(
let body = response.bytes().await?;
// Construct an http::Response
let mut http_response = http::Response::builder()
.status(status_code);
let mut http_response = http::Response::builder().status(status_code);
for (k, v) in headers.iter() {
http_response = http_response.header(k, v);
@@ -147,7 +151,8 @@ pub async fn input_post(
) -> Result<Html<String>, Redirect> {
// Check if user is authenticated
if let Some(user_id) = session.get::<String>("user_id") {
super::models::insert_weight(&state.pool, &user_id, &form.date, form.weight)
let datetime = format!("{}T{}", form.date, form.time);
super::models::insert_weight(&state.pool, &user_id, &datetime, form.weight)
.await
.unwrap();
let weights = super::models::get_all_weights(&state.pool)
@@ -157,7 +162,7 @@ pub async fn input_post(
for weight in weights {
html.push_str(&format!(
"<p>{}: {} kg by {}</p>\n",
weight.date, weight.weight, weight.user_id
weight.date, weight.weight, &user_id
));
}
Ok(Html(html))

View File

@@ -11,10 +11,7 @@ use axum_session_sqlx::SessionSqlitePool;
use sqlx::SqlitePool;
use tower_http::services::ServeDir;
#[derive(Clone)]
pub struct AppState {
pub pool: SqlitePool,
pub oidc_client: oauth2::Client<
pub type OidcClient = oauth2::Client<
oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>,
oauth2::StandardTokenResponse<oauth2::EmptyExtraTokenFields, oauth2::basic::BasicTokenType>,
oauth2::StandardTokenIntrospectionResponse<
@@ -28,7 +25,12 @@ pub struct AppState {
oauth2::EndpointNotSet,
oauth2::EndpointNotSet,
oauth2::EndpointSet,
>,
>;
#[derive(Clone)]
pub struct AppState {
pub pool: SqlitePool,
pub oidc_client: OidcClient,
}
pub async fn create_app(state: AppState, session_secret: Vec<u8>, pool: SqlitePool) -> Router {

View File

@@ -1,11 +1,10 @@
use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl, basic::BasicClient};
use std::str::FromStr;
use sqlx::SqlitePool;
use weight_tracker::{AppState, create_app, config::Config};
use weight_tracker::{AppState, config::Config, create_app};
#[tokio::main]
async fn main() {
// Load configuration from config.toml or environment variables
// config-rs automatically merges file + environment + defaults
let config = Config::load("config.toml").unwrap_or_else(|e| {
eprintln!("Failed to load configuration: {}", e);
eprintln!("Using default values. Set environment variables prefixed with WEIGHT_TRACKER_ to override.");
@@ -13,26 +12,20 @@ async fn main() {
});
// Set up database
let pool = SqlitePool::connect(&config.database.url)
let pool = SqlitePool::connect_with(
sqlx::sqlite::SqliteConnectOptions::from_str(&config.database.url)
.expect("Could not parse database URL")
.create_if_missing(true),
)
.await
.expect("Failed to connect to database");
sqlx::migrate!()
.run(&pool)
.await
.expect("Failed to connect to database");
.expect("Could not run database migrations");
// Set up OIDC client
let client_id = ClientId::new(config.oidc.client_id);
let client_secret = ClientSecret::new(config.oidc.client_secret);
let auth_url = AuthUrl::new(config.oidc.auth_url)
.unwrap();
let token_url = TokenUrl::new(config.oidc.token_url)
.unwrap();
let redirect_url = RedirectUrl::new(config.oidc.redirect_url)
.unwrap();
let oidc_client = BasicClient::new(client_id)
.set_client_secret(client_secret)
.set_auth_uri(auth_url)
.set_token_uri(token_url)
// Set the URL the user will be redirected to after the authorization process.
.set_redirect_uri(redirect_url);
let oidc_client = config.oidc.to_client();
let secret = config.session.secret.as_bytes().to_vec();
@@ -44,9 +37,7 @@ async fn main() {
let app = create_app(app_state, secret, pool).await;
let addr = format!("{}:{}", config.server.host, config.server.port);
let listener = tokio::net::TcpListener::bind(&addr)
.await
.unwrap();
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}

View File

@@ -1,10 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>Weight Tracker</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script>
function fillCurrentDateTime() {
// Fill date and time with current values
const now = new Date();
// Format date as YYYY-MM-DD
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
document.getElementById('date').value = `${year}-${month}-${day}`;
// Format time as HH:MM
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
document.getElementById('time').value = `${hours}:${minutes}`;
// Fill weight with the latest weight from the list
const weightElements = document.querySelectorAll('#weights p');
if (weightElements.length > 0) {
const latestWeightText = weightElements[0].textContent;
// Extract weight value from format "YYYY-MM-DD: XX.X kg by username"
const match = latestWeightText.match(/(\d+\.?\d*)\s*kg/);
if (match) {
document.getElementById('weight').value = match[1];
}
}
}
function showInputDialog() {
fillCurrentDateTime();
document.getElementById('inputDialog').showModal();
}
</script>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1>Weight Tracker</h1>
<div id="weights">
@@ -12,16 +48,20 @@
<p>{{ weight.date }}: {{ weight.weight }} kg by {{ weight.user_id }}</p>
{% endfor %}
</div>
<button onclick="document.getElementById('inputDialog').showModal()">Add Weight</button>
<button onclick="showInputDialog()">Add Weight</button>
<dialog id="inputDialog">
<h1>Add Weight</h1>
<form hx-post="/input" hx-target="#weights" hx-swap="innerHTML" hx-on:htmx:after-request="document.getElementById('inputDialog').close()">
<form hx-post="/input" hx-target="#weights" hx-swap="innerHTML"
hx-on:htmx:after-request="document.getElementById('inputDialog').close()">
<label for="date">Date:</label>
<input type="date" id="date" name="date" required><br>
<label for="time">Time:</label>
<input type="time" id="time" name="time" required><br>
<label for="weight">Weight (kg):</label>
<input type="number" step="0.1" id="weight" name="weight" required><br>
<button type="submit">Submit</button>
</form>
</dialog>
</body>
</html>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Add Weight</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1>Add Weight</h1>
<form hx-post="/input" hx-target="#result" hx-swap="innerHTML">
<label for="date">Date:</label>
<input type="date" id="date" name="date" required><br>
<label for="weight">Weight (kg):</label>
<input type="number" step="0.1" id="weight" name="weight" required><br>
<button type="submit">Submit</button>
</form>
<div id="result"></div>
<a href="/">Back to Tracker</a>
</body>
</html>