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
},
})