Compare commits

...

17 Commits

Author SHA1 Message Date
phoenix d4faa7976e Update icarus_models (#38)
Reviewed-on: #38
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-05-28 23:26:17 +00:00
phoenix ed77cab700 Environment and docker changes (#37)
Reviewed-on: #37
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-05-27 20:55:53 +00:00
phoenix 2c30abb5c6 Updated gitignore (#36)
Reviewed-on: #36
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-05-27 20:23:50 +00:00
phoenix 1817ab01d6 Test fix (#35)
Reviewed-on: #35
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-27 18:06:13 +00:00
phoenix 31be156be3 Added ssh default for building docker image (#33)
Reviewed-on: #33
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-27 16:31:11 +00:00
phoenix fc6b66f2e6 Docker changes (#31)
Reviewed-on: #31
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-13 18:38:38 +00:00
phoenix 6dec9942cc Version bump (#29)
Reviewed-on: #29
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-12 00:16:34 +00:00
phoenix a855db9ecc Workflow changes (#30)
Reviewed-on: #30
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-11 23:57:37 +00:00
phoenix 17af1a00c0 Add docker (#28)
Reviewed-on: #28
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-11 01:07:20 +00:00
phoenix 50e735e1a9 Update last_login of user (#26)
Reviewed-on: #26
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-11 01:01:18 +00:00
phoenix f6cf968f86 main merge (#25)
Reviewed-on: #25
2025-04-10 22:46:29 +00:00
phoenix 70a547ca94 Next release (#23)
Reviewed-on: #23
2025-04-08 23:42:04 +00:00
phoenix 89c89a5524 Login endpoint bug fix (#24)
Reviewed-on: #24
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-07 20:15:58 +00:00
phoenix a58b0cb40b Changes to token (#21)
Reviewed-on: #21
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-04-07 17:35:47 +00:00
phoenix f601442f0e Release with login functionality (#19)
Reviewed-on: #19
2025-04-07 02:04:46 +00:00
phoenix 2229d98ab6 Next release (#15)
Reviewed-on: #15
2025-04-05 19:41:15 +00:00
phoenix 88f45645b3 devel (#5)
Reviewed-on: #5
2025-03-30 18:07:12 +00:00
15 changed files with 310 additions and 88 deletions
+21
View File
@@ -0,0 +1,21 @@
# Ignore build artifacts
target/
pkg/
# Ignore git directory
.git/
.gitea/
# Ignore environment files (configure via docker-compose instead)
.env*
# Ignore IDE/editor specific files
.idea/
.vscode/
# Ignore OS specific files
*.DS_Store
# Add any other files/directories you don't need in the image
# e.g., logs/, tmp/
+6
View File
@@ -0,0 +1,6 @@
SECRET_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
POSTGRES_AUTH_USER=icarus_op
POSTGRES_AUTH_PASSWORD=password
POSTGRES_AUTH_DB=icarus_auth_db
POSTGRES_AUTH_HOST=auth_db
DATABASE_URL=postgresql://${POSTGRES_AUTH_USER}:${POSTGRES_AUTH_PASSWORD}@${POSTGRES_AUTH_HOST}:5432/${POSTGRES_AUTH_DB}
+6 -2
View File
@@ -1,2 +1,6 @@
DATABASE_URL=postgres://username:password@localhost/database_name
SECRET_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
SECRET_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
POSTGRES_AUTH_USER=icarus_op_test
POSTGRES_AUTH_PASSWORD=password
POSTGRES_AUTH_DB=icarus_auth_test_db
POSTGRES_AUTH_HOST=localhost
DATABASE_URL=postgresql://${POSTGRES_AUTH_USER}:${POSTGRES_AUTH_PASSWORD}@${POSTGRES_AUTH_HOST}:5432/${POSTGRES_AUTH_DB}
+1 -5
View File
@@ -4,8 +4,6 @@ on:
push:
branches:
- devel
tags:
- 'v*' # Trigger on tags matching v*
jobs:
release:
@@ -52,6 +50,4 @@ jobs:
body: |
Release of version ${{ steps.version.outputs.project_tag_release }}
# draft: false
# prerelease: ${{ startsWith(github.ref, 'v') == false }} # prerelease if not a valid release tag
# prerelease: ${{ startsWith(github.ref, 'v') == false }} # prerelease if not a valid release tag
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "icarus_auth"
version = "0.3.0"
version = "0.3.4"
edition = "2024"
rust-version = "1.86"
@@ -19,7 +19,7 @@ argon2 = { version = "0.5.3", features = ["std"] } # Use the latest 0.5.x versio
rand = { version = "0.9" }
time = { version = "0.3.41", features = ["macros", "serde"] }
josekit = { version = "0.10.1" }
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.4.1" }
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.4.3" }
[dev-dependencies]
http-body-util = { version = "0.1.3" }
+71
View File
@@ -0,0 +1,71 @@
# Stage 1: Build the application
# Use a specific Rust version for reproducibility. Choose one that matches your development environment.
# Using slim variant for smaller base image
FROM rust:1.86 as builder
# Set the working directory inside the container
WORKDIR /usr/src/app
# Install build dependencies if needed (e.g., for certain crates like sqlx with native TLS)
# RUN apt-get update && apt-get install -y pkg-config libssl-dev
# Install build dependencies if needed (e.g., git for cloning)
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl3 \
ca-certificates \
openssh-client git \
&& rm -rf /var/lib/apt/lists/*
# << --- ADD HOST KEY HERE --- >>
# Replace 'yourgithost.com' with the actual hostname (e.g., github.com)
RUN mkdir -p -m 0700 ~/.ssh && \
ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
# Copy Cargo manifests
COPY Cargo.toml Cargo.lock ./
# Build *only* dependencies to leverage Docker cache
# This dummy build caches dependencies as a separate layer
RUN --mount=type=ssh mkdir src && \
echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \
cargo build --release --quiet && \
rm -rf src target/release/deps/icarus_auth* # Clean up dummy build artifacts (replace icarus_auth)
# Copy the actual source code
COPY src ./src
# If you have other directories like `templates` or `static`, copy them too
COPY .env ./.env
COPY migrations ./migrations
# << --- SSH MOUNT ADDED HERE --- >>
# Build *only* dependencies to leverage Docker cache
# This dummy build caches dependencies as a separate layer
# Mount the SSH agent socket for this command
RUN --mount=type=ssh \
cargo build --release --quiet
# Stage 2: Create the final, smaller runtime image
# Use a minimal base image like debian-slim or even distroless for security/size
FROM ubuntu:24.04
# Install runtime dependencies if needed (e.g., SSL certificates)
RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /usr/local/bin
# Copy the compiled binary from the builder stage
# Replace 'icarus_auth' with the actual name of your binary (usually the crate name)
COPY --from=builder /usr/src/app/target/release/icarus_auth .
# Copy other necessary files like .env (if used for runtime config) or static assets
# It's generally better to configure via environment variables in Docker though
COPY --from=builder /usr/src/app/.env .
COPY --from=builder /usr/src/app/migrations ./migrations
# Expose the port your Axum app listens on (e.g., 3000 or 8000)
EXPOSE 3000
# Set the command to run your application
# Ensure this matches the binary name copied above
CMD ["./icarus_auth"]
+26
View File
@@ -0,0 +1,26 @@
# Getting Started
Copy the `.env.sample` file to `.env` and ensure that the variables are populated. This project
can be used with regular hosting or with docker. For the sake of getting up to speed quickly,
Docker will be covered. Make sure docker is running and your ssh identity has been loaded.
Build image
```
docker compose build
```
Start images
```
docker compose up -d --force-recreate
```
Bring it down
```
docker compose down -v
```
Pruning
```
docker system prune -a
```
+45
View File
@@ -0,0 +1,45 @@
version: '3.8' # Use a recent version
services:
# Your Rust Application Service
auth_api:
build: # Tells docker-compose to build the Dockerfile in the current directory
context: .
ssh: ["default"] # Uses host's SSH agent
container_name: icarus_auth # Optional: Give the container a specific name
ports:
# Map host port 8000 to container port 3000 (adjust as needed)
- "8000:3000"
env_file:
- .env
depends_on:
auth_db:
condition: service_healthy # Wait for the DB to be healthy before starting the app
restart: unless-stopped # Optional: Restart policy
# PostgreSQL Database Service
auth_db:
image: postgres:17.4-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: icarus_auth_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_AUTH_USER:-icarus_op}
POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_AUTH_DB:-icarus_auth_db}
volumes:
# Persist database data using a named volume
- postgres_data:/var/lib/postgresql/data
ports: []
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always # Optional: Restart policy
# Define the named volume for data persistence
volumes:
postgres_data:
driver: local # Use the default local driver
+34 -27
View File
@@ -1,30 +1,37 @@
use axum::{Extension, Json, http::StatusCode};
pub mod response {
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct TestResult {
message: String,
}
// basic handler that responds with a static string
pub async fn root() -> &'static str {
"Hello, World!"
}
pub async fn db_ping(Extension(pool): Extension<sqlx::PgPool>) -> (StatusCode, Json<TestResult>) {
match sqlx::query("SELECT 1").execute(&pool).await {
Ok(_) => {
let tr = TestResult {
message: String::from("This works"),
};
(StatusCode::OK, Json(tr))
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(TestResult {
message: e.to_string(),
}),
),
#[derive(Deserialize, Serialize)]
pub struct TestResult {
pub message: String,
}
}
pub mod endpoint {
use super::*;
use axum::{Extension, Json, http::StatusCode};
// basic handler that responds with a static string
pub async fn root() -> &'static str {
"Hello, World!"
}
pub async fn db_ping(
Extension(pool): Extension<sqlx::PgPool>,
) -> (StatusCode, Json<response::TestResult>) {
match sqlx::query("SELECT 1").execute(&pool).await {
Ok(_) => {
let tr = response::TestResult {
message: String::from("This works"),
};
(StatusCode::OK, Json(tr))
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(response::TestResult {
message: e.to_string(),
}),
),
}
}
}
+7 -26
View File
@@ -42,44 +42,25 @@ pub mod endpoint {
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
Json(payload): Json<request::Request>,
) -> (StatusCode, Json<response::Response>) {
let usr = icarus_models::user::User {
username: payload.username,
password: payload.password,
..Default::default()
};
// Check if user exists
match repo::user::exists(&pool, &usr.username).await {
Ok(exists) => {
if !exists {
return not_found("Not Found").await;
}
}
Err(err) => {
return not_found(&err.to_string()).await;
}
};
let user = repo::user::get(&pool, &usr.username).await.unwrap();
let salt = repo::salt::get(&pool, &user.salt_id).await.unwrap();
let salt_str = hashing::get_salt(&salt.salt).unwrap();
// Check if password is correct
match hashing::hash_password(&usr.password, &salt_str) {
Ok(hash_password) => {
if hashing::verify_password(&usr.password, hash_password.clone()).unwrap() {
match repo::user::get(&pool, &payload.username).await {
Ok(user) => {
if hashing::verify_password(&payload.password, user.password.clone()).unwrap() {
// Create token
let key = token_stuff::get_key().unwrap();
let (token_literal, duration) = token_stuff::create_token(&key).unwrap();
if token_stuff::verify_token(&key, &token_literal) {
let current_time = time::OffsetDateTime::now_utc();
let _ = repo::user::update_last_login(&pool, &user, &current_time).await;
(
StatusCode::OK,
Json(response::Response {
message: String::from("Successful"),
data: vec![icarus_models::login_result::LoginResult {
id: user.id,
username: user.username,
username: user.username.clone(),
token: token_literal,
token_type: String::from(token_stuff::TOKENTYPE),
expiration: duration,
+27 -10
View File
@@ -11,8 +11,7 @@ use argon2::{
pub fn generate_salt() -> Result<SaltString, argon2::Error> {
// Generate a random salt
// SaltString::generate uses OsRng internally for cryptographic security
let salt = SaltString::generate(&mut OsRng);
Ok(salt)
Ok(SaltString::generate(&mut OsRng))
}
pub fn get_salt(s: &str) -> Result<SaltString, argon2::password_hash::Error> {
@@ -32,9 +31,7 @@ pub fn hash_password(
// Hash the password with the salt
// The output is a PasswordHash string format that includes algorithm, version,
// parameters, salt, and the hash itself.
let password_hash = argon2.hash_password(password_bytes, salt)?.to_string();
Ok(password_hash)
Ok(argon2.hash_password(password_bytes, salt)?.to_string())
}
pub fn verify_password(
@@ -48,11 +45,9 @@ pub fn verify_password(
let parsed_hash = argon2::PasswordHash::new(stored_hash.as_str())?;
// Create an Argon2 instance (it will use the parameters from the parsed hash)
let argon2 = Argon2::default();
// Verify the password against the parsed hash
// This automatically uses the correct salt and parameters embedded in `parsed_hash`
match argon2.verify_password(password_bytes, &parsed_hash) {
match Argon2::default().verify_password(password_bytes, &parsed_hash) {
Ok(()) => Ok(true), // Passwords match
Err(argon2::password_hash::Error::Password) => Ok(false), // Passwords don't match
Err(e) => Err(e), // Some other error occurred (e.g., invalid hash format)
@@ -66,8 +61,7 @@ mod tests {
#[test]
fn test_hash_password() {
let some_password = String::from("somethingrandom");
let salt = generate_salt().unwrap();
match hash_password(&some_password, &salt) {
match hash_password(&some_password, &generate_salt().unwrap()) {
Ok(p) => match verify_password(&some_password, p.clone()) {
Ok(res) => {
assert_eq!(res, true);
@@ -81,4 +75,27 @@ mod tests {
}
}
}
#[test]
fn test_wrong_password() {
let some_password = String::from("somethingrandom");
match hash_password(&some_password, &generate_salt().unwrap()) {
Ok(p) => {
match verify_password(&some_password, p.clone()) {
Ok(res) => {
assert_eq!(res, true, "Passwords are not verified");
}
Err(err) => {
assert!(false, "Error: {:?}", err.to_string());
}
}
let wrong_password = String::from("Differentanotherlevel");
let result = verify_password(&wrong_password, p.clone()).unwrap();
assert_eq!(false, result, "Passwords should not match");
}
Err(err) => {
assert!(false, "Error: {:?}", err.to_string());
}
}
}
}
-1
View File
@@ -36,7 +36,6 @@ pub mod db {
async fn get_db_url() -> String {
#[cfg(debug_assertions)] // Example: Only load .env in debug builds
dotenvy::dotenv().ok();
env::var(keys::DBURL).expect(keys::error::ERROR)
}
+9 -2
View File
@@ -25,8 +25,14 @@ mod init {
pub async fn routes() -> Router {
// build our application with a route
Router::new()
.route(callers::endpoints::DBTEST, get(callers::common::db_ping))
.route(callers::endpoints::ROOT, get(callers::common::root))
.route(
callers::endpoints::DBTEST,
get(callers::common::endpoint::db_ping),
)
.route(
callers::endpoints::ROOT,
get(callers::common::endpoint::root),
)
.route(
callers::endpoints::REGISTER,
post(callers::register::register_user),
@@ -68,6 +74,7 @@ mod tests {
pub const LIMIT: usize = 6;
pub async fn get_pool() -> Result<sqlx::PgPool, sqlx::Error> {
dotenvy::dotenv().ok(); // Load .env file if it exists
let tm_db_url = std::env::var(keys::DBURL).expect("DATABASE_URL must be present");
let tm_options = sqlx::postgres::PgConnectOptions::from_str(&tm_db_url).unwrap();
sqlx::PgPool::connect_with(tm_options).await
+33
View File
@@ -42,6 +42,39 @@ pub mod user {
}
}
pub async fn update_last_login(
pool: &sqlx::PgPool,
user: &icarus_models::user::User,
time: &time::OffsetDateTime,
) -> Result<time::OffsetDateTime, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "user" SET last_login = $1 WHERE id = $2 RETURNING last_login
"#,
)
.bind(time)
.bind(user.id)
.fetch_optional(pool)
.await
.map_err(|e| {
eprintln!("Error updating time: {}", e);
e
});
match result {
Ok(row) => match row {
Some(r) => {
let last_login: time::OffsetDateTime = r
.try_get("last_login")
.map_err(|_e| sqlx::Error::RowNotFound)?;
Ok(last_login)
}
None => Err(sqlx::Error::RowNotFound),
},
Err(err) => Err(err),
}
}
pub async fn exists(pool: &sqlx::PgPool, username: &String) -> Result<bool, sqlx::Error> {
let result = sqlx::query(
r#"
+22 -13
View File
@@ -18,11 +18,22 @@ pub fn get_key() -> Result<String, dotenvy::Error> {
Ok(key)
}
pub fn get_expiration() -> time::Result<time::Duration> {
let now = time::OffsetDateTime::now_utc();
let epoch = time::OffsetDateTime::UNIX_EPOCH;
let since_the_epoch = now - epoch;
Ok(since_the_epoch)
pub fn get_issued() -> time::Result<time::OffsetDateTime> {
Ok(time::OffsetDateTime::now_utc())
}
pub fn get_expiration(issued: &time::OffsetDateTime) -> Result<time::OffsetDateTime, time::Error> {
let duration_expire = time::Duration::hours(4);
Ok(*issued + duration_expire)
}
mod util {
pub fn time_to_std_time(
provided_time: &time::OffsetDateTime,
) -> Result<std::time::SystemTime, std::time::SystemTimeError> {
let converted = std::time::SystemTime::from(*provided_time);
Ok(converted)
}
}
pub fn create_token(provided_key: &String) -> Result<(String, i64), josekit::JoseError> {
@@ -33,13 +44,11 @@ pub fn create_token(provided_key: &String) -> Result<(String, i64), josekit::Jos
payload.set_subject(MESSAGE);
payload.set_issuer(ISSUER);
payload.set_audience(vec![AUDIENCE]);
match get_expiration() {
Ok(duration) => {
let expire = duration.whole_seconds();
let _ = payload.set_claim(
"expiration",
Some(serde_json::to_value(expire.to_string()).unwrap()),
);
match get_issued() {
Ok(issued) => {
let expire = get_expiration(&issued).unwrap();
payload.set_issued_at(&util::time_to_std_time(&issued).unwrap());
payload.set_expires_at(&util::time_to_std_time(&expire).unwrap());
let key: String = if provided_key.is_empty() {
get_key().unwrap()
@@ -50,7 +59,7 @@ pub fn create_token(provided_key: &String) -> Result<(String, i64), josekit::Jos
let signer = Hs256.signer_from_bytes(key.as_bytes()).unwrap();
Ok((
josekit::jwt::encode_with_signer(&payload, &header, &signer).unwrap(),
duration.whole_seconds(),
(expire - time::OffsetDateTime::UNIX_EPOCH).whole_seconds(),
))
}
Err(e) => Err(josekit::JoseError::InvalidClaim(e.into())),