[FEAT] Implement Email service & Send emails for workspace invitations#18
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an SMTP-backed email sender and an invite-accept flow for workspace invitations, while also removing backend/UI support for uploaded/external images (avatars/covers/logos, Unsplash proxy) and the favorites feature.
Changes:
- Add SMTP email sender wiring via the queue consumer and generate invite links using
APP_BASE_URL. - Add
/inviteUI route andjoinByTokenAPI/UI path for accepting workspace invitations from email links. - Remove image upload/serving, Unsplash proxy, and favorites endpoints/UI/types.
Reviewed changes
Copilot reviewed 53 out of 53 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/vite.config.ts | Adjust manual chunks and build warnings configuration after feature removals. |
| ui/src/types/index.ts | Remove coverImageUrl from domain User type. |
| ui/src/services/workspaceService.ts | Remove logo from workspace update payload; add joinByToken() API call. |
| ui/src/services/userService.ts | Minor formatting change in createToken signature. |
| ui/src/services/uploadService.ts | Remove image upload client service. |
| ui/src/services/projectService.ts | Remove image/icon-related update fields from project update payload typing. |
| ui/src/services/instanceService.ts | Remove Unsplash proxy typings/calls. |
| ui/src/routes/index.tsx | Add /invite route and related lazy imports; adjust instance-admin login route handling. |
| ui/src/pages/instance-admin/InstanceAdminImagePage.tsx | Simplify updateSection call typing/casting. |
| ui/src/pages/instance-admin/InstanceAdminEmailPage.tsx | Simplify updateSection call typing/casting. |
| ui/src/pages/instance-admin/InstanceAdminAuthenticationPage.tsx | Remove React import usage; adjust icon typing and state naming. |
| ui/src/pages/instance-admin/InstanceAdminAIPage.tsx | Simplify updateSection call typing/casting. |
| ui/src/pages/WorkspaceViewsPage.tsx | Remove now-unused user/avatar helper typing. |
| ui/src/pages/WorkspaceHomePage.tsx | Re-enable previously “reserved” helper functions and rename state variables. |
| ui/src/pages/SettingsPage.tsx | Remove cover/avatar/project-icon/workspace-logo modals and image display logic; update UI placeholders. |
| ui/src/pages/ProjectsListPage.tsx | Remove favorites UI and cover/icon rendering; simplify project cards. |
| ui/src/pages/ProfilePage.tsx | Remove cover image usage; stub out several computations pending API support. |
| ui/src/pages/LoginPage.tsx | Preserve search when redirecting back after login. |
| ui/src/pages/IssueListPage.tsx | Remove image URL helper usage and adjust assignee avatar handling. |
| ui/src/pages/IssueDetailPage.tsx | Adjust data passed into CreateWorkItemModal for sub-item creation. |
| ui/src/pages/InviteAcceptPage.tsx | New page to accept workspace invites from token query param. |
| ui/src/pages/CyclesPage.tsx | Remove now-unused user/avatar helper typing. |
| ui/src/pages/BoardPage.tsx | Add UI imports (incl. Avatar) in preparation for board changes. |
| ui/src/pages/AnalyticsWorkItemsPage.tsx | Modify per-project analytics aggregation logic. |
| ui/src/lib/utils.ts | Remove getImageUrl and related env dependency. |
| ui/src/contexts/FavoritesContext.tsx | Remove Favorites context/provider. |
| ui/src/contexts/AuthContext.tsx | Stop mapping cover_image into UI user shape. |
| ui/src/components/work-item/CommentEditor.tsx | Change TipTap codeBlock configuration. |
| ui/src/components/layout/Sidebar.tsx | Remove favorites backend integration and adjust CreateWorkItemModal project mapping. |
| ui/src/components/layout/PageHeader.tsx | Rename previously intentionally-unused state variables. |
| ui/src/components/UploadImageModal.tsx | Remove upload modal component. |
| ui/src/components/ProjectIconModal.tsx | Remove project icon picker/display component. |
| ui/src/components/CoverImageModal.tsx | Remove cover image selection modal (Unsplash/upload). |
| ui/src/api/types.ts | Remove API fields for images/project icons/workspace logo; trim some page fields. |
| ui/src/App.tsx | Remove FavoritesProvider wrapper. |
| api/internal/store/user_favorite.go | Remove user favorites persistence store. |
| api/internal/service/workspace.go | Remove logo support from workspace update service signature. |
| api/internal/service/project.go | Remove cover/icon fields from project update service signature. |
| api/internal/router/router.go | Remove upload/favorites/unsplash routes and wiring; add AppBaseURL support and invite-email wiring. |
| api/internal/model/user_favorite.go | Adjust favorite model tags (unique index removal). |
| api/internal/model/user.go | Remove CoverImage from user model. |
| api/internal/model/project.go | Remove CoverImage from project model. |
| api/internal/minio/minio.go | Remove unused MinIO object helper methods. |
| api/internal/mail/mail.go | New SMTP sender that reads instance settings and sends email via net/smtp. |
| api/internal/handler/workspace.go | Remove workspace logo update field; enqueue invite emails on invite creation. |
| api/internal/handler/upload.go | Remove upload/serve handlers. |
| api/internal/handler/project.go | Remove project cover/icon update handling and model JSONMap usage. |
| api/internal/handler/instance.go | Remove Unsplash proxy handler/types. |
| api/internal/handler/favorite.go | Remove favorites handler/endpoints. |
| api/internal/handler/auth.go | Remove avatar/cover update handling and cover image from user response. |
| api/internal/config/config.go | Add APP_BASE_URL config loading for invite links. |
| api/cmd/api/main.go | Wire SMTP sender into queue consumer; stop passing MinIO into router. |
| api/.env.example | Document APP_BASE_URL. |
Comments suppressed due to low confidence (1)
ui/src/pages/SettingsPage.tsx:3374
- The "Edit logo" button no longer has an
onClickhandler (and isn't disabled), so it will look clickable but do nothing. If workspace logos were removed, remove/disable the button or replace it with non-interactive text.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const refetchRecents = () => { | ||
| if (workspaceSlug) { | ||
| recentsService | ||
| .list(workspaceSlug) | ||
| .then(setRecents) | ||
| .catch(() => {}); | ||
| } | ||
| }; |
There was a problem hiding this comment.
refetchRecents is declared but never used. With noUnusedLocals: true, this will fail the TypeScript build. Remove it until it’s needed, or wire it into the UI (e.g., a refresh button).
| import { useEffect, useState } from "react"; | ||
| import { Link, useParams } from "react-router-dom"; | ||
| import { Card, CardContent, Badge } from "../components/ui"; | ||
| import { Card, CardContent, Badge, Avatar } from "../components/ui"; |
There was a problem hiding this comment.
Avatar is imported but never used. With noUnusedLocals: true, this will fail the TypeScript build. Remove the unused import or use Avatar in the board UI.
| onClose={() => setCreateWorkItemOpen(false)} | ||
| workspaceSlug={workspace?.slug ?? ""} | ||
| projects={projects} | ||
| projects={projectsForModal} |
There was a problem hiding this comment.
CreateWorkItemModal is typed to receive projects: ProjectApiResponse[], but projectsForModal is a different domain type (Project with workspaceId, etc.). This will fail type-checking under strict. Either keep passing the projects API response list, or update CreateWorkItemModal to accept the domain Project shape consistently.
| projects={projectsForModal} | |
| projects={projects} |
| addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) | ||
| auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) | ||
| msg := buildMessage(to, from, subject, body) | ||
| if err := smtp.SendMail(addr, auth, from, []string{to}, msg); err != nil { |
There was a problem hiding this comment.
The instance "email.security" setting (TLS/SSL/None) is parsed into cfg.Security but never used here. As a result, "SSL" (implicit TLS, typically port 465) and "None" are not honored, and smtp.PlainAuth is always used even if cfg.Username is empty. Implement behavior for the supported security modes (e.g., no auth when security is None/username empty; implicit TLS for SSL), or remove the security option from the UI/settings to avoid misconfiguration.
| inviteLink := strings.TrimSuffix(h.AppBaseURL, "/") + "/invite?token=" + inv.Token | ||
| subject := fmt.Sprintf("You're invited to join %s on Devlane", workspaceName) | ||
| bodyText := fmt.Sprintf("You have been invited to join the workspace \"%s\" on Devlane.\n\nAccept your invitation by visiting:\n%s\n\nIf you don't have an account yet, you can sign up at the same link.\n", workspaceName, inviteLink) | ||
| _ = h.Queue.PublishSendEmail(c.Request.Context(), queue.SendEmailPayload{ | ||
| To: inv.Email, | ||
| Subject: subject, | ||
| Body: bodyText, | ||
| Kind: "workspace_invite", | ||
| Extra: map[string]string{"workspace_slug": slug, "invite_id": inv.ID.String()}, | ||
| }) |
There was a problem hiding this comment.
The invite link is built via string concatenation (..."/invite?token=" + inv.Token). If tokens can contain characters that need escaping, the link can break. Use url.QueryEscape for the token (and consider url.JoinPath for the base URL) and avoid silently ignoring publish errors—at minimum log the error so invite-email failures are diagnosable.
| projects={[ | ||
| { | ||
| id: project.id, | ||
| workspace_id: project.workspace_id, | ||
| workspaceId: project.workspace_id, | ||
| name: project.name, | ||
| identifier: project.identifier ?? project.id.slice(0, 8), | ||
| description: project.description ?? undefined, | ||
| description: project.description ?? null, | ||
| }, | ||
| ]} |
There was a problem hiding this comment.
CreateWorkItemModal expects projects: ProjectApiResponse[], but this code passes a custom object with different keys (workspaceId instead of workspace_id) and description: string | null. With strict typing this should not compile. Prefer passing the existing project object (or build an object that matches ProjectApiResponse exactly).
| const [workspace, setWorkspace] = useState<WorkspaceApiResponse | null>(null); | ||
| const [projects, setProjects] = useState<ProjectApiResponse[]>([]); |
There was a problem hiding this comment.
workspace and projects state values are set but never read in this component. With noUnusedLocals: true, these unused state variables will break the TypeScript build. Either use them in the render logic or rename them to _workspace / _projects (as before) to indicate intentional unused values.
| const [workspace, setWorkspace] = useState<WorkspaceApiResponse | null>(null); | |
| const [projects, setProjects] = useState<ProjectApiResponse[]>([]); | |
| const [_workspace, setWorkspace] = useState<WorkspaceApiResponse | null>(null); | |
| const [_projects, setProjects] = useState<ProjectApiResponse[]>([]); |
| const InstanceAdminLoginPage = lazy(() => | ||
| import("../pages/instance-admin").then((m) => | ||
| page({ InstanceAdminLoginPage: m.InstanceAdminLoginPage }), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
InstanceAdminLoginPage is declared but never used (the /instance-admin/login route renders a <Navigate />). With noUnusedLocals: true, this unused lazy import will fail the build. Remove this constant or wire it into the router.
| bulletList: { keepMarks: true, keepAttributes: true }, | ||
| orderedList: { keepMarks: true, keepAttributes: true }, | ||
| codeBlock: {}, | ||
| codeBlock: true, |
There was a problem hiding this comment.
@tiptap/starter-kit options usually expect codeBlock to be false or an options object. Setting codeBlock: true may not type-check (and can change behavior vs the previous {} config). If you just want the default CodeBlock extension enabled, omit codeBlock entirely or keep it as an empty options object.
| codeBlock: true, | |
| codeBlock: {}, |
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| className="absolute bottom-2 right-2 z-10 gap-1.5 text-[13px]" | ||
| onClick={() => setAccountCoverModalOpen(true)} | ||
| className="absolute bottom-2 right-2 gap-1.5 text-[13px]" | ||
| > | ||
| Change cover | ||
| </Button> |
There was a problem hiding this comment.
This "Change cover" button no longer has an onClick (and isn't disabled), so it appears interactive but does nothing. If cover images were removed, consider removing the button or disabling it with a short explanatory tooltip/text to avoid a dead UI control.
This pull request removes support for user-uploaded images and related features across the backend API. It eliminates endpoints and data fields for uploading, storing, and retrieving images such as avatars, cover images, project icons, and workspace logos. Additionally, it removes the Unsplash proxy endpoint and updates configuration to support a frontend base URL for invite links. The most important changes are grouped below:
Image Upload and Storage Removal:
UploadHandlerand all related file upload and serving logic, removing the backend's ability to accept and serve uploaded files (api/internal/handler/upload.go).FavoriteHandler, which included endpoints for managing user favorite projects, some of which referenced image data (api/internal/handler/favorite.go).API and Model Field Cleanup:
avatarandcover_imagefields from user profile update requests and responses, and eliminated their handling in the user update logic (api/internal/handler/auth.go) [1] [2] [3].cover_image,emoji, andicon_propfields from project update requests and logic, and updated the call signature for project updates accordingly (api/internal/handler/project.go) [1] [2] [3].logofield from workspace update requests and logic, and updated the workspace update call signature (api/internal/handler/workspace.go) [1] [2].Configuration and Infrastructure Improvements:
APP_BASE_URLto.env.exampleand configuration loading, allowing the backend to generate frontend invite links for emails (api/.env.example, api/internal/config/config.go) [1] [2] [3].APP_BASE_URLand refactored email sending to use a real SMTP sender instead of a no-op sender (api/cmd/api/main.go) [1] [2] [3].Dependency Cleanup:
Handler and Struct Updates:
WorkspaceHandlerstruct to include a queue publisher and app base URL for invite emails, reflecting the new configuration approach (api/internal/handler/workspace.go).These changes collectively simplify the backend by removing all support for user-uploaded and external images, focusing the API on core business logic and improving configuration for email invite links.
Closes #17