Rewrite identity and access management
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -21,10 +21,10 @@ import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
Assets []*Asset
|
||||
|
||||
ErrAssetNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrAssetAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrAssetNotFound) Error() string {
|
||||
return fmt.Sprintf("asset not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrAssetAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (a *Asset) CursorKey(field AssetOrderField) page.CursorKey {
|
||||
switch field {
|
||||
case AssetOrderFieldCreatedAt:
|
||||
@@ -112,7 +96,7 @@ LIMIT 1;
|
||||
asset, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Asset])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrAssetNotFound{Identifier: assetID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect asset: %w", err)
|
||||
@@ -162,7 +146,7 @@ LIMIT 1;
|
||||
asset, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Asset])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrAssetNotFound{Identifier: a.OwnerID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect asset: %w", err)
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
Audits []*Audit
|
||||
|
||||
ErrAuditNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrAuditAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrAuditNotFound) Error() string {
|
||||
return fmt.Sprintf("audit not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrAuditAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (a *Audit) CursorKey(field AuditOrderField) page.CursorKey {
|
||||
switch field {
|
||||
case AuditOrderFieldCreatedAt:
|
||||
@@ -116,7 +100,7 @@ LIMIT 1;
|
||||
audit, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Audit])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrAuditNotFound{Identifier: auditID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect audit: %w", err)
|
||||
@@ -530,7 +514,7 @@ LIMIT 1;
|
||||
audit, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Audit])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrAuditNotFound{Identifier: reportID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect audit: %w", err)
|
||||
|
||||
@@ -44,24 +44,8 @@ type (
|
||||
}
|
||||
|
||||
Controls []*Control
|
||||
|
||||
ErrControlNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrControlAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrControlNotFound) Error() string {
|
||||
return fmt.Sprintf("control not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrControlAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (c Control) CursorKey(orderBy ControlOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case ControlOrderFieldCreatedAt:
|
||||
@@ -661,7 +645,7 @@ LIMIT 1;
|
||||
control, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Control])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrControlNotFound{Identifier: fmt.Sprintf("%s:%s", frameworkID, sectionTitle)}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect control: %w", err)
|
||||
@@ -710,7 +694,7 @@ LIMIT 1;
|
||||
control, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Control])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrControlNotFound{Identifier: controlID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect control: %w", err)
|
||||
@@ -778,9 +762,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "controls_framework_ref_unique" {
|
||||
return &ErrControlAlreadyExists{
|
||||
message: fmt.Sprintf("control with framework_id %s and section_title %q already exists", c.FrameworkID, c.SectionTitle),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert control: %w", err)
|
||||
@@ -848,9 +830,7 @@ WHERE %s
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "controls_framework_ref_unique" {
|
||||
return &ErrControlAlreadyExists{
|
||||
message: fmt.Sprintf("control with section_title %q already exists", c.SectionTitle),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot update control: %w", err)
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -37,17 +37,8 @@ type (
|
||||
}
|
||||
|
||||
ControlDocuments []*ControlDocument
|
||||
|
||||
ErrControlDocumentMappingAlreadyExists struct {
|
||||
ControlID gid.GID
|
||||
DocumentID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrControlDocumentMappingAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("control %s is already mapped to document %s", e.ControlID, e.DocumentID)
|
||||
}
|
||||
|
||||
func (cp ControlDocument) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -84,10 +75,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "controls_policies_pkey" {
|
||||
return &ErrControlDocumentMappingAlreadyExists{
|
||||
ControlID: cp.ControlID,
|
||||
DocumentID: cp.DocumentID,
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,24 +52,8 @@ type (
|
||||
}
|
||||
|
||||
CustomDomains []*CustomDomain
|
||||
|
||||
ErrCustomDomainNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrCustomDomainAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrCustomDomainNotFound) Error() string {
|
||||
return fmt.Sprintf("custom domain not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrCustomDomainAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func NewCustomDomain(tenantID gid.TenantID, domain string) *CustomDomain {
|
||||
now := time.Now()
|
||||
return &CustomDomain{
|
||||
@@ -385,9 +369,7 @@ INSERT INTO custom_domains (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "custom_domains_domain_key" {
|
||||
return &ErrCustomDomainAlreadyExists{
|
||||
message: fmt.Sprintf("custom domain with domain %q already exists", cd.Domain),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert custom domain: %w", err)
|
||||
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
Documents []*Document
|
||||
|
||||
ErrDocumentNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrDocumentAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrDocumentNotFound) Error() string {
|
||||
return fmt.Sprintf("document not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrDocumentAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (p Document) CursorKey(orderBy DocumentOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case DocumentOrderFieldCreatedAt:
|
||||
@@ -114,7 +98,7 @@ LIMIT 1;
|
||||
document, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Document])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrDocumentNotFound{Identifier: documentID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect document: %w", err)
|
||||
@@ -168,7 +152,7 @@ LIMIT 1;
|
||||
document, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Document])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrDocumentNotFound{Identifier: documentID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect document: %w", err)
|
||||
|
||||
@@ -46,32 +46,8 @@ type (
|
||||
}
|
||||
|
||||
DocumentVersions []*DocumentVersion
|
||||
|
||||
ErrDocumentVersionNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrDocumentVersionAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
|
||||
ErrDocumentVersionNoChanges struct {
|
||||
Message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrDocumentVersionNotFound) Error() string {
|
||||
return fmt.Sprintf("document version not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrDocumentVersionAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e ErrDocumentVersionNoChanges) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (p *DocumentVersions) LoadByDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -245,15 +221,8 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" {
|
||||
if pgErr.ConstraintName == "document_versions_document_id_version_number_key" {
|
||||
return &ErrDocumentVersionAlreadyExists{
|
||||
message: fmt.Sprintf("document version with document_id %s and version_number %d already exists", p.DocumentID, p.VersionNumber),
|
||||
}
|
||||
}
|
||||
if pgErr.ConstraintName == "document_one_draft_version_idx" {
|
||||
return &ErrDocumentVersionAlreadyExists{
|
||||
message: fmt.Sprintf("document %s already has a draft version", p.DocumentID),
|
||||
}
|
||||
if pgErr.ConstraintName == "document_versions_document_id_version_number_key" || pgErr.ConstraintName == "document_one_draft_version_idx" {
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,30 +50,7 @@ type (
|
||||
}
|
||||
|
||||
DocumentVersionSignaturesWithPeople []*DocumentVersionSignatureWithPeople
|
||||
|
||||
ErrDocumentVersionSignatureNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrDocumentVersionSignatureAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
|
||||
ErrDocumentVersionSignatureAlreadySigned struct{}
|
||||
)
|
||||
|
||||
func (e ErrDocumentVersionSignatureNotFound) Error() string {
|
||||
return fmt.Sprintf("document version signature not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrDocumentVersionSignatureAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e ErrDocumentVersionSignatureAlreadySigned) Error() string {
|
||||
return "document version already signed"
|
||||
}
|
||||
|
||||
func (pvs DocumentVersionSignature) CursorKey(orderBy DocumentVersionSignatureOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case DocumentVersionSignatureOrderFieldCreatedAt:
|
||||
@@ -225,9 +202,7 @@ INSERT INTO document_version_signatures (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "policy_version_signatures_policy_version_id_signed_by_key" {
|
||||
return &ErrDocumentVersionSignatureAlreadyExists{
|
||||
message: fmt.Sprintf("document version signature with document_version_id %s and signed_by %s already exists", pvs.DocumentVersionID, pvs.SignedBy),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert document version signature: %w", err)
|
||||
|
||||
11
pkg/coredata/errors.go
Normal file
11
pkg/coredata/errors.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrResourceNotFound = errors.New("resource not found")
|
||||
ErrResourceAlreadyExists = errors.New("resource already exists")
|
||||
ErrResourceInUse = errors.New("resource is in use")
|
||||
)
|
||||
@@ -45,24 +45,8 @@ type (
|
||||
}
|
||||
|
||||
Evidences []*Evidence
|
||||
|
||||
ErrEvidenceNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrEvidenceAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrEvidenceNotFound) Error() string {
|
||||
return fmt.Sprintf("evidence not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrEvidenceAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e Evidence) CursorKey(orderBy EvidenceOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case EvidenceOrderFieldCreatedAt:
|
||||
@@ -192,9 +176,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "evidences_reference_id_key" {
|
||||
return &ErrEvidenceAlreadyExists{
|
||||
message: fmt.Sprintf("evidence with task_id %s and reference_id %q already exists", e.TaskID, e.ReferenceID),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert evidence: %w", err)
|
||||
|
||||
11
pkg/coredata/expire_reason.go
Normal file
11
pkg/coredata/expire_reason.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package coredata
|
||||
|
||||
type (
|
||||
ExpireReason string
|
||||
)
|
||||
|
||||
const (
|
||||
ExpireReasonIdleTimeout ExpireReason = "idle_timeout"
|
||||
ExpireReasonRevoked ExpireReason = "revoked"
|
||||
ExpireReasonClosed ExpireReason = "closed"
|
||||
)
|
||||
@@ -42,24 +42,8 @@ type (
|
||||
}
|
||||
|
||||
Files []*File
|
||||
|
||||
ErrFileNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrFileAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrFileNotFound) Error() string {
|
||||
return fmt.Sprintf("file not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrFileAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (f *File) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -100,7 +84,7 @@ LIMIT 1;
|
||||
file, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[File])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrFileNotFound{Identifier: fileID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect file: %w", err)
|
||||
@@ -165,9 +149,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "files_file_key_key" {
|
||||
return &ErrFileAlreadyExists{
|
||||
message: fmt.Sprintf("file with file_key %q already exists", f.FileKey),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert file: %w", err)
|
||||
|
||||
@@ -42,33 +42,8 @@ type (
|
||||
}
|
||||
|
||||
Frameworks []*Framework
|
||||
|
||||
ErrFrameworkNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrFrameworkAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
|
||||
ErrFrameworkReferenceIDAlreadyExists struct {
|
||||
ReferenceID string
|
||||
OrganizationID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrFrameworkNotFound) Error() string {
|
||||
return fmt.Sprintf("framework not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrFrameworkAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e ErrFrameworkReferenceIDAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("framework with reference ID %q already exists for organization %s", e.ReferenceID, e.OrganizationID)
|
||||
}
|
||||
|
||||
func (f *Framework) CursorKey(orderBy FrameworkOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case FrameworkOrderFieldCreatedAt:
|
||||
@@ -192,7 +167,7 @@ LIMIT 1;
|
||||
framework, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Framework])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrFrameworkNotFound{Identifier: referenceID}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect framework: %w", err)
|
||||
@@ -240,7 +215,7 @@ LIMIT 1;
|
||||
framework, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Framework])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrFrameworkNotFound{Identifier: frameworkID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect framework: %w", err)
|
||||
@@ -302,10 +277,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "frameworks_org_ref_unique" {
|
||||
return &ErrFrameworkReferenceIDAlreadyExists{
|
||||
ReferenceID: f.ReferenceID,
|
||||
OrganizationID: f.OrganizationID,
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,24 +42,8 @@ type (
|
||||
}
|
||||
|
||||
Invitations []*Invitation
|
||||
|
||||
InvitationData struct {
|
||||
InvitationID gid.GID `json:"invitation_id"`
|
||||
OrganizationID gid.GID `json:"organization_id"`
|
||||
Email mail.Addr `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
Role MembershipRole `json:"role"`
|
||||
}
|
||||
|
||||
ErrInvitationNotFound struct {
|
||||
ID string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrInvitationNotFound) Error() string {
|
||||
return fmt.Sprintf("invitation not found: %s", e.ID)
|
||||
}
|
||||
|
||||
func (i Invitation) CursorKey(orderBy InvitationOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case InvitationOrderFieldFullName:
|
||||
@@ -83,7 +67,7 @@ func (i Invitation) CursorKey(orderBy InvitationOrderField) page.CursorKey {
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
func (i *Invitation) Create(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
func (i *Invitation) Insert(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
INSERT INTO
|
||||
authz_invitations (
|
||||
@@ -170,8 +154,9 @@ WHERE
|
||||
invitation, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Invitation])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrInvitationNotFound{ID: id.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect invitation: %w", err)
|
||||
}
|
||||
|
||||
@@ -204,25 +189,25 @@ WHERE
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrInvitationNotFound{ID: i.ID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Invitation) Delete(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
func (i *Invitation) Delete(ctx context.Context, conn pg.Conn, scope Scoper, invitationID gid.GID) error {
|
||||
query := `
|
||||
DELETE FROM
|
||||
authz_invitations
|
||||
WHERE
|
||||
id = @id
|
||||
AND %s
|
||||
%s
|
||||
AND id = @invitation_id
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": i.ID,
|
||||
"invitation_id": invitationID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
@@ -232,13 +217,13 @@ WHERE
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrInvitationNotFound{ID: i.ID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Invitations) LoadByEmail(
|
||||
func (i *Invitations) LoadByIdentityID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
@@ -386,7 +371,7 @@ WHERE
|
||||
func (i *Invitations) CountByEmail(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
email string,
|
||||
email mail.Addr,
|
||||
filter *InvitationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
Measures []*Measure
|
||||
|
||||
ErrMeasureNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrMeasureAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrMeasureNotFound) Error() string {
|
||||
return fmt.Sprintf("measure not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrMeasureAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (m Measure) CursorKey(orderBy MeasureOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case MeasureOrderFieldCreatedAt:
|
||||
@@ -414,7 +398,7 @@ LIMIT 1;
|
||||
measure, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Measure])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrMeasureNotFound{Identifier: measureID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect measures: %w", err)
|
||||
@@ -552,9 +536,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "mitigations_org_ref_unique" {
|
||||
return &ErrMeasureAlreadyExists{
|
||||
message: fmt.Sprintf("measure with organization_id %s and reference_id %q already exists", m.OrganizationID, m.ReferenceID),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert measure: %w", err)
|
||||
|
||||
@@ -39,24 +39,8 @@ type (
|
||||
}
|
||||
|
||||
Meetings []*Meeting
|
||||
|
||||
ErrMeetingNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrMeetingAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrMeetingNotFound) Error() string {
|
||||
return fmt.Sprintf("meeting not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrMeetingAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (m Meeting) CursorKey(orderBy MeetingOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case MeetingOrderFieldCreatedAt:
|
||||
@@ -106,7 +90,7 @@ LIMIT 1;
|
||||
meeting, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Meeting])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrMeetingNotFound{Identifier: meetingID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect meeting: %w", err)
|
||||
@@ -271,7 +255,7 @@ WHERE %s
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return &ErrMeetingNotFound{Identifier: m.ID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -300,7 +284,7 @@ WHERE %s
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return &ErrMeetingNotFound{Identifier: m.ID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -43,26 +43,8 @@ type (
|
||||
}
|
||||
|
||||
Memberships []*Membership
|
||||
|
||||
ErrMembershipNotFound struct {
|
||||
UserID gid.GID
|
||||
OrgID gid.GID
|
||||
}
|
||||
|
||||
ErrMembershipAlreadyExists struct {
|
||||
UserID gid.GID
|
||||
OrgID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrMembershipNotFound) Error() string {
|
||||
return fmt.Sprintf("membership not found for user %s in organization %s", e.UserID, e.OrgID)
|
||||
}
|
||||
|
||||
func (e ErrMembershipAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("membership already exists for user %s in organization %s", e.UserID, e.OrgID)
|
||||
}
|
||||
|
||||
func (m Membership) CursorKey(orderBy MembershipOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case MembershipOrderFieldFullName:
|
||||
@@ -78,7 +60,46 @@ func (m Membership) CursorKey(orderBy MembershipOrderField) page.CursorKey {
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
func (m *Membership) Create(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
func (m *Membership) LoadByUserInOrganization(ctx context.Context, conn pg.Conn, userID gid.GID, organizationID gid.GID) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
organization_id,
|
||||
role,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
authz_memberships
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
AND organization_id = @organization_id
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": userID,
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query membership: %w", err)
|
||||
}
|
||||
|
||||
membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Membership])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect membership: %w", err)
|
||||
}
|
||||
|
||||
*m = membership
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Membership) Insert(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
query := `
|
||||
INSERT INTO
|
||||
authz_memberships (
|
||||
@@ -115,8 +136,9 @@ VALUES (
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return ErrMembershipAlreadyExists{UserID: m.UserID, OrgID: m.OrganizationID}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot create membership: %w", err)
|
||||
}
|
||||
|
||||
@@ -178,8 +200,9 @@ JOIN
|
||||
membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Membership])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrMembershipNotFound{UserID: gid.GID{}, OrgID: gid.GID{}}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect membership: %w", err)
|
||||
}
|
||||
|
||||
@@ -243,7 +266,7 @@ LIMIT 1;
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
return &ErrMembershipNotFound{UserID: userID, OrgID: entityID}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
var membership Membership
|
||||
@@ -269,9 +292,9 @@ func (m *Membership) LoadByUserAndOrg(
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
userID gid.GID,
|
||||
orgID gid.GID,
|
||||
organizationID gid.GID,
|
||||
) error {
|
||||
query := `
|
||||
q := `
|
||||
WITH mbr AS (
|
||||
SELECT
|
||||
am.id,
|
||||
@@ -302,20 +325,15 @@ JOIN
|
||||
users u ON mbr.user_id = u.id
|
||||
`
|
||||
|
||||
// Build scope fragment with table alias
|
||||
scopeFragment := scope.SQLFragment()
|
||||
// Replace column references with table-qualified versions
|
||||
scopeFragment = strings.ReplaceAll(scopeFragment, "tenant_id =", "am.tenant_id =")
|
||||
|
||||
query = fmt.Sprintf(query, scopeFragment)
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": userID,
|
||||
"organization_id": orgID,
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, query, args)
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query membership: %w", err)
|
||||
}
|
||||
@@ -323,8 +341,9 @@ JOIN
|
||||
membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Membership])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrMembershipNotFound{UserID: userID, OrgID: orgID}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect membership: %w", err)
|
||||
}
|
||||
|
||||
@@ -359,25 +378,25 @@ WHERE
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrMembershipNotFound{UserID: m.UserID, OrgID: m.OrganizationID}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Membership) Delete(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||
func (m *Membership) Delete(ctx context.Context, conn pg.Conn, scope Scoper, membershipID gid.GID) error {
|
||||
query := `
|
||||
DELETE FROM
|
||||
authz_memberships
|
||||
WHERE
|
||||
id = @id
|
||||
AND %s
|
||||
%s
|
||||
AND id = @membership_id
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": m.ID,
|
||||
"membership_id": membershipID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
@@ -387,7 +406,7 @@ WHERE
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrMembershipNotFound{UserID: m.UserID, OrgID: m.OrganizationID}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -398,6 +417,7 @@ func (m *Memberships) LoadByUserID(
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
userID gid.GID,
|
||||
cursor *page.Cursor[MembershipOrderField],
|
||||
) error {
|
||||
query := `
|
||||
WITH mbr AS (
|
||||
@@ -552,3 +572,29 @@ WHERE
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (m *Memberships) CountByUserID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
userID gid.GID,
|
||||
) (int, error) {
|
||||
query := `
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
authz_memberships
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
`
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
row := conn.QueryRow(ctx, query, args)
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot count memberships: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
9
pkg/coredata/migrations/20251205T120000Z.sql
Normal file
9
pkg/coredata/migrations/20251205T120000Z.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TYPE session_expire_reason AS ENUM (
|
||||
'idle_timeout',
|
||||
'revoked',
|
||||
'closed'
|
||||
);
|
||||
|
||||
ALTER TABLE sessions ADD COLUMN expire_reason session_expire_reason;
|
||||
|
||||
UPDATE sessions SET expire_reason = 'idle_timeout' WHERE expired_at < NOW();
|
||||
2
pkg/coredata/migrations/20251205T140000Z.sql
Normal file
2
pkg/coredata/migrations/20251205T140000Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sessions ADD COLUMN user_agent TEXT DEFAULT 'SESSION_CREATED_BEFORE_USER_AGENT_COLUMN_ADDED';
|
||||
ALTER TABLE sessions ADD COLUMN ip_address INET DEFAULT '::1';
|
||||
8
pkg/coredata/migrations/20251205T150000Z.sql
Normal file
8
pkg/coredata/migrations/20251205T150000Z.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE sessions ADD COLUMN tenant_id TEXT;
|
||||
ALTER TABLE sessions ADD COLUMN parent_session_id TEXT REFERENCES sessions(id);
|
||||
|
||||
ALTER TABLE sessions ADD CONSTRAINT session_tenant_check CHECK (
|
||||
(parent_session_id IS NULL AND tenant_id IS NULL) OR
|
||||
(parent_session_id IS NOT NULL AND tenant_id IS NOT NULL)
|
||||
);
|
||||
|
||||
2
pkg/coredata/migrations/20251212T120000Z.sql
Normal file
2
pkg/coredata/migrations/20251212T120000Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE auth_user_api_keys ADD COLUMN expire_reason TEXT;
|
||||
|
||||
@@ -44,24 +44,8 @@ type (
|
||||
}
|
||||
|
||||
Organizations []*Organization
|
||||
|
||||
ErrOrganizationNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrOrganizationAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrOrganizationNotFound) Error() string {
|
||||
return fmt.Sprintf("organization not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrOrganizationAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (o Organization) CursorKey(orderBy OrganizationOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case OrganizationOrderFieldName:
|
||||
@@ -116,7 +100,7 @@ LIMIT 1;
|
||||
organization, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Organization])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrOrganizationNotFound{Identifier: organizationID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect organization: %w", err)
|
||||
@@ -442,19 +426,14 @@ WHERE
|
||||
func (o *Organization) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM organizations
|
||||
WHERE
|
||||
%s
|
||||
AND id = @id
|
||||
WHERE id = @id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"id": o.ID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
@@ -505,7 +484,7 @@ LIMIT 1
|
||||
organization, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Organization])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrOrganizationNotFound{Identifier: customDomainID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect organization: %w", err)
|
||||
|
||||
@@ -33,16 +33,8 @@ type (
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
ErrOrganizationContextNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrOrganizationContextNotFound) Error() string {
|
||||
return fmt.Sprintf("organization context not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (oc *OrganizationContext) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -76,7 +68,7 @@ LIMIT 1;
|
||||
orgContext, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[OrganizationContext])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrOrganizationContextNotFound{Identifier: organizationID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect organization context: %w", err)
|
||||
@@ -155,7 +147,7 @@ WHERE
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return &ErrOrganizationContextNotFound{Identifier: oc.OrganizationID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -45,32 +45,8 @@ type (
|
||||
}
|
||||
|
||||
Peoples []*People
|
||||
|
||||
ErrPeopleNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrPeopleAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
|
||||
ErrPeopleReferenced struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrPeopleNotFound) Error() string {
|
||||
return fmt.Sprintf("people not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrPeopleAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e ErrPeopleReferenced) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (p People) CursorKey(orderBy PeopleOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case PeopleOrderFieldCreatedAt:
|
||||
@@ -124,7 +100,7 @@ LIMIT 1;
|
||||
people, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[People])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrPeopleNotFound{Identifier: peopleID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect people: %w", err)
|
||||
@@ -175,7 +151,7 @@ LIMIT 1;
|
||||
people, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[People])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrPeopleNotFound{Identifier: primaryEmailAddress}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect people: %w", err)
|
||||
@@ -231,7 +207,7 @@ LIMIT 1;
|
||||
people, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[People])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrPeopleNotFound{Identifier: primaryEmailAddress.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect people: %w", err)
|
||||
@@ -362,9 +338,7 @@ DELETE FROM peoples WHERE %s AND id = @people_id
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23503" {
|
||||
return &ErrPeopleReferenced{
|
||||
message: fmt.Sprintf("person with id %s cannot be deleted because it is referenced by other records", p.ID),
|
||||
}
|
||||
return ErrResourceInUse
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot delete person: %w", err)
|
||||
|
||||
@@ -79,7 +79,7 @@ LIMIT 1;
|
||||
report, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Report])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrDocumentNotFound{Identifier: reportID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect report: %w", err)
|
||||
|
||||
@@ -57,24 +57,8 @@ type (
|
||||
RiskSnapshotter interface {
|
||||
InsertRiskSnapshots(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error
|
||||
}
|
||||
|
||||
ErrRiskNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrRiskAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrRiskNotFound) Error() string {
|
||||
return fmt.Sprintf("risk not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrRiskAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case RiskOrderFieldCreatedAt:
|
||||
@@ -392,7 +376,7 @@ LIMIT 1;
|
||||
risk, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Risk])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrRiskNotFound{Identifier: riskID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect risk: %w", err)
|
||||
|
||||
@@ -16,12 +16,14 @@ package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type SAMLAssertion struct {
|
||||
@@ -31,27 +33,17 @@ type SAMLAssertion struct {
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
}
|
||||
|
||||
type ErrAssertionAlreadyUsed struct {
|
||||
AssertionID string
|
||||
}
|
||||
|
||||
func (e ErrAssertionAlreadyUsed) Error() string {
|
||||
return fmt.Sprintf("assertion ID %q has already been used (replay attack)", e.AssertionID)
|
||||
}
|
||||
|
||||
func (s *SAMLAssertion) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
query := `
|
||||
INSERT INTO auth_saml_assertions (id, tenant_id, organization_id, used_at, expires_at)
|
||||
VALUES (@id, @tenant_id, @organization_id, @used_at, @expires_at)
|
||||
INSERT INTO auth_saml_assertions (id, organization_id, used_at, expires_at)
|
||||
VALUES (@id, @organization_id, @used_at, @expires_at)
|
||||
`
|
||||
|
||||
args := pgx.NamedArgs{
|
||||
"id": s.ID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"organization_id": s.OrganizationID,
|
||||
"used_at": s.UsedAt,
|
||||
"expires_at": s.ExpiresAt,
|
||||
@@ -59,6 +51,11 @@ VALUES (@id, @tenant_id, @organization_id, @used_at, @expires_at)
|
||||
|
||||
_, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" && pgErr.ConstraintName == "auth_saml_assertions_pkey" {
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot insert saml_assertion: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
@@ -23,28 +25,55 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type SAMLConfiguration struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
EmailDomain string `db:"email_domain"`
|
||||
Enabled bool `db:"enabled"`
|
||||
EnforcementPolicy SAMLEnforcementPolicy `db:"enforcement_policy"`
|
||||
IdPEntityID string `db:"idp_entity_id"`
|
||||
IdPSsoURL string `db:"idp_sso_url"`
|
||||
IdPCertificate string `db:"idp_certificate"`
|
||||
IdPMetadataURL *string `db:"idp_metadata_url"`
|
||||
AttributeEmail string `db:"attribute_email"`
|
||||
AttributeFirstname string `db:"attribute_firstname"`
|
||||
AttributeLastname string `db:"attribute_lastname"`
|
||||
AttributeRole string `db:"attribute_role"`
|
||||
AutoSignupEnabled bool `db:"auto_signup_enabled"`
|
||||
DomainVerified bool `db:"domain_verified"`
|
||||
DomainVerificationToken *string `db:"domain_verification_token"`
|
||||
DomainVerifiedAt *time.Time `db:"domain_verified_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
type (
|
||||
SAMLConfiguration struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
EmailDomain string `db:"email_domain"`
|
||||
EnforcementPolicy SAMLEnforcementPolicy `db:"enforcement_policy"`
|
||||
IdPEntityID string `db:"idp_entity_id"`
|
||||
IdPSsoURL string `db:"idp_sso_url"`
|
||||
IdPCertificate string `db:"idp_certificate"`
|
||||
IdPMetadataURL *string `db:"idp_metadata_url"`
|
||||
AttributeEmail string `db:"attribute_email"`
|
||||
AttributeFirstname string `db:"attribute_firstname"`
|
||||
AttributeLastname string `db:"attribute_lastname"`
|
||||
AttributeRole string `db:"attribute_role"`
|
||||
AutoSignupEnabled bool `db:"auto_signup_enabled"`
|
||||
DomainVerified bool `db:"domain_verified"`
|
||||
DomainVerificationToken *string `db:"domain_verification_token"`
|
||||
DomainVerifiedAt *time.Time `db:"domain_verified_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
SAMLConfigurations []*SAMLConfiguration
|
||||
)
|
||||
|
||||
func (s *SAMLConfiguration) CursorKey(orderBy SAMLConfigurationOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case SAMLConfigurationOrderFieldCreatedAt:
|
||||
return page.NewCursorKey(s.ID, s.CreatedAt)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
func (s *SAMLConfiguration) GetIdPCertificate() (*x509.Certificate, error) {
|
||||
block, _ := pem.Decode([]byte(s.IdPCertificate))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("cannot decode PEM block from IdP certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse X.509 certificate: %w", err)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (s *SAMLConfiguration) LoadByOrganizationIDAndEmailDomain(
|
||||
@@ -154,6 +183,10 @@ LIMIT 1;
|
||||
|
||||
config, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SAMLConfiguration])
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect saml_configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -173,7 +206,6 @@ INSERT INTO auth_saml_configurations (
|
||||
tenant_id,
|
||||
organization_id,
|
||||
email_domain,
|
||||
enabled,
|
||||
enforcement_policy,
|
||||
idp_entity_id,
|
||||
idp_sso_url,
|
||||
@@ -194,7 +226,6 @@ INSERT INTO auth_saml_configurations (
|
||||
@tenant_id,
|
||||
@organization_id,
|
||||
@email_domain,
|
||||
@enabled,
|
||||
@enforcement_policy,
|
||||
@idp_entity_id,
|
||||
@idp_sso_url,
|
||||
@@ -218,7 +249,6 @@ INSERT INTO auth_saml_configurations (
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"organization_id": s.OrganizationID,
|
||||
"email_domain": s.EmailDomain,
|
||||
"enabled": s.Enabled,
|
||||
"enforcement_policy": s.EnforcementPolicy,
|
||||
"idp_entity_id": s.IdPEntityID,
|
||||
"idp_sso_url": s.IdPSsoURL,
|
||||
@@ -252,7 +282,6 @@ func (s *SAMLConfiguration) Update(
|
||||
q := `
|
||||
UPDATE auth_saml_configurations
|
||||
SET
|
||||
enabled = @enabled,
|
||||
enforcement_policy = @enforcement_policy,
|
||||
idp_entity_id = @idp_entity_id,
|
||||
idp_sso_url = @idp_sso_url,
|
||||
@@ -276,7 +305,6 @@ WHERE
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": s.ID,
|
||||
"enabled": s.Enabled,
|
||||
"enforcement_policy": s.EnforcementPolicy,
|
||||
"idp_entity_id": s.IdPEntityID,
|
||||
"idp_sso_url": s.IdPSsoURL,
|
||||
@@ -328,18 +356,17 @@ WHERE
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadSAMLConfigurationsByOrganizationID(
|
||||
func (s *SAMLConfigurations) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) ([]*SAMLConfiguration, error) {
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
email_domain,
|
||||
enabled,
|
||||
enforcement_policy,
|
||||
idp_entity_id,
|
||||
idp_sso_url,
|
||||
@@ -370,20 +397,17 @@ ORDER BY email_domain ASC;
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot query auth_saml_configurations: %w", err)
|
||||
return fmt.Errorf("cannot query auth_saml_configurations: %w", err)
|
||||
}
|
||||
|
||||
configs, err := pgx.CollectRows(rows, pgx.RowToStructByName[SAMLConfiguration])
|
||||
samlConfigurations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[SAMLConfiguration])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot collect saml_configurations: %w", err)
|
||||
return fmt.Errorf("cannot collect saml_configurations: %w", err)
|
||||
}
|
||||
|
||||
result := make([]*SAMLConfiguration, len(configs))
|
||||
for i := range configs {
|
||||
result[i] = &configs[i]
|
||||
}
|
||||
*s = samlConfigurations
|
||||
|
||||
return result, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAllEnabledSAMLConfigurationsByEmailDomain loads all enabled SAML configurations for a given email domain
|
||||
@@ -398,7 +422,6 @@ SELECT
|
||||
id,
|
||||
organization_id,
|
||||
email_domain,
|
||||
enabled,
|
||||
enforcement_policy,
|
||||
idp_entity_id,
|
||||
idp_sso_url,
|
||||
@@ -458,7 +481,6 @@ SELECT
|
||||
id,
|
||||
organization_id,
|
||||
email_domain,
|
||||
enabled,
|
||||
enforcement_policy,
|
||||
idp_entity_id,
|
||||
idp_sso_url,
|
||||
@@ -503,3 +525,38 @@ WHERE
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SAMLConfigurations) CountByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
auth_saml_configurations
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot query auth_saml_configurations: %w", err)
|
||||
}
|
||||
|
||||
var count int
|
||||
err = rows.Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot collect count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
40
pkg/coredata/saml_configuration_order_field.go
Normal file
40
pkg/coredata/saml_configuration_order_field.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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
|
||||
|
||||
type (
|
||||
SAMLConfigurationOrderField string
|
||||
)
|
||||
|
||||
const (
|
||||
SAMLConfigurationOrderFieldCreatedAt SAMLConfigurationOrderField = "CREATED_AT"
|
||||
)
|
||||
|
||||
func (p SAMLConfigurationOrderField) Column() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (p SAMLConfigurationOrderField) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (p SAMLConfigurationOrderField) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *SAMLConfigurationOrderField) UnmarshalText(text []byte) error {
|
||||
*p = SAMLConfigurationOrderField(text)
|
||||
return nil
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type SAMLRequest struct {
|
||||
@@ -31,37 +31,18 @@ type SAMLRequest struct {
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
}
|
||||
|
||||
type ErrSAMLRequestNotFound struct {
|
||||
RequestID string
|
||||
}
|
||||
|
||||
func (e ErrSAMLRequestNotFound) Error() string {
|
||||
return fmt.Sprintf("SAML request ID %q not found", e.RequestID)
|
||||
}
|
||||
|
||||
type ErrSAMLRequestExpired struct {
|
||||
RequestID string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (e ErrSAMLRequestExpired) Error() string {
|
||||
return fmt.Sprintf("SAML request ID %q expired at %v", e.RequestID, e.ExpiresAt)
|
||||
}
|
||||
|
||||
func (s *SAMLRequest) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
query := `
|
||||
INSERT INTO auth_saml_requests (id, organization_id, tenant_id, created_at, expires_at)
|
||||
VALUES (@id, @organization_id, @tenant_id, @created_at, @expires_at)
|
||||
INSERT INTO auth_saml_requests (id, organization_id, created_at, expires_at)
|
||||
VALUES (@id, @organization_id, @created_at, @expires_at)
|
||||
`
|
||||
|
||||
args := pgx.NamedArgs{
|
||||
"id": s.ID,
|
||||
"organization_id": s.OrganizationID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"created_at": s.CreatedAt,
|
||||
"expires_at": s.ExpiresAt,
|
||||
}
|
||||
@@ -99,8 +80,9 @@ LIMIT 1
|
||||
|
||||
req, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[SAMLRequest])
|
||||
if err == pgx.ErrNoRows {
|
||||
return ErrSAMLRequestNotFound{RequestID: requestID}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect saml_request: %w", err)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ package coredata
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -62,6 +62,10 @@ func NewScope(tenantID gid.TenantID) *Scope {
|
||||
}
|
||||
}
|
||||
|
||||
func NewScopeFromObjectID(objectID gid.GID) *Scope {
|
||||
return NewScope(objectID.TenantID())
|
||||
}
|
||||
|
||||
func (s *Scope) SQLArguments() pgx.StrictNamedArgs {
|
||||
return pgx.StrictNamedArgs{
|
||||
"tenant_id": s.tenantID,
|
||||
|
||||
@@ -18,26 +18,35 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
Session struct {
|
||||
ID gid.GID `db:"id"`
|
||||
UserID gid.GID `db:"user_id"`
|
||||
Data SessionData `db:"data"`
|
||||
ExpiredAt time.Time `db:"expired_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
ID gid.GID `db:"id"`
|
||||
UserID gid.GID `db:"user_id"`
|
||||
TenantID *gid.TenantID `db:"tenant_id"`
|
||||
ParentSessionID *gid.GID `db:"parent_session_id"`
|
||||
Data SessionData `db:"data"`
|
||||
UserAgent string `db:"user_agent"`
|
||||
IPAddress net.IP `db:"ip_address"`
|
||||
ExpireReason *ExpireReason `db:"expire_reason"`
|
||||
ExpiredAt time.Time `db:"expired_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
Sessions []*Session
|
||||
|
||||
SessionData struct {
|
||||
PasswordAuthenticated bool `json:"password_authenticated"`
|
||||
PasswordAuthenticated bool `json:"password_authenticated"`
|
||||
SAMLAuthenticatedOrgs map[string]SAMLAuthInfo `json:"saml_authenticated_orgs,omitempty"`
|
||||
}
|
||||
|
||||
@@ -46,33 +55,39 @@ type (
|
||||
SAMLConfigID gid.GID `json:"saml_config_id"`
|
||||
SAMLSubject string `json:"saml_subject"`
|
||||
}
|
||||
|
||||
ErrSessionNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrSessionAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrSessionNotFound) Error() string {
|
||||
return fmt.Sprintf("session not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrSessionAlreadyExists) Error() string {
|
||||
return e.message
|
||||
func NewRootSession(userID gid.GID, duration time.Duration) *Session {
|
||||
return &Session{
|
||||
ID: gid.New(gid.NilTenant, SessionEntityType),
|
||||
UserID: userID,
|
||||
ExpiredAt: time.Now().Add(duration),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s Session) CursorKey(orderBy SessionOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case SessionOrderFieldCreatedAt:
|
||||
return page.NewCursorKey(s.ID, s.CreatedAt)
|
||||
case SessionOrderFieldExpiredAt:
|
||||
return page.NewCursorKey(s.ID, s.ExpiredAt)
|
||||
case SessionOrderFieldUpdatedAt:
|
||||
return page.NewCursorKey(s.ID, s.UpdatedAt)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
func (s *Session) IsRootSession() bool {
|
||||
return s.ParentSessionID == nil
|
||||
}
|
||||
|
||||
func (s *Session) IsChildSession() bool {
|
||||
return s.ParentSessionID != nil
|
||||
}
|
||||
|
||||
func (s *Session) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -82,7 +97,12 @@ func (s *Session) LoadByID(
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
data,
|
||||
tenant_id,
|
||||
data,
|
||||
parent_session_id,
|
||||
expire_reason,
|
||||
user_agent,
|
||||
ip_address,
|
||||
expired_at,
|
||||
created_at,
|
||||
updated_at
|
||||
@@ -103,7 +123,7 @@ LIMIT 1;
|
||||
session, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Session])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrSessionNotFound{Identifier: sessionID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect session: %w", err)
|
||||
@@ -119,11 +139,16 @@ func (s *Session) Insert(
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
sessions (id, user_id, data, expired_at, created_at, updated_at)
|
||||
sessions (id, user_id, tenant_id, data, parent_session_id, expire_reason, user_agent, ip_address, expired_at, created_at, updated_at)
|
||||
VALUES (
|
||||
@session_id,
|
||||
@user_id,
|
||||
@tenant_id,
|
||||
@data,
|
||||
@parent_session_id,
|
||||
@expire_reason,
|
||||
@user_agent,
|
||||
@ip_address,
|
||||
@expired_at,
|
||||
@created_at,
|
||||
@updated_at
|
||||
@@ -131,12 +156,17 @@ VALUES (
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"session_id": s.ID,
|
||||
"user_id": s.UserID,
|
||||
"data": s.Data,
|
||||
"expired_at": s.ExpiredAt,
|
||||
"created_at": s.CreatedAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
"session_id": s.ID,
|
||||
"user_id": s.UserID,
|
||||
"tenant_id": s.TenantID,
|
||||
"data": s.Data,
|
||||
"parent_session_id": s.ParentSessionID,
|
||||
"expire_reason": s.ExpireReason,
|
||||
"user_agent": s.UserAgent,
|
||||
"ip_address": s.IPAddress,
|
||||
"expired_at": s.ExpiredAt,
|
||||
"created_at": s.CreatedAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
@@ -152,36 +182,121 @@ UPDATE sessions
|
||||
SET
|
||||
expired_at = @expired_at,
|
||||
updated_at = @updated_at,
|
||||
user_agent = @user_agent,
|
||||
ip_address = @ip_address,
|
||||
expire_reason = @expire_reason,
|
||||
data = @data
|
||||
WHERE
|
||||
id = @session_id
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"session_id": s.ID,
|
||||
"data": s.Data,
|
||||
"expired_at": s.ExpiredAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
"session_id": s.ID,
|
||||
"user_agent": s.UserAgent,
|
||||
"ip_address": s.IPAddress,
|
||||
"expire_reason": s.ExpireReason,
|
||||
"data": s.Data,
|
||||
"expired_at": s.ExpiredAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
result, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update session: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteSession(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
sessionID gid.GID,
|
||||
) error {
|
||||
func (s *Sessions) LoadByUserID(ctx context.Context, conn pg.Conn, userID gid.GID, cursor *page.Cursor[SessionOrderField]) error {
|
||||
q := `
|
||||
DELETE FROM
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
tenant_id,
|
||||
data,
|
||||
parent_session_id,
|
||||
expire_reason,
|
||||
user_agent,
|
||||
ip_address,
|
||||
expired_at,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
sessions
|
||||
WHERE
|
||||
id = @session_id
|
||||
user_id = @user_id
|
||||
AND %s
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"session_id": sessionID}
|
||||
q = fmt.Sprintf(q, cursor.SQLFragment())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query sessions: %w", err)
|
||||
}
|
||||
|
||||
sessions, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Session])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect sessions: %w", err)
|
||||
}
|
||||
|
||||
*s = sessions
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sessions) CountByUserID(ctx context.Context, conn pg.Conn, userID gid.GID) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
sessions
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot scan count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *Sessions) ExpireAllForUserExceptOneSession(ctx context.Context, conn pg.Conn, userID gid.GID, sessionID gid.GID) (int64, error) {
|
||||
q := `
|
||||
UPDATE sessions
|
||||
SET
|
||||
expired_at = NOW(),
|
||||
updated_at = NOW(),
|
||||
expire_reason = 'revoked'
|
||||
WHERE
|
||||
id != @session_id
|
||||
AND user_id = @user_id
|
||||
AND expire_reason IS NULL
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"session_id": sessionID,
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
result, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot query sessions: %w", err)
|
||||
}
|
||||
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
@@ -20,9 +20,20 @@ type (
|
||||
|
||||
const (
|
||||
SessionOrderFieldCreatedAt SessionOrderField = "CREATED_AT"
|
||||
SessionOrderFieldExpiredAt SessionOrderField = "EXPIRED_AT"
|
||||
SessionOrderFieldUpdatedAt SessionOrderField = "UPDATED_AT"
|
||||
)
|
||||
|
||||
func (p SessionOrderField) Column() string {
|
||||
switch p {
|
||||
case SessionOrderFieldCreatedAt:
|
||||
return "created_at"
|
||||
case SessionOrderFieldExpiredAt:
|
||||
return "expired_at"
|
||||
case SessionOrderFieldUpdatedAt:
|
||||
return "updated_at"
|
||||
}
|
||||
|
||||
return string(p)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,24 +41,8 @@ type (
|
||||
}
|
||||
|
||||
StatesOfApplicability []*StateOfApplicability
|
||||
|
||||
ErrStateOfApplicabilityNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrStateOfApplicabilityAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrStateOfApplicabilityNotFound) Error() string {
|
||||
return fmt.Sprintf("state of applicability not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrStateOfApplicabilityAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (s StateOfApplicability) CursorKey(orderBy StateOfApplicabilityOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case StateOfApplicabilityOrderFieldCreatedAt:
|
||||
@@ -107,7 +91,7 @@ LIMIT 1;
|
||||
stateOfApplicability, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[StateOfApplicability])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrStateOfApplicabilityNotFound{Identifier: stateOfApplicabilityID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect state_of_applicability: %w", err)
|
||||
@@ -246,9 +230,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" {
|
||||
return &ErrStateOfApplicabilityAlreadyExists{
|
||||
message: fmt.Sprintf("state of applicability with name %q already exists", s.Name),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert state_of_applicability: %w", err)
|
||||
@@ -287,16 +269,14 @@ WHERE %s
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" {
|
||||
return &ErrStateOfApplicabilityAlreadyExists{
|
||||
message: fmt.Sprintf("state of applicability with name %q already exists", s.Name),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot update state_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return &ErrStateOfApplicabilityNotFound{Identifier: s.ID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -325,7 +305,7 @@ WHERE %s
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return &ErrStateOfApplicabilityNotFound{Identifier: s.ID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -46,24 +46,8 @@ type (
|
||||
}
|
||||
|
||||
Tasks []*Task
|
||||
|
||||
ErrTaskNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrTaskAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrTaskNotFound) Error() string {
|
||||
return fmt.Sprintf("task not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrTaskAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (c Task) CursorKey(orderBy TaskOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case TaskOrderFieldCreatedAt:
|
||||
@@ -114,7 +98,7 @@ LIMIT 1;
|
||||
task, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Task])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrTaskNotFound{Identifier: taskID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect tasks: %w", err)
|
||||
@@ -185,9 +169,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "tasks_reference_id_unique" {
|
||||
return &ErrTaskAlreadyExists{
|
||||
message: fmt.Sprintf("task with measure_id %s and reference_id %q already exists", c.MeasureID, c.ReferenceID),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert task: %w", err)
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -41,24 +41,8 @@ type (
|
||||
}
|
||||
|
||||
TrustCenters []*TrustCenter
|
||||
|
||||
ErrTrustCenterNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrTrustCenterAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrTrustCenterNotFound) Error() string {
|
||||
return fmt.Sprintf("trust center not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrTrustCenterAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (tc *TrustCenter) CursorKey(orderBy TrustCenterOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case TrustCenterOrderFieldCreatedAt:
|
||||
@@ -239,9 +223,7 @@ INSERT INTO trust_centers (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "trust_centers_slug_key" {
|
||||
return &ErrTrustCenterAlreadyExists{
|
||||
message: fmt.Sprintf("trust center with slug %q already exists", tc.Slug),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert trust center: %w", err)
|
||||
|
||||
@@ -48,24 +48,8 @@ type (
|
||||
}
|
||||
|
||||
TrustCenterAccesses []*TrustCenterAccess
|
||||
|
||||
ErrTrustCenterAccessNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrTrustCenterAccessAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrTrustCenterAccessNotFound) Error() string {
|
||||
return fmt.Sprintf("trust center access not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrTrustCenterAccessAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (tca *TrustCenterAccess) CursorKey(orderBy TrustCenterAccessOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case TrustCenterAccessOrderFieldCreatedAt:
|
||||
@@ -117,7 +101,7 @@ LIMIT 1;
|
||||
access, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[TrustCenterAccess])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrTrustCenterAccessNotFound{Identifier: accessID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect trust center access: %w", err)
|
||||
@@ -175,7 +159,7 @@ LIMIT 1;
|
||||
access, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[TrustCenterAccess])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrTrustCenterAccessNotFound{Identifier: fmt.Sprintf("trust_center_id=%s, email=%s", trustCenterID, email)}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect trust center access: %w", err)
|
||||
@@ -235,9 +219,7 @@ INSERT INTO trust_center_accesses (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "trust_center_accesses_trust_center_id_email_key" {
|
||||
return &ErrTrustCenterAccessAlreadyExists{
|
||||
message: "trust center access already exists",
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert trust center access: %w", err)
|
||||
|
||||
@@ -42,24 +42,8 @@ type (
|
||||
}
|
||||
|
||||
TrustCenterDocumentAccesses []*TrustCenterDocumentAccess
|
||||
|
||||
ErrTrustCenterDocumentAccessNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrTrustCenterDocumentAccessAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrTrustCenterDocumentAccessNotFound) Error() string {
|
||||
return fmt.Sprintf("trust center document access not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrTrustCenterDocumentAccessAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (tcda *TrustCenterDocumentAccess) CursorKey(orderBy TrustCenterDocumentAccessOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case TrustCenterDocumentAccessOrderFieldCreatedAt:
|
||||
@@ -107,7 +91,7 @@ LIMIT 1;
|
||||
access, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[TrustCenterDocumentAccess])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrTrustCenterDocumentAccessNotFound{Identifier: accessID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
return fmt.Errorf("cannot collect trust center document access: %w", err)
|
||||
}
|
||||
@@ -267,18 +251,10 @@ INSERT INTO trust_center_document_accesses (
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" {
|
||||
switch pgErr.ConstraintName {
|
||||
case "trust_center_document_accesse_trust_center_access_id_docume_key":
|
||||
return &ErrTrustCenterDocumentAccessAlreadyExists{
|
||||
message: fmt.Sprintf("trust center document access with trust_center_access_id %s and document_id %s already exists", tcda.TrustCenterAccessID, tcda.DocumentID),
|
||||
}
|
||||
case "trust_center_document_accesse_trust_center_access_id_report_key":
|
||||
return &ErrTrustCenterDocumentAccessAlreadyExists{
|
||||
message: fmt.Sprintf("trust center document access with trust_center_access_id %s and report_id %s already exists", tcda.TrustCenterAccessID, tcda.ReportID),
|
||||
}
|
||||
case "trust_center_document_accesses_trust_center_file_id_key":
|
||||
return &ErrTrustCenterDocumentAccessAlreadyExists{
|
||||
message: fmt.Sprintf("trust center document access with trust_center_access_id %s and trust_center_file_id %s already exists", tcda.TrustCenterAccessID, tcda.TrustCenterFileID),
|
||||
}
|
||||
case "trust_center_document_accesse_trust_center_access_id_docume_key",
|
||||
"trust_center_document_accesse_trust_center_access_id_report_key",
|
||||
"trust_center_document_accesses_trust_center_file_id_key":
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
TrustCenterReferences []*TrustCenterReference
|
||||
|
||||
ErrTrustCenterReferenceNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrTrustCenterReferenceAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrTrustCenterReferenceNotFound) Error() string {
|
||||
return fmt.Sprintf("trust center reference not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrTrustCenterReferenceAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (t TrustCenterReference) CursorKey(orderBy TrustCenterReferenceOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case TrustCenterReferenceOrderFieldRank:
|
||||
@@ -174,9 +158,7 @@ RETURNING rank;
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "trust_center_references_trust_center_id_rank_key" {
|
||||
return &ErrTrustCenterReferenceAlreadyExists{
|
||||
message: fmt.Sprintf("trust center reference with trust_center_id %s and rank already exists", t.TrustCenterID),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert trust center reference: %w", err)
|
||||
@@ -221,7 +203,7 @@ WHERE
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrTrustCenterReferenceNotFound{Identifier: t.ID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
Users []*User
|
||||
|
||||
ErrUserNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrUserAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrUserNotFound) Error() string {
|
||||
return fmt.Sprintf("user not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrUserAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (u User) CursorKey(orderBy UserOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case UserOrderFieldCreatedAt:
|
||||
@@ -180,7 +164,7 @@ LIMIT 1;
|
||||
user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrUserNotFound{Identifier: email.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect user: %w", err)
|
||||
@@ -224,7 +208,7 @@ LIMIT 1;
|
||||
user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrUserNotFound{Identifier: userID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect user: %w", err)
|
||||
@@ -238,7 +222,6 @@ LIMIT 1;
|
||||
func (u *User) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
@@ -272,9 +255,7 @@ VALUES (
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && strings.Contains(pgErr.ConstraintName, "email_address") {
|
||||
return &ErrUserAlreadyExists{
|
||||
message: fmt.Sprintf("user with email %s already exists", u.EmailAddress),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,71 +265,6 @@ VALUES (
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) UpdateEmailVerification(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
verified bool,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
email_address_verified = @email_address_verified,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
id = @user_id
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": u.ID,
|
||||
"email_address_verified": verified,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update user email verification: %w", err)
|
||||
}
|
||||
|
||||
u.EmailAddressVerified = verified
|
||||
u.UpdatedAt = args["updated_at"].(time.Time)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) UpdatePassword(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
hashedPassword []byte,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
hashed_password = @hashed_password,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
id = @user_id
|
||||
`
|
||||
|
||||
now := time.Now()
|
||||
args := pgx.StrictNamedArgs{
|
||||
"user_id": u.ID,
|
||||
"hashed_password": hashedPassword,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update user password: %w", err)
|
||||
}
|
||||
|
||||
u.HashedPassword = hashedPassword
|
||||
u.UpdatedAt = now
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Update(ctx context.Context, conn pg.Conn) error {
|
||||
q := `
|
||||
UPDATE
|
||||
@@ -357,6 +273,8 @@ SET
|
||||
email_address = @email_address,
|
||||
email_address_verified = @email_address_verified,
|
||||
saml_subject = @saml_subject,
|
||||
fullname = @fullname,
|
||||
hashed_password = @hashed_password,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
id = @user_id
|
||||
@@ -368,13 +286,19 @@ WHERE
|
||||
"email_address_verified": u.EmailAddressVerified,
|
||||
"saml_subject": u.SAMLSubject,
|
||||
"updated_at": u.UpdatedAt,
|
||||
"fullname": u.FullName,
|
||||
"hashed_password": u.HashedPassword,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
result, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update user: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -411,7 +335,7 @@ LIMIT 1;
|
||||
user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrUserNotFound{Identifier: samlSubject}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect user: %w", err)
|
||||
@@ -422,10 +346,6 @@ LIMIT 1;
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadByEmailAndTenant, LoadByEmailGlobal, and IsTenantUser methods removed
|
||||
// All users are now global (no tenant_id distinction)
|
||||
// Use LoadByEmail() for all email-based lookups
|
||||
|
||||
func (u *User) CountMemberships(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
|
||||
@@ -23,27 +23,30 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
UserAPIKey struct {
|
||||
ID gid.GID `db:"id"`
|
||||
UserID gid.GID `db:"user_id"`
|
||||
Name string `db:"name"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
ID gid.GID `db:"id"`
|
||||
UserID gid.GID `db:"user_id"`
|
||||
Name string `db:"name"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
ExpireReason *ExpireReason `db:"expire_reason"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
UserAPIKeys []*UserAPIKey
|
||||
|
||||
ErrUserAPIKeyNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrUserAPIKeyNotFound) Error() string {
|
||||
return fmt.Sprintf("user api key not found: %q", e.Identifier)
|
||||
func (a *UserAPIKey) CursorKey(orderBy UserAPIKeyOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case UserAPIKeyOrderFieldCreatedAt:
|
||||
return page.NewCursorKey(a.ID, a.CreatedAt)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
func (a *UserAPIKey) LoadByID(
|
||||
@@ -57,6 +60,7 @@ SELECT
|
||||
user_id,
|
||||
name,
|
||||
expires_at,
|
||||
expire_reason,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -76,7 +80,7 @@ LIMIT 1;
|
||||
apiKey, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[UserAPIKey])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrUserAPIKeyNotFound{Identifier: apiKeyID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect user api key: %w", err)
|
||||
@@ -98,6 +102,7 @@ SELECT
|
||||
user_id,
|
||||
name,
|
||||
expires_at,
|
||||
expire_reason,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -124,30 +129,53 @@ ORDER BY created_at DESC;
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *UserAPIKeys) CountByUserID(ctx context.Context, conn pg.Conn, userID gid.GID) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
auth_user_api_keys
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
ORDER BY created_at DESC;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot scan count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (a *UserAPIKey) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
auth_user_api_keys (id, user_id, name, expires_at, created_at, updated_at)
|
||||
auth_user_api_keys (id, user_id, name, expires_at, expire_reason, created_at, updated_at)
|
||||
VALUES (
|
||||
@api_key_id,
|
||||
@user_id,
|
||||
@name,
|
||||
@expires_at,
|
||||
@expire_reason,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"api_key_id": a.ID,
|
||||
"user_id": a.UserID,
|
||||
"name": a.Name,
|
||||
"expires_at": a.ExpiresAt,
|
||||
"created_at": a.CreatedAt,
|
||||
"updated_at": a.UpdatedAt,
|
||||
"api_key_id": a.ID,
|
||||
"user_id": a.UserID,
|
||||
"name": a.Name,
|
||||
"expires_at": a.ExpiresAt,
|
||||
"expire_reason": a.ExpireReason,
|
||||
"created_at": a.CreatedAt,
|
||||
"updated_at": a.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
@@ -168,16 +196,18 @@ UPDATE
|
||||
SET
|
||||
name = @name,
|
||||
expires_at = @expires_at,
|
||||
expire_reason = @expire_reason,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
id = @api_key_id
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"api_key_id": a.ID,
|
||||
"name": a.Name,
|
||||
"expires_at": a.ExpiresAt,
|
||||
"updated_at": a.UpdatedAt,
|
||||
"api_key_id": a.ID,
|
||||
"name": a.Name,
|
||||
"expires_at": a.ExpiresAt,
|
||||
"expire_reason": a.ExpireReason,
|
||||
"updated_at": a.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
|
||||
40
pkg/coredata/user_api_key_order_field.go
Normal file
40
pkg/coredata/user_api_key_order_field.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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
|
||||
|
||||
type (
|
||||
UserAPIKeyOrderField string
|
||||
)
|
||||
|
||||
const (
|
||||
UserAPIKeyOrderFieldCreatedAt UserAPIKeyOrderField = "CREATED_AT"
|
||||
)
|
||||
|
||||
func (p UserAPIKeyOrderField) Column() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (p UserAPIKeyOrderField) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (p UserAPIKeyOrderField) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *UserAPIKeyOrderField) UnmarshalText(text []byte) error {
|
||||
*p = UserAPIKeyOrderField(text)
|
||||
return nil
|
||||
}
|
||||
@@ -63,24 +63,8 @@ type (
|
||||
VendorSnapshotter interface {
|
||||
InsertVendorSnapshots(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error
|
||||
}
|
||||
|
||||
ErrVendorNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrVendorAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrVendorNotFound) Error() string {
|
||||
return fmt.Sprintf("vendor not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrVendorAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (v Vendor) CursorKey(orderBy VendorOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case VendorOrderFieldCreatedAt:
|
||||
@@ -151,7 +135,7 @@ LIMIT 1;
|
||||
vendor, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Vendor])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrVendorNotFound{Identifier: vendorID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect vendor: %w", err)
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
VendorBusinessAssociateAgreements []*VendorBusinessAssociateAgreement
|
||||
|
||||
ErrVendorBusinessAssociateAgreementNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrVendorBusinessAssociateAgreementAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrVendorBusinessAssociateAgreementNotFound) Error() string {
|
||||
return fmt.Sprintf("vendor business associate agreement not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrVendorBusinessAssociateAgreementAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (v VendorBusinessAssociateAgreement) CursorKey(orderBy VendorBusinessAssociateAgreementOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case VendorBusinessAssociateAgreementOrderFieldValidFrom:
|
||||
@@ -263,9 +247,7 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "vendor_business_associate_agreements_source_id_snapshot_id_key" {
|
||||
return &ErrVendorBusinessAssociateAgreementAlreadyExists{
|
||||
message: fmt.Sprintf("vendor business associate agreement with source_id %s and snapshot_id %s already exists", vbaa.SourceID, vbaa.SnapshotID),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot upsert vendor business associate agreement: %w", err)
|
||||
|
||||
@@ -44,16 +44,8 @@ type (
|
||||
}
|
||||
|
||||
VendorContacts []*VendorContact
|
||||
|
||||
ErrVendorContactNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrVendorContactNotFound) Error() string {
|
||||
return fmt.Sprintf("vendor contact not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (vc VendorContact) CursorKey(orderBy VendorContactOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case VendorContactOrderFieldCreatedAt:
|
||||
@@ -108,7 +100,7 @@ LIMIT 1;
|
||||
vendorContact, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[VendorContact])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrVendorContactNotFound{Identifier: vendorContactID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect vendor contact: %w", err)
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -43,24 +43,8 @@ type (
|
||||
}
|
||||
|
||||
VendorDataPrivacyAgreements []*VendorDataPrivacyAgreement
|
||||
|
||||
ErrVendorDataPrivacyAgreementNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrVendorDataPrivacyAgreementAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrVendorDataPrivacyAgreementNotFound) Error() string {
|
||||
return fmt.Sprintf("vendor data privacy agreement not found: %q", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrVendorDataPrivacyAgreementAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (v VendorDataPrivacyAgreement) CursorKey(orderBy VendorDataPrivacyAgreementOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case VendorDataPrivacyAgreementOrderFieldValidFrom:
|
||||
@@ -263,9 +247,7 @@ ON CONFLICT (organization_id, vendor_id) DO UPDATE SET
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" && pgErr.ConstraintName == "vendor_data_privacy_agreements_source_id_snapshot_id_key" {
|
||||
return &ErrVendorDataPrivacyAgreementAlreadyExists{
|
||||
message: fmt.Sprintf("vendor data privacy agreement with source_id %s and snapshot_id %s already exists", vdpa.SourceID, vdpa.SnapshotID),
|
||||
}
|
||||
return ErrResourceAlreadyExists
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot upsert vendor data privacy agreement: %w", err)
|
||||
|
||||
@@ -41,16 +41,8 @@ type (
|
||||
}
|
||||
|
||||
VendorServices []*VendorService
|
||||
|
||||
ErrVendorServiceNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrVendorServiceNotFound) Error() string {
|
||||
return fmt.Sprintf("vendor service not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (vs VendorService) CursorKey(orderBy VendorServiceOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case VendorServiceOrderFieldCreatedAt:
|
||||
@@ -101,7 +93,7 @@ LIMIT 1;
|
||||
vendorService, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[VendorService])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrVendorServiceNotFound{Identifier: vendorServiceID.String()}
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect vendor service: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user