feat: Add basic project structure and configuration

- Add `.env.template` file for environment variable configuration.
- Add `.gitignore` file to ignore generated files and IDE artifacts.
- Add `Cargo.toml` file specifying project dependencies.
- Add basic project documentation in `README.md` and configuration
  guide in `docs/configuration.md`.
- Add Gitea authentication guide in `docs/gitea-auth.md`.
- Add installation guide in `docs/installation.md`.
- Add MVC architecture guide in `docs/mvc.md`.
- Add views guide in `docs/views.md`.
This commit is contained in:
Mahmoud Emad
2025-05-07 14:03:08 +03:00
parent 84d357f0c5
commit 645a387528
26 changed files with 2646 additions and 1 deletions

68
src/config/mod.rs Normal file
View File

@@ -0,0 +1,68 @@
use config::{Config, ConfigError, File};
use serde::Deserialize;
use std::env;
// Export OAuth module
pub mod oauth;
/// Application configuration
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
/// Server configuration
pub server: ServerConfig,
/// Template configuration
pub templates: TemplateConfig,
}
/// Server configuration
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
/// Host address to bind to
pub host: String,
/// Port to listen on
pub port: u16,
/// Workers count
pub workers: Option<u32>,
}
/// Template configuration
#[derive(Debug, Deserialize, Clone)]
pub struct TemplateConfig {
/// Directory containing templates
pub dir: String,
}
impl AppConfig {
/// Loads configuration from files and environment variables
pub fn new() -> Result<Self, ConfigError> {
// Set default values
let mut config_builder = Config::builder()
.set_default("server.host", "127.0.0.1")?
.set_default("server.port", 9999)?
.set_default("server.workers", None::<u32>)?
.set_default("templates.dir", "./src/views")?;
// Load from config file if it exists
if let Ok(config_path) = env::var("APP_CONFIG") {
config_builder = config_builder.add_source(File::with_name(&config_path));
} else {
// Try to load from default locations
config_builder = config_builder
.add_source(File::with_name("config/default").required(false))
.add_source(File::with_name("config/local").required(false));
}
// Override with environment variables (e.g., SERVER__HOST, SERVER__PORT)
config_builder =
config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
// Build and deserialize the config
let config = config_builder.build()?;
config.try_deserialize()
}
}
/// Returns the application configuration
pub fn get_config() -> AppConfig {
AppConfig::new().expect("Failed to load configuration")
}

62
src/config/oauth.rs Normal file
View File

@@ -0,0 +1,62 @@
use oauth2::{
AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl,
basic::BasicClient, AuthorizationCode, CsrfToken, Scope, TokenResponse,
};
use serde::{Deserialize, Serialize};
use std::env;
/// Gitea OAuth configuration
#[derive(Clone, Debug)]
pub struct GiteaOAuthConfig {
/// OAuth client
pub client: BasicClient,
/// Gitea instance URL
pub instance_url: String,
}
impl GiteaOAuthConfig {
/// Creates a new Gitea OAuth configuration
pub fn new() -> Self {
// Get configuration from environment variables
let client_id = env::var("GITEA_CLIENT_ID")
.expect("Missing GITEA_CLIENT_ID environment variable");
let client_secret = env::var("GITEA_CLIENT_SECRET")
.expect("Missing GITEA_CLIENT_SECRET environment variable");
let instance_url = env::var("GITEA_INSTANCE_URL")
.expect("Missing GITEA_INSTANCE_URL environment variable");
// Create OAuth client
let auth_url = format!("{}/login/oauth/authorize", instance_url);
let token_url = format!("{}/login/oauth/access_token", instance_url);
let client = BasicClient::new(
ClientId::new(client_id),
Some(ClientSecret::new(client_secret)),
AuthUrl::new(auth_url).unwrap(),
Some(TokenUrl::new(token_url).unwrap()),
)
.set_redirect_uri(
RedirectUrl::new(format!("{}/auth/gitea/callback", env::var("APP_URL").unwrap_or_else(|_| "http://localhost:9999".to_string()))).unwrap(),
);
Self {
client,
instance_url,
}
}
}
/// Gitea user information structure
#[derive(Debug, Deserialize, Serialize)]
pub struct GiteaUser {
/// User ID
pub id: i64,
/// Username
pub login: String,
/// Full name
pub full_name: String,
/// Email address
pub email: String,
/// Avatar URL
pub avatar_url: String,
}

