From 82adeda6777d6810cd070088645606c09b06f6d9 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Sun, 16 Aug 2026 11:20:28 -0500 Subject: [PATCH 1/4] remove getRobloxUserId --- pages/api/auth/login.ts | 80 ++++- pages/api/setupworkspace.ts | 88 ++++- .../api/workspace/[id]/settings/users/add.ts | 329 ++++++++++++------ .../migrations/20260816154509/migration.sql | 53 +++ utils/roblox.ts | 11 +- 5 files changed, 428 insertions(+), 133 deletions(-) create mode 100644 prisma/migrations/20260816154509/migration.sql diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index 581353a3..232e36e5 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -4,7 +4,6 @@ import { getThumbnail, getDisplayName, } from "@/utils/userinfoEngine"; -import { getRobloxUserId } from "@/utils/roblox"; import bcryptjs from "bcryptjs"; import * as noblox from "noblox.js"; import prisma from "@/utils/database"; @@ -13,6 +12,54 @@ import { NextApiHandler } from "next"; import { createSession } from "@/utils/session"; import cache from "@/utils/cache"; +async function lookupRobloxUserId(username: string): Promise { + const response = await fetch("https://users.roblox.com/v1/usernames/users", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + usernames: [username.trim()], + excludeBannedUsers: false, + }), + }); + + const text = await response.text(); + + if (!response.ok) { + console.error("[Roblox] Username lookup failed:", { + username, + status: response.status, + statusText: response.statusText, + body: text, + }); + + throw new Error( + `Roblox username lookup failed with HTTP ${response.status}`, + ); + } + + let data: { + data?: Array<{ + requestedUsername: string; + hasVerifiedBadge: boolean; + id: number; + name: string; + displayName: string; + }>; + }; + + try { + data = JSON.parse(text); + } catch { + console.error("[Roblox] Invalid JSON response:", text); + throw new Error("Roblox returned invalid JSON"); + } + + return data.data?.[0]?.id ?? null; +} + async function getCachedGroupInfo(groupId: number) { const cacheKey = `roblox:group:${groupId}`; @@ -165,13 +212,32 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { let id = await cache.get(`roblox:id:${usernameKey}`); if (!id) { - id = (await getRobloxUserId(req.body.username).catch((e) => { - console.error("Roblox API error:", e); - return null; - })) as number | undefined; + try { + id = await lookupRobloxUserId(req.body.username); + + if (id) { + await cache.set(`roblox:id:${usernameKey}`, id, 3600); + } + } catch (error) { + console.error("[Roblox] Login username lookup failed:", { + username: req.body.username, + error, + }); + - if (id) { - await cache.set(`roblox:id:${usernameKey}`, id, 3600); + if (error instanceof Error && error.message.includes("429")) { + return res.status(503).json({ + success: false, + error: + "Roblox is temporarily limiting requests. Please wait a moment and try again.", + }); + } + + return res.status(502).json({ + success: false, + error: + "We couldn't contact Roblox to verify your username. Please try again in a moment.", + }); } } diff --git a/pages/api/setupworkspace.ts b/pages/api/setupworkspace.ts index 60d7873a..d58995aa 100644 --- a/pages/api/setupworkspace.ts +++ b/pages/api/setupworkspace.ts @@ -6,7 +6,7 @@ import prisma from "@/utils/database"; import * as noblox from "noblox.js"; import bcryptjs from "bcryptjs"; import { setRegistry } from "@/utils/registryManager"; -import { getRobloxUserId, isGroupAllied } from "@/utils/roblox"; +import { isGroupAllied } from "@/utils/roblox"; import { createSession } from "@/utils/session"; type Data = { @@ -53,15 +53,87 @@ export default async function handler( } try { - const userid = await getRobloxUserId(username).catch((err) => { - console.error("Failed getting Roblox user ID:", err); - return null; - }); + const trimmedUsername = username.trim(); + + let userid: number; + + try { + const robloxResponse = await fetch( + "https://users.roblox.com/v1/usernames/users", + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + usernames: [trimmedUsername], + excludeBannedUsers: false, + }), + }, + ); + + const responseText = await robloxResponse.text(); + + if (!robloxResponse.ok) { + console.error("[Roblox] Username lookup failed:", { + username: trimmedUsername, + status: robloxResponse.status, + statusText: robloxResponse.statusText, + body: responseText, + }); + + if (robloxResponse.status === 400) { + return res.status(400).json({ + success: false, + error: + "Roblox rejected the username lookup. Please check the username and try again.", + }); + } - if (!userid) { - return res.status(404).json({ + return res.status(502).json({ + success: false, + error: `Roblox could not be reached or returned an error (HTTP ${robloxResponse.status}). Please try again in a moment.`, + }); + } + + let data: { + data?: Array<{ + requestedUsername: string; + hasVerifiedBadge: boolean; + id: number; + name: string; + displayName: string; + }>; + }; + + try { + data = JSON.parse(responseText); + } catch { + console.error("[Roblox] Invalid JSON response:", responseText); + return res.status(502).json({ + success: false, + error: + "Roblox returned an invalid response. Please try again in a moment.", + }); + } + const robloxUser = data.data?.[0]; + if (!robloxUser) { + return res.status(404).json({ + success: false, + error: `Roblox username "${trimmedUsername}" was not found. Please check that you entered your username correctly.`, + }); + } + userid = robloxUser.id; + } catch (error) { + console.error("[Roblox] Username lookup request failed:", { + username: trimmedUsername, + error, + }); + return res.status(502).json({ success: false, - error: "Username not found", + error: + "We couldn't contact Roblox to verify your username. Please try again in a moment.", }); } diff --git a/pages/api/workspace/[id]/settings/users/add.ts b/pages/api/workspace/[id]/settings/users/add.ts index 704fe457..29e62127 100644 --- a/pages/api/workspace/[id]/settings/users/add.ts +++ b/pages/api/workspace/[id]/settings/users/add.ts @@ -1,112 +1,225 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from 'next' -import prisma from '@/utils/database'; -import { withPermissionCheck } from '@/utils/permissionsManager' -import { logAudit } from '@/utils/logs'; -import { getUsername, getThumbnail, getDisplayName } from '@/utils/userinfoEngine' -import { getRobloxUserId } from "@/utils/roblox"; +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "@/utils/database"; +import { withPermissionCheck } from "@/utils/permissionsManager"; +import { logAudit } from "@/utils/logs"; +import { + getUsername, + getThumbnail, + getDisplayName, +} from "@/utils/userinfoEngine"; + type Data = { - success: boolean - error?: string - user?: any + success: boolean; + error?: string; + user?: any; +}; + +async function lookupRobloxUserId(username: string): Promise { + const response = await fetch("https://users.roblox.com/v1/usernames/users", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + usernames: [username.trim()], + excludeBannedUsers: false, + }), + }); + + const text = await response.text(); + + if (!response.ok) { + console.error("[Roblox] Username lookup failed:", { + username, + status: response.status, + statusText: response.statusText, + body: text, + }); + + throw new Error( + `Roblox username lookup failed with HTTP ${response.status}`, + ); + } + + let data: { + data?: Array<{ + requestedUsername: string; + hasVerifiedBadge: boolean; + id: number; + name: string; + displayName: string; + }>; + }; + + try { + data = JSON.parse(text); + } catch { + console.error("[Roblox] Invalid JSON response:", text); + throw new Error("Roblox returned invalid JSON"); + } + + return data.data?.[0]?.id ?? null; } -export default withPermissionCheck(handler, 'admin'); - -export async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method !== 'POST') return res.status(405).json({ success: false, error: 'Method not allowed' }); - const userid = await getRobloxUserId(req.body.username).catch(() => null) as bigint | null; - if (!userid) return res.status(400).json({ success: false, error: 'Invalid username' }); - - const role = await prisma.role.findFirst({ - where: { - workspaceGroupId: parseInt(req.query.id as string), - } - }); - const u = await prisma.user.findFirst({ - where: { - userid: userid, - roles: { - some: { - workspaceGroupId: parseInt(req.query.id as string) - } - } - }, - }); - if (u) return res.status(400).json({ success: false, error: 'User already exists' }); - if (!role) return res.status(404).json({ success: false, error: 'Role not found' }); - - const user = await prisma.user.upsert({ - where: { - userid: userid - }, - update: { - username: await getUsername(userid), - roles: { - connect: { - id: role.id - } - } - }, - create: { - userid: userid, - username: await getUsername(userid), - - roles: { - connect: { - id: role.id - } - } - }, - }); - - await prisma.roleMember.upsert({ - where: { - roleId_userId: { - roleId: role.id, - userId: userid - } - }, - update: { - manuallyAdded: true - }, - create: { - roleId: role.id, - userId: userid, - manuallyAdded: true - } - }); - - await prisma.workspaceMember.upsert({ - where: { - workspaceGroupId_userId: { - workspaceGroupId: parseInt(req.query.id as string), - userId: userid - } - }, - update: {}, - create: { - workspaceGroupId: parseInt(req.query.id as string), - userId: userid, - joinDate: new Date(), - isAdmin: false - } - }); - - const newuser = { - roles: [ - role - ], - userid: Number(user.userid), - username: req.body.username, - displayName: await getDisplayName(userid), - thumbnail: getThumbnail(userid) - } - - try { await logAudit(parseInt(req.query.id as string), (req as any).auth?.userId || null, 'settings.users.add', `user:${Number(user.userid)}`, { userId: Number(user.userid), username: req.body.username, role: role.id }); } catch (e) {} - - res.status(200).json({ success: true, user: newuser }) +export default withPermissionCheck(handler, "admin"); + +export async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return res.status(405).json({ + success: false, + error: "Method not allowed", + }); + } + + const username = req.body?.username?.trim(); + + if (!username) { + return res.status(400).json({ + success: false, + error: "Username is required", + }); + } + + let userid: number | null; + + try { + userid = await lookupRobloxUserId(username); + } catch (error) { + console.error("[Roblox] Failed to resolve username:", { + username, + error, + }); + + return res.status(502).json({ + success: false, + error: + "We couldn't contact Roblox to verify this username. Please try again in a moment.", + }); + } + + if (!userid) { + return res.status(400).json({ + success: false, + error: "Invalid Roblox username", + }); + } + + const workspaceGroupId = parseInt(req.query.id as string); + + const role = await prisma.role.findFirst({ + where: { + workspaceGroupId, + }, + }); + + const u = await prisma.user.findFirst({ + where: { + userid, + roles: { + some: { + workspaceGroupId, + }, + }, + }, + }); + + if (u) { + return res.status(400).json({ + success: false, + error: "User already exists", + }); + } + + if (!role) { + return res.status(404).json({ + success: false, + error: "Role not found", + }); + } + + const usernameFromRoblox = await getUsername(userid); + + const user = await prisma.user.upsert({ + where: { + userid, + }, + update: { + username: usernameFromRoblox, + roles: { + connect: { + id: role.id, + }, + }, + }, + create: { + userid, + username: usernameFromRoblox, + roles: { + connect: { + id: role.id, + }, + }, + }, + }); + + await prisma.roleMember.upsert({ + where: { + roleId_userId: { + roleId: role.id, + userId: userid, + }, + }, + update: { + manuallyAdded: true, + }, + create: { + roleId: role.id, + userId: userid, + manuallyAdded: true, + }, + }); + + await prisma.workspaceMember.upsert({ + where: { + workspaceGroupId_userId: { + workspaceGroupId, + userId: userid, + }, + }, + update: {}, + create: { + workspaceGroupId, + userId: userid, + joinDate: new Date(), + isAdmin: false, + }, + }); + + const newuser = { + roles: [role], + userid: Number(user.userid), + username: usernameFromRoblox, + displayName: await getDisplayName(userid), + thumbnail: getThumbnail(userid), + }; + + try { + await logAudit( + workspaceGroupId, + (req as any).auth?.userId || null, + "settings.users.add", + `user:${Number(user.userid)}`, + { + userId: Number(user.userid), + username: usernameFromRoblox, + role: role.id, + }, + ); + } catch {} + + return res.status(200).json({ + success: true, + user: newuser, + }); } diff --git a/prisma/migrations/20260816154509/migration.sql b/prisma/migrations/20260816154509/migration.sql new file mode 100644 index 00000000..88f1b7f4 --- /dev/null +++ b/prisma/migrations/20260816154509/migration.sql @@ -0,0 +1,53 @@ +/* + Warnings: + + - You are about to drop the `WallPost` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `WallReaction` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `media` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "WallPost" DROP CONSTRAINT "WallPost_authorId_fkey"; + +-- DropForeignKey +ALTER TABLE "WallPost" DROP CONSTRAINT "WallPost_mediaId_fkey"; + +-- DropForeignKey +ALTER TABLE "WallPost" DROP CONSTRAINT "WallPost_workspaceGroupId_fkey"; + +-- DropForeignKey +ALTER TABLE "WallReaction" DROP CONSTRAINT "WallReaction_postId_fkey"; + +-- DropForeignKey +ALTER TABLE "WallReaction" DROP CONSTRAINT "WallReaction_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "media" DROP CONSTRAINT "media_uploadedBy_fkey"; + +-- DropTable +DROP TABLE "WallPost"; + +-- DropTable +DROP TABLE "WallReaction"; + +-- DropTable +DROP TABLE "media"; + +-- CreateTable +CREATE TABLE "wallPost" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "workspaceGroupId" INTEGER NOT NULL, + "authorId" BIGINT NOT NULL, + "image" TEXT, + + CONSTRAINT "wallPost_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "wallPost" ADD CONSTRAINT "wallPost_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "user"("userid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "wallPost" ADD CONSTRAINT "wallPost_workspaceGroupId_fkey" FOREIGN KEY ("workspaceGroupId") REFERENCES "workspace"("groupId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/utils/roblox.ts b/utils/roblox.ts index 51ad6045..ab9ad1c0 100644 --- a/utils/roblox.ts +++ b/utils/roblox.ts @@ -4,7 +4,7 @@ import { OpenCloud } from "@relatiohq/opencloud"; import packageInfo from "@/package.json"; interface groupAlly { - relatedGroups: + relatedGroups: { id: 0; }[]; @@ -544,15 +544,6 @@ export async function getUsersWithinAGroupRoleset( } } -export async function getRobloxUserId(username: string): Promise { - try { - return await withTimeout(noblox.getIdFromUsername(username)); - } catch (error) { - console.error(`Error getting user ID for username ${username}:`, error); - throw error; - } -} - export async function isGroupAllied(groupid: string | number): Promise { try { const alliedGroups = await axios.get( From 017f5d502ba05c5535501e07b8c52db2c9198c3a Mon Sep 17 00:00:00 2001 From: buddywinte Date: Sun, 16 Aug 2026 11:22:35 -0500 Subject: [PATCH 2/4] fix api key error --- utils/permissionsManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/utils/permissionsManager.ts b/utils/permissionsManager.ts index e2945bcc..d4474913 100644 --- a/utils/permissionsManager.ts +++ b/utils/permissionsManager.ts @@ -500,6 +500,11 @@ export async function checkGroupRoles(groupID: number) { console.log(`[update-group] Starting sync for group ${safeGroupId}`); const apiKey = await getConfig("roblox_opencloud", groupID); + if (!apiKey?.key) { + throw new Error( + `No Roblox Open Cloud API key configured for workspace/group ${groupID}`, + ); + } let successful = true; try { From 173faf73fed6eb426234a3c8838055b845e24933 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Sun, 16 Aug 2026 11:31:53 -0500 Subject: [PATCH 3/4] don't throw, just console.log --- pages/login.tsx | 16 +++++----------- utils/permissionsManager.ts | 5 ++--- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/pages/login.tsx b/pages/login.tsx index d42a646c..54f05c84 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -458,17 +458,11 @@ const Login: NextPage = () => {
{loginBg ? ( - <> -
-
- +
) : null}
Date: Sun, 16 Aug 2026 12:14:30 -0500 Subject: [PATCH 4/4] package cleanups + popup cleanup + logo clickable --- bun.lock | 18 +- components/ThemeToggle.tsx | 4 +- components/nav/ThemeToggler.tsx | 2 +- components/topbar.tsx | 291 +++++++++++--------------------- package.json | 8 +- utils/closesessions.ts | 11 +- utils/database.ts | 63 +++++-- 7 files changed, 169 insertions(+), 228 deletions(-) diff --git a/bun.lock b/bun.lock index 4eacb945..4d5ff6c1 100644 --- a/bun.lock +++ b/bun.lock @@ -9,9 +9,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.7.19", - "@heroicons/react": "^2.2.0", - "@intercom/messenger-js-sdk": "^0.0.14", - "@prisma/adapter-pg": "^7.8.0", + "@prisma/adapter-pg": "^7.9.1", "@relatiohq/opencloud": "^1.10.2", "@tabler/icons-react": "^3.44.0", "@tailwindcss/forms": "^0.5.11", @@ -32,7 +30,7 @@ "bcryptjs": "^3.0.3", "chart.js": "^4.5.1", "clsx": "^2.1.1", - "cookie": "^1.1.1", + "cookie": "^1.0.2", "cross-env": "^10.1.0", "dotenv": "^16.6.1", "emoji-picker-react": "^4.19.1", @@ -54,7 +52,7 @@ "noblox.js": "^7.3.1", "node-cache": "^5.1.2", "node-cron": "^4.6.0", - "pg": "^8.22.0", + "pg": "^8.23.0", "posthog-js": "^1.396.6", "react": "^18.3.1", "react-chartjs-2": "^5.3.1", @@ -146,8 +144,6 @@ "@headlessui/react": ["@headlessui/react@1.7.19", "", { "dependencies": { "@tanstack/react-virtual": "^3.0.0-beta.60", "client-only": "^0.0.1" }, "peerDependencies": { "react": "^16 || ^17 || ^18", "react-dom": "^16 || ^17 || ^18" } }, "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw=="], - "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], - "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], @@ -212,8 +208,6 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@intercom/messenger-js-sdk": ["@intercom/messenger-js-sdk@0.0.14", "", {}, "sha512-2dH4BDAh9EI90K7hUkAdZ76W79LM45Sd1OBX7t6Vzy8twpNiQ5X+7sH9G5hlJlkSGnf+vFWlFcy9TOYAyEs1hA=="], - "@ioredis/commands": ["@ioredis/commands@2.0.0", "", {}, "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg=="], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -1590,7 +1584,7 @@ "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], - "pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="], + "pg": ["pg@8.23.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg=="], "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], @@ -2098,6 +2092,8 @@ "@parcel/watcher/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "@prisma/adapter-pg/pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.9.1", "", { "dependencies": { "@prisma/debug": "7.9.1" } }, "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.9.1", "", { "dependencies": { "@prisma/debug": "7.9.1" } }, "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw=="], @@ -2194,6 +2190,8 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "pg/pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="], + "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postman-request/form-data": ["form-data@2.3.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ=="], diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx index ad9d3919..97ab7b29 100644 --- a/components/ThemeToggle.tsx +++ b/components/ThemeToggle.tsx @@ -1,5 +1,5 @@ import { useTheme } from "next-themes"; -import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; +import { MoonIcon, SunIcon } from "lucide-react"; const ThemeToggle = () => { const { theme, setTheme, resolvedTheme } = useTheme(); @@ -24,4 +24,4 @@ const ThemeToggle = () => { ); }; -export default ThemeToggle; \ No newline at end of file +export default ThemeToggle; diff --git a/components/nav/ThemeToggler.tsx b/components/nav/ThemeToggler.tsx index c1dddbfb..8fe6d333 100644 --- a/components/nav/ThemeToggler.tsx +++ b/components/nav/ThemeToggler.tsx @@ -1,6 +1,6 @@ "use client"; -import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; +import { MoonIcon, SunIcon } from "lucide-react"; import { useTheme } from "next-themes"; import { useState, useEffect } from "react"; diff --git a/components/topbar.tsx b/components/topbar.tsx index 87fa62fc..a73cc407 100644 --- a/components/topbar.tsx +++ b/components/topbar.tsx @@ -16,15 +16,17 @@ import { IconChevronLeft, IconTrash, IconRefresh, + IconX, + IconCrown, + IconXd, } from "@tabler/icons-react"; -import { workspacesModalPanelClass } from "@/components/workspaces/shell" import axios from "axios"; import { Fragment, useEffect, useRef, useState } from "react"; import toast from "react-hot-toast"; import { DiscordOAuthAvailable } from "@/hooks/useDiscordOAuth"; import { GoogleOAuthAvailable } from "@/hooks/useGoogleOAuth"; -import { CrownIcon } from "lucide-react"; import moment from "moment"; +import Link from "next/link"; type Session = { id: string; @@ -46,7 +48,7 @@ function DeviceIcon({ device }: { device: string | null }) { return ; } -type Panel = "main" | "settings" | "sessions"; +type Panel = "settings" | "sessions"; const Topbar: NextPage = () => { const [login, setLogin] = useRecoilState(loginState); @@ -54,7 +56,7 @@ const Topbar: NextPage = () => { const { isAvailable: isDiscordOAuth } = DiscordOAuthAvailable(); const { isAvailable: isGoogleOAuth } = GoogleOAuthAvailable(); const [open, setOpen] = useState(false); - const [panel, setPanel] = useState("main"); + const [panel, setPanel] = useState("settings"); const [sessions, setSessions] = useState([]); const [sessionsLoading, setSessionsLoading] = useState(false); const router = useRouter(); @@ -64,13 +66,6 @@ const Topbar: NextPage = () => { setTheme(resolvedTheme === "dark" ? "light" : "dark"); }; - const openPanel = (p: Panel) => setPanel(p); - - const handleOpen = () => { - setPanel("main"); - setOpen(true); - }; - const fetchSessions = async () => { setSessionsLoading(true); try { @@ -169,7 +164,7 @@ const Topbar: NextPage = () => {
- Planetary + Planetary
- - )} - */} - {login.canMakeWorkspace && ( <> - - -
-
- -
-
- - )} - {panel === "sessions" && ( <>

Active sessions @@ -539,10 +447,10 @@ const Topbar: NextPage = () => { <>

@@ -556,103 +464,105 @@ const Topbar: NextPage = () => {
-
-

- Connected accounts -

- -
- {isDiscordOAuth && - (login.discordUser ? ( -
- - -
-

- {login.discordUser.username} -

-
- -

- Discord connected + {isDiscordOAuth || isGoogleOAuth && ( +

+

+ Connected accounts +

+ +
+ {isDiscordOAuth && + (login.discordUser ? ( +
+ + +
+

+ {login.discordUser.username}

+
+ +

+ Discord connected +

+
-
+ +
+ ) : ( -
- ) : ( - - ))} - - {isGoogleOAuth && - (login.googleUser ? ( -
- + - -
-

- {login.googleUser.email} -

-
- -

- Google connected + : "/default-avatar.jpg" + } + alt="" + className="h-9 w-9 rounded-full" + /> + +

+

+ {login.googleUser.email}

+
+ +

+ Google connected +

+
-
+ +
+ ) : ( -
- ) : ( - - ))} -
-
+ ))} +
+ + )}
@@ -670,9 +580,6 @@ const Topbar: NextPage = () => {

Sign out all devices

-

- Revoke all active sessions -

diff --git a/package.json b/package.json index caee9258..095fb276 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.7.19", - "@heroicons/react": "^2.2.0", - "@intercom/messenger-js-sdk": "^0.0.14", - "@prisma/adapter-pg": "^7.8.0", + "@prisma/adapter-pg": "^7.9.1", "@relatiohq/opencloud": "^1.10.2", "@tabler/icons-react": "^3.44.0", "@tailwindcss/forms": "^0.5.11", @@ -40,7 +38,7 @@ "bcryptjs": "^3.0.3", "chart.js": "^4.5.1", "clsx": "^2.1.1", - "cookie": "^1.1.1", + "cookie": "^1.0.2", "cross-env": "^10.1.0", "dotenv": "^16.6.1", "emoji-picker-react": "^4.19.1", @@ -62,7 +60,7 @@ "noblox.js": "^7.3.1", "node-cache": "^5.1.2", "node-cron": "^4.6.0", - "pg": "^8.22.0", + "pg": "^8.23.0", "posthog-js": "^1.396.6", "react": "^18.3.1", "react-chartjs-2": "^5.3.1", diff --git a/utils/closesessions.ts b/utils/closesessions.ts index e1077848..a647ddcc 100644 --- a/utils/closesessions.ts +++ b/utils/closesessions.ts @@ -11,15 +11,15 @@ export async function closeActiveSessions() { }, }); + console.log( + `[STARTUP] Found ${activeSessions.length} active session(s).` + ); + if (activeSessions.length === 0) { console.log("[STARTUP] No active sessions found."); return; } - console.log( - `[STARTUP] Found ${activeSessions.length} active session(s). Closing them now...` - ); - const result = await prisma.activitySession.updateMany({ where: { active: true, @@ -34,6 +34,7 @@ export async function closeActiveSessions() { `[STARTUP] Successfully closed ${result.count} active session(s).` ); } catch (error) { - console.error("[STARTUP] Error closing active sessions:", error); + console.error("[STARTUP] Error closing active sessions:"); + console.error(error); } } diff --git a/utils/database.ts b/utils/database.ts index dd6ee79a..b0847b55 100644 --- a/utils/database.ts +++ b/utils/database.ts @@ -1,26 +1,63 @@ -import { PrismaClient, role, workspace, user, Session, SessionType, schedule, ActivitySession, document, wallPost, inactivityNotice, sessionUser, Quota, Ally, allyVisit, RoleMember, AuthSession } from "@prisma/client"; -import { PrismaPg } from '@prisma/adapter-pg'; -import { Pool } from 'pg'; +import { + PrismaClient, + role, + workspace, + user, + Session, + SessionType, + schedule, + ActivitySession, + document, + wallPost, + inactivityNotice, + sessionUser, + Quota, + Ally, + allyVisit, + RoleMember, + AuthSession, +} from "@prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; declare global { - var prisma: PrismaClient; - var pgPool: Pool; + var prisma: PrismaClient | undefined; } -const pool = globalThis.pgPool || new Pool({ - connectionString: process.env.DATABASE_URL, +const adapter = new PrismaPg({ + connectionString: process.env.DATABASE_URL!, }); -if (process.env.NODE_ENV === 'development') globalThis.pgPool = pool; +const prisma = + globalThis.prisma ?? + new PrismaClient({ + adapter, + }); -const adapter = new PrismaPg(pool); -const prisma = globalThis.prisma || new PrismaClient({ adapter }); +if (process.env.NODE_ENV !== "production") { + globalThis.prisma = prisma; +} (BigInt.prototype as any).toJSON = function () { return this.toString(); }; -if (process.env.NODE_ENV === 'development') globalThis.prisma = prisma +export type { + role, + workspace, + user, + Session, + SessionType, + schedule, + ActivitySession, + document, + wallPost, + inactivityNotice, + sessionUser, + Quota, + Ally, + allyVisit, + RoleMember, + AuthSession, +}; -export type { role, workspace, user, Session, SessionType, schedule, ActivitySession, document, wallPost, inactivityNotice, sessionUser, Quota, Ally, allyVisit, RoleMember, AuthSession }; -export default prisma; \ No newline at end of file +export default prisma;