Closer to lavender v2

This commit is contained in:
Melon 2024-09-02 22:54:03 +01:00
parent 33c7ac9b06
commit 51e33322d3
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
30 changed files with 830 additions and 303 deletions

10
Makefile Normal file
View File

@ -0,0 +1,10 @@
SQL_SRC_DIR := database
SQL_FILES := $(wildcard $(SQL_SRC_DIR)/{migrations,queries}/*.sql)
all: sqlc
sqlc: $(SQL_FILES)
sqlc generate
build: sqlc
go build ./cmd/lavender

View File

@ -1,6 +1,10 @@
package database package database
import "github.com/go-oauth2/oauth2/v4" import (
"bufio"
"github.com/go-oauth2/oauth2/v4"
"strings"
)
var _ oauth2.ClientInfo = &ClientStore{} var _ oauth2.ClientInfo = &ClientStore{}
@ -8,7 +12,7 @@ func (c *ClientStore) GetID() string { return c.Subject }
func (c *ClientStore) GetSecret() string { return c.Secret } func (c *ClientStore) GetSecret() string { return c.Secret }
func (c *ClientStore) GetDomain() string { return c.Domain } func (c *ClientStore) GetDomain() string { return c.Domain }
func (c *ClientStore) IsPublic() bool { return c.Public } func (c *ClientStore) IsPublic() bool { return c.Public }
func (c *ClientStore) GetUserID() string { return c.Owner } func (c *ClientStore) GetUserID() string { return c.OwnerSubject }
// GetName is an extra field for the oauth handler to display the application // GetName is an extra field for the oauth handler to display the application
// name // name
@ -22,4 +26,12 @@ func (c *ClientStore) IsSSO() bool { return c.Sso }
func (c *ClientStore) IsActive() bool { return c.Active } func (c *ClientStore) IsActive() bool { return c.Active }
// UsePerms is an extra field for the userinfo handler to return user permissions matching the requested values // UsePerms is an extra field for the userinfo handler to return user permissions matching the requested values
func (c *ClientStore) UsePerms() string { return c.Perms } func (c *ClientStore) UsePerms() []string {
perms := make([]string, 0)
sc := bufio.NewScanner(strings.NewReader(c.Perms))
sc.Split(bufio.ScanWords)
if sc.Scan() {
perms = append(perms, sc.Text())
}
return perms
}

View File

@ -10,15 +10,22 @@ import (
) )
const getAppList = `-- name: GetAppList :many const getAppList = `-- name: GetAppList :many
SELECT subject, name, domain, owner, perms, public, sso, active SELECT subject,
name,
domain,
owner_subject,
perms,
public,
sso,
active
FROM client_store FROM client_store
WHERE owner = ? WHERE owner_subject = ?
OR ? = 1 OR ? = 1
LIMIT 25 OFFSET ? LIMIT 25 OFFSET ?
` `
type GetAppListParams struct { type GetAppListParams struct {
Owner string `json:"owner"` OwnerSubject string `json:"owner_subject"`
Column2 interface{} `json:"column_2"` Column2 interface{} `json:"column_2"`
Offset int64 `json:"offset"` Offset int64 `json:"offset"`
} }
@ -27,7 +34,7 @@ type GetAppListRow struct {
Subject string `json:"subject"` Subject string `json:"subject"`
Name string `json:"name"` Name string `json:"name"`
Domain string `json:"domain"` Domain string `json:"domain"`
Owner string `json:"owner"` OwnerSubject string `json:"owner_subject"`
Perms string `json:"perms"` Perms string `json:"perms"`
Public bool `json:"public"` Public bool `json:"public"`
Sso bool `json:"sso"` Sso bool `json:"sso"`
@ -35,7 +42,7 @@ type GetAppListRow struct {
} }
func (q *Queries) GetAppList(ctx context.Context, arg GetAppListParams) ([]GetAppListRow, error) { func (q *Queries) GetAppList(ctx context.Context, arg GetAppListParams) ([]GetAppListRow, error) {
rows, err := q.db.QueryContext(ctx, getAppList, arg.Owner, arg.Column2, arg.Offset) rows, err := q.db.QueryContext(ctx, getAppList, arg.OwnerSubject, arg.Column2, arg.Offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -47,7 +54,7 @@ func (q *Queries) GetAppList(ctx context.Context, arg GetAppListParams) ([]GetAp
&i.Subject, &i.Subject,
&i.Name, &i.Name,
&i.Domain, &i.Domain,
&i.Owner, &i.OwnerSubject,
&i.Perms, &i.Perms,
&i.Public, &i.Public,
&i.Sso, &i.Sso,
@ -67,7 +74,7 @@ func (q *Queries) GetAppList(ctx context.Context, arg GetAppListParams) ([]GetAp
} }
const getClientInfo = `-- name: GetClientInfo :one const getClientInfo = `-- name: GetClientInfo :one
SELECT subject, name, secret, domain, owner, perms, public, sso, active SELECT subject, name, secret, domain, owner_subject, perms, public, sso, active
FROM client_store FROM client_store
WHERE subject = ? WHERE subject = ?
LIMIT 1 LIMIT 1
@ -81,7 +88,7 @@ func (q *Queries) GetClientInfo(ctx context.Context, subject string) (ClientStor
&i.Name, &i.Name,
&i.Secret, &i.Secret,
&i.Domain, &i.Domain,
&i.Owner, &i.OwnerSubject,
&i.Perms, &i.Perms,
&i.Public, &i.Public,
&i.Sso, &i.Sso,
@ -91,7 +98,7 @@ func (q *Queries) GetClientInfo(ctx context.Context, subject string) (ClientStor
} }
const insertClientApp = `-- name: InsertClientApp :exec const insertClientApp = `-- name: InsertClientApp :exec
INSERT INTO client_store (subject, name, secret, domain, owner, perms, public, sso, active) INSERT INTO client_store (subject, name, secret, domain, perms, public, sso, active, owner_subject)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
@ -100,11 +107,11 @@ type InsertClientAppParams struct {
Name string `json:"name"` Name string `json:"name"`
Secret string `json:"secret"` Secret string `json:"secret"`
Domain string `json:"domain"` Domain string `json:"domain"`
Owner string `json:"owner"`
Perms string `json:"perms"` Perms string `json:"perms"`
Public bool `json:"public"` Public bool `json:"public"`
Sso bool `json:"sso"` Sso bool `json:"sso"`
Active bool `json:"active"` Active bool `json:"active"`
OwnerSubject string `json:"owner_subject"`
} }
func (q *Queries) InsertClientApp(ctx context.Context, arg InsertClientAppParams) error { func (q *Queries) InsertClientApp(ctx context.Context, arg InsertClientAppParams) error {
@ -113,11 +120,11 @@ func (q *Queries) InsertClientApp(ctx context.Context, arg InsertClientAppParams
arg.Name, arg.Name,
arg.Secret, arg.Secret,
arg.Domain, arg.Domain,
arg.Owner,
arg.Perms, arg.Perms,
arg.Public, arg.Public,
arg.Sso, arg.Sso,
arg.Active, arg.Active,
arg.OwnerSubject,
) )
return err return err
} }
@ -126,17 +133,17 @@ const resetClientAppSecret = `-- name: ResetClientAppSecret :exec
UPDATE client_store UPDATE client_store
SET secret = ? SET secret = ?
WHERE subject = ? WHERE subject = ?
AND owner = ? AND owner_subject = ?
` `
type ResetClientAppSecretParams struct { type ResetClientAppSecretParams struct {
Secret string `json:"secret"` Secret string `json:"secret"`
Subject string `json:"subject"` Subject string `json:"subject"`
Owner string `json:"owner"` OwnerSubject string `json:"owner_subject"`
} }
func (q *Queries) ResetClientAppSecret(ctx context.Context, arg ResetClientAppSecretParams) error { func (q *Queries) ResetClientAppSecret(ctx context.Context, arg ResetClientAppSecretParams) error {
_, err := q.db.ExecContext(ctx, resetClientAppSecret, arg.Secret, arg.Subject, arg.Owner) _, err := q.db.ExecContext(ctx, resetClientAppSecret, arg.Secret, arg.Subject, arg.OwnerSubject)
return err return err
} }
@ -149,7 +156,7 @@ SET name = ?,
sso = ?, sso = ?,
active = ? active = ?
WHERE subject = ? WHERE subject = ?
AND owner = ? AND owner_subject = ?
` `
type UpdateClientAppParams struct { type UpdateClientAppParams struct {
@ -161,7 +168,7 @@ type UpdateClientAppParams struct {
Sso bool `json:"sso"` Sso bool `json:"sso"`
Active bool `json:"active"` Active bool `json:"active"`
Subject string `json:"subject"` Subject string `json:"subject"`
Owner string `json:"owner"` OwnerSubject string `json:"owner_subject"`
} }
func (q *Queries) UpdateClientApp(ctx context.Context, arg UpdateClientAppParams) error { func (q *Queries) UpdateClientApp(ctx context.Context, arg UpdateClientAppParams) error {
@ -174,7 +181,7 @@ func (q *Queries) UpdateClientApp(ctx context.Context, arg UpdateClientAppParams
arg.Sso, arg.Sso,
arg.Active, arg.Active,
arg.Subject, arg.Subject,
arg.Owner, arg.OwnerSubject,
) )
return err return err
} }

View File

@ -7,26 +7,50 @@ package database
import ( import (
"context" "context"
"strings"
"time" "time"
) )
const changeUserActive = `-- name: ChangeUserActive :exec
UPDATE users
SET active = cast(? as boolean)
WHERE subject = ?
`
type ChangeUserActiveParams struct {
Column1 bool `json:"column_1"`
Subject string `json:"subject"`
}
func (q *Queries) ChangeUserActive(ctx context.Context, arg ChangeUserActiveParams) error {
_, err := q.db.ExecContext(ctx, changeUserActive, arg.Column1, arg.Subject)
return err
}
const getUserList = `-- name: GetUserList :many const getUserList = `-- name: GetUserList :many
SELECT subject, SELECT users.subject,
name,
picture,
website,
email, email,
email_verified, email_verified,
roles, users.updated_at as user_updated_at,
updated_at, p.updated_at as profile_updated_at,
active active
FROM users FROM users
LIMIT 25 OFFSET ? INNER JOIN main.profiles p on users.subject = p.subject
LIMIT 50 OFFSET ?
` `
type GetUserListRow struct { type GetUserListRow struct {
Subject string `json:"subject"` Subject string `json:"subject"`
Name string `json:"name"`
Picture string `json:"picture"`
Website string `json:"website"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
Roles string `json:"roles"` UserUpdatedAt time.Time `json:"user_updated_at"`
UpdatedAt time.Time `json:"updated_at"` ProfileUpdatedAt time.Time `json:"profile_updated_at"`
Active bool `json:"active"` Active bool `json:"active"`
} }
@ -41,10 +65,13 @@ func (q *Queries) GetUserList(ctx context.Context, offset int64) ([]GetUserListR
var i GetUserListRow var i GetUserListRow
if err := rows.Scan( if err := rows.Scan(
&i.Subject, &i.Subject,
&i.Name,
&i.Picture,
&i.Website,
&i.Email, &i.Email,
&i.EmailVerified, &i.EmailVerified,
&i.Roles, &i.UserUpdatedAt,
&i.UpdatedAt, &i.ProfileUpdatedAt,
&i.Active, &i.Active,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -60,22 +87,50 @@ func (q *Queries) GetUserList(ctx context.Context, offset int64) ([]GetUserListR
return items, nil return items, nil
} }
const updateUser = `-- name: UpdateUser :exec const getUsersRoles = `-- name: GetUsersRoles :many
UPDATE users SELECT r.role, u.id
SET active = ?, FROM users_roles
roles=? INNER JOIN roles r on r.id = users_roles.role_id
WHERE subject = ? INNER JOIN users u on u.id = users_roles.user_id
WHERE u.id in /*SLICE:user_ids*/?
` `
type UpdateUserParams struct { type GetUsersRolesRow struct {
Active bool `json:"active"` Role string `json:"role"`
Roles string `json:"roles"` ID int64 `json:"id"`
Subject string `json:"subject"`
} }
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { func (q *Queries) GetUsersRoles(ctx context.Context, userIds []int64) ([]GetUsersRolesRow, error) {
_, err := q.db.ExecContext(ctx, updateUser, arg.Active, arg.Roles, arg.Subject) query := getUsersRoles
return err var queryParams []interface{}
if len(userIds) > 0 {
for _, v := range userIds {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:user_ids*/?", strings.Repeat(",?", len(userIds))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:user_ids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUsersRolesRow
for rows.Next() {
var i GetUsersRolesRow
if err := rows.Scan(&i.Role, &i.ID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
} }
const userEmailExists = `-- name: UserEmailExists :one const userEmailExists = `-- name: UserEmailExists :one

View File

@ -1,2 +0,0 @@
DROP TABLE users;
DROP TABLE client_store;

View File

@ -1,27 +0,0 @@
CREATE TABLE users
(
subject TEXT PRIMARY KEY UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
email_verified BOOLEAN DEFAULT 0 NOT NULL,
roles TEXT NOT NULL,
userinfo TEXT NOT NULL,
access_token TEXT,
refresh_token TEXT,
expiry DATETIME,
updated_at DATETIME NOT NULL,
active BOOLEAN DEFAULT 1 NOT NULL
);
CREATE TABLE client_store
(
subject TEXT PRIMARY KEY UNIQUE NOT NULL,
name TEXT NOT NULL,
secret TEXT UNIQUE NOT NULL,
domain TEXT NOT NULL,
owner TEXT NOT NULL,
perms TEXT NOT NULL,
public BOOLEAN NOT NULL,
sso BOOLEAN NOT NULL,
active BOOLEAN DEFAULT 1 NOT NULL,
FOREIGN KEY (owner) REFERENCES users (subject)
);

View File

@ -0,0 +1,5 @@
DROP TABLE users;
DROP INDEX username_index;
DROP TABLE roles;
DROP TABLE otp;
DROP TABLE client_store;

View File

@ -0,0 +1,69 @@
CREATE TABLE users
(
id INTEGER NOT NULL UNIQUE PRIMARY KEY AUTOINCREMENT,
subject TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
email TEXT NOT NULL,
email_verified BOOLEAN NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL,
registered DATETIME NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1
);
CREATE INDEX users_subject ON users (subject);
CREATE TABLE profiles
(
subject TEXT NOT NULL UNIQUE PRIMARY KEY,
name TEXT NOT NULL,
picture TEXT NOT NULL DEFAULT '',
website TEXT NOT NULL DEFAULT '',
pronouns TEXT NOT NULL DEFAULT 'they/them',
birthdate DATE NULL,
zone TEXT NOT NULL DEFAULT 'UTC',
locale TEXT NOT NULL DEFAULT 'en-US',
updated_at DATETIME NOT NULL
);
CREATE TABLE roles
(
id INTEGER NOT NULL UNIQUE PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL UNIQUE
);
CREATE TABLE users_roles
(
role_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY (role_id) REFERENCES roles (id),
FOREIGN KEY (user_id) REFERENCES users (id),
CONSTRAINT user_role UNIQUE (role_id, user_id)
);
CREATE TABLE otp
(
subject INTEGER NOT NULL UNIQUE PRIMARY KEY,
secret TEXT NOT NULL,
digits INTEGER NOT NULL,
FOREIGN KEY (subject) REFERENCES users (subject)
);
CREATE TABLE client_store
(
subject TEXT NOT NULL UNIQUE PRIMARY KEY,
name TEXT NOT NULL,
secret TEXT NOT NULL UNIQUE,
domain TEXT NOT NULL,
owner_subject TEXT NOT NULL,
perms TEXT NOT NULL,
public BOOLEAN NOT NULL,
sso BOOLEAN NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1,
FOREIGN KEY (owner_subject) REFERENCES users (subject)
);

View File

@ -5,8 +5,9 @@
package database package database
import ( import (
"database/sql"
"time" "time"
"github.com/1f349/lavender/password"
) )
type ClientStore struct { type ClientStore struct {
@ -14,22 +15,48 @@ type ClientStore struct {
Name string `json:"name"` Name string `json:"name"`
Secret string `json:"secret"` Secret string `json:"secret"`
Domain string `json:"domain"` Domain string `json:"domain"`
Owner string `json:"owner"` OwnerSubject string `json:"owner_subject"`
Perms string `json:"perms"` Perms string `json:"perms"`
Public bool `json:"public"` Public bool `json:"public"`
Sso bool `json:"sso"` Sso bool `json:"sso"`
Active bool `json:"active"` Active bool `json:"active"`
} }
type User struct { type Otp struct {
Subject int64 `json:"subject"`
Secret string `json:"secret"`
Digits int64 `json:"digits"`
}
type Profile struct {
Subject string `json:"subject"` Subject string `json:"subject"`
Name string `json:"name"`
Picture string `json:"picture"`
Website string `json:"website"`
Pronouns string `json:"pronouns"`
Birthdate interface{} `json:"birthdate"`
Zone string `json:"zone"`
Locale string `json:"locale"`
UpdatedAt time.Time `json:"updated_at"`
}
type Role struct {
ID int64 `json:"id"`
Role string `json:"role"`
}
type User struct {
ID int64 `json:"id"`
Subject string `json:"subject"`
Password password.HashString `json:"password"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
Roles string `json:"roles"`
Userinfo string `json:"userinfo"`
AccessToken sql.NullString `json:"access_token"`
RefreshToken sql.NullString `json:"refresh_token"`
Expiry sql.NullTime `json:"expiry"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Registered time.Time `json:"registered"`
Active bool `json:"active"` Active bool `json:"active"`
} }
type UsersRole struct {
RoleID int64 `json:"role_id"`
UserID int64 `json:"user_id"`
}

81
database/otp.sql.go Normal file
View File

@ -0,0 +1,81 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: otp.sql
package database
import (
"context"
)
const deleteOtp = `-- name: DeleteOtp :exec
DELETE
FROM otp
WHERE otp.subject = ?
`
func (q *Queries) DeleteOtp(ctx context.Context, subject int64) error {
_, err := q.db.ExecContext(ctx, deleteOtp, subject)
return err
}
const getOtp = `-- name: GetOtp :one
SELECT secret, digits
FROM otp
WHERE subject = ?
`
type GetOtpRow struct {
Secret string `json:"secret"`
Digits int64 `json:"digits"`
}
func (q *Queries) GetOtp(ctx context.Context, subject int64) (GetOtpRow, error) {
row := q.db.QueryRowContext(ctx, getOtp, subject)
var i GetOtpRow
err := row.Scan(&i.Secret, &i.Digits)
return i, err
}
const getUserEmail = `-- name: GetUserEmail :one
SELECT email
FROM users
WHERE subject = ?
`
func (q *Queries) GetUserEmail(ctx context.Context, subject string) (string, error) {
row := q.db.QueryRowContext(ctx, getUserEmail, subject)
var email string
err := row.Scan(&email)
return email, err
}
const hasOtp = `-- name: HasOtp :one
SELECT EXISTS(SELECT 1 FROM otp WHERE subject = ?) == 1 as hasOtp
`
func (q *Queries) HasOtp(ctx context.Context, subject int64) (bool, error) {
row := q.db.QueryRowContext(ctx, hasOtp, subject)
var hasotp bool
err := row.Scan(&hasotp)
return hasotp, err
}
const setOtp = `-- name: SetOtp :exec
INSERT OR
REPLACE
INTO otp (subject, secret, digits)
VALUES (?, ?, ?)
`
type SetOtpParams struct {
Subject int64 `json:"subject"`
Secret string `json:"secret"`
Digits int64 `json:"digits"`
}
func (q *Queries) SetOtp(ctx context.Context, arg SetOtpParams) error {
_, err := q.db.ExecContext(ctx, setOtp, arg.Subject, arg.Secret, arg.Digits)
return err
}

View File

@ -0,0 +1,78 @@
package database
import (
"context"
"github.com/1f349/lavender/database/types"
"github.com/1f349/lavender/password"
"github.com/google/uuid"
"time"
)
type AddUserParams struct {
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Role types.UserRole `json:"role"`
UpdatedAt time.Time `json:"updated_at"`
Active bool `json:"active"`
}
func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (string, error) {
pwHash, err := password.HashPassword(arg.Password)
if err != nil {
return "", err
}
n := time.Now()
a := addUserParams{
Subject: uuid.NewString(),
Password: pwHash,
Email: arg.Email,
EmailVerified: false,
UpdatedAt: n,
Registered: n,
Active: true,
}
return a.Subject, q.addUser(ctx, a)
}
type CheckLoginResult struct {
Subject string `json:"subject"`
HasOtp bool `json:"has_otp"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
func (q *Queries) CheckLogin(ctx context.Context, un, pw string) (CheckLoginResult, error) {
login, err := q.checkLogin(ctx, un)
if err != nil {
return CheckLoginResult{}, err
}
err = password.CheckPasswordHash(login.Password, pw)
if err != nil {
return CheckLoginResult{}, err
}
return CheckLoginResult{
Subject: login.Subject,
HasOtp: login.HasOtp,
Email: login.Email,
EmailVerified: login.EmailVerified,
}, nil
}
func (q *Queries) ChangePassword(ctx context.Context, subject, newPw string) error {
userPassword, err := q.getUserPassword(ctx, subject)
if err != nil {
return err
}
newPwHash, err := password.HashPassword(newPw)
if err != nil {
return err
}
return q.changeUserPassword(ctx, changeUserPasswordParams{
Password: newPwHash,
UpdatedAt: time.Now(),
Subject: subject,
Password_2: userPassword,
})
}

62
database/profile-patch.go Normal file
View File

@ -0,0 +1,62 @@
package database
import (
"fmt"
"github.com/1f349/lavender/database/types"
"github.com/hardfinhq/go-date"
"github.com/mrmelon54/pronouns"
"golang.org/x/text/language"
"net/url"
"time"
)
type ProfilePatch struct {
Name string `json:"name"`
Picture string `json:"picture"`
Website string `json:"website"`
Pronouns types.UserPronoun `json:"pronouns"`
Birthdate date.NullDate `json:"birthdate"`
Zone types.UserZone `json:"zone"`
Locale types.UserLocale `json:"locale"`
}
func (p *ProfilePatch) ParseFromForm(v url.Values) (safeErrs []error) {
var err error
p.Name = v.Get("name")
p.Picture = v.Get("picture")
p.Website = v.Get("website")
if v.Has("reset_pronouns") {
p.Pronouns.Pronoun = pronouns.TheyThem
} else {
p.Pronouns.Pronoun, err = pronouns.FindPronoun(v.Get("pronouns"))
if err != nil {
safeErrs = append(safeErrs, fmt.Errorf("invalid pronoun selected"))
}
}
if v.Has("reset_birthdate") || v.Get("birthdate") == "" {
p.Birthdate = date.NullDate{}
} else {
p.Birthdate = date.NullDate{Valid: true}
p.Birthdate.Date, err = date.FromString(v.Get("birthdate"))
if err != nil {
safeErrs = append(safeErrs, fmt.Errorf("invalid time selected"))
}
}
if v.Has("reset_zoneinfo") {
p.Zone.Location = time.UTC
} else {
p.Zone.Location, err = time.LoadLocation(v.Get("zoneinfo"))
if err != nil {
safeErrs = append(safeErrs, fmt.Errorf("invalid timezone selected"))
}
}
if v.Has("reset_locale") {
p.Locale.Tag = language.AmericanEnglish
} else {
p.Locale.Tag, err = language.Parse(v.Get("locale"))
if err != nil {
safeErrs = append(safeErrs, fmt.Errorf("invalid language selected"))
}
}
return
}

74
database/profiles.sql.go Normal file
View File

@ -0,0 +1,74 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: profiles.sql
package database
import (
"context"
"time"
)
const getProfile = `-- name: GetProfile :one
SELECT profiles.subject, profiles.name, profiles.picture, profiles.website, profiles.pronouns, profiles.birthdate, profiles.zone, profiles.locale, profiles.updated_at
FROM profiles
WHERE subject = ?
`
func (q *Queries) GetProfile(ctx context.Context, subject string) (Profile, error) {
row := q.db.QueryRowContext(ctx, getProfile, subject)
var i Profile
err := row.Scan(
&i.Subject,
&i.Name,
&i.Picture,
&i.Website,
&i.Pronouns,
&i.Birthdate,
&i.Zone,
&i.Locale,
&i.UpdatedAt,
)
return i, err
}
const modifyProfile = `-- name: ModifyProfile :exec
UPDATE profiles
SET name = ?,
picture = ?,
website = ?,
pronouns = ?,
birthdate = ?,
zone = ?,
locale = ?,
updated_at = ?
WHERE subject = ?
`
type ModifyProfileParams struct {
Name string `json:"name"`
Picture string `json:"picture"`
Website string `json:"website"`
Pronouns string `json:"pronouns"`
Birthdate interface{} `json:"birthdate"`
Zone string `json:"zone"`
Locale string `json:"locale"`
UpdatedAt time.Time `json:"updated_at"`
Subject string `json:"subject"`
}
func (q *Queries) ModifyProfile(ctx context.Context, arg ModifyProfileParams) error {
_, err := q.db.ExecContext(ctx, modifyProfile,
arg.Name,
arg.Picture,
arg.Website,
arg.Pronouns,
arg.Birthdate,
arg.Zone,
arg.Locale,
arg.UpdatedAt,
arg.Subject,
)
return err
}

View File

@ -5,14 +5,21 @@ WHERE subject = ?
LIMIT 1; LIMIT 1;
-- name: GetAppList :many -- name: GetAppList :many
SELECT subject, name, domain, owner, perms, public, sso, active SELECT subject,
name,
domain,
owner_subject,
perms,
public,
sso,
active
FROM client_store FROM client_store
WHERE owner = ? WHERE owner_subject = ?
OR ? = 1 OR ? = 1
LIMIT 25 OFFSET ?; LIMIT 25 OFFSET ?;
-- name: InsertClientApp :exec -- name: InsertClientApp :exec
INSERT INTO client_store (subject, name, secret, domain, owner, perms, public, sso, active) INSERT INTO client_store (subject, name, secret, domain, perms, public, sso, active, owner_subject)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
-- name: UpdateClientApp :exec -- name: UpdateClientApp :exec
@ -24,10 +31,10 @@ SET name = ?,
sso = ?, sso = ?,
active = ? active = ?
WHERE subject = ? WHERE subject = ?
AND owner = ?; AND owner_subject = ?;
-- name: ResetClientAppSecret :exec -- name: ResetClientAppSecret :exec
UPDATE client_store UPDATE client_store
SET secret = ? SET secret = ?
WHERE subject = ? WHERE subject = ?
AND owner = ?; AND owner_subject = ?;

View File

@ -1,17 +1,27 @@
-- name: GetUserList :many -- name: GetUserList :many
SELECT subject, SELECT users.subject,
name,
picture,
website,
email, email,
email_verified, email_verified,
roles, users.updated_at as user_updated_at,
updated_at, p.updated_at as profile_updated_at,
active active
FROM users FROM users
LIMIT 25 OFFSET ?; INNER JOIN main.profiles p on users.subject = p.subject
LIMIT 50 OFFSET ?;
-- name: UpdateUser :exec -- name: GetUsersRoles :many
SELECT r.role, u.id
FROM users_roles
INNER JOIN roles r on r.id = users_roles.role_id
INNER JOIN users u on u.id = users_roles.user_id
WHERE u.id in sqlc.slice(user_ids);
-- name: ChangeUserActive :exec
UPDATE users UPDATE users
SET active = ?, SET active = cast(? as boolean)
roles=?
WHERE subject = ?; WHERE subject = ?;
-- name: UserEmailExists :one -- name: UserEmailExists :one

23
database/queries/otp.sql Normal file
View File

@ -0,0 +1,23 @@
-- name: SetOtp :exec
INSERT OR
REPLACE
INTO otp (subject, secret, digits)
VALUES (?, ?, ?);
-- name: DeleteOtp :exec
DELETE
FROM otp
WHERE otp.subject = ?;
-- name: GetOtp :one
SELECT secret, digits
FROM otp
WHERE subject = ?;
-- name: HasOtp :one
SELECT EXISTS(SELECT 1 FROM otp WHERE subject = ?) == 1 as hasOtp;
-- name: GetUserEmail :one
SELECT email
FROM users
WHERE subject = ?;

View File

@ -0,0 +1,16 @@
-- name: GetProfile :one
SELECT profiles.*
FROM profiles
WHERE subject = ?;
-- name: ModifyProfile :exec
UPDATE profiles
SET name = ?,
picture = ?,
website = ?,
pronouns = ?,
birthdate = ?,
zone = ?,
locale = ?,
updated_at = ?
WHERE subject = ?;

View File

@ -2,21 +2,15 @@
SELECT count(subject) > 0 AS hasUser SELECT count(subject) > 0 AS hasUser
FROM users; FROM users;
-- name: AddUser :exec -- name: addUser :exec
INSERT INTO users (subject, email, email_verified, roles, userinfo, updated_at, active) INSERT INTO users (subject, password, email, email_verified, updated_at, registered, active)
VALUES (?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?);
-- name: UpdateUserInfo :exec -- name: checkLogin :one
UPDATE users SELECT subject, password, EXISTS(SELECT 1 FROM otp WHERE otp.subject = users.subject) == 1 AS has_otp, email, email_verified
SET email = ?,
email_verified = ?,
userinfo = ?
WHERE subject = ?;
-- name: GetUserRoles :one
SELECT roles
FROM users FROM users
WHERE subject = ?; WHERE users.subject = ?
LIMIT 1;
-- name: GetUser :one -- name: GetUser :one
SELECT * SELECT *
@ -24,20 +18,29 @@ FROM users
WHERE subject = ? WHERE subject = ?
LIMIT 1; LIMIT 1;
-- name: UpdateUserToken :exec -- name: GetUserRoles :many
SELECT r.role
FROM users_roles
INNER JOIN roles r on r.id = users_roles.role_id
INNER JOIN users u on u.id = users_roles.user_id
WHERE u.subject = ?;
-- name: UserHasRole :one
SELECT 1
FROM roles
INNER JOIN users_roles on users_roles.user_id = roles.id
INNER JOIN users u on u.id = users_roles.user_id = u.id
WHERE roles.role = ?
AND u.subject = ?;
-- name: getUserPassword :one
SELECT password
FROM users
WHERE subject = ?;
-- name: changeUserPassword :exec
UPDATE users UPDATE users
SET access_token = ?, SET password = ?,
refresh_token = ?, updated_at=?
expiry = ?
WHERE subject = ?;
-- name: GetUserToken :one
SELECT access_token, refresh_token, expiry
FROM users
WHERE subject = ? WHERE subject = ?
LIMIT 1; AND password = ?;
-- name: GetUserEmail :one
SELECT email
FROM users
WHERE subject = ?;

View File

@ -7,40 +7,13 @@ package database
import ( import (
"context" "context"
"database/sql"
"time" "time"
"github.com/1f349/lavender/password"
) )
const addUser = `-- name: AddUser :exec
INSERT INTO users (subject, email, email_verified, roles, userinfo, updated_at, active)
VALUES (?, ?, ?, ?, ?, ?, ?)
`
type AddUserParams struct {
Subject string `json:"subject"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Roles string `json:"roles"`
Userinfo string `json:"userinfo"`
UpdatedAt time.Time `json:"updated_at"`
Active bool `json:"active"`
}
func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) error {
_, err := q.db.ExecContext(ctx, addUser,
arg.Subject,
arg.Email,
arg.EmailVerified,
arg.Roles,
arg.Userinfo,
arg.UpdatedAt,
arg.Active,
)
return err
}
const getUser = `-- name: GetUser :one const getUser = `-- name: GetUser :one
SELECT subject, email, email_verified, roles, userinfo, access_token, refresh_token, expiry, updated_at, active SELECT id, subject, password, email, email_verified, updated_at, registered, active
FROM users FROM users
WHERE subject = ? WHERE subject = ?
LIMIT 1 LIMIT 1
@ -50,64 +23,47 @@ func (q *Queries) GetUser(ctx context.Context, subject string) (User, error) {
row := q.db.QueryRowContext(ctx, getUser, subject) row := q.db.QueryRowContext(ctx, getUser, subject)
var i User var i User
err := row.Scan( err := row.Scan(
&i.ID,
&i.Subject, &i.Subject,
&i.Password,
&i.Email, &i.Email,
&i.EmailVerified, &i.EmailVerified,
&i.Roles,
&i.Userinfo,
&i.AccessToken,
&i.RefreshToken,
&i.Expiry,
&i.UpdatedAt, &i.UpdatedAt,
&i.Registered,
&i.Active, &i.Active,
) )
return i, err return i, err
} }
const getUserEmail = `-- name: GetUserEmail :one const getUserRoles = `-- name: GetUserRoles :many
SELECT email SELECT r.role
FROM users FROM users_roles
WHERE subject = ? INNER JOIN roles r on r.id = users_roles.role_id
INNER JOIN users u on u.id = users_roles.user_id
WHERE u.subject = ?
` `
func (q *Queries) GetUserEmail(ctx context.Context, subject string) (string, error) { func (q *Queries) GetUserRoles(ctx context.Context, subject string) ([]string, error) {
row := q.db.QueryRowContext(ctx, getUserEmail, subject) rows, err := q.db.QueryContext(ctx, getUserRoles, subject)
var email string if err != nil {
err := row.Scan(&email) return nil, err
return email, err
} }
defer rows.Close()
const getUserRoles = `-- name: GetUserRoles :one var items []string
SELECT roles for rows.Next() {
FROM users var role string
WHERE subject = ? if err := rows.Scan(&role); err != nil {
` return nil, err
func (q *Queries) GetUserRoles(ctx context.Context, subject string) (string, error) {
row := q.db.QueryRowContext(ctx, getUserRoles, subject)
var roles string
err := row.Scan(&roles)
return roles, err
} }
items = append(items, role)
const getUserToken = `-- name: GetUserToken :one
SELECT access_token, refresh_token, expiry
FROM users
WHERE subject = ?
LIMIT 1
`
type GetUserTokenRow struct {
AccessToken sql.NullString `json:"access_token"`
RefreshToken sql.NullString `json:"refresh_token"`
Expiry sql.NullTime `json:"expiry"`
} }
if err := rows.Close(); err != nil {
func (q *Queries) GetUserToken(ctx context.Context, subject string) (GetUserTokenRow, error) { return nil, err
row := q.db.QueryRowContext(ctx, getUserToken, subject) }
var i GetUserTokenRow if err := rows.Err(); err != nil {
err := row.Scan(&i.AccessToken, &i.RefreshToken, &i.Expiry) return nil, err
return i, err }
return items, nil
} }
const hasUser = `-- name: HasUser :one const hasUser = `-- name: HasUser :one
@ -122,52 +78,117 @@ func (q *Queries) HasUser(ctx context.Context) (bool, error) {
return hasuser, err return hasuser, err
} }
const updateUserInfo = `-- name: UpdateUserInfo :exec const userHasRole = `-- name: UserHasRole :one
UPDATE users SELECT 1
SET email = ?, FROM roles
email_verified = ?, INNER JOIN users_roles on users_roles.user_id = roles.id
userinfo = ? INNER JOIN users u on u.id = users_roles.user_id = u.id
WHERE subject = ? WHERE roles.role = ?
AND u.subject = ?
` `
type UpdateUserInfoParams struct { type UserHasRoleParams struct {
Role string `json:"role"`
Subject string `json:"subject"`
}
func (q *Queries) UserHasRole(ctx context.Context, arg UserHasRoleParams) (int64, error) {
row := q.db.QueryRowContext(ctx, userHasRole, arg.Role, arg.Subject)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const addUser = `-- name: addUser :exec
INSERT INTO users (subject, password, email, email_verified, updated_at, registered, active)
VALUES (?, ?, ?, ?, ?, ?, ?)
`
type addUserParams struct {
Subject string `json:"subject"`
Password password.HashString `json:"password"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
Userinfo string `json:"userinfo"` UpdatedAt time.Time `json:"updated_at"`
Subject string `json:"subject"` Registered time.Time `json:"registered"`
Active bool `json:"active"`
} }
func (q *Queries) UpdateUserInfo(ctx context.Context, arg UpdateUserInfoParams) error { func (q *Queries) addUser(ctx context.Context, arg addUserParams) error {
_, err := q.db.ExecContext(ctx, updateUserInfo, _, err := q.db.ExecContext(ctx, addUser,
arg.Subject,
arg.Password,
arg.Email, arg.Email,
arg.EmailVerified, arg.EmailVerified,
arg.Userinfo, arg.UpdatedAt,
arg.Subject, arg.Registered,
arg.Active,
) )
return err return err
} }
const updateUserToken = `-- name: UpdateUserToken :exec const changeUserPassword = `-- name: changeUserPassword :exec
UPDATE users UPDATE users
SET access_token = ?, SET password = ?,
refresh_token = ?, updated_at=?
expiry = ? WHERE subject = ?
AND password = ?
`
type changeUserPasswordParams struct {
Password password.HashString `json:"password"`
UpdatedAt time.Time `json:"updated_at"`
Subject string `json:"subject"`
Password_2 password.HashString `json:"password_2"`
}
func (q *Queries) changeUserPassword(ctx context.Context, arg changeUserPasswordParams) error {
_, err := q.db.ExecContext(ctx, changeUserPassword,
arg.Password,
arg.UpdatedAt,
arg.Subject,
arg.Password_2,
)
return err
}
const checkLogin = `-- name: checkLogin :one
SELECT subject, password, EXISTS(SELECT 1 FROM otp WHERE otp.subject = users.subject) == 1 AS has_otp, email, email_verified
FROM users
WHERE users.subject = ?
LIMIT 1
`
type checkLoginRow struct {
Subject string `json:"subject"`
Password password.HashString `json:"password"`
HasOtp bool `json:"has_otp"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
func (q *Queries) checkLogin(ctx context.Context, subject string) (checkLoginRow, error) {
row := q.db.QueryRowContext(ctx, checkLogin, subject)
var i checkLoginRow
err := row.Scan(
&i.Subject,
&i.Password,
&i.HasOtp,
&i.Email,
&i.EmailVerified,
)
return i, err
}
const getUserPassword = `-- name: getUserPassword :one
SELECT password
FROM users
WHERE subject = ? WHERE subject = ?
` `
type UpdateUserTokenParams struct { func (q *Queries) getUserPassword(ctx context.Context, subject string) (password.HashString, error) {
AccessToken sql.NullString `json:"access_token"` row := q.db.QueryRowContext(ctx, getUserPassword, subject)
RefreshToken sql.NullString `json:"refresh_token"` var password password.HashString
Expiry sql.NullTime `json:"expiry"` err := row.Scan(&password)
Subject string `json:"subject"` return password, err
}
func (q *Queries) UpdateUserToken(ctx context.Context, arg UpdateUserTokenParams) error {
_, err := q.db.ExecContext(ctx, updateUserToken,
arg.AccessToken,
arg.RefreshToken,
arg.Expiry,
arg.Subject,
)
return err
} }

1
go.mod
View File

@ -17,6 +17,7 @@ require (
github.com/golang-migrate/migrate/v4 v4.17.1 github.com/golang-migrate/migrate/v4 v4.17.1
github.com/google/subcommands v1.2.0 github.com/google/subcommands v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hardfinhq/go-date v1.20240411.1
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/mrmelon54/pronouns v1.0.3 github.com/mrmelon54/pronouns v1.0.3

2
go.sum
View File

@ -83,6 +83,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hardfinhq/go-date v1.20240411.1 h1:UskRXxgD+4eCEa8CpiiWLiv2/Vnf1eB90Bojkf7AQ64=
github.com/hardfinhq/go-date v1.20240411.1/go.mod h1:7oxaI9XX4W3/MRDeQXec0fLXFnSJDS7BrazIY2XqPXA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=

View File

@ -26,10 +26,9 @@ func TestManager_CheckNamespace(t *testing.T) {
httpGet = func(url string) (resp *http.Response, err error) { httpGet = func(url string) (resp *http.Response, err error) {
return &http.Response{StatusCode: http.StatusOK, Body: testBody()}, nil return &http.Response{StatusCode: http.StatusOK, Body: testBody()}, nil
} }
manager, err := NewManager([]SsoConfig{ manager, err := NewManager(map[string]SsoConfig{
{ "example.com": {
Addr: testAddrUrl, Addr: testAddrUrl,
Namespace: "example.com",
}, },
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -41,10 +40,9 @@ func TestManager_FindServiceFromLogin(t *testing.T) {
httpGet = func(url string) (resp *http.Response, err error) { httpGet = func(url string) (resp *http.Response, err error) {
return &http.Response{StatusCode: http.StatusOK, Body: testBody()}, nil return &http.Response{StatusCode: http.StatusOK, Body: testBody()}, nil
} }
manager, err := NewManager([]SsoConfig{ manager, err := NewManager(map[string]SsoConfig{
{ "example.com": {
Addr: testAddrUrl, Addr: testAddrUrl,
Namespace: "example.com",
}, },
}) })
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -62,6 +62,7 @@ func (s SsoConfig) FetchConfig() (*WellKnownOIDC, error) {
} }
type WellKnownOIDC struct { type WellKnownOIDC struct {
Namespace string `json:"-"`
Config SsoConfig `json:"-"` Config SsoConfig `json:"-"`
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"` AuthorizationEndpoint string `json:"authorization_endpoint"`

View File

@ -23,7 +23,7 @@ var ErrAuthHttpError = errors.New("auth http error")
func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle { func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle {
return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) { return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
var roles string var roles []string
if h.DbTx(rw, func(tx *database.Queries) (err error) { if h.DbTx(rw, func(tx *database.Queries) (err error) {
roles, err = tx.GetUserRoles(req.Context(), auth.Subject) roles, err = tx.GetUserRoles(req.Context(), auth.Subject)
return return

View File

@ -30,9 +30,9 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
var isAdmin bool var isAdmin bool
h.DbTx(rw, func(tx *database.Queries) (err error) { h.DbTx(rw, func(tx *database.Queries) (err error) {
roles, err := tx.GetUserRoles(req.Context(), auth.Subject) _, err = tx.UserHasRole(req.Context(), database.UserHasRoleParams{Role: "lavender:admin", Subject: auth.Subject})
isAdmin = HasRole(roles, "lavender:admin") isAdmin = err == nil
return err return nil
}) })
pages.RenderPageTemplate(rw, "index", map[string]any{ pages.RenderPageTemplate(rw, "index", map[string]any{

View File

@ -30,9 +30,12 @@ func (j *JWTAccessGenerate) Token(ctx context.Context, data *oauth2.GenerateBasi
return "", "", err return "", "", err
} }
ps := auth.ParsePermStorage(roles) ps := auth.NewPermStorage()
for _, role := range roles {
ps.Set(role)
}
out := auth.NewPermStorage() out := auth.NewPermStorage()
ForEachRole(data.Client.(interface{ UsePerms() string }).UsePerms(), func(role string) { ForEachRole(data.Client.(interface{ UsePerms() []string }).UsePerms(), func(role string) {
for _, i := range ps.Filter(strings.Split(role, " ")).Dump() { for _, i := range ps.Filter(strings.Split(role, " ")).Dump() {
out.Set(i) out.Set(i)
} }

View File

@ -1,25 +1,16 @@
package server package server
import ( func HasRole(roles []string, test string) bool {
"bufio" for _, role := range roles {
"strings" if role == test {
)
func HasRole(roles, test string) bool {
sc := bufio.NewScanner(strings.NewReader(roles))
sc.Split(bufio.ScanWords)
for sc.Scan() {
if sc.Text() == test {
return true return true
} }
} }
return false return false
} }
func ForEachRole(roles string, next func(role string)) { func ForEachRole(roles []string, next func(role string)) {
sc := bufio.NewScanner(strings.NewReader(roles)) for _, role := range roles {
sc.Split(bufio.ScanWords) next(role)
for sc.Scan() {
next(sc.Text())
} }
} }

View File

@ -10,14 +10,14 @@ sql:
emit_json_tags: true emit_json_tags: true
overrides: overrides:
- column: "users.password" - column: "users.password"
go_type: "github.com/1f349/tulip/password.HashString" go_type: "github.com/1f349/lavender/password.HashString"
- column: "users.birthdate" - column: "users.birthdate"
go_type: "github.com/hardfinhq/go-date.NullDate" go_type: "github.com/hardfinhq/go-date.NullDate"
- column: "users.role" - column: "users.role"
go_type: "github.com/1f349/tulip/database/types.UserRole" go_type: "github.com/1f349/lavender/database/types.UserRole"
- column: "users.pronouns" - column: "users.pronouns"
go_type: "github.com/1f349/tulip/database/types.UserPronoun" go_type: "github.com/1f349/lavender/database/types.UserPronoun"
- column: "users.zoneinfo" - column: "users.zoneinfo"
go_type: "github.com/1f349/tulip/database/types.UserZone" go_type: "github.com/1f349/lavender/database/types.UserZone"
- column: "users.locale" - column: "users.locale"
go_type: "github.com/1f349/tulip/database/types.UserLocale" go_type: "github.com/1f349/lavender/database/types.UserLocale"