mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-08 02:43:21 +00:00
Compare commits
3 commits
d1cf5d3038
...
ed5cbfa848
Author | SHA1 | Date | |
---|---|---|---|
ed5cbfa848 | |||
2f6c729dc3 | |||
4693a6c599 |
16 changed files with 1067 additions and 40 deletions
1
.env.sample
Normal file
1
.env.sample
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DATABASE_URL=postgresql://server:5432/database?user=user&password=password&connectTimeout=30¤tSchema=storage;
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
||||||
**/.idea
|
**/.idea
|
||||||
target
|
target
|
||||||
config
|
config.toml
|
||||||
.env
|
.env
|
871
Cargo.lock
generated
871
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -23,3 +23,4 @@ rocket-session-store = "0.2.1"
|
||||||
uuid = { version = "1.16.0", features = ["v4"] }
|
uuid = { version = "1.16.0", features = ["v4"] }
|
||||||
rand = { version = "0.9.0", features = ["thread_rng"] }
|
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
16
config.sample.toml
Normal 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:
|
|
@ -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
27
src/config.rs
Normal 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 {
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
|
use std::env;
|
||||||
|
use std::sync::OnceLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use rocket::data::ByteUnit;
|
use rocket::data::ByteUnit;
|
||||||
use rocket::serde::Serialize;
|
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";
|
pub const SESSION_COOKIE_NAME: &'static str = "storage-session";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct FileConstants<'a> {
|
pub struct FileConstants<'a> {
|
||||||
pub display_options: &'a[&'a str],
|
pub display_options: &'a[&'a str],
|
||||||
|
@ -25,3 +26,10 @@ pub const FILE_CONSTANTS: FileConstants = FileConstants {
|
||||||
sort_keys: &["name", "last_modified", "size"],
|
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();
|
||||||
|
}
|
18
src/main.rs
18
src/main.rs
|
@ -24,7 +24,7 @@ use crate::managers::repos::RepoManager;
|
||||||
use crate::objs::library::Library;
|
use crate::objs::library::Library;
|
||||||
use crate::util::{setup_logger, JsonErrorResponse, ResponseError};
|
use crate::util::{setup_logger, JsonErrorResponse, ResponseError};
|
||||||
use routes::api;
|
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::models::user::UserModel;
|
||||||
use crate::routes::ui;
|
use crate::routes::ui;
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ mod objs;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod consts;
|
mod consts;
|
||||||
mod guards;
|
mod guards;
|
||||||
|
mod config;
|
||||||
|
|
||||||
pub type DB = Pool<Postgres>;
|
pub type DB = Pool<Postgres>;
|
||||||
|
|
||||||
|
@ -69,6 +70,7 @@ pub struct GlobalMetadata {
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
setup_logger();
|
setup_logger();
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
|
init_statics();
|
||||||
|
|
||||||
trace!("trace");
|
trace!("trace");
|
||||||
debug!("debug");
|
debug!("debug");
|
||||||
|
@ -145,17 +147,20 @@ async fn rocket() -> _ {
|
||||||
ui::auth::forgot_password::page, ui::auth::forgot_password::handler,
|
ui::auth::forgot_password::page, ui::auth::forgot_password::handler,
|
||||||
])
|
])
|
||||||
.mount("/", routes![
|
.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![
|
.mount("/", routes![
|
||||||
ui::help::about,
|
ui::help::about,
|
||||||
ui::help::test_get
|
ui::help::test_get
|
||||||
])
|
])
|
||||||
|
.mount("/admin", routes![
|
||||||
|
ui::admin::index
|
||||||
|
])
|
||||||
.register("/api", catchers![
|
.register("/api", catchers![
|
||||||
not_found_api,
|
not_found_api,
|
||||||
])
|
])
|
||||||
.register("/", catchers![
|
.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()))
|
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)]
|
#[catch(404)]
|
||||||
fn not_found(req: &Request) -> Template {
|
fn not_found(req: &Request) -> Template {
|
||||||
Template::render("errors/404", context! {
|
Template::render("errors/404", context! {
|
||||||
|
|
|
@ -10,7 +10,7 @@ use rocket::response::Responder;
|
||||||
use rocket::serde::Serialize;
|
use rocket::serde::Serialize;
|
||||||
use rocket::serde::uuid::Uuid;
|
use rocket::serde::uuid::Uuid;
|
||||||
use sqlx::{query_as, FromRow};
|
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::{LoginSessionData, SessionData, DB};
|
||||||
use crate::models::repo::RepoModel;
|
use crate::models::repo::RepoModel;
|
||||||
use crate::util::JsonErrorResponse;
|
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);
|
return Err(UserAuthError::UserNotFound);
|
||||||
};
|
};
|
||||||
if let Some(db_password) = user.password {
|
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 {
|
return Ok(UserModel {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub(crate) mod auth;
|
pub(crate) mod auth;
|
||||||
|
pub mod admin;
|
10
src/routes/ui/admin.rs
Normal file
10
src/routes/ui/admin.rs
Normal 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
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ use rocket::http::{Header, Status};
|
||||||
use rocket_dyn_templates::{context, Template};
|
use rocket_dyn_templates::{context, Template};
|
||||||
use rocket_session_store::Session;
|
use rocket_session_store::Session;
|
||||||
use crate::{GlobalMetadata, LoginSessionData, SessionData, DB};
|
use crate::{GlobalMetadata, LoginSessionData, SessionData, DB};
|
||||||
|
use crate::consts::DISABLE_LOGIN_CHECK;
|
||||||
use crate::models::user::validate_user_form;
|
use crate::models::user::validate_user_form;
|
||||||
use crate::util::{set_csrf, validate_csrf_form};
|
use crate::util::{set_csrf, validate_csrf_form};
|
||||||
|
|
||||||
|
@ -60,7 +61,9 @@ pub async fn handler(
|
||||||
return_to: Option<String>,
|
return_to: Option<String>,
|
||||||
) -> Result<HackyRedirectBecauseRocketBug, Template> {
|
) -> Result<HackyRedirectBecauseRocketBug, Template> {
|
||||||
trace!("handler");
|
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;
|
let user = validate_user_form(&mut form.context, &pool).await;
|
||||||
trace!("check form");
|
trace!("check form");
|
||||||
if form.context.status() == Status::Ok {
|
if form.context.status() == Status::Ok {
|
||||||
|
|
|
@ -21,7 +21,10 @@ use crate::objs::library::ListOptions;
|
||||||
use crate::routes::ui::auth;
|
use crate::routes::ui::auth;
|
||||||
use crate::util::{JsonErrorResponse, ResponseError};
|
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("/")]
|
#[get("/")]
|
||||||
pub async fn index(user: AuthUser, route: &Route) -> Template {
|
pub async fn index(user: AuthUser, route: &Route) -> Template {
|
||||||
Template::render("index", context! { session: user.session, route: route.uri.path(), test: "value" })
|
Template::render("index", context! { session: user.session, route: route.uri.path(), test: "value" })
|
||||||
|
|
26
templates/errors/403.html.hbs
Normal file
26
templates/errors/403.html.hbs
Normal 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>
|
92
templates/settings.html.hbs
Normal file
92
templates/settings.html.hbs
Normal 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}}
|
Loading…
Add table
Add a link
Reference in a new issue