From f5e6951a04fdbbd0436fbfe2e6b421fb3696f872 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 16 May 2026 14:49:40 +0300 Subject: [PATCH] api tokens: personal access tokens for REST API auth --- api_token.v | 76 +++++++++++++++++++++++++++++++++++++++ api_token_routes.v | 39 ++++++++++++++++++++ templates/api_tokens.html | 46 ++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 api_token.v create mode 100644 api_token_routes.v create mode 100644 templates/api_tokens.html diff --git a/api_token.v b/api_token.v new file mode 100644 index 0000000..e2e1e84 --- /dev/null +++ b/api_token.v @@ -0,0 +1,76 @@ +// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by a GPL license that can be found in the LICENSE file. +module main + +import time +import crypto.sha256 +import rand +import encoding.hex + +struct ApiToken { + id int @[primary; sql: serial] +mut: + user_id int + name string + token_hash string + created_at int + last_used_at int +} + +fn hash_api_token(plain string) string { + return sha256.sum(plain.bytes()).hex() +} + +fn generate_api_token_plaintext() string { + mut buf := []u8{len: 24} + for i in 0 .. buf.len { + buf[i] = u8(rand.intn(256) or { 0 }) + } + return 'glt_' + hex.encode(buf) +} + +fn (mut app App) add_api_token(user_id int, name string) !(int, string) { + plain := generate_api_token_plaintext() + t := ApiToken{ + user_id: user_id + name: name + token_hash: hash_api_token(plain) + created_at: int(time.now().unix()) + } + sql app.db { + insert t into ApiToken + }! + return db_last_insert_id(app.db), plain +} + +fn (mut app App) list_user_api_tokens(user_id int) []ApiToken { + return sql app.db { + select from ApiToken where user_id == user_id order by id desc + } or { []ApiToken{} } +} + +fn (mut app App) delete_api_token(user_id int, id int) ! { + sql app.db { + delete from ApiToken where id == id && user_id == user_id + }! +} + +fn (mut app App) user_for_api_token(plain string) ?User { + if plain == '' { + return none + } + hashed := hash_api_token(plain) + rows := sql app.db { + select from ApiToken where token_hash == hashed limit 1 + } or { []ApiToken{} } + if rows.len == 0 { + return none + } + t := rows.first() + now := int(time.now().unix()) + id := t.id + sql app.db { + update ApiToken set last_used_at = now where id == id + } or {} + return app.get_user_by_id(t.user_id) +} diff --git a/api_token_routes.v b/api_token_routes.v new file mode 100644 index 0000000..a1ae03a --- /dev/null +++ b/api_token_routes.v @@ -0,0 +1,39 @@ +// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by a GPL license that can be found in the LICENSE file. +module main + +import veb + +@['/:username/settings/api-tokens'] +pub fn (mut app App) view_api_tokens(mut ctx Context, username string) veb.Result { + if !ctx.logged_in || ctx.user.username != username { + return ctx.redirect_to_index() + } + tokens := app.list_user_api_tokens(ctx.user.id) + new_token := ctx.query['new_token'] or { '' } + return $veb.html('templates/api_tokens.html') +} + +@['/:username/settings/api-tokens'; post] +pub fn (mut app App) handle_create_api_token(mut ctx Context, username string) veb.Result { + if !ctx.logged_in || ctx.user.username != username { + return ctx.redirect_to_index() + } + name := ctx.form['name'].trim_space() + if name == '' { + return ctx.redirect('/${username}/settings/api-tokens') + } + _, plain := app.add_api_token(ctx.user.id, name) or { + return ctx.redirect('/${username}/settings/api-tokens') + } + return ctx.redirect('/${username}/settings/api-tokens?new_token=${plain}') +} + +@['/:username/settings/api-tokens/:id/delete'; post] +pub fn (mut app App) handle_delete_api_token(mut ctx Context, username string, id string) veb.Result { + if !ctx.logged_in || ctx.user.username != username { + return ctx.redirect_to_index() + } + app.delete_api_token(ctx.user.id, id.int()) or {} + return ctx.redirect('/${username}/settings/api-tokens') +} diff --git a/templates/api_tokens.html b/templates/api_tokens.html new file mode 100644 index 0000000..1caba82 --- /dev/null +++ b/templates/api_tokens.html @@ -0,0 +1,46 @@ + + + + @include 'layout/head.html' + + + @include 'layout/header.html' + +
+

%api_tokens

+ + @if new_token != '' +
+

%api_token_show_once

+
@new_token
+
+ @end + +
+
+ +
+
+ +
+
+ + @if tokens.len == 0 +

%api_token_none

+ @else + + @end +
+ + @include 'layout/footer.html' + + -- 2.39.5