Skip to content

Commit

Permalink
Feat: session per device (#130)
Browse files Browse the repository at this point in the history
* πŸ”§ allow for more commands in REDIS

* πŸ”§ add missing command

* ✨ add more commands to webdis

* ✏️ fix typo in command name

* ✨ add userAgent + device info to session

* πŸ› fix device information missing for desktop devices

* ✨ extend session with user's IP and last login info

* ♻️ combine redirect function with user to automatically get the user

* ✨ list all sessions

* 🏷️ fix type in middleware

* πŸ› fixed a bug with session last access not updating correctly

* πŸ’„ sort sessions by last access

* ✨ revoke active session

* πŸ”Š log public IP in middleware

* πŸ› use CF connecting IP instead of forwarded IP

* πŸ”Š log CF IP

* ♻️ pass correct arg into `nextCache`

* πŸ› keep session and location in the same order

* πŸ”‡ remove session id log

* πŸ’„ fix responsive UI on session page

* ♻️ use get/set instead of hget/hset

* βž– remove superjson as it is not used anymore

* πŸ”₯ remove unneeded and unused commands

* πŸ’„ fix UI break-all

* πŸ’„ fix UI on mobile for session detail page

* πŸ’„ fix suspense UIs
  • Loading branch information
Fredkiss3 committed Jan 14, 2024
1 parent fa17afe commit f3a0890
Show file tree
Hide file tree
Showing 16 changed files with 880 additions and 60 deletions.
13 changes: 12 additions & 1 deletion docker/webdis.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@

{
"http_basic_auth": "user:password",
"enabled": ["GET", "SET", "DEL", "SETEX", "HSET", "HGETALL"]
"enabled": [
"GET",
"SET",
"DEL",
"SETEX",
"HSET",
"HGETALL",
"HGET",
"SREM",
"SADD",
"SMEMBERS"
]
}
],

Expand Down
7 changes: 4 additions & 3 deletions src/actions/auth.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,19 @@ export const getSession = cache(async function getSession(): Promise<Session> {
return session;
});

