feat(invitations): invitation emails, SMTP hardening, resend endpoint, expose invited email (closes Sentinent-AI/Sentinent#31)#52
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes Sentinent-AI/Sentinent#31
What changed
feat(invitations): send invitation email onCreateInvitationin a safe goroutine withdefer recover()fix(mailer): custom TCP dialer with 15s dial + transaction timeout — replaces baresmtp.SendMailthat could block indefinitelyfeat(invitations): exposeemailfield inGET /api/invitations/:tokenresponse so frontend can detect wrong-account loginsfeat(invitations):POST /api/invitations/:token/resend— re-sends the invitation email for an existing unaccepted token; owner-onlyfix(invitations):ListInvitationsnow returns both pending and accepted invitations withaccepted_atincluded