Skip to content

Commit 4de5ced

Browse files
committed
add Org + OrgMember tables and basic routes
1 parent 3d31ec6 commit 4de5ced

3 files changed

Lines changed: 241 additions & 0 deletions

File tree

org.v

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2+
// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3+
module main
4+
5+
import time
6+
7+
struct Org {
8+
id int @[primary; sql: serial]
9+
name string @[unique]
10+
contact_email string
11+
kind string
12+
created_at time.Time
13+
created_by int
14+
}
15+
16+
struct OrgMember {
17+
id int @[primary; sql: serial]
18+
org_id int @[unique: 'org_member']
19+
user_id int @[unique: 'org_member']
20+
role string
21+
}
22+
23+
pub fn (mut app App) add_org(name string, contact_email string, kind string, created_by int) !int {
24+
new_org := Org{
25+
name: name
26+
contact_email: contact_email
27+
kind: kind
28+
created_at: time.now()
29+
created_by: created_by
30+
}
31+
sql app.db {
32+
insert new_org into Org
33+
}!
34+
row := app.get_org_by_name(name) or { return error('failed to load newly created org') }
35+
return row.id
36+
}
37+
38+
pub fn (app App) get_org_by_name(name string) ?Org {
39+
rows := sql app.db {
40+
select from Org where name == name limit 1
41+
} or { [] }
42+
if rows.len == 0 {
43+
return none
44+
}
45+
return rows.first()
46+
}
47+
48+
pub fn (app App) get_org_by_id(id int) ?Org {
49+
rows := sql app.db {
50+
select from Org where id == id limit 1
51+
} or { [] }
52+
if rows.len == 0 {
53+
return none
54+
}
55+
return rows.first()
56+
}
57+
58+
pub fn (mut app App) add_org_member(org_id int, user_id int, role string) ! {
59+
member := OrgMember{
60+
org_id: org_id
61+
user_id: user_id
62+
role: role
63+
}
64+
sql app.db {
65+
insert member into OrgMember
66+
}!
67+
}
68+
69+
pub fn (app App) find_orgs_for_user(user_id int) []Org {
70+
members := sql app.db {
71+
select from OrgMember where user_id == user_id
72+
} or { [] }
73+
mut orgs := []Org{cap: members.len}
74+
for m in members {
75+
org := app.get_org_by_id(m.org_id) or { continue }
76+
orgs << org
77+
}
78+
return orgs
79+
}
80+
81+
pub fn (app App) is_org_member(org_id int, user_id int) bool {
82+
count := sql app.db {
83+
select count from OrgMember where org_id == org_id && user_id == user_id
84+
} or { 0 }
85+
return count > 0
86+
}

org_routes.v

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2+
// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3+
module main
4+
5+
import veb
6+
import validation
7+
8+
@['/organizations/new']
9+
pub fn (mut app App) new_org(mut ctx Context) veb.Result {
10+
if !ctx.logged_in {
11+
return ctx.redirect_to_login()
12+
}
13+
return $veb.html('templates/new/org.html')
14+
}
15+
16+
@['/organizations/new'; post]
17+
pub fn (mut app App) handle_new_org(mut ctx Context) veb.Result {
18+
if !ctx.logged_in {
19+
return ctx.redirect_to_login()
20+
}
21+
org_name := ctx.form['org_name']
22+
contact_email := ctx.form['contact_email']
23+
org_kind := ctx.form['org_kind']
24+
accept_terms := ctx.form['accept_terms'] == '1'
25+
26+
if validation.is_string_empty(org_name) {
27+
ctx.error('Organization name is required')
28+
return app.new_org(mut ctx)
29+
}
30+
if org_name.len > max_username_len {
31+
ctx.error('The organization name is too long (should be fewer than ${max_username_len} characters)')
32+
return app.new_org(mut ctx)
33+
}
34+
if org_name.contains(' ') {
35+
ctx.error('Organization name cannot contain spaces')
36+
return app.new_org(mut ctx)
37+
}
38+
if validation.is_string_empty(contact_email) {
39+
ctx.error('Contact email is required')
40+
return app.new_org(mut ctx)
41+
}
42+
if org_kind != 'personal' && org_kind != 'business' {
43+
ctx.error('Please select who this organization belongs to')
44+
return app.new_org(mut ctx)
45+
}
46+
if !accept_terms {
47+
ctx.error('You must accept the Terms of Service')
48+
return app.new_org(mut ctx)
49+
}
50+
if _ := app.get_user_by_username(org_name) {
51+
ctx.error('The name "${org_name}" is already taken')
52+
return app.new_org(mut ctx)
53+
}
54+
if _ := app.get_org_by_name(org_name) {
55+
ctx.error('The name "${org_name}" is already taken')
56+
return app.new_org(mut ctx)
57+
}
58+
59+
org_id := app.add_org(org_name, contact_email, org_kind, ctx.user.id) or {
60+
ctx.error('Could not create organization: ${err}')
61+
return app.new_org(mut ctx)
62+
}
63+
app.add_org_member(org_id, ctx.user.id, 'admin') or {
64+
ctx.error('Could not add you as the organization owner: ${err}')
65+
return app.new_org(mut ctx)
66+
}
67+
return ctx.redirect('/new?owner=${org_name}')
68+
}

