Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions components/topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -36,6 +35,8 @@ type Session = {
createdAt: string;
expiresAt: string;
isCurrent: boolean;
region?: string;
country?: string;
};

function DeviceIcon({ device }: { device: string | null }) {
Expand Down Expand Up @@ -347,7 +348,7 @@ const Topbar: NextPage = () => {
)}
</div>
<p className="truncate text-xs text-zinc-400 dark:text-zinc-500">
{s.ipAddress} · {moment(s.createdAt).fromNow()}
{[s.region, s.country].filter(Boolean).join(', ') || `${s.ipAddress} · ${moment(s.createdAt).fromNow()}`}
</p>
</div>
{!s.isCurrent && (
Expand Down
23 changes: 20 additions & 3 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,24 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
{!showLoader && (
<Layout>
<div className="pb-8 sm:pb-0">
<Toaster position={isMobile ? "top-center" : "bottom-center"} />
<Toaster position={isMobile ? "top-center" : "bottom-center"} toastOptions={{
style: {
borderRadius: '14px',
boxShadow: '0 8px 30px rgba(0, 0, 0, 0.35)',
},
success: {
iconTheme: {
primary: '#22bf54',
secondary: '#ffffff'
},
},
error: {
iconTheme: {
primary: '#bf003d',
secondary: '#ffffff',
},
},
}} />
<Component {...pageProps} />
</div>
</Layout>
Expand Down Expand Up @@ -168,7 +185,7 @@ function Initializer() {
mounted = false;
try {
posthogRef.current?.reset();
} catch (e) {}
} catch (e) { }
};
}, []);

Expand All @@ -188,7 +205,7 @@ function Initializer() {
} else {
try {
ph.reset();
} catch (e) {}
} catch (e) { }
}
}
} catch (e) {
Expand Down
2 changes: 2 additions & 0 deletions pages/api/user/sessions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})),
})
}
Expand Down
173 changes: 173 additions & 0 deletions prisma/migrations/20260713091923_added_locations/migration.sql
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions utils/database.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
1 change: 1 addition & 0 deletions utils/randomText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
14 changes: 13 additions & 1 deletion utils/session.ts
Original file line number Diff line number Diff line change
@@ -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!

Expand Down Expand Up @@ -108,7 +114,9 @@ async function createSession(
ipAddress?: string,
userAgent?: string
) {
const rawToken = generateToken()
const rawToken = generateToken();

const info = await axios.get<IpapiRes>('https://ipapi.co/json');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files utils/session.ts

Repository: PlanetaryOrbit/orbit

Length of output: 175


🏁 Script executed:

ast-grep outline utils/session.ts --view expanded

Repository: PlanetaryOrbit/orbit

Length of output: 1099


🏁 Script executed:

cat -n utils/session.ts | sed -n '1,220p'

Repository: PlanetaryOrbit/orbit

Length of output: 6195


🏁 Script executed:

rg -n "createSession\(" -S .

Repository: PlanetaryOrbit/orbit

Length of output: 679


🏁 Script executed:

cat -n pages/api/auth/login.ts | sed -n '140,190p'

Repository: PlanetaryOrbit/orbit

Length of output: 2125


🏁 Script executed:

cat -n pages/api/auth/discord/callback.ts | sed -n '145,175p'

Repository: PlanetaryOrbit/orbit

Length of output: 1260


🏁 Script executed:

cat -n pages/api/auth/google/callback.ts | sed -n '155,185p'

Repository: PlanetaryOrbit/orbit

Length of output: 1150


🏁 Script executed:

cat -n pages/api/auth/roblox/callback.ts | sed -n '145,175p'

Repository: PlanetaryOrbit/orbit

Length of output: 1170


🏁 Script executed:

cat -n pages/api/auth/signup/finish.ts | sed -n '75,105p'

Repository: PlanetaryOrbit/orbit

Length of output: 1241


🏁 Script executed:

cat -n pages/api/setupworkspace.ts | sed -n '115,135p'

Repository: PlanetaryOrbit/orbit

Length of output: 1323


Use the request IP for the geolocation lookup
createSession already receives the client IP from its callers, but ipapi.co/json still geolocates the server. Pass ipAddress into the URL here (or skip the lookup when it’s missing) so country/region match the user instead of the datacenter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/session.ts` at line 119, Update the axios request in createSession to
query ipapi.co using the received ipAddress rather than the server’s own
location. If ipAddress is absent, skip the geolocation lookup and preserve safe
handling for country and region values.


const { browser, os, device } = parseUA(userAgent)

Expand All @@ -125,6 +133,8 @@ async function createSession(
browser,
os,
device,
country: info.data.country_name,
region: info.data.region
},

include: {
Expand Down Expand Up @@ -273,6 +283,8 @@ async function listActiveSessions(userId: bigint) {
userAgent: true,
createdAt: true,
expiresAt: true,
country: true,
region: true
},
})

Expand Down
Loading