diff --git a/components/topbar.tsx b/components/topbar.tsx index a3876990..1a4266de 100644 --- a/components/topbar.tsx +++ b/components/topbar.tsx @@ -2,7 +2,6 @@ import type { NextPage } from "next"; import { Dialog, Transition } from "@headlessui/react"; import { loginState } from "@/state"; import { useRecoilState } from "recoil"; -import { Menu } from "@headlessui/react"; import Router, { useRouter } from "next/router"; import { useTheme } from "next-themes"; import { @@ -23,8 +22,8 @@ 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 { CrownIcon, RefreshCwIcon } from "lucide-react"; type Session = { id: string; @@ -36,6 +35,8 @@ type Session = { createdAt: string; expiresAt: string; isCurrent: boolean; + region?: string; + country?: string; }; function DeviceIcon({ device }: { device: string | null }) { @@ -347,7 +348,7 @@ const Topbar: NextPage = () => { )}

- {s.ipAddress} · {moment(s.createdAt).fromNow()} + {[s.region, s.country].filter(Boolean).join(', ') || `${s.ipAddress} · ${moment(s.createdAt).fromNow()}`}

{!s.isCurrent && ( diff --git a/pages/_app.tsx b/pages/_app.tsx index 7eaa6765..63dd55a9 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -137,7 +137,24 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { {!showLoader && (
- +
@@ -168,7 +185,7 @@ function Initializer() { mounted = false; try { posthogRef.current?.reset(); - } catch (e) {} + } catch (e) { } }; }, []); @@ -188,7 +205,7 @@ function Initializer() { } else { try { ph.reset(); - } catch (e) {} + } catch (e) { } } } } catch (e) { diff --git a/pages/api/user/sessions/index.ts b/pages/api/user/sessions/index.ts index 46357225..9ad00506 100644 --- a/pages/api/user/sessions/index.ts +++ b/pages/api/user/sessions/index.ts @@ -20,6 +20,8 @@ export async function handler( createdAt: s.createdAt, expiresAt: s.expiresAt, isCurrent: s.id === req.auth.session?.id, + country: s.country, + region: s.region })), }) } diff --git a/prisma/migrations/20260713091923_added_locations/migration.sql b/prisma/migrations/20260713091923_added_locations/migration.sql new file mode 100644 index 00000000..e3de3bfe --- /dev/null +++ b/prisma/migrations/20260713091923_added_locations/migration.sql @@ -0,0 +1,173 @@ +-- AlterTable +ALTER TABLE "AuthSession" ADD COLUMN "city" TEXT, +ADD COLUMN "country" TEXT; + +-- CreateTable +CREATE TABLE "form" ( + "id" UUID NOT NULL, + "workspaceGroupId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "isEnabled" BOOLEAN NOT NULL, + "settings" JSONB NOT NULL, + "visibility" JSONB, + "createdById" BIGINT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "form_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formQuestion" ( + "id" UUID NOT NULL, + "formId" UUID NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "type" TEXT NOT NULL, + "required" BOOLEAN NOT NULL DEFAULT false, + "position" INTEGER NOT NULL, + "settings" JSONB, + "visibilityRules" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedat" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "formQuestion_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formSubmission" ( + "id" UUID NOT NULL, + "formId" UUID NOT NULL, + "workspaceGroupId" INTEGER NOT NULL, + "userId" BIGINT, + "robloxUserId" BIGINT, + "discordUserId" TEXT, + "status" TEXT NOT NULL DEFAULT 'SUBMITTED', + "metadata" JSONB, + "submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "formSubmission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formAnswer" ( + "id" UUID NOT NULL, + "submissionId" UUID NOT NULL, + "questionId" UUID NOT NULL, + "value" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "formAnswer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formReview" ( + "id" UUID NOT NULL, + "submissionId" UUID NOT NULL, + "reviewerId" BIGINT NOT NULL, + "vote" TEXT, + "score" INTEGER, + "notes" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "formReview_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formComment" ( + "id" UUID NOT NULL, + "submissionId" UUID NOT NULL, + "authorId" BIGINT NOT NULL, + "content" TEXT NOT NULL, + "internal" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "formComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formAuditLog" ( + "id" UUID NOT NULL, + "workspaceGroupId" INTEGER, + "formId" UUID, + "submissionId" UUID, + "actorId" BIGINT, + "action" TEXT NOT NULL, + "details" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "formAuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "form_workspaceGroupId_idx" ON "form"("workspaceGroupId"); + +-- CreateIndex +CREATE INDEX "formQuestion_formId_idx" ON "formQuestion"("formId"); + +-- CreateIndex +CREATE INDEX "formSubmission_formId_idx" ON "formSubmission"("formId"); + +-- CreateIndex +CREATE INDEX "formSubmission_workspaceGroupId_idx" ON "formSubmission"("workspaceGroupId"); + +-- CreateIndex +CREATE INDEX "formSubmission_status_idx" ON "formSubmission"("status"); + +-- CreateIndex +CREATE INDEX "formAnswer_submissionId_idx" ON "formAnswer"("submissionId"); + +-- CreateIndex +CREATE INDEX "formAnswer_questionId_idx" ON "formAnswer"("questionId"); + +-- CreateIndex +CREATE INDEX "formReview_submissionId_idx" ON "formReview"("submissionId"); + +-- CreateIndex +CREATE INDEX "formReview_reviewerId_idx" ON "formReview"("reviewerId"); + +-- CreateIndex +CREATE INDEX "formComment_submissionId_idx" ON "formComment"("submissionId"); + +-- CreateIndex +CREATE INDEX "formAuditLog_formId_idx" ON "formAuditLog"("formId"); + +-- CreateIndex +CREATE INDEX "formAuditLog_submissionId_idx" ON "formAuditLog"("submissionId"); + +-- CreateIndex +CREATE INDEX "formAuditLog_workspaceGroupId_idx" ON "formAuditLog"("workspaceGroupId"); + +-- AddForeignKey +ALTER TABLE "form" ADD CONSTRAINT "form_workspaceGroupId_fkey" FOREIGN KEY ("workspaceGroupId") REFERENCES "workspace"("groupId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "form" ADD CONSTRAINT "form_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "user"("userid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formQuestion" ADD CONSTRAINT "formQuestion_formId_fkey" FOREIGN KEY ("formId") REFERENCES "form"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formSubmission" ADD CONSTRAINT "formSubmission_formId_fkey" FOREIGN KEY ("formId") REFERENCES "form"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formAnswer" ADD CONSTRAINT "formAnswer_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "formSubmission"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formAnswer" ADD CONSTRAINT "formAnswer_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "formQuestion"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formReview" ADD CONSTRAINT "formReview_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "formSubmission"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formComment" ADD CONSTRAINT "formComment_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "formSubmission"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formAuditLog" ADD CONSTRAINT "formAuditLog_formId_fkey" FOREIGN KEY ("formId") REFERENCES "form"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "formAuditLog" ADD CONSTRAINT "formAuditLog_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "formSubmission"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260713092345_changed_to_region/migration.sql b/prisma/migrations/20260713092345_changed_to_region/migration.sql new file mode 100644 index 00000000..87daae10 --- /dev/null +++ b/prisma/migrations/20260713092345_changed_to_region/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `city` on the `AuthSession` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "AuthSession" DROP COLUMN "city", +ADD COLUMN "region" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 14acff7d..b4d16557 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -113,6 +113,8 @@ model AuthSession { browser String? os String? device String? + region String? + country String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt expiresAt DateTime diff --git a/utils/database.ts b/utils/database.ts index 6412c2c1..dd6ee79a 100644 --- a/utils/database.ts +++ b/utils/database.ts @@ -1,4 +1,4 @@ -import { PrismaClient, role, workspace, user, Session, SessionType, schedule, ActivitySession, document, wallPost, inactivityNotice, sessionUser, Quota, Ally, allyVisit, RoleMember } from "@prisma/client"; +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'; @@ -22,5 +22,5 @@ const prisma = globalThis.prisma || new PrismaClient({ adapter }); 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 }; +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 diff --git a/utils/randomText.ts b/utils/randomText.ts index b5c3442a..d208602c 100644 --- a/utils/randomText.ts +++ b/utils/randomText.ts @@ -176,6 +176,7 @@ const randomText = (name: string): string => { `The 2 AM club has a new member: ${name} 🎟️`, `Working in the shadows, ${name}? 👤`, `Nighttime is the best time for big ideas, right ${name}? 💡`, + `Sleepless on the onyx night, ${name}?` ]; // updated time logic const hour = new Date().getHours(); diff --git a/utils/session.ts b/utils/session.ts index f59ae853..891614c5 100644 --- a/utils/session.ts +++ b/utils/session.ts @@ -1,6 +1,12 @@ import * as crypto from 'crypto' import prisma from '@/utils/database' import { UAParser } from 'ua-parser-js' +import axios from 'axios' + +interface IpapiRes { + country_name: string, + region: string +} const SESSION_SECRET = process.env.SESSION_SECRET! @@ -108,7 +114,9 @@ async function createSession( ipAddress?: string, userAgent?: string ) { - const rawToken = generateToken() + const rawToken = generateToken(); + + const info = await axios.get('https://ipapi.co/json'); const { browser, os, device } = parseUA(userAgent) @@ -125,6 +133,8 @@ async function createSession( browser, os, device, + country: info.data.country_name, + region: info.data.region }, include: { @@ -273,6 +283,8 @@ async function listActiveSessions(userId: bigint) { userAgent: true, createdAt: true, expiresAt: true, + country: true, + region: true }, })