From 9d541bd2bc798a587eb088697b51f770a8514fad Mon Sep 17 00:00:00 2001 From: Overlord Date: Fri, 28 Nov 2025 12:31:12 +0100 Subject: [PATCH] tldr; nothing works --- app/src/{crypto => cryptography}/crypto.v | 8 +- app/src/database/db.v | 34 +---- app/src/database/files.v | 8 ++ app/src/database/logins.v | 46 ++++++ app/src/database/tables.v | 24 ---- app/src/database/user.v | 52 +++++++ app/src/jwt/hs256.v | 4 +- app/src/main.v | 168 +++++++++++++--------- app/src/template/assets/admin.css | 157 ++++++++++++++++++++ app/src/template/auth/login.html | 25 ++++ app/src/template/auth/logout.html | 21 +++ app/src/template/dashboard.html | 5 +- app/src/util/structs.v | 1 + 13 files changed, 426 insertions(+), 127 deletions(-) rename app/src/{crypto => cryptography}/crypto.v (67%) create mode 100644 app/src/database/files.v create mode 100644 app/src/database/logins.v delete mode 100644 app/src/database/tables.v create mode 100644 app/src/database/user.v create mode 100644 app/src/template/assets/admin.css create mode 100644 app/src/template/auth/login.html create mode 100644 app/src/template/auth/logout.html diff --git a/app/src/crypto/crypto.v b/app/src/cryptography/crypto.v similarity index 67% rename from app/src/crypto/crypto.v rename to app/src/cryptography/crypto.v index 40e45ba..e5bea5a 100644 --- a/app/src/crypto/crypto.v +++ b/app/src/cryptography/crypto.v @@ -1,16 +1,14 @@ -module crypto +module cryptography import rand import fleximus.argon2 -pub struct Crypto {} - -pub fn Crypto.hash_password(password string) !string { +pub fn hash_password(password string) !string { salt := rand.bytes(16) or { return error('failed to generate salt: ${err}') } hash := argon2.hash(password.bytes(), salt) or { return error('argon2 hash failed: ${err}') } return hash } -pub fn Crypto.hash_verify(password string, hash string) !bool { +pub fn hash_verify(password string, hash string) !bool { return argon2.verify(hash, password.bytes()) or { return error('argon2 verify failed: ${err}') } } diff --git a/app/src/database/db.v b/app/src/database/db.v index 62186d9..49d7bad 100644 --- a/app/src/database/db.v +++ b/app/src/database/db.v @@ -7,8 +7,6 @@ import db.sqlite pub struct Database { cfg util.Config -mut: - conn sqlite.DB } pub fn get_dummy_exclusion_list(exe string, root string) []string { @@ -18,16 +16,16 @@ pub fn get_dummy_exclusion_list(exe string, root string) []string { ] } -fn get_connection(cfg util.Config) !sqlite.DB { - conn := sqlite.connect(cfg.database)! +fn (db &Database) get_connection() !sqlite.DB { + conn := sqlite.connect(db.cfg.database)! - if cfg.debug { + if db.cfg.debug { conn.synchronization_mode(.off)! conn.journal_mode(.memory)! } sql conn { - create table Users + create table User create table Files create table Logins }! @@ -37,33 +35,15 @@ fn get_connection(cfg util.Config) !sqlite.DB { pub fn get_database(cfg util.Config) !&Database { return &Database{ - cfg: cfg - conn: get_connection(cfg)! + cfg: cfg } } pub fn (mut db Database) query[T](statement orm.QueryBuilder[T]) ![]T { - if !db.conn.validate() or { false } { - db.conn = get_connection(db.cfg)! - } - return statement.query()! } pub fn (db Database) get_query_builder[T]() &orm.QueryBuilder[T] { - return orm.new_query[T](db.conn) -} - -pub fn test(cfg util.Config) !bool { - mut db := get_database(cfg)! - - db.conn.validate()! - db.conn.close()! - - mut qb := orm.new_query[Users](db.conn) - - result := db.query[Users](qb)! - println(result.str()) - - return true + conn := db.get_connection() or { panic(err) } + return orm.new_query[T](conn) } diff --git a/app/src/database/files.v b/app/src/database/files.v new file mode 100644 index 0000000..8ebb828 --- /dev/null +++ b/app/src/database/files.v @@ -0,0 +1,8 @@ +module database + +@[table: 'files'] +pub struct Files { + id int @[primary; serial] + path string @[nonnull; unique] + visible bool @[default: false; nonnull] +} diff --git a/app/src/database/logins.v b/app/src/database/logins.v new file mode 100644 index 0000000..7668cc6 --- /dev/null +++ b/app/src/database/logins.v @@ -0,0 +1,46 @@ +module database + +import time + +@[table: 'login_attempts'] +pub struct Logins { +mut: + ip string @[primary] + attempts int @[nonnull] + attempt_time time.Time @[nonnull] +} + +pub fn Logins.by_ip(ip string, mut database_ Database) ?Logins { + query := database_.get_query_builder[Logins]().where('ip = ?', ip) or { return none } + result := database_.query[Logins](query) or { return none } + + return result.first() +} + +pub fn Logins.create_or_update(ip string, mut database_ Database) ?Logins { + mut login := Logins{ + ip: ip + attempts: 1 + attempt_time: time.now() + } + + if existing := Logins.by_ip(ip, mut database_) { + login.attempts = existing.attempts + 1 + login.attempt_time = time.now() + + db := database_.get_connection() or { panic(err) } + + sql db { + update Logins set attempts = login.attempts, attempt_time = login.attempt_time + where ip == ip + } or { return none } + } else { + db := database_.get_connection() or { panic(err) } + + sql db { + insert login into Logins + } or { return none } + } + + return login +} diff --git a/app/src/database/tables.v b/app/src/database/tables.v deleted file mode 100644 index d08c8a4..0000000 --- a/app/src/database/tables.v +++ /dev/null @@ -1,24 +0,0 @@ -module database - -import time - -@[table: 'users'] -pub struct Users { - id int @[primary; serial] - username string @[nonnull; unique] - password string @[nonnull] -} - -@[table: 'login_attempts'] -pub struct Logins { - ip string @[primary] - attempts int @[nonnull] - attempt_time time.Time @[nonnull] -} - -@[table: 'files'] -pub struct Files { - id int @[primary; serial] - path string @[nonnull; unique] - visible bool @[default: false; nonnull] -} diff --git a/app/src/database/user.v b/app/src/database/user.v new file mode 100644 index 0000000..01f69ab --- /dev/null +++ b/app/src/database/user.v @@ -0,0 +1,52 @@ +module database + +import orm +import cryptography + +@[table: 'users'] +pub struct User { +pub: + id ?int @[primary; serial] + username string @[nonnull; unique] + password string @[nonnull] +} + +pub fn User.by_id(user_id orm.Primitive, mut database_ Database) ?User { + query := database_.get_query_builder[User]().where('id = ?', user_id) or { return none } + result := database_.query[User](query) or { return none } + + return result.first() +} + +pub fn User.by_name(username orm.Primitive, mut database_ Database) ?User { + eprintln('qb') + query := database_.get_query_builder[User]().where('username = ?', username) or { return none } + eprintln('result') + result := database_.query[User](*query) or { return none } + + eprintln('first') + return result.first() +} + +pub fn User.create(username string, password string, database_ Database) ?User { + hash := cryptography.hash_password(password) or { return none } + mut user := User{ + id: none + username: username + password: hash + } + + db := database_.get_connection() or { panic(err) } + + sql db { + insert user into User + } or { return none } + + return user +} + +pub fn User.verify(username orm.Primitive, password string, mut database_ Database) ?User { + eprintln('by_name') + user := User.by_name(username, mut database_) or { return none } + return if cryptography.hash_verify(password, user.password) or { false } { user } else { none } +} diff --git a/app/src/jwt/hs256.v b/app/src/jwt/hs256.v index 06501ce..71b606b 100644 --- a/app/src/jwt/hs256.v +++ b/app/src/jwt/hs256.v @@ -10,9 +10,7 @@ pub: } pub fn (algorithm HS256) sign(content string, secret string) !string { - signature := base64.url_encode(hmac.new(secret.bytes(), content.bytes(), sha256.sum, - sha256.block_size)) - return signature + return base64.url_encode(hmac.new(secret.bytes(), content.bytes(), sha256.sum, sha256.block_size)) } pub fn (algorithm HS256) verify(token_raw string, secret string) !Token { diff --git a/app/src/main.v b/app/src/main.v index e005fba..79ed5a6 100644 --- a/app/src/main.v +++ b/app/src/main.v @@ -4,30 +4,25 @@ import os import veb import jwt import util +import sync import database import thomaspeissl.dotenv -import sync -import orm import time +import net.http // structs -pub struct User { -pub mut: - name string - id int -} - pub struct CachedUser { pub: - user User - expires int // unix seconds + user database.User + expires int } pub struct Context { veb.Context pub mut: - user User + app &App + user database.User } pub struct App { @@ -35,15 +30,17 @@ pub struct App { veb.StaticHandler veb.Middleware[Context] pub: - cfg util.Config - embed util.Embedded + cfg util.Config + embed util.Embedded +pub mut: + database database.Database cache_lock sync.RwMutex user_cache map[string]CachedUser -pub mut: - database database.Database } pub struct Auth { + veb.Controller + veb.Middleware[Context] pub: app &App } @@ -74,19 +71,78 @@ pub fn (app &App) root(mut ctx Context, path string) veb.Result { files, meta := util.HtmlBuilder.generate_file_list(entries, abs_path) style := app.embed.style_css + username := if ctx.user.username != '' { ctx.user.username } else { 'noone' } return ctx.html($tmpl('template/dashboard.html')) } // auth endpoints +@['/'] +pub fn (auth &Auth) root(mut ctx Context) veb.Result { + return ctx.redirect('/') +} + @[get; post] pub fn (auth &Auth) login(mut ctx Context) veb.Result { - return ctx.text('login') + if ctx.req.method == .get { + style := auth.app.embed.admin_css + return ctx.html($tmpl('template/auth/login.html')) + } + + token := ctx.get_cookie('veb_session') or { '' } + if token != '' { + return ctx.redirect('/') + } + + username := ctx.form['username'] or { return ctx.request_error('') } + password := ctx.form['password'] or { return ctx.request_error('') } + + if password.len < 8 { + return ctx.request_error('') + } + + if user := database.User.verify(username, password, mut ctx.app.database) { + id := user.id or { return ctx.server_error('') } + ctx.set_cookie(http.Cookie{ + name: 'veb_session' + value: jwt.create(id.str(), ctx.app.cfg.jwt_key) or { return ctx.server_error('') } + path: '/' + secure: true + http_only: true + same_site: .same_site_strict_mode + }) + } else { + // todo cache login attempts per ip as well and block early. + database.Logins.create_or_update(ctx.ip(), mut ctx.app.database) + return ctx.forbidden() + } + + return ctx.redirect('/') } @[get; post] pub fn (auth &Auth) logout(mut ctx Context) veb.Result { - return ctx.text('logout') + if ctx.req.method == .get { + style := auth.app.embed.admin_css + return ctx.html($tmpl('template/auth/logout.html')) + } + + token := ctx.get_cookie('veb_session') or { '' } + if token == '' { + return ctx.redirect('/') + } + + ctx.set_cookie(http.Cookie{ + name: 'veb_session' + value: '' + path: '/' + secure: true + http_only: true + same_site: .same_site_strict_mode + max_age: -1 + }) + + return ctx.redirect('/') } @[get; post; put] @@ -96,7 +152,7 @@ pub fn (auth &Auth) register(mut ctx Context) veb.Result { // middleware -fn (mut app &App) prepare() fn (mut Context) bool { +fn (mut app App) prepare() fn (mut Context) bool { return fn [mut app] (mut ctx Context) bool { ctx.app = &app return true @@ -109,62 +165,45 @@ pub fn session(mut ctx Context) bool { return true } - if id := jwt.decode(token, ctx.app.cfg.jwt_key) { - query := ctx.app.database.get_query_builder[database.Users]().where('id = ?', id) or { return true } - result := ctx.app.database.query[database.Users](query) or { return true } - - ctx.user = result.first() + if id := jwt.decode(token, ctx.app.cfg.jwt_key) { + if user := ctx.app.get_user(id) { + ctx.user = user + } } return true } -// fn (mut app App) get_user_by_id_cached(id int) ?User { -// key := id.str() -// now := time.now().unix() -// -// // read lock -> quick path -// app.cache_lock.rlock() -// if cu := app.user_cache[key] { -// if cu.expires > int(now) { -// app.cache_lock.runlock() -// return cu.user -// } -// } -// app.cache_lock.runlock() -// -// // miss or expired -> fetch from DB -// // assume you have a DB pool on app or ctx.db gives you a connection -// // This example uses app-level DB access via a helper; adapt if your DB is on ctx. -// row := app.get_db().exec_param('SELECT id, name FROM users WHERE id = $1', key) or { -// return error('db error') -// } -// if row.len == 0 { -// return error('not found') -// } -// -// fetched := User{ -// id: row[0].valint(0) -// name: row[0].vals[1] -// } -// -// // write lock -> populate cache -// app.cache_lock.lock() -// app.user_cache[key] = CachedUser{ -// user: fetched -// expires: int(now) + 60 // TTL = 60s, tune to taste -// } -// app.cache_lock.unlock() -// -// return fetched -// } +fn (mut app App) get_user(user_id string) ?database.User { + now := time.now().unix() + app.cache_lock.rlock() + if cu := app.user_cache[user_id] { + if cu.expires > int(now) { + app.cache_lock.runlock() + return cu.user + } + } + + app.cache_lock.runlock() + + user := database.User.by_id(user_id, mut app.database) or { return none } + + app.cache_lock.lock() + app.user_cache[user_id] = CachedUser{ + user: user + expires: int(now) + 300 + } + app.cache_lock.unlock() + + return user +} // utility fn (mut ctx Context) error_page(code int, short string, long string) string { - style := ctx.app.embed.error_css.clone() + style := ctx.app.embed.error_css return $tmpl('template/error.html') } @@ -209,6 +248,7 @@ fn populate() (util.Config, util.Embedded) { }, util.Embedded{ style_css: $embed_file('template/assets/style.css', .zlib).to_string() error_css: $embed_file('template/assets/error.css', .zlib).to_string() + admin_css: $embed_file('template/assets/admin.css', .zlib).to_string() } // vfmt on } @@ -217,7 +257,7 @@ fn main() { // vfmt off mut cfg, mut embed := populate() mut app := &App{ cfg: cfg, embed: embed, database: database.get_database(cfg)! } - mut auth := &Auth{ app: &app } + mut auth := &Auth{ app: app } // vfmt on app.register_controller[Auth, Context]('/auth', mut auth)! diff --git a/app/src/template/assets/admin.css b/app/src/template/assets/admin.css new file mode 100644 index 0000000..7220817 --- /dev/null +++ b/app/src/template/assets/admin.css @@ -0,0 +1,157 @@ +/* General Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Light Theme */ +:root[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f5f7fb; + --bg-tertiary: #eff2f5; + --text-primary: #1a202c; + --text-secondary: #6c757d; + --border-color: #d4d7de; + --accent: #0051ba; + --accent-hover: #003e8f; +} + +/* Dark Theme */ +:root[data-theme="dark"] { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --border-color: #30363d; + --accent: #58a6ff; + --accent-hover: #79c0ff; +} + +/* Default theme */ +:root { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --border-color: #30363d; + --accent: #58a6ff; + --accent-hover: #79c0ff; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + flex-direction: column; + text-align: center; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.container { + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + padding: 2rem 3rem; + border-radius: 0.5rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + width: 100%; + max-width: 400px; +} + +h1 { + font-size: 2rem; + margin-bottom: 1.5rem; + color: var(--accent); +} + +form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +label { + text-align: left; + font-size: 0.875rem; + color: var(--text-secondary); +} + +input[type="text"], +input[type="password"] { + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + border: 1px solid var(--border-color); + background-color: var(--bg-tertiary); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +input[type="text"]:focus, +input[type="password"]:focus { + border-color: var(--accent); + outline: none; +} + +button { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + border: 1px solid var(--border-color); + background-color: var(--bg-tertiary); + color: var(--text-primary); + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; +} + +button:hover { + background-color: var(--accent); + border-color: var(--accent); + color: #fff; +} + +button:active { + transform: scale(0.98); +} + +a.logout-link { + color: var(--accent); + text-decoration: none; + margin-top: 1rem; + display: inline-block; + font-size: 0.875rem; +} + +a.logout-link:hover { + color: var(--accent-hover); +} + +.theme-toggle { + margin-top: 1rem; + width: 1.75rem; + height: 1.75rem; + border: none; + background-color: var(--text-secondary); + border-radius: 6px; + cursor: pointer; +} + +.theme-toggle:active { + transform: scale(0.98); +} + +@media (max-width: 480px) { + .container { + padding: 1.5rem 2rem; + } + + h1 { + font-size: 1.75rem; + } +} diff --git a/app/src/template/auth/login.html b/app/src/template/auth/login.html new file mode 100644 index 0000000..fe6e7a6 --- /dev/null +++ b/app/src/template/auth/login.html @@ -0,0 +1,25 @@ + + + + + + + Login + + + +
+

Login

+
+ + + + +
+
+ + + diff --git a/app/src/template/auth/logout.html b/app/src/template/auth/logout.html new file mode 100644 index 0000000..9831cac --- /dev/null +++ b/app/src/template/auth/logout.html @@ -0,0 +1,21 @@ + + + + + + Logout + + + +
+

Log Out?

+
+ +
+
+ + + diff --git a/app/src/template/dashboard.html b/app/src/template/dashboard.html index 60a783f..1b6bfb2 100644 --- a/app/src/template/dashboard.html +++ b/app/src/template/dashboard.html @@ -3,10 +3,7 @@ - File Browser - + Logged in as @{username} diff --git a/app/src/util/structs.v b/app/src/util/structs.v index 95769dd..cf4a4c4 100644 --- a/app/src/util/structs.v +++ b/app/src/util/structs.v @@ -19,6 +19,7 @@ pub struct Embedded { pub mut: style_css string error_css string + admin_css string } pub struct FileEntry {