Skip to content

[FEAT] Implement Email service & Send emails for workspace invitations#18

Merged
martian56 merged 1 commit into
mainfrom
17-implement-email-service-send-emails-for-workspace-invitations
Mar 8, 2026
Merged

[FEAT] Implement Email service & Send emails for workspace invitations#18
martian56 merged 1 commit into
mainfrom
17-implement-email-service-send-emails-for-workspace-invitations

Conversation

@martian56
Copy link
Copy Markdown
Member

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:

  • Deleted the UploadHandler and all related file upload and serving logic, removing the backend's ability to accept and serve uploaded files (api/internal/handler/upload.go).
  • Removed the FavoriteHandler, which included endpoints for managing user favorite projects, some of which referenced image data (api/internal/handler/favorite.go).
  • Removed the Unsplash search proxy endpoint and associated types, eliminating the ability to search for and use Unsplash images via the backend (api/internal/handler/instance.go).

API and Model Field Cleanup:

  • Removed avatar and cover_image fields from user profile update requests and responses, and eliminated their handling in the user update logic (api/internal/handler/auth.go) [1] [2] [3].
  • Removed cover_image, emoji, and icon_prop fields from project update requests and logic, and updated the call signature for project updates accordingly (api/internal/handler/project.go) [1] [2] [3].
  • Removed the logo field from workspace update requests and logic, and updated the workspace update call signature (api/internal/handler/workspace.go) [1] [2].

Configuration and Infrastructure Improvements:

  • Added APP_BASE_URL to .env.example and configuration loading, allowing the backend to generate frontend invite links for emails (api/.env.example, api/internal/config/config.go) [1] [2] [3].
  • Updated the main application wiring to use APP_BASE_URL and 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:

  • Removed unused imports related to image handling, file uploads, and Unsplash integration (api/internal/handler/instance.go, api/internal/handler/project.go) [1] [2].

Handler and Struct Updates:

  • Updated the WorkspaceHandler struct 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

@martian56 martian56 added this to the Deadline milestone Mar 8, 2026
@martian56 martian56 self-assigned this Mar 8, 2026
Copilot AI review requested due to automatic review settings March 8, 2026 22:46
@martian56 martian56 added enhancement New feature or request API labels Mar 8, 2026
@martian56 martian56 merged commit d0ba55a into main Mar 8, 2026
4 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /invite UI route and joinByToken API/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 onClick handler (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.

Comment on lines +389 to +396
const refetchRecents = () => {
if (workspaceSlug) {
recentsService
.list(workspaceSlug)
.then(setRecents)
.catch(() => {});
}
};
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
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";
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
onClose={() => setCreateWorkItemOpen(false)}
workspaceSlug={workspace?.slug ?? ""}
projects={projects}
projects={projectsForModal}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
projects={projectsForModal}
projects={projects}

Copilot uses AI. Check for mistakes.
Comment thread api/internal/mail/mail.go
Comment on lines +85 to +88
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 {
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +387 to +396
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()},
})
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 982 to 990
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,
},
]}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +1197 to +1198
const [workspace, setWorkspace] = useState<WorkspaceApiResponse | null>(null);
const [projects, setProjects] = useState<ProjectApiResponse[]>([]);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const [workspace, setWorkspace] = useState<WorkspaceApiResponse | null>(null);
const [projects, setProjects] = useState<ProjectApiResponse[]>([]);
const [_workspace, setWorkspace] = useState<WorkspaceApiResponse | null>(null);
const [_projects, setProjects] = useState<ProjectApiResponse[]>([]);

Copilot uses AI. Check for mistakes.
Comment thread ui/src/routes/index.tsx
Comment on lines +136 to +140
const InstanceAdminLoginPage = lazy(() =>
import("../pages/instance-admin").then((m) =>
page({ InstanceAdminLoginPage: m.InstanceAdminLoginPage }),
),
);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
bulletList: { keepMarks: true, keepAttributes: true },
orderedList: { keepMarks: true, keepAttributes: true },
codeBlock: {},
codeBlock: true,
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Suggested change
codeBlock: true,
codeBlock: {},

Copilot uses AI. Check for mistakes.
Comment on lines 1511 to 1517
<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>
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@martian56 martian56 deleted the 17-implement-email-service-send-emails-for-workspace-invitations branch May 2, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Email service & Send emails for workspace invitations

2 participants