193
src/controllers/auth.rs Normal file
View File

@@ -0,0 +1,193 @@
use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie};
use actix_session::Session;
use tera::Tera;
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
use crate::utils::render_template;
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};
use lazy_static::lazy_static;
// JWT Claims structure
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String, // Subject (email)
pub exp: usize, // Expiration time
pub iat: usize, // Issued at
pub role: String, // User role
}
// JWT Secret key
lazy_static! {
static ref JWT_SECRET: String = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string());
}
/// Controller for handling authentication-related routes
pub struct AuthController;
impl AuthController {
/// Generate a JWT token for a user
pub fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {
let role_str = match role {
UserRole::Admin => "admin",
UserRole::User => "user",
};
let expiration = Utc::now()
.checked_add_signed(Duration::hours(24))
.expect("valid timestamp")
.timestamp() as usize;
let claims = Claims {
sub: email.to_owned(),
exp: expiration,
iat: Utc::now().timestamp() as usize,
role: role_str.to_string(),
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(JWT_SECRET.as_bytes())
)
}
/// Validate a JWT token
pub fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let validation = Validation::new(Algorithm::HS256);
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(JWT_SECRET.as_bytes()),
&validation
)?;
Ok(token_data.claims)
}
/// Extract token from session
pub fn extract_token_from_session(session: &Session) -> Option<String> {
session.get::<String>("auth_token").ok().flatten()
}
/// Extract token from cookie
pub fn extract_token_from_cookie(req: &actix_web::HttpRequest) -> Option<String> {
req.cookie("auth_token").map(|c| c.value().to_string())
}
/// Renders the login page
pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "login");
render_template(&tmpl, "auth/login.html", &ctx)
}
/// Handles user login
pub async fn login(
form: web::Form<LoginCredentials>,
session: Session,
_tmpl: web::Data<Tera>
) -> Result<impl Responder> {
// For simplicity, always log in the user without checking credentials
// Create a user object with admin role
let mut test_user = User::new(
"Admin User".to_string(),
form.email.clone()
);
// Set the ID and admin role
test_user.id = Some(1);
test_user.role = UserRole::Admin;
// Generate JWT token
let token = Self::generate_token(&test_user.email, &test_user.role)
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
// Store user data in session
let user_json = serde_json::to_string(&test_user).unwrap();
session.insert("user", &user_json)?;
session.insert("auth_token", &token)?;
// Create a cookie with the JWT token
let cookie = Cookie::build("auth_token", token)
.path("/")
.http_only(true)
.secure(false) // Set to true in production with HTTPS
.max_age(actix_web::cookie::time::Duration::hours(24))
.finish();
// Redirect to the home page with JWT token in cookie
Ok(HttpResponse::Found()
.cookie(cookie)
.append_header((header::LOCATION, "/"))
.finish())
}
/// Renders the registration page
pub async fn register_page(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "register");
render_template(&tmpl, "auth/register.html", &ctx)
}
/// Handles user registration
pub async fn register(
form: web::Form<RegistrationData>,
session: Session,
_tmpl: web::Data<Tera>
) -> Result<impl Responder> {
// Skip validation and always create an admin user
let mut user = User::new(
form.name.clone(),
form.email.clone()
);
// Set the ID and admin role
user.id = Some(1);
user.role = UserRole::Admin;
// Generate JWT token
let token = Self::generate_token(&user.email, &user.role)
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
// Store user data in session
let user_json = serde_json::to_string(&user).unwrap();
session.insert("user", &user_json)?;
session.insert("auth_token", &token)?;
// Create a cookie with the JWT token
let cookie = Cookie::build("auth_token", token)
.path("/")
.http_only(true)
.secure(false) // Set to true in production with HTTPS
.max_age(actix_web::cookie::time::Duration::hours(24))
.finish();
// Redirect to the home page with JWT token in cookie
Ok(HttpResponse::Found()
.cookie(cookie)
.append_header((header::LOCATION, "/"))
.finish())
}
/// Handles user logout
pub async fn logout(session: Session) -> Result<impl Responder> {
// Clear the session
session.purge();
// Create an expired cookie to remove the JWT token
let cookie = Cookie::build("auth_token", "")
.path("/")
.http_only(true)
.max_age(actix_web::cookie::time::Duration::seconds(0))
.finish();
// Redirect to the home page and clear the auth token cookie
Ok(HttpResponse::Found()
.cookie(cookie)
.append_header((header::LOCATION, "/"))
.finish())
}
}

