-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rework invite system with invite emails
- Loading branch information
Showing
31 changed files
with
1,125 additions
and
169 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,10 +12,11 @@ import { NextRequest, NextResponse } from 'next/server'; | |
import { z } from 'zod'; | ||
|
||
export async function POST(req: NextRequest) { | ||
const { identifier, scope } = z | ||
const { identifier, scope, followup } = z | ||
.object({ | ||
identifier: z.string().email(), | ||
scope: z.union([z.literal('admin'), z.literal('user')]), | ||
followup: z.string().optional(), | ||
}) | ||
.parse(await req.json()); | ||
|
||
|
@@ -49,18 +50,20 @@ export async function POST(req: NextRequest) { | |
from: '[email protected]', | ||
to: email, | ||
subject: 'PlayrBase signin link', | ||
text: render(AuthMagicLinkEmail({ token }), { | ||
text: render(AuthMagicLinkEmail({ token, followup }), { | ||
plainText: true, | ||
}), | ||
html: render(AuthMagicLinkEmail({ token })), | ||
html: render(AuthMagicLinkEmail({ token, followup })), | ||
}); | ||
} | ||
|
||
return NextResponse.json({ success: true }); | ||
} | ||
|
||
export async function GET(req: NextRequest) { | ||
const token = z.string().parse(new URL(req.url).searchParams.get('token')); | ||
const searchParams = new URL(req.url).searchParams; | ||
const token = z.string().parse(searchParams.get('token')); | ||
const followup = z.string().parse(searchParams.get('followup')); | ||
const decoded = await verifyEmailVerificationToken(token); | ||
|
||
if (!decoded) { | ||
|
@@ -72,9 +75,9 @@ export async function GET(req: NextRequest) { | |
|
||
const isEmail = z.string().email().safeParse(decoded.subject).success; | ||
if (isEmail) { | ||
const url = `/account/create-profile?${new URLSearchParams({ | ||
token, | ||
})}`; | ||
const params = new URLSearchParams({ token }); | ||
if (followup) params.set('followup', followup); | ||
const url = `/account/create-profile?${params}`; | ||
|
||
return new NextResponse(`Success! Redirecting to ${url}`, { | ||
status: 302, | ||
|
@@ -105,13 +108,16 @@ export async function GET(req: NextRequest) { | |
secure: req.nextUrl.protocol !== 'http:', | ||
}); | ||
|
||
return new NextResponse('Success! Redirecting to /account', { | ||
status: 302, | ||
headers: { | ||
Location: '/account', | ||
'Set-Cookie': header, | ||
}, | ||
}); | ||
return new NextResponse( | ||
`Success! Redirecting to ${followup || '/account'}`, | ||
{ | ||
status: 302, | ||
headers: { | ||
Location: followup || '/account', | ||
'Set-Cookie': header, | ||
}, | ||
} | ||
); | ||
} | ||
|
||
export async function PUT(req: NextRequest) { | ||
|
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import { surreal } from '@/app/(api)/lib/surreal'; | ||
import { extractUserTokenFromRequest } from '@/app/(api)/lib/token'; | ||
import OrganisationInviteEmail from '@/emails/organisation-invite'; | ||
import TeamInviteEmail from '@/emails/team-invite'; | ||
import { Invite } from '@/schema/resources/invite'; | ||
import { Organisation } from '@/schema/resources/organisation'; | ||
import { Team } from '@/schema/resources/team'; | ||
import { User } from '@/schema/resources/user'; | ||
import { render } from '@react-email/components'; | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
import { z } from 'zod'; | ||
import { sendEmail } from '../../lib/email'; | ||
|
||
const Body = Invite.pick({ | ||
origin: true, | ||
target: true, | ||
role: true, | ||
}); | ||
|
||
type Body = z.infer<typeof Body>; | ||
|
||
async function createInvite({ | ||
user, | ||
origin, | ||
target, | ||
role, | ||
}: Body & { | ||
user: User['id']; | ||
}) { | ||
const [res, target_email, fetched_user, fetched_target] = await surreal | ||
.query<[Invite, string, User, Team | Organisation]>( | ||
/* surrealql */ ` | ||
RETURN IF meta::tb(<record> $target) == "team" { | ||
RETURN IF $user IN $target.players { | ||
RETURN CREATE ONLY invite CONTENT { | ||
origin: $origin, | ||
target: $target, | ||
invited_by: $user, | ||
} | ||
} | ||
} ELSE IF meta::tb(<record> $target) == "organisation" { | ||
RETURN IF $user IN $target.managers[WHERE role = "owner" OR (role = "administrator" AND org != NONE)].user { | ||
RETURN IF type::is::string($role) { | ||
RETURN CREATE ONLY invite CONTENT { | ||
origin: $origin, | ||
target: $target, | ||
invited_by: $user, | ||
role: $role, | ||
} | ||
} | ||
} | ||
}; | ||
$origin.email OR $origin; | ||
$user.*; | ||
$target.*; | ||
`, | ||
{ | ||
user, | ||
origin, | ||
target, | ||
role, | ||
} | ||
) | ||
.catch(() => []); | ||
|
||
console.log(res); | ||
|
||
const invite = Invite.safeParse(res); | ||
if (invite.success) { | ||
return { | ||
invite: invite.data, | ||
target_email, | ||
fetched_user, | ||
fetched_target, | ||
}; | ||
} else { | ||
console.log(invite.error); | ||
} | ||
} | ||
|
||
export async function POST(req: NextRequest) { | ||
const res = extractUserTokenFromRequest(req); | ||
if (!res.success) return NextResponse.json(res); | ||
|
||
if (!res.decoded?.ID) | ||
return NextResponse.json({ | ||
success: false, | ||
error: 'id_lookup_failed', | ||
}); | ||
|
||
const user = User.shape.id.parse(res.decoded.ID); | ||
const body = Body.safeParse(await req.json()); | ||
if (!body.success) | ||
return NextResponse.json( | ||
{ success: false, error: 'invalid_body' }, | ||
{ status: 400 } | ||
); | ||
|
||
const { origin, target, role } = body.data; | ||
const invite = await createInvite({ | ||
user, | ||
origin, | ||
target, | ||
role, | ||
}); | ||
|
||
if (!invite) | ||
return NextResponse.json( | ||
{ success: false, error: 'invite_creation_failed' }, | ||
{ status: 400 } | ||
); | ||
|
||
if (invite.fetched_target.type == 'team') { | ||
const renderProps = { | ||
invite_id: invite.invite.id.slice(7), | ||
invited_by: invite.fetched_user.name, | ||
team: invite.fetched_target.name, | ||
email: invite.target_email, | ||
}; | ||
|
||
await sendEmail({ | ||
from: '[email protected]', | ||
to: invite.target_email, | ||
subject: 'PlayrBase Team invite', | ||
text: render(TeamInviteEmail(renderProps), { | ||
plainText: true, | ||
}), | ||
html: render(TeamInviteEmail(renderProps)), | ||
}); | ||
} else if (invite.fetched_target.type == 'organisation') { | ||
const renderProps = { | ||
invite_id: invite.invite.id.slice(7), | ||
invited_by: invite.fetched_user.name, | ||
organisation: invite.fetched_target.name, | ||
email: invite.target_email, | ||
}; | ||
|
||
await sendEmail({ | ||
from: '[email protected]', | ||
to: invite.target_email, | ||
subject: 'PlayrBase Team invite', | ||
text: render(OrganisationInviteEmail(renderProps), { | ||
plainText: true, | ||
}), | ||
html: render(OrganisationInviteEmail(renderProps)), | ||
}); | ||
} | ||
|
||
return NextResponse.json({ | ||
success: true, | ||
}); | ||
} |
This file contains 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
Oops, something went wrong.