export const redirectIfNotAuthed = cache(async function getUserOrRedirect(
export const getUserOrRedirect = cache(async function getUserOrRedirect(
redirectToPath?: string
) {
const session = await getSession();
const user = await getAuthedUser();

if (!session.user) {
if (!user) {
const searchParams = new URLSearchParams();
if (redirectToPath) {
searchParams.set("nextUrl", redirectToPath);
}
redirect("/login?" + searchParams.toString());
}
return user;
});

export const getAuthedUser = cache(async function getUser() {
Expand Down
75 changes: 75 additions & 0 deletions src/actions/session.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { withAuth, type AuthState } from "~/actions/middlewares";
import { nextCache } from "~/lib/server/rsc-utils.server";
import { Session } from "~/lib/server/session.server";
import { CacheKeys } from "~/lib/shared/cache-keys.shared";
import { jsonFetch } from "~/lib/shared/utils.shared";

export type SuccessfulLocationData = {
status: "success";
country: string;
countryCode: string;
region: string;
regionName: string;
city: string;
zip: string;
lat: number;
lon: number;
timezone: string;
isp: string;
org: string;
as: string;
query: string;
};
export type FailedLocationData = {
status: "fail";
message: string;
query: string;
};
export type LocationData = SuccessfulLocationData | FailedLocationData;

export async function getLocationData(session: Session) {
const fn = nextCache(
(ip: string) =>
jsonFetch<LocationData>(`http://ip-api.com/json/${ip}`, {
cache: "force-cache"
}),
{
tags: CacheKeys.geo(session.ip)
}
);

return await fn(session.ip);
}

export const revokeSession = withAuth(async function revokeSession(
sessionId: string,
_: FormData,
{ session: currentSession, currentUser }: AuthState
) {
const foundSesssion = await Session.getUserSession(currentUser.id, sessionId);

if (!foundSesssion) {
await currentSession.addFlash({
type: "error",
message: "The session you are trying to revoke is no longer accessible."
});
} else if (foundSesssion.id === currentSession.id) {
await currentSession.addFlash({
type: "warning",
message: "You cannot revoke the current active session."
});
} else {
await Session.endUserSession(currentUser.id, sessionId);
await currentSession.addFlash({
type: "success",
message: "Session succesfully revoked."
});
}

revalidatePath("/");
redirect("/settings/sessions");
});
5 changes: 2 additions & 3 deletions src/app/(app)/settings/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { UpdateUserInfosForm } from "~/components/update-user-infos-form";
import { Button } from "~/components/button";

// utils
import { getAuthedUser, redirectIfNotAuthed } from "~/actions/auth.action";
import { getUserOrRedirect } from "~/actions/auth.action";
import { updateUserProfileInfosInputValidator } from "~/models/dto/update-profile-info-input-validator";

// types
Expand All @@ -16,9 +16,8 @@ export const metadata: Metadata = {
};

export default async function Page() {
await redirectIfNotAuthed("/settings/account");
const user = await getUserOrRedirect("/settings/account");

const user = (await getAuthedUser())!;
return (
<div className="flex flex-col gap-24">
<section className="flex flex-col gap-4 md:gap-8">
Expand Down
4 changes: 2 additions & 2 deletions src/app/(app)/settings/appearance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from "react";
import { ThemeForm } from "~/components/theme-form";

// utils
import { redirectIfNotAuthed } from "~/actions/auth.action";
import { getUserOrRedirect } from "~/actions/auth.action";
import { getTheme } from "~/actions/theme.action";

// types
Expand All @@ -15,7 +15,7 @@ export const metadata: Metadata = {
};

export default async function Page() {
await redirectIfNotAuthed("/settings/appearance");
await getUserOrRedirect("/settings/appearance");
const theme = await getTheme();
return (
<div>
Expand Down
17 changes: 11 additions & 6 deletions src/app/(app)/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
// components
import { Avatar } from "~/components/avatar";

import { VerticalNavlist } from "~/components/vertical-navlist";
import { SettingsVerticalNavlist } from "~/components/settings-vertical-navlist";

// utils
import { getAuthedUser, redirectIfNotAuthed } from "~/actions/auth.action";
import { getUserOrRedirect } from "~/actions/auth.action";
import { clsx } from "~/lib/shared/utils.shared";

export default async function SettingsLayout({
children
}: {
children: React.ReactNode;
}) {
await redirectIfNotAuthed("/settings/account");
const user = (await getAuthedUser())!;
const user = await getUserOrRedirect("/settings/account");
return (
<>
<main
Expand All @@ -27,13 +26,19 @@ export default async function SettingsLayout({
<Avatar username={user.username} src={user.avatar_url} size="large" />

<div>
<h1 className="text-xl font-semibold">{user.username}</h1>
<h1 className="text-xl font-semibold">
{user.name ?? user.username} &nbsp;
{user.name && (
<span className="text-grey">({user.username})</span>
)}
</h1>

<p className="text-grey">Your personnal account</p>
</div>
</section>

<div className="grid gap-4 md:grid-cols-9">
<VerticalNavlist className="md:col-span-3 lg:col-span-2" />
<SettingsVerticalNavlist className="md:col-span-3 lg:col-span-2" />
<div className="md:col-span-6 lg:col-span-7">{children}</div>
</div>
</main>
Expand Down
4 changes: 2 additions & 2 deletions src/app/(app)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { redirectIfNotAuthed } from "~/actions/auth.action";
import { getUserOrRedirect } from "~/actions/auth.action";

export default async function Page() {
await redirectIfNotAuthed("/settings/account");
await getUserOrRedirect("/settings/account");
redirect("/settings/account");
}
Loading

0 comments on commit f3a0890

Please sign in to comment.