Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved device id hash generation #530

Merged
merged 17 commits into from
Aug 29, 2024
17 changes: 14 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"axios": "^1.7.4",
"bcryptjs": "^2.4.3",
"classnames": "^2.3.2",
"cookie": "^0.6.0",
"cron": "^3.1.5",
"daisyui": "^4.12.10",
"ejs": "^3.1.10",
Expand Down
4 changes: 3 additions & 1 deletion src/components/adminPage/users/userGroups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type GroupLabelProps = {
};

const GroupLabel = ({ groups }: GroupLabelProps) => {
if (!Array.isArray(groups) || !groups) return null;
const t = useTranslations("admin");
const m = useTranslations("commonToast");

Expand All @@ -45,6 +44,9 @@ const GroupLabel = ({ groups }: GroupLabelProps) => {
toastMessage: m("deletedSuccessfully"),
}),
});

if (!Array.isArray(groups) || !groups) return null;

return (
<div className="flex flex-wrap gap-3 text-center">
{groups?.map((group) => {
Expand Down
19 changes: 10 additions & 9 deletions src/components/auth/userDevices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import Monitor from "~/icons/monitor";
import Tablet from "~/icons/tablet";
import { api } from "~/utils/api";
import { useTranslations } from "next-intl";
import { signOut } from "next-auth/react";
import { generateDeviceId, parseUA } from "~/utils/devices";
import { signOut, useSession } from "next-auth/react";

const formatLastActive = (date) => {
return new Date(date).toLocaleString("no-NO");
Expand Down Expand Up @@ -49,8 +48,9 @@ const DeviceIcon = ({ deviceType }: { deviceType: string }) => {

const ListUserDevices: React.FC<{ devices: UserDevice[] }> = ({ devices }) => {
const t = useTranslations();
const { data: me, refetch } = api.auth.me.useQuery();

const { refetch } = api.auth.me.useQuery();
const { data: session } = useSession();
const { mutate: deleteUserDevice, isLoading: deleteLoading } =
api.auth.deleteUserDevice.useMutation({
onSuccess: () => {
Expand All @@ -59,18 +59,19 @@ const ListUserDevices: React.FC<{ devices: UserDevice[] }> = ({ devices }) => {
},
});

const currentDeviceId = session?.user?.deviceId;

const isCurrentDevice = (device: UserDevice) => {
return (
device?.deviceId === generateDeviceId(parseUA(navigator.userAgent), me.id)?.deviceId
);
return device?.deviceId === currentDeviceId;
};

// sort devices, current device first
devices?.sort((a, b) => {
const sortedDevices = [...(devices || [])].sort((a, b) => {
if (isCurrentDevice(a)) return -1;
if (isCurrentDevice(b)) return 1;
return 0;
});

return (
<div className="mx-auto">
<div className="flex justify-between">
Expand All @@ -80,8 +81,8 @@ const ListUserDevices: React.FC<{ devices: UserDevice[] }> = ({ devices }) => {
{/* <button className="btn btn-sm btn-error btn-outline">Logout All</button> */}
</div>
<div className="space-y-2 max-h-[500px] overflow-auto custom-scrollbar">
{devices && devices.length > 0 ? (
devices.map((device) => (
{sortedDevices && sortedDevices.length > 0 ? (
sortedDevices.map((device) => (
<div
key={device.id}
className={cn(
Expand Down
4 changes: 3 additions & 1 deletion src/components/userSettings/apiToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useTranslations } from "next-intl";
import { AuthorizationType } from "~/types/apiTypes";

const ApiLables = ({ tokens }) => {
if (!Array.isArray(tokens) || !tokens) return null;
const t = useTranslations("userSettings");

const { refetch } = api.auth.getApiToken.useQuery();
Expand All @@ -34,6 +33,9 @@ const ApiLables = ({ tokens }) => {
refetch();
},
});

if (!Array.isArray(tokens) || !tokens) return null;

return (
<div className="flex flex-wrap gap-3 text-center">
{tokens?.map((token) => {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import NextAuth from "next-auth";
import { getAuthOptions } from "~/server/auth";

export default async function auth(req: NextApiRequest, res: NextApiResponse) {
const authOptions = getAuthOptions(req);
const authOptions = getAuthOptions(req, res);

return await NextAuth(req, res, authOptions);
}
2 changes: 1 addition & 1 deletion src/pages/api/auth/two-factor/totp/disable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(405).json({ message: "Method not allowed" });
}

const session = await getServerSession(req, res, getAuthOptions(req));
const session = await getServerSession(req, res, getAuthOptions(req, res));
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/auth/two-factor/totp/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(405).json({ message: "Method not allowed" });
}

const session = await getServerSession(req, res, getAuthOptions(req));
const session = await getServerSession(req, res, getAuthOptions(req, res));
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/auth/two-factor/totp/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(405).json({ message: "Method not allowed" });
}

const session = await getServerSession(req, res, getAuthOptions(req));
const session = await getServerSession(req, res, getAuthOptions(req, res));
if (!session) {
return res.status(401).json({ error: ErrorCode.InternalServerError });
}
Expand Down
6 changes: 5 additions & 1 deletion src/pages/api/auth/user/invalidateUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export default async function handler(
}

try {
const session = await getServerSession(request, response, getAuthOptions(request));
const session = await getServerSession(
request,
response,
getAuthOptions(request, response),
);
if (!session) {
return response.status(401).json({ message: "Not authenticated" });
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/mkworld/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const config = {
};

export default async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getServerSession(req, res, getAuthOptions(req));
const session = await getServerSession(req, res, getAuthOptions(req, res));
if (!session) {
res.status(401).json({ message: "Authorization Error" });
return;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface SocketIoExtension {

export type NextApiResponseWithSocketIo = NextApiResponse & SocketIoExtension;
const SocketHandler = async (req: NextApiRequest, res: NextApiResponseWithSocketIo) => {
const session = await getServerSession(req, res, getAuthOptions(req));
const session = await getServerSession(req, res, getAuthOptions(req, res));
if (!session) {
res.status(401).json({ message: "Authorization Error" });
return;
Expand Down
30 changes: 14 additions & 16 deletions src/pages/user-settings/network/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,20 @@ const UserNetworkSetting = () => {
<div className="flex justify-between">
<div>
<p className="font-medium">Enable global node naming</p>
<p className="text-sm text-gray-500">
When enabled, this feature will:
<ul className="list-disc list-inside mt-2">
<li>
Maintain a consistent name for each node across all networks you manage.
</li>
<li>
Update the node's name in all your networks when you rename it in one
network.
</li>
<li>
Upon member / node registration, check if the member exists in your
other networks and use the first name found.
</li>
</ul>
</p>
<p className="text-sm text-gray-500">When enabled, this feature will:</p>
<ul className="list-disc list-inside mt-2 text-sm text-gray-500">
<li>
Maintain a consistent name for each node across all networks you manage.
</li>
<li>
Update the node's name in all your networks when you rename it in one
network.
</li>
<li>
Upon member / node registration, check if the member exists in your other
networks and use the first name found.
</li>
</ul>
<p className="mt-2 text-sm text-gray-500">
Note: This feature has priority over "Add Member ID as Name". It applies
only to networks where you are the author and doesn't affect networks
Expand Down
10 changes: 6 additions & 4 deletions src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface IUserAgent {

declare module "next-auth" {
interface Session extends DefaultSession {
user: IUser;
user: IUser & { deviceId: string };
error: string;
// userAgent?: IUserAgent | unknown;
// deviceId?: string;
Expand Down Expand Up @@ -105,6 +105,7 @@ const genericOAuthAuthorization = buildAuthorizationConfig(
*/
export const getAuthOptions = (
req: GetServerSidePropsContext["req"],
res: GetServerSidePropsContext["res"],
): NextAuthOptions => ({
adapter: MyAdapter,
providers: [
Expand All @@ -130,6 +131,7 @@ export const getAuthOptions = (
id: profile.sub || profile.id.toString(),
name: profile.name || profile.login || profile.username,
email: profile.email,
deviceId: null,
// image: profile.picture || profile.avatar_url || profile.image_url,
lastLogin: new Date().toISOString(),
role: "USER",
Expand Down Expand Up @@ -278,8 +280,8 @@ export const getAuthOptions = (
/**
* @see https://next-auth.js.org/configuration/callbacks#sign-in-callback
*/
signIn: signInCallback(req),
jwt: jwtCallback(req),
signIn: signInCallback(req, res),
jwt: jwtCallback(),
session: sessionCallback(req),
redirect({ url, baseUrl }) {
// Allows relative callback URLs
Expand All @@ -304,5 +306,5 @@ export const getServerAuthSession = (ctx: {
req: GetServerSidePropsContext["req"];
res: GetServerSidePropsContext["res"];
}) => {
return getServerSession(ctx.req, ctx.res, getAuthOptions(ctx.req));
return getServerSession(ctx.req, ctx.res, getAuthOptions(ctx.req, ctx.res));
};
19 changes: 7 additions & 12 deletions src/server/callbacks/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { IncomingMessage } from "http";
import { prisma } from "../db";
import { generateDeviceId, parseUA } from "~/utils/devices";

export function jwtCallback(
req: IncomingMessage & { cookies: Partial<{ [key: string]: string }> },
) {
return async function jwt({ token, user, trigger, account, session }) {
export function jwtCallback() {
return async function jwt({ token, user, trigger, account, session, profile }) {
// console.log(user);

if (trigger === "update") {
if (session.update) {
const updateObject: Record<string, string | Date> = {};
Expand Down Expand Up @@ -60,12 +58,9 @@ export function jwtCallback(
if (user) {
const { id, name, email, role } = user;
Object.assign(token, { id, name, email, role });

if (account?.provider === "oauth") {
const userAgent = req.headers["user-agent"];
const { deviceId } = generateDeviceId(parseUA(userAgent), id);
token.deviceId = deviceId;

// set the device from sign in callback
token.deviceId = profile.deviceId;
token.accessToken = account.accessToken;
} else if (account?.provider === "credentials") {
token.deviceId = user.deviceId;
Expand All @@ -83,7 +78,7 @@ export function jwtCallback(
});

if (!userDevice) {
// Device doesn't exist, invalidate the token
// Device doesn't exist, invalidate the deviceId in the token
token.deviceId = undefined;
return token;
}
Expand Down
1 change: 0 additions & 1 deletion src/server/callbacks/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export function sessionCallback(
...session,
user: {
...token,
userAgent: token.userAgent,
},
};
};
Expand Down
Loading
Loading