diff --git a/app/src/database/db.v b/app/src/database/db.v index 5a6a688..62186d9 100644 --- a/app/src/database/db.v +++ b/app/src/database/db.v @@ -18,7 +18,7 @@ pub fn get_dummy_exclusion_list(exe string, root string) []string { ] } -fn Database.get_connection(cfg util.Config) !sqlite.DB { +fn get_connection(cfg util.Config) !sqlite.DB { conn := sqlite.connect(cfg.database)! if cfg.debug { @@ -35,23 +35,27 @@ fn Database.get_connection(cfg util.Config) !sqlite.DB { return conn } -pub fn Database.get_database(cfg util.Config) !&Database { +pub fn get_database(cfg util.Config) !&Database { return &Database{ cfg: cfg - conn: Database.get_connection(cfg)! + conn: get_connection(cfg)! } } pub fn (mut db Database) query[T](statement orm.QueryBuilder[T]) ![]T { if !db.conn.validate() or { false } { - db.conn = Database.get_connection(db.cfg)! + 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 := Database.get_database(cfg)! + mut db := get_database(cfg)! db.conn.validate()! db.conn.close()! diff --git a/app/src/database/tables.v b/app/src/database/tables.v index bdbd620..d08c8a4 100644 --- a/app/src/database/tables.v +++ b/app/src/database/tables.v @@ -3,21 +3,21 @@ module database import time @[table: 'users'] -struct Users { +pub struct Users { id int @[primary; serial] username string @[nonnull; unique] password string @[nonnull] } @[table: 'login_attempts'] -struct Logins { +pub struct Logins { ip string @[primary] attempts int @[nonnull] attempt_time time.Time @[nonnull] } @[table: 'files'] -struct Files { +pub struct Files { id int @[primary; serial] path string @[nonnull; unique] visible bool @[default: false; nonnull] diff --git a/app/src/jwt/hs256.v b/app/src/jwt/hs256.v index d10dd78..06501ce 100644 --- a/app/src/jwt/hs256.v +++ b/app/src/jwt/hs256.v @@ -17,7 +17,6 @@ pub fn (algorithm HS256) sign(content string, secret string) !string { pub fn (algorithm HS256) verify(token_raw string, secret string) !Token { token := parse_token(token_raw)! - parts := token_raw.split('.') header := parts[0] claims := parts[1] diff --git a/app/src/jwt/jwt.v b/app/src/jwt/jwt.v index dd3b3d9..17305ea 100644 --- a/app/src/jwt/jwt.v +++ b/app/src/jwt/jwt.v @@ -9,10 +9,10 @@ pub fn encode[T](claims T, algorithm Algorithm, secretOrKey string, exp int) !st header := new_header(algorithm) header_b64 := base64.url_encode(json.encode(header).bytes()) - mut claims_final := json2.raw_decode(json.encode(claims))!.as_map() + mut claims_final := json2.decode[json2.Any](json.encode(claims))!.as_map() if exp != 0 { - claims_final["exp"] = time.now().unix_time() + exp + claims_final['exp'] = time.now().unix() + exp } claims_b64 := base64.url_encode(claims_final.str().bytes()) @@ -23,23 +23,18 @@ pub fn encode[T](claims T, algorithm Algorithm, secretOrKey string, exp int) !st } pub fn verify[T](token string, algorithm Algorithm, secretOrKey string) !T { - return algorithm.verify(token, secretOrKey)!.parse_claims() + return algorithm.verify(token, secretOrKey)!.parse_claims[T]() } -pub fn create_jwt(user_id int) string { - alg := new_algorithm(jwt.AlgorithmType.hs256) - token := jwt.encode({ 'id': user_id }, alg, secret_key, 7 * 24 * 60 * 60 * 1000) or { - error('JWT encode error: $err') - } - - return token +pub struct UserJwt { +pub: + id string } -pub fn verify_jwt(token string) !int { - alg := jwt.new_algorithm(jwt.AlgorithmType.hs256) - claims := jwt.verify[token](token, alg, secret_key) or { - return error('Invalid token: $err') - } - - return claims['id'].int() or { return error('ID not found in token') } +pub fn create(user_id string, secret string) !string { + return encode[UserJwt](UserJwt{ id: user_id }, new_algorithm(.hs256), secret, 7 * 24 * 60 * 60)! +} + +pub fn decode(token string, secret string) !string { + return verify[UserJwt](token, new_algorithm(.hs256), secret)!.id } diff --git a/app/src/jwt/structs.v b/app/src/jwt/structs.v index 78e1741..9c1fe73 100644 --- a/app/src/jwt/structs.v +++ b/app/src/jwt/structs.v @@ -45,7 +45,7 @@ pub fn parse_token(token_raw string) !Token { decoded_header := base64.decode_str(header_raw) decoded_claims := base64.decode_str(claims_raw) - claims := json2.raw_decode(decoded_claims)!.as_map() + claims := json2.decode[json2.Any](decoded_claims)!.as_map() mut expiration_given := true diff --git a/app/src/main.v b/app/src/main.v index 9a72c4d..e005fba 100644 --- a/app/src/main.v +++ b/app/src/main.v @@ -2,9 +2,13 @@ module main import os import veb +import jwt import util import database import thomaspeissl.dotenv +import sync +import orm +import time // structs @@ -14,12 +18,16 @@ pub mut: id int } +pub struct CachedUser { +pub: + user User + expires int // unix seconds +} + pub struct Context { veb.Context pub mut: - embed util.Embedded - user User - session_id string + user User } pub struct App { @@ -27,8 +35,12 @@ pub struct App { veb.StaticHandler veb.Middleware[Context] pub: - cfg &util.Config - embed util.Embedded + cfg util.Config + embed util.Embedded + cache_lock sync.RwMutex + user_cache map[string]CachedUser +pub mut: + database database.Database } pub struct Auth { @@ -82,10 +94,77 @@ pub fn (auth &Auth) register(mut ctx Context) veb.Result { return ctx.text('register') } +// middleware + +fn (mut app &App) prepare() fn (mut Context) bool { + return fn [mut app] (mut ctx Context) bool { + ctx.app = &app + return true + } +} + +pub fn session(mut ctx Context) bool { + token := ctx.get_cookie('veb_session') or { '' } + if token == '' { + 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() + } + + 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 +// } + + + // utility fn (mut ctx Context) error_page(code int, short string, long string) string { - style := ctx.embed.error_css.clone() + style := ctx.app.embed.error_css.clone() return $tmpl('template/error.html') } @@ -111,7 +190,7 @@ pub fn (mut ctx Context) server_error(msg string) veb.Result { // main -fn populate() (&util.Config, &util.Embedded) { +fn populate() (util.Config, util.Embedded) { dotenv.load() executable := os.executable() @@ -121,13 +200,13 @@ fn populate() (&util.Config, &util.Embedded) { def_jwt := $d('jwt', 'supersecurejwttokenkey') // vfmt off - return &util.Config{ + return util.Config{ exe: util.Utility.normalize_path(executable) root: util.Utility.normalize_path(if os.getenv('CDN_ROOT') != '' { util.Utility.resolve_path(def_root, os.getenv('CDN_ROOT').str()) } else { def_root }) port: if os.getenv('CDN_PORT') != '' { os.getenv('CDN_PORT').int() } else { def_port } jwt_key: if os.getenv('CDN_JWT_KEY') != '' { os.getenv('CDN_JWT_KEY').str() } else { def_jwt } database: if os.getenv('CDN_SQL_DSN') != '' { os.getenv('CDN_SQL_DSN').str() } else { def_sqlt } - }, &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() } @@ -136,24 +215,17 @@ fn populate() (&util.Config, &util.Embedded) { fn main() { // vfmt off - cfg, mut embed := populate() - mut app := &App{ cfg: cfg, embed: embed } + mut cfg, mut embed := populate() + mut app := &App{ cfg: cfg, embed: embed, database: database.get_database(cfg)! } mut auth := &Auth{ app: &app } // vfmt on - database.test(cfg)! - app.register_controller[Auth, Context]('/auth', mut auth)! app.enable_static_compression = true app.use(veb.encode_auto[Context]()) - - app.use(veb.MiddlewareOptions[Context]{ - handler: fn [mut embed] (mut ctx Context) bool { - ctx.embed = embed - return true - } - }) + app.use(handler: app.prepare()) + app.use(handler: session) veb.run[App, Context](mut app, app.cfg.port) }