Skip to content

feat(invitations): invitation emails, SMTP hardening, resend endpoint, expose invited email (closes Sentinent-AI/Sentinent#31)#52

Merged
neethika12 merged 4 commits into
mainfrom
feature/invitation-acceptance-flow
Apr 29, 2026
Merged

feat(invitations): invitation emails, SMTP hardening, resend endpoint, expose invited email (closes Sentinent-AI/Sentinent#31)#52
neethika12 merged 4 commits into
mainfrom
feature/invitation-acceptance-flow

Conversation

@neethika12
Copy link
Copy Markdown
Contributor

Closes Sentinent-AI/Sentinent#31

What changed

  • feat(invitations): send invitation email on CreateInvitation in a safe goroutine with defer recover()
  • fix(mailer): custom TCP dialer with 15s dial + transaction timeout — replaces bare smtp.SendMail that could block indefinitely
  • feat(invitations): expose email field in GET /api/invitations/:token response so frontend can detect wrong-account logins
  • feat(invitations): POST /api/invitations/:token/resend — re-sends the invitation email for an existing unaccepted token; owner-only
  • fix(invitations): ListInvitations now returns both pending and accepted invitations with accepted_at included

When a workspace owner sends an invitation, the backend now emails
the recipient with:
  - the inviter's email address
  - the workspace name
  - a direct accept link  (FRONTEND_BASE_URL/invitations/<token>)
  - a reminder that the invitation expires in 7 days

Implementation:
  - services/mailer.go: add SendInvitationEmail() — same SMTP path
    as SendPasswordResetEmail, reuses LoadSMTPConfigFromEnv()
  - handlers/invitations.go: fire SendInvitationEmail() in a
    goroutine after the DB insert; failures are logged but do not
    block the 201 response so the invitation record is always saved

SMTP must be configured via SMTP_HOST / SMTP_PORT / SMTP_FROM_EMAIL
(+ optionally SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM_NAME).
If SMTP is not configured the email is skipped and a log line is
printed — existing behaviour is unchanged.
- Replace bare smtp.SendMail (no timeout) with a custom TCP dialer
  that enforces a 15-second deadline on both the connection and the
  entire SMTP conversation; prevents the goroutine from blocking
  indefinitely when a firewall silently drops port 587
- Refactor shared message-building into buildMessage() helper;
  both SendInvitationEmail and SendPasswordResetEmail use sendSMTP()
- Add defer recover() to the invitation email goroutine so any
  unexpected panic cannot crash the HTTP server before the 201
  response is flushed to the client
Add the invited email to the GET /api/invitations/:token response so the
frontend can compare it against the currently logged-in user's email and
show an explicit wrong-account error instead of a generic 403.
…s in list

- Add POST /api/invitations/:token/resend — re-sends the invitation email
  for an existing unaccepted token; only the workspace owner can call it
- Update ListInvitations to return all invitations (pending and accepted)
  so the frontend can show an 'Accepted' section alongside 'Pending'
- Scan accepted_at in the list query so the status is visible
@neethika12 neethika12 merged commit 8a65c03 into main Apr 29, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: redesign invitation acceptance flow with inline signup/login and auto-join

1 participant