Compare commits

...

3 commits

Author SHA1 Message Date
ed5cbfa848 Add login bypass for dev 2025-04-20 13:08:39 -05:00
2f6c729dc3 Add 403 page 2025-04-20 12:56:14 -05:00
4693a6c599 Work on config and settings UI 2025-04-20 12:41:38 -05:00
16 changed files with 1067 additions and 40 deletions

1
.env.sample Normal file
View file

@ -0,0 +1 @@
DATABASE_URL=postgresql://server:5432/database?user=user&password=password&connectTimeout=30&currentSchema=storage;

4
.gitignore vendored
View file

@ -1,4 +1,4 @@
**/.idea
target
config
.env
config.toml
.env

871
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -22,4 +22,5 @@ humanize-bytes = "1.0.6"
rocket-session-store = "0.2.1"
uuid = { version = "1.16.0", features = ["v4"] }
rand = { version = "0.9.0", features = ["thread_rng"] }
bcrypt = "0.17.0"
bcrypt = "0.17.0"
openidconnect = "4.0.0"

16
config.sample.toml Normal file
View file

@ -0,0 +1,16 @@
[general]
listen_ip = "0.0.0.0"
listen_port = 80
[backends.local]
path = "/var/tmp/test"
[auth]
enable_registration = true
openid_enabled = true
# Where the .well-known/openid-configuration exists
openid_issuer_url = "https://accounts.example.com"
openid_client_id = ""
openid_client_secret = ""
[smtp]
# TODO:

View file

@ -1,12 +0,0 @@
[general]
listen_ip = "0.0.0.0"
listen_port = 80
[backends.local]
path = "/var/tmp/test"
[auth]
enable_registration = true
[smtp]
# TODO:

27
src/config.rs Normal file
View file