35
src/controllers/home.rs Normal file
View File

@@ -0,0 +1,35 @@
use actix_web::{web, Result, Responder};
use tera::Tera;
use crate::utils::render_template;
use actix_session::Session;
/// Controller for handling home-related routes
pub struct HomeController;
impl HomeController {
/// Renders the home page
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "home");
// Add user to context if available
if let Ok(Some(user)) = session.get::<String>("user") {
ctx.insert("user_json", &user);
}
render_template(&tmpl, "home/index.html", &ctx)
}
/// Renders the about page
pub async fn about(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "about");
// Add user to context if available
if let Ok(Some(user)) = session.get::<String>("user") {
ctx.insert("user_json", &user);
}
render_template(&tmpl, "home/about.html", &ctx)
}
}

3
src/controllers/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
// Export controllers
pub mod home;
pub mod auth;

92
src/main.rs Normal file
View File

@@ -0,0 +1,92 @@
use actix_web::{web, App, HttpServer, middleware::Logger};
use actix_files as fs;
use tera::Tera;
use std::{io, env};
use dotenv::dotenv;
mod config;
mod controllers;
mod middleware;
mod models;
mod routes;
mod utils;
// Session key for cookie store
use actix_web::cookie::Key;
use lazy_static::lazy_static;
lazy_static! {
static ref SESSION_KEY: Key = {
// Load key from environment variable or generate a random one
match env::var("SECRET_KEY") {
Ok(key) if key.as_bytes().len() >= 32 => {
log::info!("Using SECRET_KEY from environment");
Key::from(key.as_bytes())
}
_ => {
log::warn!("No valid SECRET_KEY provided; generating random key (sessions will be invalidated on restart)");
Key::generate() // Generates a secure 32-byte key
}
}
};
}
#[actix_web::main]
async fn main() -> io::Result<()> {
// Initialize environment
dotenv().ok();
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
// Load configuration
let config = config::get_config();
// Check for port override from command line arguments
let args: Vec<String> = env::args().collect();
let mut port = config.server.port;
for i in 1..args.len() {
if args[i] == "--port" && i + 1 < args.len() {
if let Ok(p) = args[i + 1].parse::<u16>() {
port = p;
break;
}
}
}
let bind_address = format!("{}:{}", config.server.host, port);
log::info!("Starting server at http://{}", bind_address);
// Create and configure the HTTP server
HttpServer::new(move || {
// Initialize Tera templates
let mut tera = match Tera::new(&format!("{}/**/*.html", config.templates.dir)) {
Ok(t) => t,
Err(e) => {
log::error!("Parsing error(s): {}", e);
::std::process::exit(1);
}
};
// Register custom Tera functions
utils::register_tera_functions(&mut tera);
App::new()
// Enable logger middleware
.wrap(Logger::default())
// Add custom middleware
.wrap(middleware::RequestTimer)
.wrap(middleware::SecurityHeaders)
.wrap(middleware::JwtAuth)
// Configure static files
.service(fs::Files::new("/static", "./src/static"))
// Add Tera template engine
.app_data(web::Data::new(tera))
// Configure routes
.configure(routes::configure_routes)
})
.workers(config.server.workers.unwrap_or_else(|| num_cpus::get() as u32) as usize)
.bind(bind_address)?
.run()
.await
}

