Refactor invitation system
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -163,7 +163,6 @@ JOIN snapshot_vendors sv ON sv.source_id = av.vendor_id
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": snapshotID,
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
|
||||
@@ -158,7 +158,6 @@ JOIN snapshot_vendors sv ON sv.source_id = dv.vendor_id
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": snapshotID,
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
|
||||
@@ -81,17 +81,17 @@ func (i Invitation) CursorKey(orderBy InvitationOrderField) page.CursorKey {
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation.
|
||||
func (i *Invitation) Create(ctx context.Context, conn pg.Conn) error {
|
||||
func (i *Invitation) Create(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
INSERT INTO authz_invitations (
|
||||
id, organization_id, email, full_name, role, expires_at, created_at
|
||||
tenant_id, id, organization_id, email, full_name, role, expires_at, created_at
|
||||
) VALUES (
|
||||
@id, @organization_id, @email, @full_name, @role, @expires_at, @created_at
|
||||
@tenant_id, @id, @organization_id, @email, @full_name, @role, @expires_at, @created_at
|
||||
)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"id": i.ID,
|
||||
"organization_id": i.OrganizationID,
|
||||
"email": i.Email,
|
||||
@@ -109,21 +109,24 @@ func (i *Invitation) Create(ctx context.Context, conn pg.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because we want to access invitations across all tenants for authentication purposes.
|
||||
func (i *Invitation) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
id gid.GID,
|
||||
) error {
|
||||
query := `
|
||||
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
||||
FROM authz_invitations
|
||||
WHERE id = @id
|
||||
WHERE id = @id AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": id,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -142,18 +145,20 @@ func (i *Invitation) LoadByID(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation.
|
||||
func (i *Invitation) Update(ctx context.Context, conn pg.Conn) error {
|
||||
func (i *Invitation) Update(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
UPDATE authz_invitations
|
||||
SET accepted_at = @accepted_at
|
||||
WHERE id = @id
|
||||
WHERE id = @id AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": i.ID,
|
||||
"accepted_at": i.AcceptedAt,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
result, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -167,16 +172,18 @@ func (i *Invitation) Update(ctx context.Context, conn pg.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation.
|
||||
func (i *Invitation) Delete(ctx context.Context, conn pg.Conn) error {
|
||||
func (i *Invitation) Delete(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
DELETE FROM authz_invitations
|
||||
WHERE id = @id
|
||||
WHERE id = @id AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": i.ID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
result, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -190,21 +197,30 @@ func (i *Invitation) Delete(ctx context.Context, conn pg.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant scope is not applied because this is used to query invitations across all tenants
|
||||
// for a user who doesn't have tenant access yet (before accepting an invitation).
|
||||
func (i *Invitations) LoadByEmail(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
email string,
|
||||
cursor *page.Cursor[InvitationOrderField],
|
||||
filter *InvitationFilter,
|
||||
) error {
|
||||
query := `
|
||||
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
||||
FROM authz_invitations
|
||||
WHERE email = @email AND accepted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
WHERE email = @email
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"email": email,
|
||||
}
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -223,19 +239,23 @@ func (i *Invitations) LoadByEmail(
|
||||
func (i *Invitations) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
orgID gid.GID,
|
||||
cursor *page.Cursor[InvitationOrderField],
|
||||
) error {
|
||||
query := `
|
||||
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
||||
FROM authz_invitations
|
||||
WHERE organization_id = @organization_id
|
||||
WHERE organization_id = @organization_id AND %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, cursor.SQLFragment())
|
||||
query = fmt.Sprintf(query, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": orgID}
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": orgID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
@@ -255,6 +275,7 @@ func (i *Invitations) LoadByOrganizationID(
|
||||
func (i *Invitations) CountByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
orgID gid.GID,
|
||||
) (int, error) {
|
||||
q := `
|
||||
@@ -263,10 +284,51 @@ SELECT
|
||||
FROM
|
||||
authz_invitations
|
||||
WHERE
|
||||
organization_id = @organization_id
|
||||
organization_id = @organization_id AND %s
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": orgID}
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": orgID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
err := row.Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot count invitations: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Tenant scope is not applied because this is used to count invitations across all tenants
|
||||
// for a user who doesn't have tenant access yet (before accepting an invitation).
|
||||
func (i *Invitations) CountByEmail(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
email string,
|
||||
filter *InvitationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
authz_invitations
|
||||
WHERE
|
||||
email = @email
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, filter.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"email": email,
|
||||
}
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
|
||||
49
pkg/coredata/invitation_filter.go
Normal file
49
pkg/coredata/invitation_filter.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
InvitationFilter struct {
|
||||
onlyPending *bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewInvitationFilter(onlyPending *bool) *InvitationFilter {
|
||||
return &InvitationFilter{
|
||||
onlyPending: onlyPending,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *InvitationFilter) SQLArguments() pgx.NamedArgs {
|
||||
return pgx.NamedArgs{
|
||||
"only_pending": f.onlyPending,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *InvitationFilter) SQLFragment() string {
|
||||
return `
|
||||
(
|
||||
CASE
|
||||
WHEN @only_pending::boolean IS NOT NULL AND @only_pending::boolean = true THEN
|
||||
(accepted_at IS NULL AND expires_at > NOW())
|
||||
ELSE TRUE
|
||||
END
|
||||
)`
|
||||
}
|
||||
|
||||
@@ -76,28 +76,20 @@ func (m Membership) CursorKey(orderBy MembershipOrderField) page.CursorKey {
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation.
|
||||
func (m *Membership) Create(ctx context.Context, conn pg.Conn) error {
|
||||
func (m *Membership) Create(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
INSERT INTO authz_memberships (id, user_id, organization_id, role, created_at, updated_at)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(o.tenant_id), @entity_type),
|
||||
@user_id,
|
||||
@organization_id,
|
||||
@role,
|
||||
@created_at,
|
||||
@updated_at
|
||||
FROM organizations o
|
||||
WHERE o.id = @organization_id
|
||||
INSERT INTO authz_memberships (tenant_id, id, user_id, organization_id, role, created_at, updated_at)
|
||||
VALUES (@tenant_id, @id, @user_id, @organization_id, @role, @created_at, @updated_at)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"id": m.ID,
|
||||
"user_id": m.UserID,
|
||||
"organization_id": m.OrganizationID,
|
||||
"role": m.Role,
|
||||
"created_at": m.CreatedAt,
|
||||
"updated_at": m.UpdatedAt,
|
||||
"entity_type": MembershipEntityType,
|
||||
}
|
||||
|
||||
result, err := conn.Exec(ctx, query, args)
|
||||
@@ -116,10 +108,10 @@ func (m *Membership) Create(ctx context.Context, conn pg.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes.
|
||||
func (m *Membership) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
membershipID gid.GID,
|
||||
) error {
|
||||
query := `
|
||||
@@ -134,12 +126,15 @@ func (m *Membership) LoadByID(
|
||||
m.updated_at
|
||||
FROM authz_memberships m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
WHERE m.id = @membership_id
|
||||
WHERE m.id = @membership_id AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"membership_id": membershipID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -158,10 +153,10 @@ func (m *Membership) LoadByID(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes.
|
||||
func (m *Membership) LoadByUserAndOrg(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
userID gid.GID,
|
||||
orgID gid.GID,
|
||||
) error {
|
||||
@@ -177,13 +172,16 @@ func (m *Membership) LoadByUserAndOrg(
|
||||
m.updated_at
|
||||
FROM authz_memberships m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
WHERE m.user_id = @user_id AND m.organization_id = @organization_id
|
||||
WHERE m.user_id = @user_id AND m.organization_id = @organization_id AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": userID,
|
||||
"organization_id": orgID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -202,19 +200,21 @@ func (m *Membership) LoadByUserAndOrg(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation.
|
||||
func (m *Membership) Update(ctx context.Context, conn pg.Conn) error {
|
||||
func (m *Membership) Update(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
UPDATE authz_memberships
|
||||
SET role = @role, updated_at = @updated_at
|
||||
WHERE id = @id
|
||||
WHERE id = @id AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": m.ID,
|
||||
"role": m.Role,
|
||||
"updated_at": m.UpdatedAt,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
result, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -228,16 +228,18 @@ func (m *Membership) Update(ctx context.Context, conn pg.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation.
|
||||
func (m *Membership) Delete(ctx context.Context, conn pg.Conn) error {
|
||||
func (m *Membership) Delete(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
DELETE FROM authz_memberships
|
||||
WHERE id = @id
|
||||
WHERE id = @id AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": m.ID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
result, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -251,10 +253,10 @@ func (m *Membership) Delete(ctx context.Context, conn pg.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because we want to access all user's memberships across tenants for authentication purposes.
|
||||
func (m *Memberships) LoadByUserID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
userID gid.GID,
|
||||
) error {
|
||||
query := `
|
||||
@@ -272,11 +274,17 @@ FROM
|
||||
JOIN users u ON m.user_id = u.id
|
||||
WHERE
|
||||
m.user_id = @user_id
|
||||
AND %s
|
||||
ORDER BY
|
||||
m.created_at DESC
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": userID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
if err != nil {
|
||||
@@ -292,10 +300,10 @@ ORDER BY
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes.
|
||||
func (m *Memberships) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[MembershipOrderField],
|
||||
) error {
|
||||
@@ -315,11 +323,15 @@ JOIN users u ON m.user_id = u.id
|
||||
WHERE
|
||||
m.organization_id = @organization_id
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, cursor.SQLFragment())
|
||||
query = fmt.Sprintf(query, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
@@ -339,14 +351,19 @@ WHERE
|
||||
func (m *Memberships) CountByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) (int, error) {
|
||||
query := `
|
||||
SELECT COUNT(*)
|
||||
FROM authz_memberships
|
||||
WHERE organization_id = @organization_id
|
||||
WHERE organization_id = @organization_id AND %s
|
||||
`
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
row := conn.QueryRow(ctx, query, args)
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ CREATE TYPE authz_role AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
|
||||
-- Create authz_memberships table with id as primary key
|
||||
CREATE TABLE authz_memberships (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
organization_id TEXT NOT NULL,
|
||||
role authz_role NOT NULL,
|
||||
@@ -19,6 +20,7 @@ CREATE TABLE authz_memberships (
|
||||
-- Create authz_invitations table
|
||||
CREATE TABLE authz_invitations (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
organization_id TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
@@ -29,8 +31,9 @@ CREATE TABLE authz_invitations (
|
||||
);
|
||||
|
||||
-- Copy data from users_organizations to authz_memberships
|
||||
INSERT INTO authz_memberships (id, user_id, organization_id, role, created_at, updated_at)
|
||||
INSERT INTO authz_memberships (tenant_id, id, user_id, organization_id, role, created_at, updated_at)
|
||||
SELECT
|
||||
organizations.tenant_id,
|
||||
generate_gid(decode_base64_unpadded(organizations.tenant_id), 38) as id,
|
||||
users_organizations.user_id,
|
||||
users_organizations.organization_id,
|
||||
|
||||
@@ -32,7 +32,6 @@ type (
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Kind PeopleKind `db:"kind"`
|
||||
UserID *gid.GID `db:"user_id"`
|
||||
FullName string `db:"full_name"`
|
||||
PrimaryEmailAddress string `db:"primary_email_address"`
|
||||
AdditionalEmailAddresses []string `db:"additional_email_addresses"`
|
||||
@@ -78,7 +77,6 @@ SELECT
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
user_id,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
@@ -126,7 +124,6 @@ func (p *People) LoadByEmail(
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
user_id,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
@@ -167,58 +164,6 @@ func (p *People) LoadByEmail(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *People) LoadByUserID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
userID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
user_id,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
position,
|
||||
contract_start_date,
|
||||
contract_end_date,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
peoples
|
||||
WHERE
|
||||
%s
|
||||
AND user_id = @user_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query people: %w", err)
|
||||
}
|
||||
|
||||
people, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[People])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrPeopleNotFound{Identifier: userID.String()}
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect people: %w", err)
|
||||
}
|
||||
|
||||
*p = people
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p People) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -230,7 +175,6 @@ INSERT INTO
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
user_id,
|
||||
kind,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
@@ -245,7 +189,6 @@ VALUES (
|
||||
@tenant_id,
|
||||
@people_id,
|
||||
@organization_id,
|
||||
@user_id,
|
||||
@kind,
|
||||
@full_name,
|
||||
@primary_email_address,
|
||||
@@ -262,7 +205,6 @@ VALUES (
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"people_id": p.ID,
|
||||
"organization_id": p.OrganizationID,
|
||||
"user_id": p.UserID,
|
||||
"kind": p.Kind,
|
||||
"full_name": p.FullName,
|
||||
"primary_email_address": p.PrimaryEmailAddress,
|
||||
@@ -343,7 +285,6 @@ SELECT
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
user_id,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
@@ -390,7 +331,6 @@ func (p *People) Update(
|
||||
) error {
|
||||
q := `
|
||||
UPDATE peoples SET
|
||||
user_id = @user_id,
|
||||
full_name = @full_name,
|
||||
primary_email_address = @primary_email_address,
|
||||
additional_email_addresses = @additional_email_addresses,
|
||||
@@ -406,7 +346,6 @@ WHERE %s
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"people_id": p.ID,
|
||||
"user_id": p.UserID,
|
||||
"full_name": p.FullName,
|
||||
"primary_email_address": p.PrimaryEmailAddress,
|
||||
"additional_email_addresses": p.AdditionalEmailAddresses,
|
||||
@@ -447,7 +386,6 @@ SELECT
|
||||
id,
|
||||
organization_id,
|
||||
kind,
|
||||
user_id,
|
||||
full_name,
|
||||
primary_email_address,
|
||||
additional_email_addresses,
|
||||
|
||||
@@ -115,6 +115,7 @@ WHERE
|
||||
func (u *Users) CountByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) (int, error) {
|
||||
q := `
|
||||
@@ -124,11 +125,14 @@ FROM
|
||||
users
|
||||
WHERE
|
||||
id IN (
|
||||
SELECT user_id FROM authz_memberships WHERE organization_id = @organization_id
|
||||
SELECT user_id FROM authz_memberships WHERE organization_id = @organization_id AND %s
|
||||
)
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
)
|
||||
|
||||
type (
|
||||
UserOrganization struct {
|
||||
UserID gid.GID `db:"user_id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
UserOrganizations []*UserOrganization
|
||||
)
|
||||
|
||||
func (uo UserOrganization) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO users_organizations (user_id, organization_id, created_at)
|
||||
VALUES (@user_id, @organization_id, @created_at)
|
||||
`
|
||||
|
||||
_, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"user_id": uo.UserID, "organization_id": uo.OrganizationID, "created_at": uo.CreatedAt})
|
||||
return err
|
||||
}
|
||||
|
||||
// Tenant id scope is not applied because user organizations are managed at the organization level and don't require tenant isolation.
|
||||
func (uo UserOrganization) Delete(ctx context.Context, conn pg.Conn) error {
|
||||
q := `
|
||||
DELETE FROM users_organizations WHERE user_id = @user_id AND organization_id = @organization_id
|
||||
`
|
||||
|
||||
_, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"user_id": uo.UserID, "organization_id": uo.OrganizationID})
|
||||
return err
|
||||
}
|
||||
|
||||
func (uo *UserOrganizations) ForUserID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
userID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT user_id, organization_id, created_at FROM users_organizations WHERE user_id = @user_id
|
||||
`
|
||||
|
||||
rows, err := conn.Query(ctx, q, pgx.StrictNamedArgs{"user_id": userID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userOrganizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[UserOrganization])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*uo = userOrganizations
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user