feat(api): allow API key authentication for domain endpoints

Switch /domains controller from `isAuthenticated` (cookie-only) to
`requireAuth` (cookie OR API key), matching the pattern used by other
project-scoped API endpoints (/v1/send, /contacts, etc.).

API keys are project-scoped credentials with full access; for write
operations the projectId in the request must equal the API key's
projectId. JWT (dashboard) auth retains role-based checks
(requireAdminAccess for POST/DELETE).

Also: improve UX when a domain is already linked to the same project
by returning a clear error instead of the generic "linked to another
project" message.

Refactor DomainService.checkDomainOwnership to make `userId` optional
(needed for API key path) while preserving its existing return shape
and adding `projectId` to the result.
This commit is contained in:
ReylanLugo
2026-05-09 22:25:17 -04:00
parent 15bdcf6e49
commit 3f30a48c40
2 changed files with 59 additions and 23 deletions
+18 -7
View File
@@ -438,15 +438,14 @@ export class DomainService {
* @param userId User ID to check membership
* @returns Object with exists flag and membership info
*/
public static async checkDomainOwnership(domain: string, userId: string) {
public static async checkDomainOwnership(domain: string, userId?: string) {
const existingDomain = await prisma.domain.findFirst({
where: {domain},
include: {
project: {
include: {
members: {
where: {userId},
},
select: {
id: true,
name: true,
},
},
},
@@ -456,8 +455,20 @@ export class DomainService {
return {exists: false};
}
// Check if user is a member of the project that owns this domain
const isMember = existingDomain.project.members.length > 0;
let isMember = false;
if (userId) {
const membership = await prisma.membership.findUnique({
where: {
userId_projectId: {
userId,
projectId: existingDomain.project.id,
},
},
});
isMember = membership !== null;
}
return {
exists: true,