197
src/middleware/mod.rs Normal file
View File

@@ -0,0 +1,197 @@
use std::{
future::{ready, Ready},
time::Instant,
};
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::LocalBoxFuture;
// Request Timer Middleware
pub struct RequestTimer;
impl<S, B> Transform<S, ServiceRequest> for RequestTimer
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = RequestTimerMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(RequestTimerMiddleware { service }))
}
}
pub struct RequestTimerMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for RequestTimerMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let start = Instant::now();
let path = req.path().to_string();
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let elapsed = start.elapsed();
log::info!("Request to {} took {:?}", path, elapsed);
Ok(res)
})
}
}
// Security Headers Middleware
pub struct SecurityHeaders;
impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = SecurityHeadersMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(SecurityHeadersMiddleware { service }))
}
}
pub struct SecurityHeadersMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let fut = self.service.call(req);
Box::pin(async move {
let mut res = fut.await?;
// Add security headers
res.headers_mut().insert(
actix_web::http::header::X_CONTENT_TYPE_OPTIONS,
actix_web::http::header::HeaderValue::from_static("nosniff"),
);
res.headers_mut().insert(
actix_web::http::header::X_FRAME_OPTIONS,
actix_web::http::header::HeaderValue::from_static("DENY"),
);
res.headers_mut().insert(
actix_web::http::header::X_XSS_PROTECTION,
actix_web::http::header::HeaderValue::from_static("1; mode=block"),
);
Ok(res)
})
}
}
// JWT Authentication Middleware
pub struct JwtAuth;
impl<S, B> Transform<S, ServiceRequest> for JwtAuth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = JwtAuthMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(JwtAuthMiddleware { service }))
}
}
pub struct JwtAuthMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for JwtAuthMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
// Define public routes that don't require authentication
let path = req.path().to_string();
let public_routes = vec![
"/login",
"/register",
"/static",
"/favicon.ico",
"/",
"/about",
"/auth/gitea",
"/auth/gitea/callback"
];
// Check if the current path is a public route
let is_public_route = public_routes.iter().any(|route| path.starts_with(route));
if is_public_route {
// For public routes, just pass through without authentication check
let fut = self.service.call(req);
return Box::pin(async move {
fut.await
});
}
// For protected routes, check for authentication
// This is a simplified version - in a real application, you would check for a valid JWT token
// For now, just pass through all requests
let fut = self.service.call(req);
Box::pin(async move {
fut.await
})
}
}

2
src/models/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
// Export models
pub mod user;

62
src/models/user.rs Normal file
View File

@@ -0,0 +1,62 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// Represents a user in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
/// Unique identifier for the user
pub id: Option<i32>,
/// User's full name
pub name: String,
/// User's email address
pub email: String,
/// User's hashed password
#[serde(skip_serializing)]
pub password_hash: Option<String>,
/// User's role in the system
pub role: UserRole,
/// When the user was created
pub created_at: Option<DateTime<Utc>>,
/// When the user was last updated
pub updated_at: Option<DateTime<Utc>>,
}
/// Represents the possible roles a user can have
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UserRole {
/// Regular user with limited permissions
User,
/// Administrator with full permissions
Admin,
}
impl User {
/// Creates a new user with default values
pub fn new(name: String, email: String) -> Self {
Self {
id: None,
name,
email,
password_hash: None,
role: UserRole::User,
created_at: Some(Utc::now()),
updated_at: Some(Utc::now()),
}
}
}
/// Represents user login credentials
#[derive(Debug, Deserialize)]
pub struct LoginCredentials {
pub email: String,
pub password: String,
}
/// Represents user registration data
#[derive(Debug, Deserialize)]
pub struct RegistrationData {
pub name: String,
pub email: String,
pub password: String,
pub password_confirmation: String,
}