templates/new/org.html

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
@include '../layout/head.html'
5+
</head>
6+
<body>
7+
@include '../layout/header.html'
8+
9+
<div class="content">
10+
<div class="new-org">
11+
<div class="new-org__header">
12+
<p class="new-org__subtitle">%new_org_subtitle</p>
13+
<h1>%new_org_title</h1>
14+
</div>
15+
16+
@if ctx.form_error != ''
17+
<div class='form-error'>@ctx.form_error</div>
18+
@end
19+
20+
<form class="new-org__form" method='post' action='/organizations/new'>
21+
<div class="new-org__field">
22+
<label class="new-org__label" for="org_name">%new_org_name <span class="new-org__req">*</span></label>
23+
<input id="org_name" class="new-org__input" type='text' name='org_name' required autofocus maxlength=@max_username_len>
24+
<p class="new-org__hint">%new_org_name_hint</p>
25+
<p class="new-org__hint">%new_org_url_hint https://@app.config.hostname/<span class="new-org__url-slot" id="url_slot"></span></p>
26+
</div>
27+
28+
<div class="new-org__field">
29+
<label class="new-org__label" for="contact_email">%new_org_contact_email <span class="new-org__req">*</span></label>
30+
<input id="contact_email" class="new-org__input" type='email' name='contact_email' required>
31+
</div>
32+
33+
<div class="new-org__field">
34+
<p class="new-org__label">%new_org_belongs_to</p>
35+
<label class="new-org__option">
36+
<input type="radio" name="org_kind" value="personal" required>
37+
<span class="new-org__option-body">
38+
<span class="new-org__option-title">%new_org_personal</span>
39+
<span class="new-org__option-desc">
40+
%new_org_personal_example @ctx.user.username
41+
@if ctx.user.full_name != ''
42+
(@ctx.user.full_name)
43+
@end
44+
</span>
45+
</span>
46+
</label>
47+
<label class="new-org__option">
48+
<input type="radio" name="org_kind" value="business" required>
49+
<span class="new-org__option-body">
50+
<span class="new-org__option-title">%new_org_business</span>
51+
<span class="new-org__option-desc">%new_org_business_example</span>
52+
</span>
53+
</label>
54+
</div>
55+
56+
<div class="new-org__field new-org__field--inline">
57+
<label class="new-org__checkbox">
58+
<input type="checkbox" id="accept_terms" name="accept_terms" value="1" required>
59+
<span>%new_org_accept_terms</span>
60+
</label>
61+
</div>
62+
63+
<div class="new-org__actions">
64+
<input id="new_org_submit" type='submit' class='new-org__submit' value='%new_org_next' disabled>
65+
</div>
66+
</form>
67+
</div>
68+
</div>
69+
70+
<script>
71+
(function() {
72+
var nameInput = document.getElementById('org_name');
73+
var slot = document.getElementById('url_slot');
74+
var terms = document.getElementById('accept_terms');
75+
var submit = document.getElementById('new_org_submit');
76+
function syncUrl() { slot.textContent = nameInput.value; }
77+
function syncSubmit() { submit.disabled = !terms.checked; }
78+
nameInput.addEventListener('input', syncUrl);
79+
terms.addEventListener('change', syncSubmit);
80+
syncUrl();
81+
syncSubmit();
82+
})();
83+
</script>
84+
85+
@include '../layout/footer.html'
86+
</body>
87+
</html>

0 commit comments

Comments
 (0)