-
- Expired Sessions ({expiredSessions.length})
-
-
+
+ setGlobalFilter(e.target.value)}
+ className="max-w-xs"
+ />
+
+ setStatusFilter(v as "all" | "active" | "expired")
+ }
+ >
+
+
+
+
+ All statuses
+ Active
+ Expired
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No sessions match your filters.
+
+
+ )}
+
+
+
+ {table.getPageCount() > 1 && (
+
+
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
+ {table.getPageCount()}
+
+
+ table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+ Previous
+
+ table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+ Next
+
+
)}
>
@@ -110,93 +414,6 @@ export const ShowSessions = () => {
);
};
-type SessionRow = {
- id: string;
- email: string;
- firstName: string | null;
- lastName: string | null;
- ipAddress: string | null;
- userAgent: string | null;
- createdAt: Date;
- expiresAt: Date;
- isCurrent: boolean;
- userId: string;
-};
-
-const SessionTable = ({
- sessions,
- onRevoke,
- isRevoking,
-}: {
- sessions: SessionRow[];
- onRevoke: (id: string) => void;
- isRevoking: boolean;
-}) => (
-
-
-
- User
- IP Address
- Device
- Active Since
- Expires
- Actions
-
-
-
- {sessions.map((s) => (
-
-
-
-
- {s.firstName} {s.lastName}
-
- ({s.email})
- {s.isCurrent && (
-
- Current
-
- )}
-
-
-
- {s.ipAddress || "-"}
-
-
- {s.userAgent ? parseUserAgent(s.userAgent) : "-"}
-
-
- {format(new Date(s.createdAt), "MMM d, HH:mm")}
-
-
- {format(new Date(s.expiresAt), "MMM d, HH:mm")}
-
-
- {!s.isCurrent && (
- onRevoke(s.id)}
- >
-
-
-
-
- )}
-
-
- ))}
-
-
-);
-
-/** ponyta: parse UA just enough to show OS + browser, no lib needed */
function parseUserAgent(ua: string): string {
if (ua.includes("Chrome/") && !ua.includes("Edg/")) return "Chrome";
if (ua.includes("Edg/")) return "Edge";
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx
index 03101e78f..2d9731224 100644
--- a/apps/dokploy/components/layouts/side.tsx
+++ b/apps/dokploy/components/layouts/side.tsx
@@ -297,6 +297,12 @@ const MENU: Menu = {
url: "/dashboard/settings/profile",
icon: User,
},
+ {
+ isSingle: true,
+ title: "Sessions",
+ icon: LogIn,
+ url: "/dashboard/settings/sessions",
+ },
{
isSingle: true,
title: "Remote Servers",
@@ -320,13 +326,6 @@ const MENU: Menu = {
// Only enabled for users with member.read permission
isEnabled: ({ permissions }) => !!permissions?.member.read,
},
- {
- isSingle: true,
- title: "Sessions",
- icon: LogIn,
- url: "/dashboard/settings/sessions",
- isEnabled: ({ permissions }) => !!permissions?.member.read,
- },
{
isSingle: true,
title: "Audit Logs",
diff --git a/apps/dokploy/pages/dashboard/settings/sessions.tsx b/apps/dokploy/pages/dashboard/settings/sessions.tsx
index d777ff67c..fdde096df 100644
--- a/apps/dokploy/pages/dashboard/settings/sessions.tsx
+++ b/apps/dokploy/pages/dashboard/settings/sessions.tsx
@@ -6,15 +6,8 @@ import superjson from "superjson";
import { ShowSessions } from "@/components/dashboard/settings/sessions/show-sessions";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
-import { api } from "@/utils/api";
const Page = () => {
- const { data: permissions } = api.user.getPermissions.useQuery();
-
- if (!permissions?.member.read) {
- return null;
- }
-
return (
@@ -58,17 +51,6 @@ export async function getServerSideProps(
try {
await helpers.user.get.prefetch();
- const userPermissions = await helpers.user.getPermissions.fetch();
-
- if (!userPermissions?.member.read) {
- return {
- redirect: {
- permanent: false,
- destination: "/",
- },
- };
- }
-
return {
props: {
trpcState: helpers.dehydrate(),
diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts
index b1ed52796..0dd06c6e0 100644
--- a/apps/dokploy/server/api/routers/user.ts
+++ b/apps/dokploy/server/api/routers/user.ts
@@ -286,7 +286,9 @@ export const userRouter = createTRPCRouter({
});
}
}),
- listSessions: adminProcedure.query(async ({ ctx }) => {
+ listSessions: protectedProcedure.query(async ({ ctx }) => {
+ const isOwner = ctx.user.role === "owner";
+
const sessions = await db
.select({
id: session.id,
@@ -298,23 +300,50 @@ export const userRouter = createTRPCRouter({
userAgent: session.userAgent,
createdAt: session.createdAt,
expiresAt: session.expiresAt,
- isCurrent: eq(session.id, ctx.session.id),
})
.from(session)
.innerJoin(user, eq(session.userId, user.id))
+ .innerJoin(
+ member,
+ and(
+ eq(member.userId, session.userId),
+ eq(member.organizationId, ctx.session.activeOrganizationId),
+ ),
+ )
+ .where(isOwner ? undefined : eq(session.userId, ctx.user.id))
.orderBy(desc(session.createdAt));
- return sessions;
+
+ return sessions.map((s) => ({
+ ...s,
+ isCurrent: s.id === ctx.session.id,
+ }));
}),
- revokeSession: adminProcedure
+ revokeSession: protectedProcedure
.input(
z.object({
sessionId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
- const targetSession = await db.query.session.findFirst({
- where: eq(session.id, input.sessionId),
- });
+ const isOwner = ctx.user.role === "owner";
+
+ const [targetSession] = await db
+ .select({
+ id: session.id,
+ userId: session.userId,
+ userEmail: user.email,
+ })
+ .from(session)
+ .innerJoin(user, eq(session.userId, user.id))
+ .innerJoin(
+ member,
+ and(
+ eq(member.userId, session.userId),
+ eq(member.organizationId, ctx.session.activeOrganizationId),
+ ),
+ )
+ .where(eq(session.id, input.sessionId));
+
if (!targetSession) {
throw new TRPCError({
code: "NOT_FOUND",
@@ -327,7 +356,21 @@ export const userRouter = createTRPCRouter({
message: "Cannot revoke your own session. Use logout instead.",
});
}
+ if (!isOwner && targetSession.userId !== ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You can only revoke your own sessions",
+ });
+ }
await db.delete(session).where(eq(session.id, input.sessionId));
+
+ await audit(ctx, {
+ action: "logout",
+ resourceType: "session",
+ resourceId: targetSession.id,
+ resourceName: targetSession.userEmail,
+ });
+
return true;
}),
getUserByToken: publicProcedure
diff --git a/packages/server/src/lib/access-control.ts b/packages/server/src/lib/access-control.ts
index 1242e725f..2d9ceccf0 100644
--- a/packages/server/src/lib/access-control.ts
+++ b/packages/server/src/lib/access-control.ts
@@ -202,6 +202,5 @@ export const memberRole = ac.newRole({
auditLog: [],
// Members need provider/secret names for env editor autocomplete; values are never exposed
vaultProvider: ["read"],
- // Members can see configured DNS providers to pick one when adding a domain
- dnsProvider: ["read"],
+ dnsProvider: [],
});