35
src/routes/mod.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::controllers::auth::AuthController;
use crate::controllers::home::HomeController;
use crate::middleware::JwtAuth;
use crate::SESSION_KEY;
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::web;
/// Configures all application routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
// Configure session middleware with the consistent key
let session_middleware =
SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
.cookie_secure(false) // Set to true in production with HTTPS
.build();
// Public routes that don't require authentication
cfg.service(
web::scope("")
.wrap(session_middleware)
// Home routes
.route("/", web::get().to(HomeController::index))
.route("/about", web::get().to(HomeController::about))
// Auth routes
.route("/login", web::get().to(AuthController::login_page))
.route("/login", web::post().to(AuthController::login))
.route("/register", web::get().to(AuthController::register_page))
.route("/register", web::post().to(AuthController::register))
.route("/logout", web::get().to(AuthController::logout)),
);
// Protected routes that require authentication
cfg.service(
web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware
);
}

49
src/static/css/styles.css Normal file
View File

@@ -0,0 +1,49 @@
/* Custom styles for Hostbasket */
/* Global styles */
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
/* Navigation */
.navbar-brand {
font-weight: bold;
}
/* Cards */
.card {
margin-bottom: 1rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
transition: all 0.3s ease;
}
.card-main {
height: 120px;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
transform: translateY(-0.25rem);
}
/* Buttons */
.btn {
border-radius: 0.25rem;
}
/* Forms */
.form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
/* Footer */
.footer {
margin-top: auto;
}

86
src/utils/mod.rs Normal file
View File