@ -0,0 +1,27 @@
use rocket::serde::{Serialize,Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
general: GeneralConfig,
auth: AuthConfig,
smtp: EmailConfig
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GeneralConfig {
pub listen_ip: Option<String>,
pub listen_port: Option<u32>
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthConfig {
pub disable_registration: bool,
pub openid_enabled: Option<bool>,
pub openid_issuer_url: Option<String>,
pub openid_client_id: Option<String>,
pub openid_client_secret: Option<String>
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EmailConfig {
}

View file

@ -1,4 +1,6 @@
use std::cell::OnceCell;
use std::env;
use std::sync::OnceLock;
use std::time::Duration;
use rocket::data::ByteUnit;
use rocket::serde::Serialize;
@ -14,7 +16,6 @@ pub const SESSION_LIFETIME_SECONDS: u64 = 3600 * 24 * 14; // 14 days
pub const SESSION_COOKIE_NAME: &'static str = "storage-session";
#[derive(Serialize)]
pub struct FileConstants<'a> {
pub display_options: &'a[&'a str],
@ -25,3 +26,10 @@ pub const FILE_CONSTANTS: FileConstants = FileConstants {
sort_keys: &["name", "last_modified", "size"],
};
/// Disables CSRF & password verification for login
/// Used for development due to no session persistence
pub static DISABLE_LOGIN_CHECK: OnceLock<bool> = OnceLock::new();
pub fn init_statics() {
DISABLE_LOGIN_CHECK.set(env::var("DANGER_DISABLE_LOGIN_CHECKS").is_ok()).unwrap();
}

View file

@ -24,7 +24,7 @@ use crate::managers::repos::RepoManager;
use crate::objs::library::Library;
use crate::util::{setup_logger, JsonErrorResponse, ResponseError};
use routes::api;
use crate::consts::{SESSION_COOKIE_NAME, SESSION_LIFETIME_SECONDS};
use crate::consts::{init_statics, SESSION_COOKIE_NAME, SESSION_LIFETIME_SECONDS};
use crate::models::user::UserModel;
use crate::routes::ui;
@ -39,6 +39,7 @@ mod objs;
mod helpers;
mod consts;
mod guards;
mod config;
pub type DB = Pool<Postgres>;
@ -69,6 +70,7 @@ pub struct GlobalMetadata {
async fn rocket() -> _ {
setup_logger();
dotenvy::dotenv().ok();
init_statics();
trace!("trace");
debug!("debug");
@ -145,17 +147,20 @@ async fn rocket() -> _ {
ui::auth::forgot_password::page, ui::auth::forgot_password::handler,
])
.mount("/", routes![
ui::user::index, ui::user::redirect_list_library_files, ui::user::list_library_files, ui::user::get_library_file,
ui::user::user_settings, ui::user::index, ui::user::redirect_list_library_files, ui::user::list_library_files, ui::user::get_library_file,
])
.mount("/", routes![
ui::help::about,
ui::help::test_get
])
.mount("/admin", routes![
ui::admin::index
])
.register("/api", catchers![
not_found_api,
])
.register("/", catchers![
not_found, not_authorized
not_found, not_authorized, forbidden
])
}
@ -165,6 +170,13 @@ pub fn not_authorized(req: &Request) -> Redirect {
Redirect::to(format!("/auth/login?return_to={}", req.uri().path().percent_encode()))
}
#[catch(403)]
pub fn forbidden(req: &Request) -> Template {
Template::render("errors/403", context! {
})
}
#[catch(404)]
fn not_found(req: &Request) -> Template {
Template::render("errors/404", context! {

View file

@ -10,7 +10,7 @@ use rocket::response::Responder;
use rocket::serde::Serialize;
use rocket::serde::uuid::Uuid;
use sqlx::{query_as, FromRow};
use crate::consts::ENCRYPTION_ROUNDS;
use crate::consts::{DISABLE_LOGIN_CHECK, ENCRYPTION_ROUNDS};
use crate::{LoginSessionData, SessionData, DB};
use crate::models::repo::RepoModel;
use crate::util::JsonErrorResponse;
@ -124,7 +124,7 @@ pub async fn validate_user(pool: &DB, email_or_usrname: &str, password: &str) ->
return Err(UserAuthError::UserNotFound);
};
if let Some(db_password) = user.password {
if bcrypt::verify(password, &db_password).map_err(|e| UserAuthError::EncryptionError(e))? {
if !DISABLE_LOGIN_CHECK.get().unwrap() || bcrypt::verify(password, &db_password).map_err(|e| UserAuthError::EncryptionError(e))? {
return Ok(UserModel {
id: user.id,
email: user.email,

View file

@ -1,3 +1,4 @@
pub mod user;
pub mod help;
pub(crate) mod auth;
pub(crate) mod auth;
pub mod admin;

10
src/routes/ui/admin.rs Normal file
View file

@ -0,0 +1,10 @@
use rocket::{get, Route};
use rocket::http::Status;
use rocket::response::status;
use rocket_dyn_templates::{context, Template};
use crate::guards::AuthUser;
#[get("/")]
pub async fn index(user: AuthUser, route: &Route) -> Status {
Status::Forbidden
}

View file

@ -6,6 +6,7 @@ use rocket::http::{Header, Status};
use rocket_dyn_templates::{context, Template};
use rocket_session_store::Session;
use crate::{GlobalMetadata, LoginSessionData, SessionData, DB};
use crate::consts::DISABLE_LOGIN_CHECK;
use crate::models::user::validate_user_form;
use crate::util::{set_csrf, validate_csrf_form};
@ -60,7 +61,9 @@ pub async fn handler(
return_to: Option<String>,
) -> Result<HackyRedirectBecauseRocketBug, Template> {
trace!("handler");
validate_csrf_form(&mut form.context, &session).await;
if !DISABLE_LOGIN_CHECK.get().unwrap() {
validate_csrf_form(&mut form.context, &session).await;
}
let user = validate_user_form(&mut form.context, &pool).await;
trace!("check form");
if form.context.status() == Status::Ok {

View file

@ -21,7 +21,10 @@ use crate::objs::library::ListOptions;
use crate::routes::ui::auth;
use crate::util::{JsonErrorResponse, ResponseError};
#[get("/settings")]
pub async fn user_settings(user: AuthUser, route: &Route) -> Template {
Template::render("settings", context! { session: user.session, route: route.uri.path() })
}
#[get("/")]
pub async fn index(user: AuthUser, route: &Route) -> Template {
Template::render("index", context! { session: user.session, route: route.uri.path(), test: "value" })

View file

@ -0,0 +1,26 @@
{{#> layouts/default body-class="has-background-white-ter login-bg" }}
<br><br>
<div class="container py-6" style="width:20%"> <!-- TODO: fix width on mobile -->
<h1 class="title is-1 has-text-centered">storage-app</h1>
<div class="box is-radiusless">
<h4 class="title is-4 has-text-centered">403 Forbidden</h4>
<p>You do not have permission to view the resource at <code></code></p>
<br>
<!-- Hide go back unless javascript enabled -->
<p><span id="backlink" style="display:none"><a href="">Go Back</a> | </span><a href="/">Return home</a></p>
</div>
</div>
{{/layouts/default}}
<script>
// Enable 'go back' link:
const element = document.querySelector('#backlink');
element.style.display = "inline"
const elementLink = document.querySelector('#backlink a');
elementLink.setAttribute('href', document.referrer);
elementLink.onclick = function() {
history.back();
return false;
}
</script>

View file

@ -0,0 +1,92 @@
{{#> layouts/main body-class="" }}
<div class="columns">
<div class="column">
<div class="box is-radiusless" id="account">
<h4 class="title is-4 has-text-link">Account Settings</h4>
<form method="post" action="/settings/account">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<div class="field">
<label class="label">Name</label>
<div class="control has-icons-left">
<input autofocus required name="username" value="{{ session.user.name }}"
class="input {{#if errors.name}}is-danger{{/if}}" type="text" placeholder="Name">
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
</div>
{{#if errors.name }}
<p class="help is-danger">{{errors.name}}</p>
{{/if}}
</div>
<div class="field">
<label class="label">Password</label>
<div class="control">
<a class="button is-small">Reset password</a>
</div>
</div>
<br>
<div class="buttons">
<button class="button is-success" type="submit">Save Changes</button>
</div>
</form>
</div>
<div class="box is-radiusless" id="account">
<h4 class="title is-4 has-text-link">UI Preferences</h4>
<form method="post" action="/settings/ui">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<div class="field">
<label class="label">Language</label>
<div class="control">
<div class="select">
<select>
<option selected value="en-us">English</option>
</select>
</div>
</div>
</div>
<br>
<div class="buttons">
<button class="button is-success" type="submit">Save Changes</button>
</div>
</form>
</div>
<div class="box is-radiusless" id="sessions">
<h4 class="title is-4 has-text-link">Active Sessions</h4>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>IP</th>
<th>Location</th>
<th>Last Active</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>UUID</td>
<td>IP</td>
<td>Location</td>
<td>Now</td>
<td><em>current</em></td>
</tr>
</tb>
</table>
<br>
</div>
</div>
<div class="column is-3">
<div class="box">
<aside class="sidebar pl-0 mb-0">
<p class="sidebar-header">Sections</p>
<ul class="sidebar-list mb-0">
<li><a href="#account"><i class="fa fa-user"></i>Account</a></li>
<li><a href="#ui"><i class="fa fa-cog"></i>UI Preferences</a></li>
<li><a href="#sessions"><i class="fa fa-laptop"></i>Active Sessions</a></li>
</ul>
</aside>
</div>
</div>
</div>
{{/layouts/main}}