Go · 2886 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package orgs owns the organization domain: create, members,
4 // invitations, suspend/restore, soft-delete, and the principals
5 // resolver that unifies /{slug} routing across users + orgs.
6 //
7 // The package is the single entry point for the rest of the runtime —
8 // web handlers, the policy engine, and the worker pool import here
9 // rather than poking the sqlc surface directly.
10 package orgs
11
12 import (
13 "errors"
14 "log/slog"
15
16 "github.com/jackc/pgx/v5/pgxpool"
17
18 "github.com/tenseleyFlow/shithub/internal/auth/audit"
19 "github.com/tenseleyFlow/shithub/internal/auth/email"
20 )
21
22 // Deps wires this package against the runtime. Pool is required;
23 // EmailSender is optional — when nil, invitation + role-change
24 // notifications are skipped (matches the "headless test" pattern
25 // the rest of the auth surface uses).
26 type Deps struct {
27 Pool *pgxpool.Pool
28 Logger *slog.Logger
29 Audit *audit.Recorder
30 // Email side: only used by the invitation flow today; the
31 // org-suspend / org-deletion notification kinds are deferred
32 // to S29's lifecycle email work.
33 EmailSender email.Sender
34 EmailFrom string
35 SiteName string
36 BaseURL string
37 }
38
39 // Errors surfaced by the orchestrator. Web handlers map these to
40 // status codes + friendly messages.
41 var (
42 ErrEmptySlug = errors.New("orgs: slug is required")
43 ErrSlugTooLong = errors.New("orgs: slug too long (max 39)")
44 ErrSlugInvalid = errors.New("orgs: slug must be lowercase letters, digits, and hyphens")
45 ErrSlugReserved = errors.New("orgs: slug is reserved")
46 ErrSlugTaken = errors.New("orgs: slug is already in use")
47 ErrOrgNotFound = errors.New("orgs: org not found")
48 ErrUserNotFound = errors.New("orgs: user not found")
49 ErrNotAMember = errors.New("orgs: user is not a member of this org")
50 ErrAlreadyMember = errors.New("orgs: user is already a member of this org")
51 ErrLastOwner = errors.New("orgs: cannot remove or demote the only owner; transfer ownership first")
52 ErrInvitationNotFound = errors.New("orgs: invitation not found")
53 ErrInvitationExpired = errors.New("orgs: invitation has expired")
54 ErrInvitationConsumed = errors.New("orgs: invitation already accepted, declined, or canceled")
55 ErrInvitationDuplicate = errors.New("orgs: a pending invitation for this target already exists")
56 ErrInvalidInvitationKind = errors.New("orgs: invitation must target either a user or an email")
57 ErrSuspended = errors.New("orgs: org is suspended")
58 ErrDeleted = errors.New("orgs: org is soft-deleted")
59 )
60
61 // InvitationLifetime is how long a fresh invitation is honored before
62 // the recipient must request a new one. 7 days matches the spec.
63 const InvitationLifetime = 7 * 24 * 60 * 60 // seconds; converted to time.Duration at call sites
64