From 4de5ced2a76bc53b998b59aafbda96221ab37dfe Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 27 May 2026 16:39:01 +0300 Subject: [PATCH] add Org + OrgMember tables and basic routes --- org.v | 86 +++++++++++++++++++++++++++++++++++++++++ org_routes.v | 68 +++++++++++++++++++++++++++++++++ templates/new/org.html | 87 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 org.v create mode 100644 org_routes.v create mode 100644 templates/new/org.html diff --git a/org.v b/org.v new file mode 100644 index 0000000..debcd0f --- /dev/null +++ b/org.v @@ -0,0 +1,86 @@ +// 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 + +struct Org { + id int @[primary; sql: serial] + name string @[unique] + contact_email string + kind string + created_at time.Time + created_by int +} + +struct OrgMember { + id int @[primary; sql: serial] + org_id int @[unique: 'org_member'] + user_id int @[unique: 'org_member'] + role string +} + +pub fn (mut app App) add_org(name string, contact_email string, kind string, created_by int) !int { + new_org := Org{ + name: name + contact_email: contact_email + kind: kind + created_at: time.now() + created_by: created_by + } + sql app.db { + insert new_org into Org + }! + row := app.get_org_by_name(name) or { return error('failed to load newly created org') } + return row.id +} + +pub fn (app App) get_org_by_name(name string) ?Org { + rows := sql app.db { + select from Org where name == name limit 1 + } or { [] } + if rows.len == 0 { + return none + } + return rows.first() +} + +pub fn (app App) get_org_by_id(id int) ?Org { + rows := sql app.db { + select from Org where id == id limit 1 + } or { [] } + if rows.len == 0 { + return none + } + return rows.first() +} + +pub fn (mut app App) add_org_member(org_id int, user_id int, role string) ! { + member := OrgMember{ + org_id: org_id + user_id: user_id + role: role + } + sql app.db { + insert member into OrgMember + }! +} + +pub fn (app App) find_orgs_for_user(user_id int) []Org { + members := sql app.db { + select from OrgMember where user_id == user_id + } or { [] } + mut orgs := []Org{cap: members.len} + for m in members { + org := app.get_org_by_id(m.org_id) or { continue } + orgs << org + } + return orgs +} + +pub fn (app App) is_org_member(org_id int, user_id int) bool { + count := sql app.db { + select count from OrgMember where org_id == org_id && user_id == user_id + } or { 0 } + return count > 0 +} diff --git a/org_routes.v b/org_routes.v new file mode 100644 index 0000000..540c3a0 --- /dev/null +++ b/org_routes.v @@ -0,0 +1,68 @@ +// 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 +import validation + +@['/organizations/new'] +pub fn (mut app App) new_org(mut ctx Context) veb.Result { + if !ctx.logged_in { + return ctx.redirect_to_login() + } + return $veb.html('templates/new/org.html') +} + +@['/organizations/new'; post] +pub fn (mut app App) handle_new_org(mut ctx Context) veb.Result { + if !ctx.logged_in { + return ctx.redirect_to_login() + } + org_name := ctx.form['org_name'] + contact_email := ctx.form['contact_email'] + org_kind := ctx.form['org_kind'] + accept_terms := ctx.form['accept_terms'] == '1' + + if validation.is_string_empty(org_name) { + ctx.error('Organization name is required') + return app.new_org(mut ctx) + } + if org_name.len > max_username_len { + ctx.error('The organization name is too long (should be fewer than ${max_username_len} characters)') + return app.new_org(mut ctx) + } + if org_name.contains(' ') { + ctx.error('Organization name cannot contain spaces') + return app.new_org(mut ctx) + } + if validation.is_string_empty(contact_email) { + ctx.error('Contact email is required') + return app.new_org(mut ctx) + } + if org_kind != 'personal' && org_kind != 'business' { + ctx.error('Please select who this organization belongs to') + return app.new_org(mut ctx) + } + if !accept_terms { + ctx.error('You must accept the Terms of Service') + return app.new_org(mut ctx) + } + if _ := app.get_user_by_username(org_name) { + ctx.error('The name "${org_name}" is already taken') + return app.new_org(mut ctx) + } + if _ := app.get_org_by_name(org_name) { + ctx.error('The name "${org_name}" is already taken') + return app.new_org(mut ctx) + } + + org_id := app.add_org(org_name, contact_email, org_kind, ctx.user.id) or { + ctx.error('Could not create organization: ${err}') + return app.new_org(mut ctx) + } + app.add_org_member(org_id, ctx.user.id, 'admin') or { + ctx.error('Could not add you as the organization owner: ${err}') + return app.new_org(mut ctx) + } + return ctx.redirect('/new?owner=${org_name}') +} diff --git a/templates/new/org.html b/templates/new/org.html new file mode 100644 index 0000000..7f15cd2 --- /dev/null +++ b/templates/new/org.html @@ -0,0 +1,87 @@ + + + + @include '../layout/head.html' + + + @include '../layout/header.html' + +
+
+
+

%new_org_subtitle

+

%new_org_title

+
+ + @if ctx.form_error != '' +
@ctx.form_error
+ @end + +
+
+ + +

%new_org_name_hint

+

%new_org_url_hint https://@app.config.hostname/

+
+ +
+ + +
+ +
+

%new_org_belongs_to

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