@@ -0,0 +1,86 @@
use actix_web::{web, HttpResponse, Result};
use chrono::{DateTime, Utc};
use serde_json::Value;
use std::collections::HashMap;
use tera::{Context, Tera};
/// Renders a template with the given context
pub fn render_template(
tmpl: &web::Data<Tera>,
template_name: &str,
context: &Context,
) -> Result<HttpResponse> {
let rendered = tmpl.render(template_name, context).map_err(|e| {
log::error!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
/// Registers custom functions with Tera
pub fn register_tera_functions(tera: &mut Tera) {
tera.register_function("format_date", format_date);
tera.register_function("active_class", active_class);
}
/// Custom Tera function to format dates
fn format_date(args: &HashMap<String, Value>) -> tera::Result<Value> {
let date = match args.get("date") {
Some(val) => val
.as_str()
.ok_or_else(|| tera::Error::msg("Date must be a string"))?,
None => return Err(tera::Error::msg("Date is required")),
};
let format = match args.get("format") {
Some(val) => val.as_str().unwrap_or("%Y-%m-%d %H:%M:%S"),
None => "%Y-%m-%d %H:%M:%S",
};
// Parse the date string
let datetime = match DateTime::parse_from_rfc3339(date) {
Ok(dt) => dt.with_timezone(&Utc),
Err(_) => {
// Try parsing as a timestamp
match date.parse::<i64>() {
Ok(ts) => DateTime::from_timestamp(ts, 0)
.ok_or_else(|| tera::Error::msg("Invalid timestamp"))?,
Err(_) => return Err(tera::Error::msg("Invalid date format")),
}
}
};
// Format the date
let formatted = datetime.format(format).to_string();
Ok(Value::String(formatted))
}
/// Custom Tera function to set active class for navigation
fn active_class(args: &HashMap<String, Value>) -> tera::Result<Value> {
let current = match args.get("current") {
Some(val) => val
.as_str()
.ok_or_else(|| tera::Error::msg("Current must be a string"))?,
None => return Err(tera::Error::msg("Current is required")),
};
let page = match args.get("page") {
Some(val) => val
.as_str()
.ok_or_else(|| tera::Error::msg("Page must be a string"))?,
None => return Err(tera::Error::msg("Page is required")),
};
let class = match args.get("class") {
Some(val) => val.as_str().unwrap_or("active"),
None => "active",
};
if current == page {
Ok(Value::String(class.to_string()))
} else {
Ok(Value::String("".to_string()))
}
}

45
src/views/auth/login.html Normal file
View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}Login - Hostbasket{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-5">
<div class="card-header">
<h2>Login</h2>
</div>
<div class="card-body">
<form method="post" action="/login">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<hr>
<div class="text-center">
<p>Or login with:</p>
<a href="/auth/gitea" class="btn btn-secondary">
Login with Gitea
</a>
</div>
<hr>
<div class="text-center">
<p>Don't have an account? <a href="/register">Register</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Register - Hostbasket{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-5">
<div class="card-header">
<h2>Register</h2>
</div>
<div class="card-body">
<form method="post" action="/register">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="password_confirmation" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="password_confirmation" name="password_confirmation" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
<hr>
<div class="text-center">
<p>Already have an account? <a href="/login">Login</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

66
src/views/base.html Normal file
View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Hostbasket{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/styles.css">
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">Hostbasket</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {{ active_class(current=active_page, page=" home") }}" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link {{ active_class(current=active_page, page=" about") }}"
href="/about">About</a>
</li>
</ul>
<ul class="navbar-nav">
{% if user_json %}
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link {{ active_class(current=active_page, page=" login") }}"
href="/login">Login</a>
</li>
<li class="nav-item">
<a class="nav-link {{ active_class(current=active_page, page=" register") }}"
href="/register">Register</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="py-4">
{% block content %}{% endblock %}
</main>
<footer class="footer mt-auto py-3 bg-light">
<div class="container text-center">
<span class="text-muted">© 2023 Hostbasket Powered byThreefold. All rights reserved.</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

51
src/views/home/about.html Normal file
View File

@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}About - Hostbasket{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>About Hostbasket</h1>
<p class="lead">A web application framework built with Actix Web and Rust.</p>
<hr>
<h2>Features</h2>
<ul>
<li><strong>Actix Web</strong>: A powerful, pragmatic, and extremely fast web framework for Rust</li>
<li><strong>Tera Templates</strong>: A template engine inspired by Jinja2 and Django templates</li>
<li><strong>Bootstrap 5.3.5</strong>: A popular CSS framework for responsive web design</li>
<li><strong>MVC Architecture</strong>: Clean separation of concerns with Models, Views, and Controllers
</li>
<li><strong>Middleware Support</strong>: Custom middleware for request timing and security headers</li>
<li><strong>Configuration Management</strong>: Flexible configuration system with environment variable
support</li>
<li><strong>Static File Serving</strong>: Serve CSS, JavaScript, and other static assets</li>
</ul>
<h2>Project Structure</h2>
<pre>
hostbasket/
├── Cargo.toml # Project dependencies
├── src/
│ ├── config/ # Configuration management
│ ├── controllers/ # Request handlers
│ ├── middleware/ # Custom middleware components
│ ├── models/ # Data models and business logic
│ ├── routes/ # Route definitions
│ ├── static/ # Static assets (CSS, JS, images)
│ │ ├── css/ # CSS files including Bootstrap
│ │ ├── js/ # JavaScript files
│ │ └── images/ # Image files
│ ├── utils/ # Utility functions
│ ├── views/ # Tera templates
│ └── main.rs # Application entry point
</pre>
<h2>Getting Started</h2>
<p>To get started with Hostbasket, check out the <a target="_blank"
href="https://git.ourworld.tf/herocode/rweb_starterkit">GitHub repository</a>.</p>
</div>
</div>
</div>
{% endblock %}

48
src/views/home/index.html Normal file
View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}Home - Hostbasket{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="jumbotron">
<h1 class="display-4">Welcome to Hostbasket!</h1>
<p class="lead">A web application framework built with Actix Web and Rust.</p>
<hr class="my-4">
<p>This is a starter template for building web applications with Actix Web and Rust.</p>
<p class="lead">
<a class="btn btn-primary btn-lg" href="/about" role="button">Learn more</a>
</p>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-4">
<div class="card card-main">
<div class="card-body">
<h5 class="card-title">Fast</h5>
<p class="card-text">Built with Actix Web, one of the fastest web frameworks available.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card card-main">
<div class="card-body">
<h5 class="card-title">Secure</h5>
<p class="card-text">Rust's memory safety guarantees help prevent common security issues.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card card-main">
<div class="card-body">
<h5 class="card-title">Scalable</h5>
<p class="card-text">Designed to handle high loads and scale horizontally.</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}