mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-06-30 15:56:58 +00:00
98 lines
2.4 KiB
TypeScript
98 lines
2.4 KiB
TypeScript
import crypto from "crypto";
|
|
import { decode, encode, JWT } from "next-auth/jwt";
|
|
import getSuffixFromFormat from "@/lib/shared/getSuffixFromFormat";
|
|
import { ArchivedFormat } from "@linkwarden/types/global";
|
|
|
|
export const PRESERVED_FORMAT_SCOPE = "preserved-format";
|
|
export const PRESERVED_FORMAT_TOKEN_TTL_SECONDS = 300;
|
|
|
|
export type PreservedFormatToken = JWT & {
|
|
scope: typeof PRESERVED_FORMAT_SCOPE;
|
|
linkId: number;
|
|
filePath: string;
|
|
format: ArchivedFormat;
|
|
iat: number;
|
|
exp: number;
|
|
};
|
|
|
|
const getPreservedTokenSecret = () => {
|
|
if (!process.env.NEXTAUTH_SECRET) {
|
|
throw new Error("NEXTAUTH_SECRET is not configured.");
|
|
}
|
|
|
|
return process.env.NEXTAUTH_SECRET;
|
|
};
|
|
|
|
const isPreservedFormatToken = (
|
|
token: JWT | null
|
|
): token is PreservedFormatToken => {
|
|
const suffix =
|
|
typeof token?.format === "number" ? getSuffixFromFormat(token.format) : null;
|
|
|
|
return (
|
|
!!token &&
|
|
token.scope === PRESERVED_FORMAT_SCOPE &&
|
|
typeof token.linkId === "number" &&
|
|
Number.isFinite(token.linkId) &&
|
|
typeof token.filePath === "string" &&
|
|
!!suffix &&
|
|
token.filePath.endsWith(suffix) &&
|
|
typeof token.format === "number" &&
|
|
typeof token.iat === "number" &&
|
|
typeof token.exp === "number"
|
|
);
|
|
};
|
|
|
|
export default async function createPreservedFormatUrl({
|
|
linkId,
|
|
filePath,
|
|
format,
|
|
}: {
|
|
linkId: number;
|
|
filePath: string;
|
|
format: ArchivedFormat;
|
|
}) {
|
|
const userContentDomain = process.env.NEXT_PUBLIC_USER_CONTENT_DOMAIN;
|
|
if (!userContentDomain) {
|
|
throw new Error("User content domain is not configured.");
|
|
}
|
|
|
|
const suffix = getSuffixFromFormat(format);
|
|
if (!suffix || !filePath.endsWith(suffix)) {
|
|
throw new Error("Invalid archived format.");
|
|
}
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const token = await encode({
|
|
secret: getPreservedTokenSecret(),
|
|
maxAge: PRESERVED_FORMAT_TOKEN_TTL_SECONDS,
|
|
token: {
|
|
id: 0,
|
|
scope: PRESERVED_FORMAT_SCOPE,
|
|
linkId,
|
|
filePath,
|
|
format,
|
|
iat: now,
|
|
exp: now + PRESERVED_FORMAT_TOKEN_TTL_SECONDS,
|
|
jti: crypto.randomUUID(),
|
|
},
|
|
});
|
|
|
|
return `${userContentDomain}/api/v1/preserved/view?token=${encodeURIComponent(
|
|
token
|
|
)}`;
|
|
}
|
|
|
|
export async function decodePreservedFormatToken(token: string) {
|
|
const decoded = await decode({
|
|
token,
|
|
secret: getPreservedTokenSecret(),
|
|
}).catch(() => null);
|
|
|
|
if (!isPreservedFormatToken(decoded)) {
|
|
return null;
|
|
}
|
|
|
|
return decoded;
|
|
}
|