diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5b5be5..9e06bbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,17 +8,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@master - + - name: Install Node v14 uses: actions/setup-node@v2 with: node-version: '14' - + - name: Install dependencies run: npm install + - name: Generate prisma client + run: npx prisma generate + - name: Run Linter run: npm run lint - name: Run typescript pretty check - run: npm run tspretty \ No newline at end of file + run: npm run tspretty diff --git a/components/user/DropDown.tsx b/components/user/DropDown.tsx index d666f95..2312e76 100644 --- a/components/user/DropDown.tsx +++ b/components/user/DropDown.tsx @@ -3,11 +3,6 @@ import Link from 'next/link'; import { signOut } from 'next-auth/react'; import React, { Fragment } from 'react'; import styles from 'styles/navbar.module.scss'; -import { ApiUser } from 'typings/typings'; - -interface props { - user: ApiUser; -} function AccountSettingsIcon() { return ( @@ -69,7 +64,7 @@ function ProfileIcon() { // }, // ]; -export default function UserDropDown({ user }: props) { +export default function UserDropDown({ user }) { return ( diff --git a/components/user/Loader.tsx b/components/user/Loader.tsx index 6f1a2ff..68058ea 100644 --- a/components/user/Loader.tsx +++ b/components/user/Loader.tsx @@ -35,7 +35,7 @@ export default function UserLoader() { } if (status === 'authenticated') { - return ; + return ; } return ( diff --git a/components/user/profile.tsx b/components/user/Profile.tsx similarity index 100% rename from components/user/profile.tsx rename to components/user/Profile.tsx diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..304c20f --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,10 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + // eslint-disable-next-line vars-on-top, no-var + var prisma: PrismaClient | undefined; +} + +export const prisma = global.prisma || new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') global.prisma = prisma; diff --git a/package-lock.json b/package-lock.json index 1e605c9..3afaff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@headlessui/react": "^1.2.0", "@next-auth/mongodb-adapter": "^0.0.2-next.298", + "@next-auth/prisma-adapter": "^0.5.2-next.19", "@popperjs/core": "^2.9.2", + "@prisma/client": "^3.5.0", "@y0c/react-datepicker": "^1.0.4", "axios": "^0.24.0", "btoa": "^1.2.1", @@ -70,6 +72,7 @@ "lint-staged": "^10.4.2", "postcss": "^8.3.0", "prettier": "^2.2.1", + "prisma": "^3.5.0", "sass": "^1.32.13", "typescript": "^4.2.4" } @@ -936,6 +939,15 @@ "next-auth": ">4 || 4.0.0-beta.1 - 4.0.0-beta.x" } }, + "node_modules/@next-auth/prisma-adapter": { + "version": "0.5.2-next.19", + "resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-0.5.2-next.19.tgz", + "integrity": "sha512-146DWE4PEbgVKF6W0CjJSxeP2rrw2T2s8zX1GQzbQsMK8trRSzheURJRphCn6ZGhVSpHJhU4NZe7F0kwRL5lwQ==", + "peerDependencies": { + "@prisma/client": ">=2.26.0", + "next-auth": ">4 || 4.0.0-beta.1 - 4.0.0-beta.x" + } + }, "node_modules/@next/env": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-12.0.4.tgz", @@ -1222,6 +1234,38 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@prisma/client": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.5.0.tgz", + "integrity": "sha512-LuaaisknLe9CCfJ1Rtqe9b9knvPgEEcC77OMmWdo3fSanxl5oTDxcH3IIhpULQQlJfZvDcaEXuXNU4dsNF+q1w==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e" + }, + "engines": { + "node": ">=12.6" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e.tgz", + "integrity": "sha512-MqZUrxuLlIbjB3wu8LrRJOKcvR4k3dunKoI4Q2bPfAwLQY0XlpsLZ3TRVW1c32ooVk939p6iGNkaCUo63Et36g==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e.tgz", + "integrity": "sha512-X16YmBmj7Omso4ZbkNBe6gPYlNcnwZMUPtXsguCkn+KoMqm3DJD9M4X31gx0Gf13Q44dY3SKPJZUk44/XUj/WA==" + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -6479,6 +6523,23 @@ "node": ">= 0.8" } }, + "node_modules/prisma": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.5.0.tgz", + "integrity": "sha512-WEYQ+H98O0yigG+lI0gfh4iyBChvnM6QTXPDtY9eFraLXAmyb6tf/T2mUdrUAU1AEvHLVzQA5A+RpONZlQozBg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=12.6" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -9027,6 +9088,12 @@ "integrity": "sha512-CsfgTeMaoTwBX1jQK5GDlINzpXWR3CuH99X1boN3i6SjLODmGG9ZFt7g8XX3ga3WsVhzMAHYCoieG2Q9azCY/A==", "requires": {} }, + "@next-auth/prisma-adapter": { + "version": "0.5.2-next.19", + "resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-0.5.2-next.19.tgz", + "integrity": "sha512-146DWE4PEbgVKF6W0CjJSxeP2rrw2T2s8zX1GQzbQsMK8trRSzheURJRphCn6ZGhVSpHJhU4NZe7F0kwRL5lwQ==", + "requires": {} + }, "@next/env": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-12.0.4.tgz", @@ -9176,6 +9243,25 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" }, + "@prisma/client": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.5.0.tgz", + "integrity": "sha512-LuaaisknLe9CCfJ1Rtqe9b9knvPgEEcC77OMmWdo3fSanxl5oTDxcH3IIhpULQQlJfZvDcaEXuXNU4dsNF+q1w==", + "requires": { + "@prisma/engines-version": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e" + } + }, + "@prisma/engines": { + "version": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e.tgz", + "integrity": "sha512-MqZUrxuLlIbjB3wu8LrRJOKcvR4k3dunKoI4Q2bPfAwLQY0XlpsLZ3TRVW1c32ooVk939p6iGNkaCUo63Et36g==", + "devOptional": true + }, + "@prisma/engines-version": { + "version": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e.tgz", + "integrity": "sha512-X16YmBmj7Omso4ZbkNBe6gPYlNcnwZMUPtXsguCkn+KoMqm3DJD9M4X31gx0Gf13Q44dY3SKPJZUk44/XUj/WA==" + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -13124,6 +13210,15 @@ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" }, + "prisma": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.5.0.tgz", + "integrity": "sha512-WEYQ+H98O0yigG+lI0gfh4iyBChvnM6QTXPDtY9eFraLXAmyb6tf/T2mUdrUAU1AEvHLVzQA5A+RpONZlQozBg==", + "devOptional": true, + "requires": { + "@prisma/engines": "3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e" + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/package.json b/package.json index 75ea7a1..4041ce8 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "dependencies": { "@headlessui/react": "^1.2.0", "@next-auth/mongodb-adapter": "^0.0.2-next.298", + "@next-auth/prisma-adapter": "^0.5.2-next.19", "@popperjs/core": "^2.9.2", + "@prisma/client": "^3.5.0", "@y0c/react-datepicker": "^1.0.4", "axios": "^0.24.0", "btoa": "^1.2.1", @@ -79,6 +81,7 @@ "lint-staged": "^10.4.2", "postcss": "^8.3.0", "prettier": "^2.2.1", + "prisma": "^3.5.0", "sass": "^1.32.13", "typescript": "^4.2.4" } diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index d6582a7..991ccf8 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -1,21 +1,11 @@ -import { MongoDBAdapter } from '@next-auth/mongodb-adapter'; +import { PrismaAdapter } from '@next-auth/prisma-adapter'; import { hasBetaAccess } from 'lib/backend-utils'; -import clientPromise from 'lib/mongodb'; +import { prisma } from 'lib/prisma'; import NextAuth from 'next-auth'; import Discord from 'next-auth/providers/discord'; -interface User { - id: string; - username: string; - discriminator: string; - avatar: string; - email: string; - verified: boolean; - public_flags: number; -} - export default NextAuth({ - adapter: MongoDBAdapter(clientPromise), + adapter: PrismaAdapter(prisma), providers: [ Discord({ clientId: process.env.CLIENT_ID, @@ -23,6 +13,7 @@ export default NextAuth({ authorization: 'https://discord.com/api/oauth2/authorize?scope=identify+guilds', profile(profile) { let image_url: string; + let banner: string; if (profile.avatar === null) { const defaultAvatarNumber = parseInt(profile.discriminator, 10) % 5; @@ -32,15 +23,27 @@ export default NextAuth({ image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; } + if (profile.banner === null) { + banner = `color:${profile.banner_color}`; + } else { + const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'; + banner = `https://cdn.discordapp.com/banners/${profile.id}/${profile.banner}.${format}`; + } + return { id: profile.id, uid: profile.id, username: profile.username, discriminator: profile.discriminator, avatar: image_url, - verified: profile.verified, - public_flags: profile.public_flags, - vanity: `${profile.id}`, + profile: { + create: { + bio: "I'm new to dmod! Not much is known about me yet :(", + public: false, + banner, + flags: 0, + }, + }, }; }, }), @@ -61,4 +64,5 @@ export default NextAuth({ return betaUser ? true : '/?error=AccessDenied'; }, }, + secret: process.env.AUTH_SECRET, }); diff --git a/pages/api/guilds/fetch.ts b/pages/api/guilds/fetch.ts new file mode 100644 index 0000000..7bfd5b4 --- /dev/null +++ b/pages/api/guilds/fetch.ts @@ -0,0 +1,72 @@ +import { prisma } from 'lib/prisma'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getSession } from 'next-auth/react'; + +const endpoint = 'https://discord.com/api/users/@me/guilds'; + +// GET users/sync - sync user data with Discord +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Get session + const session = await getSession({ req }); + + // Check if user is logged in + if (!session) { + res.status(401).json({ message: 'Unauthorized' }); + return; + } + + // Get access token for the session from the database + const { access_token } = await prisma.account.findFirst({ + where: { + userId: session.user.id, + }, + select: { + access_token: true, + }, + }); + + // Get guilds from Discord + const discord_guilds = await fetch(endpoint, { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }).then((r) => r.json()); + + // Get guilds from the database + const db_guilds = await prisma.guild.findMany({ + where: { + members: { + some: { + userId: session.user.id, + }, + } + }, + select: { + id: true, + name: true, + icon: true, + }, + }); + + // Filter discord guilds where the user does not have MANAGE_GUILD permissions + const filtered_guilds = discord_guilds.filter((guild) => { + return (guild.permissions & (1 << 5)) === 1 << 5 ? guild : false; + }); + + // Get Discord guilds that are in the database + const included = filtered_guilds.filter((guild) => { + return db_guilds.find((db_guild) => { + return db_guild.id === guild.id; + }); + }); + + // Get Discord guilds that are not in the database + const excluded = filtered_guilds.filter((guild) => { + return !included.find((included_guild) => { + return included_guild.id === guild.id; + }); + }); + + // Return guilds + res.status(200).json({ included, excluded }); +}; \ No newline at end of file diff --git a/pages/api/auth-old/current_user.ts b/pages/api/old/auth-old/current_user.ts similarity index 100% rename from pages/api/auth-old/current_user.ts rename to pages/api/old/auth-old/current_user.ts diff --git a/pages/api/auth-old/invite.ts b/pages/api/old/auth-old/invite.ts similarity index 100% rename from pages/api/auth-old/invite.ts rename to pages/api/old/auth-old/invite.ts diff --git a/pages/api/auth-old/invite_redirect.ts b/pages/api/old/auth-old/invite_redirect.ts similarity index 100% rename from pages/api/auth-old/invite_redirect.ts rename to pages/api/old/auth-old/invite_redirect.ts diff --git a/pages/api/auth-old/login.ts b/pages/api/old/auth-old/login.ts similarity index 100% rename from pages/api/auth-old/login.ts rename to pages/api/old/auth-old/login.ts diff --git a/pages/api/auth-old/logout.ts b/pages/api/old/auth-old/logout.ts similarity index 100% rename from pages/api/auth-old/logout.ts rename to pages/api/old/auth-old/logout.ts diff --git a/pages/api/auth-old/redirect.ts b/pages/api/old/auth-old/redirect.ts similarity index 100% rename from pages/api/auth-old/redirect.ts rename to pages/api/old/auth-old/redirect.ts diff --git a/pages/api/auth-old/updates.ts b/pages/api/old/auth-old/updates.ts similarity index 100% rename from pages/api/auth-old/updates.ts rename to pages/api/old/auth-old/updates.ts diff --git a/pages/api/v1/kei/search_g.ts b/pages/api/old/kei/search_g.ts similarity index 100% rename from pages/api/v1/kei/search_g.ts rename to pages/api/old/kei/search_g.ts diff --git a/pages/api/v1/kei/search_u.ts b/pages/api/old/kei/search_u.ts similarity index 100% rename from pages/api/v1/kei/search_u.ts rename to pages/api/old/kei/search_u.ts diff --git a/pages/api/v1/servers/[guildID]/settings.ts b/pages/api/old/servers/[guildID]/settings.ts similarity index 100% rename from pages/api/v1/servers/[guildID]/settings.ts rename to pages/api/old/servers/[guildID]/settings.ts diff --git a/pages/api/users/[id].ts b/pages/api/users/[id].ts deleted file mode 100644 index 168476e..0000000 --- a/pages/api/users/[id].ts +++ /dev/null @@ -1,41 +0,0 @@ -import clientPromise from 'lib/mongodb'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { getSession } from 'next-auth/react'; -import { ApiUser } from 'typings/typings'; - -// TODO: Add checks for user permissions - allow admins to edit other users -export default async (req: NextApiRequest, res: NextApiResponse) => { - // request information - const { - query: { id }, - method, - } = req; - - // get the session - const session = await getSession({ req }); - const { user }: any = (session as unknown) || {}; - - // get database - const client = await clientPromise; - const db = client.db(); - - switch (method) { - case 'GET': - // Get data from the database - res.status(200).json({ id, username: `${user.username}#${user.discriminator}` }); - break; - case 'PUT': - // if the session is not authenticated, return an error - if (!session || user.uid !== id) { - res.status(401).json({ message: 'You are not authenticated' }); - return; - } - // Update data in the database - const data = await db.collection('users').findOneAndUpdate({ uid: id }, { $set: JSON.parse(req.body) }); - res.status(200).json(JSON.parse(JSON.stringify(data))); - break; - default: - res.setHeader('Allow', ['GET', 'PUT']); - res.status(405).end(`Method ${method} Not Allowed`); - } -}; diff --git a/pages/api/users/[uid].ts b/pages/api/users/[uid].ts new file mode 100644 index 0000000..a32b9ca --- /dev/null +++ b/pages/api/users/[uid].ts @@ -0,0 +1,89 @@ +import { prisma } from 'lib/prisma'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getSession } from 'next-auth/react'; + +// GET / PUT users/[uid] - get and update user data +export default async (req: NextApiRequest, res: NextApiResponse) => { + // query: { uid: string } + const { + query: { uid }, + method, + } = req; + + // make uid string + const uidStr = typeof uid === 'string' ? uid : uid[0]; + + // Get session + const session = await getSession({ req }); + // Get user data + const user = await prisma.user.findUnique({ + where: { + uid: uidStr, + }, + }); + + // Check if user exists + if (!user) { + res.status(404).json({ + message: 'User not found', + }); + return; + } + + // Get profile data + const profile = await prisma.profile.findFirst({ + where: { + userId: user.id, + }, + }); + + switch (method) { + case 'GET': + // Get data from the database + if (session && session.user.uid === uidStr) { + // Return all the user data + res.status(200).json({ ...user, profile }); + } else { + // Check profile is public + profile.public + ? // Return only the public data + res.status(200).json({ + uid: user.uid, + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + profile: { + bio: profile.bio, + banner: profile.banner, + pronouns: profile.pronouns, + vanity: profile.vanity, + timezone: profile.timezone, + flags: profile.flags, + }, + }) + : // Return nothing + res.status(200).end('Profile not found'); + } + break; + case 'PUT': + // if the session is not authenticated, return an error + if (!session || user.uid !== uid) { + res.status(401).json({ message: 'You are not authenticated' }); + return; + } + // Update data in the database + const data = await prisma.user.update({ + where: { + uid: uidStr, + }, + data: { + ...req.body, + }, + }); + res.status(200).json(data); + break; + default: + res.setHeader('Allow', ['GET', 'PUT']); + res.status(405).end(`Method ${method} Not Allowed`); + } +}; diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts new file mode 100644 index 0000000..fc5710d --- /dev/null +++ b/pages/api/users/index.ts @@ -0,0 +1,74 @@ +import { prisma } from 'lib/prisma'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +// GET all users - get all users (pageinated) +export default async (req: NextApiRequest, res: NextApiResponse) => { + // query: { page: number, count: number } + const { + query: { page = 1, count = 10 }, + method, + } = req; + + // only allow GET requests + if (method !== 'GET') { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${method} Not Allowed`); + return; + } + + // make page and count numbers + const pageNum = + typeof page === 'string' ? parseInt(page, 10) : typeof page === 'number' ? page : parseInt(page[0], 10); + const countNum = + typeof count === 'string' + ? parseInt(count, 10) + : typeof count === 'number' + ? count + : parseInt(count[0], 10); + + // prevent negative page numbers + if (pageNum < 1) { + res.status(400).end('Page must be greater than 0'); + return; + } + + // prevent count being negative + if (countNum < 1) { + res.status(400).end('Count must be greater than 0'); + return; + } + + // prevent count being too large + if (countNum > 100) { + res.status(400).end('Count must be less than 100'); + return; + } + + // get users + const users = await prisma.user.findMany({ + skip: (pageNum - 1) * countNum, + take: countNum, + select: { + uid: true, + username: true, + discriminator: true, + avatar: true, + profile: { + select: { + bio: true, + banner: true, + pronouns: true, + vanity: true, + timezone: true, + flags: true, + }, + }, + }, + orderBy: { + id: 'asc', + }, + }); + + // send response + res.status(200).json(users); +}; diff --git a/pages/api/users/search.ts b/pages/api/users/search.ts new file mode 100644 index 0000000..00e4bc5 --- /dev/null +++ b/pages/api/users/search.ts @@ -0,0 +1,90 @@ +import { prisma } from 'lib/prisma'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +// GET search users - get users that match a query (pageinated) +export default async (req: NextApiRequest, res: NextApiResponse) => { + // query: { query: string, page: number, count: number } + const { + query: { query = '', page = 1, count = 10 }, + method, + } = req; + + // only allow GET requests + if (method !== 'GET') { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${method} Not Allowed`); + return; + } + + // don't allow empty queries + if (!query) { + res.status(400).end('Query is required'); + return; + } + + // make page and count numbers + const pageNum = + typeof page === 'string' ? parseInt(page, 10) : typeof page === 'number' ? page : parseInt(page[0], 10); + const countNum = + typeof count === 'string' + ? parseInt(count, 10) + : typeof count === 'number' + ? count + : parseInt(count[0], 10); + + // prevent negative page numbers + if (pageNum < 1) { + res.status(400).end('Page must be greater than 0'); + return; + } + + // prevent count being negative + if (countNum < 1) { + res.status(400).end('Count must be greater than 0'); + return; + } + + // prevent count being too large + if (countNum > 100) { + res.status(400).end('Count must be less than 100'); + return; + } + + // make query string + const q = typeof query === 'string' ? query : query[0]; + + // get users that match the query + const users = await prisma.user.findMany({ + where: { + OR: [ + { username: { contains: q, mode: 'insensitive' } }, + { + profile: { + vanity: { equals: q, mode: 'insensitive' }, + }, + }, + ], + }, + select: { + uid: true, + username: true, + discriminator: true, + avatar: true, + profile: { + select: { + bio: true, + banner: true, + pronouns: true, + vanity: true, + timezone: true, + flags: true, + }, + }, + }, + skip: (pageNum - 1) * countNum, + take: countNum, + }); + + // send response + res.status(200).json(users); +}; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..9bf3424 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,114 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + previewFeatures = ["mongoDb"] +} + +datasource db { + provider = "mongodb" + url = env("MONGODB_URI") +} + +model Account { + id String @id @default(dbgenerated()) @map("_id") @db.ObjectId + userId String @map("user_id") + type String + provider String + providerAccountId String @map("provider_account_id") + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@map("accounts") +} + +model User { + id String @id @default(dbgenerated()) @map("_id") @db.ObjectId + uid String @unique + username String? + discriminator String? + email String? @unique + emailVerified DateTime? @map("email_verified") + avatar String? + locale String? + profile Profile? + accounts Account[] + sessions Session[] + guilds GuildUser[] + + @@map("users") +} + +model Session { + id String @id @default(dbgenerated()) @map("_id") @db.ObjectId + sessionToken String @unique @map("session_token") + userId String @map("user_id") + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("sessions") +} + +model VerificationToken { + identifier String @id @map("_id") @db.ObjectId + token String @unique + expires DateTime + + @@unique([identifier, token]) + @@map("verificationtokens") +} + +model Profile { + id String @id @default(dbgenerated()) @map("_id") @db.ObjectId + userId String @map("user_id") + public Boolean @default(false) + bio String? + banner String? + pronouns String? + vanity String? @unique + website String? + timezone String? + flags Int? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("profiles") +} + +model Guild { + id String @id @default(dbgenerated()) @map("_id") @db.ObjectId + name String + icon String? + members GuildUser[] + + // TODO: Add guild settings + + @@map("guilds") +} + +model GuildUser { + id String @id @default(dbgenerated()) @map("_id") @db.ObjectId + guildId String @map("guild_id") + userId String @map("user_id") + type GuildUserType @map("type") + permissions Int? + + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("guild_users") +} + +enum GuildUserType { + OWNER + MANAGER + MODERATOR +} diff --git a/typings/next-auth.d.ts b/typings/next-auth.d.ts new file mode 100644 index 0000000..a4b4ac4 --- /dev/null +++ b/typings/next-auth.d.ts @@ -0,0 +1,22 @@ +import NextAuth from 'next-auth'; + +declare module 'next-auth' { + /** + * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context + */ + interface Session { + user: User; + } + + /** + * The shape of the user object returned in the OAuth providers' `profile` callback, + * or the second parameter of the `session` callback, when using a database. + */ + interface User { + uid: String; + username?: String; + discriminator?: String; + avatar?: String; + locale?: String; + } +}