From 4cafc41b888a79284d455b0bf320609e1238f30f Mon Sep 17 00:00:00 2001 From: Jorge Vargas Date: Fri, 21 Mar 2025 11:35:03 -0600 Subject: [PATCH] Implement Discord linking --- astro.config.mjs | 7 +- messages/en.json | 144 +++++++++--------- .../migration.sql | 2 + prisma/schema.prisma | 2 +- src/auth.ts | 14 ++ src/components/header/LoginButton.tsx | 13 +- src/components/header/LoginNav.astro | 35 ++++- src/env.d.ts | 1 + src/middleware.ts | 3 +- src/pages/donator/check.astro | 46 ++++++ src/utils/auth-client.ts | 2 +- 11 files changed, 192 insertions(+), 77 deletions(-) create mode 100644 prisma/migrations/20250321172750_fix_verification_table_column/migration.sql create mode 100644 src/pages/donator/check.astro diff --git a/astro.config.mjs b/astro.config.mjs index eceea68..d9f60dd 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -21,7 +21,11 @@ export default defineConfig({ optional: true, default: 'http://localhost:4321' }), - WEBHOOK_URL: envField.string({ context: 'server', access: 'secret' }) + WEBHOOK_URL: envField.string({ context: 'server', access: 'secret' }), + DISCORD_OAUTH_ID: envField.string({ context: 'server', access: 'public' }), + DISCORD_OAUTH_SECRET: envField.string({ context: 'server', access: 'secret' }), + DISCORD_GUILD_ID: envField.string({ context: 'server', access: 'public' }), + DISCORD_DONATOR_ID: envField.string({ context: 'server', access: 'public' }) }, validateSecrets: true }, @@ -53,6 +57,7 @@ export default defineConfig({ '/game/list': { status: 307, destination: '/maintenance' }, '/platform/list': { status: 307, destination: '/maintenance' }, '/platform/[id]': { status: 307, destination: '/maintenance' }, + '/profile': { status: 307, destination: '/maintenance' }, '/profile/[username]': { status: 307, destination: '/maintenance' }, '/series/[slug]': { status: 307, destination: '/maintenance' }, '/series/list': { status: 307, destination: '/maintenance' }, diff --git a/messages/en.json b/messages/en.json index 0ac3792..97dbcd9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,69 +1,77 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", - "register": "Register", - "login": "Login", - "logout": "Logout", - "username": "Username", - "password": "Password", - "email": "Email", - "recoverPassword": "Recover Password", - "home": "Home", - "lastaddednav": "Last Added", - "albumlist": "Album List", - "games": "Games", - "albums": "Albums", - "series": "Series", - "publishers": "Publishers", - "platforms": "Platforms", - "gamelist": "Game List", - "animation": "Animation", - "animationlist": "Animation List", - "studios": "Studios", - "requests": "Requests", - "submitalbum": "Submit Album", - "adminGrounds": "Admin Grounds", - "manageAlbums": "Manage Albums", - "manageUsers": "Manage Users", - "manageRequests": "Manage Requests", - "manageSubmissions": "Manage Submissions", - "profilePic": "Profile picture", - "emailSuccess": "An email with further instructions has been sent to the address linked to the account. Check your spam folder.", - "close": "Close", - "newPassword": "New password", - "newPasswordRetype": "Re-type new password", - "savePassword": "Save Password", - "passwordResetSuccesful": "Password reset succesfully", - "displayName": "Display name", - "lastAddedSidebar": "Last Added", - "getLucky": "Get Lucky", - "randomPull": "Random Pull", - "highlightAlbum": "Highlight Soundtrack", - "ostCount": "Soundtrack Count", - "recentReleases": "Recent Releases", - "moreGameReleases": "More Game Releases", - "moreAnimReleases": "more Animation releases", - "moreLastAdded": "more Last Added", - "lastAdded": "Last Added", - "releaseDate": "Release Date", - "artists": "Artists", - "classification": "Classification", - "AnimationOsts": "Animation Soundtracks", - "GameOsts": "Game Soundtracks", - "publishedBy": "Published by", - "animations": "Animations", - "avgRating": "Average Rating", - "tracklist": "Tracklist", - "donationCall": "Consider Donating to remove ads", - "donationSteps": "After donating, if the donation e-mail is the same as the one used in the notation, it should be\r\n available in a few hours. If not, contact us on", - "brokenLinkContact": "Broken Link? Contact us at Join our Discord!", - "mediafirePermission": "MediaFire permission denied?", - "mediafirePermissionGuide": "Check this guide", - "disc": "Disc", - "checkVGMDB": "Check album at", - "buyOriginal": "Buy The Original Soundtrack to support the artists", - "download": "Download", - "flyInc": "Fly.inc", - "ouoIO": "ouo.io", - "direct": "Direct", - "relatedAlbums": "Related Albums" -} \ No newline at end of file + "$schema": "https://inlang.com/schema/inlang-message-format", + "register": "Register", + "login": "Login", + "logout": "Logout", + "username": "Username", + "password": "Password", + "email": "Email", + "recoverPassword": "Recover Password", + "home": "Home", + "lastaddednav": "Last Added", + "albumlist": "Album List", + "games": "Games", + "albums": "Albums", + "series": "Series", + "publishers": "Publishers", + "platforms": "Platforms", + "gamelist": "Game List", + "animation": "Animation", + "animationlist": "Animation List", + "studios": "Studios", + "requests": "Requests", + "submitalbum": "Submit Album", + "adminGrounds": "Admin Grounds", + "manageAlbums": "Manage Albums", + "manageUsers": "Manage Users", + "manageRequests": "Manage Requests", + "manageSubmissions": "Manage Submissions", + "profilePic": "Profile picture", + "emailSuccess": "An email with further instructions has been sent to the address linked to the account. Check your spam folder.", + "close": "Close", + "newPassword": "New password", + "newPasswordRetype": "Re-type new password", + "savePassword": "Save Password", + "passwordResetSuccesful": "Password reset succesfully", + "displayName": "Display name", + "lastAddedSidebar": "Last Added", + "getLucky": "Get Lucky", + "randomPull": "Random Pull", + "highlightAlbum": "Highlight Soundtrack", + "ostCount": "Soundtrack Count", + "recentReleases": "Recent Releases", + "moreGameReleases": "More Game Releases", + "moreAnimReleases": "more Animation releases", + "moreLastAdded": "more Last Added", + "lastAdded": "Last Added", + "releaseDate": "Release Date", + "artists": "Artists", + "classification": "Classification", + "AnimationOsts": "Animation Soundtracks", + "GameOsts": "Game Soundtracks", + "publishedBy": "Published by", + "animations": "Animations", + "avgRating": "Average Rating", + "tracklist": "Tracklist", + "donationCall": "Consider Donating to remove ads", + "donationSteps": "After donating, if the donation e-mail is the same as the one used in the notation, it should be\r\n available in a few hours. If not, contact us on", + "brokenLinkContact": "Broken Link? Contact us at Join our Discord!", + "mediafirePermission": "MediaFire permission denied?", + "mediafirePermissionGuide": "Check this guide", + "disc": "Disc", + "checkVGMDB": "Check album at", + "buyOriginal": "Buy The Original Soundtrack to support the artists", + "download": "Download", + "flyInc": "Fly.inc", + "ouoIO": "ouo.io", + "direct": "Direct", + "relatedAlbums": "Related Albums", + "loginDiscord": "Login using Discord", + "profile": "Profile", + "linkDiscord": "Link Discord account", + "loggedInPage": "You need to be logged in to access this page", + "alreadyDonator": "You are already a donator!", + "discordNeeded": "You need to link your Discord account to access this page", + "discordRoleNeeded": "You need the Donator Discord role to access this page", + "addedDonator": "Added donator benefits to your account!" +} diff --git a/prisma/migrations/20250321172750_fix_verification_table_column/migration.sql b/prisma/migrations/20250321172750_fix_verification_table_column/migration.sql new file mode 100644 index 0000000..6e38503 --- /dev/null +++ b/prisma/migrations/20250321172750_fix_verification_table_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `verification` MODIFY `value` TEXT NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d6d1f7a..5970e08 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -442,7 +442,7 @@ model session { model verification { id String @id identifier String - value String + value String @db.Text expiresAt DateTime createdAt DateTime updatedAt DateTime diff --git a/src/auth.ts b/src/auth.ts index 362d2cb..2e0e3ff 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,6 +1,7 @@ import { betterAuth } from 'better-auth' import { prismaAdapter } from 'better-auth/adapters/prisma' import { username, bearer } from 'better-auth/plugins' +import { DISCORD_OAUTH_ID, DISCORD_OAUTH_SECRET } from 'astro:env/server' import prismaClient from './utils/prisma-client' import { sendEmail } from './utils/email' @@ -11,6 +12,19 @@ export const auth = betterAuth({ database: prismaAdapter(prismaClient, { provider: 'mysql' }), user: { modelName: 'users' }, plugins: [username(), bearer()], + account: { + accountLinking: { + enabled: true, + allowDifferentEmails: true + } + }, + socialProviders: { + discord: { + clientId: DISCORD_OAUTH_ID, + clientSecret: DISCORD_OAUTH_SECRET, + scope: ['identify', 'email', 'guilds.members.read'] + } + }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, diff --git a/src/components/header/LoginButton.tsx b/src/components/header/LoginButton.tsx index 9a76884..c443ffb 100644 --- a/src/components/header/LoginButton.tsx +++ b/src/components/header/LoginButton.tsx @@ -79,13 +79,20 @@ function LoginForm(props: { setForm: SetState; setModalOpen: SetSta /> -
- +
-
+ ) : null} + + + + + ) : ( <> @@ -18,3 +40,12 @@ const session = Astro.locals.session ) }
+ + diff --git a/src/env.d.ts b/src/env.d.ts index 3c2ae49..da7ec4a 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -6,5 +6,6 @@ declare namespace App { session: import('better-auth').Session | null permissions: string[] pages: string[] + roles: string[] } } diff --git a/src/middleware.ts b/src/middleware.ts index e8c2ba5..1cd6e7c 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -14,7 +14,7 @@ export const onRequest = defineMiddleware(async (context, next) => { context.locals.session = isAuthed.session const user = await prismaClient.users.findUnique({ - select: { roles: { select: { roles: { select: { permissions: true } } } } }, + select: { roles: { select: { roleName: true, roles: { select: { permissions: true } } } } }, where: { id: isAuthed.user.id } }) const permissions = (user?.roles.map((r) => r.roles.permissions).flat() as string[]) ?? [] @@ -22,6 +22,7 @@ export const onRequest = defineMiddleware(async (context, next) => { context.locals.permissions = permissions context.locals.pages = pages + context.locals.roles = user?.roles.map((r) => r.roleName) ?? [] } else { context.locals.user = null context.locals.session = null diff --git a/src/pages/donator/check.astro b/src/pages/donator/check.astro new file mode 100644 index 0000000..8d085db --- /dev/null +++ b/src/pages/donator/check.astro @@ -0,0 +1,46 @@ +--- +import * as m from 'paraglide/messages' +import { DISCORD_DONATOR_ID } from 'astro:env/server' +import prismaClient from 'utils/prisma-client' + +const { session, user, roles } = Astro.locals + +async function getMessage() { + if (!user || !session) { + return m.loggedInPage() + } + + if (roles.includes('Donator')) { + return m.alreadyDonator() + } + + const discordAcc = await prismaClient.account.findFirst({ + where: { providerId: 'discord', userId: session.userId } + }) + + if (!discordAcc) { + return m.discordNeeded() + } + + const memberInfoRes = await fetch(`https://discord.com/api/users/@me/guilds/535484312124915714/member`, { + headers: { + Authorization: `Bearer ${discordAcc.accessToken}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + + const memberInfo: { roles: string[] } = await memberInfoRes.json() + const isDiscordDonator = memberInfo.roles.includes(DISCORD_DONATOR_ID) + + if (!isDiscordDonator) { + return m.discordRoleNeeded() + } + + await prismaClient.user_Role.create({ data: { userUsername: user.id, roleName: 'Donator' } }) + return m.addedDonator() +} + +const message = await getMessage() +--- + +
{message}
diff --git a/src/utils/auth-client.ts b/src/utils/auth-client.ts index 3b7e592..cf9dbe1 100644 --- a/src/utils/auth-client.ts +++ b/src/utils/auth-client.ts @@ -4,4 +4,4 @@ import { usernameClient } from 'better-auth/client/plugins' export const authClient = createAuthClient({ plugins: [usernameClient()] }) -export const { useSession, signIn, signUp, signOut, forgetPassword, resetPassword } = authClient +export const { useSession, signIn, signUp, signOut, forgetPassword, resetPassword, linkSocial } = authClient