Start migration, and re-organizing files.

This commit is contained in:
mbaharip
2023-05-20 21:31:24 +07:00
parent c823f7f730
commit a234559321
59 changed files with 3263 additions and 939 deletions
+1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>
+2 -4
View File
@@ -1,9 +1,7 @@
"use strict";
const tailwindConfig = require("prettier-plugin-tailwindcss");
module.exports = {
plugins: [tailwindConfig],
printWidth: 80,
plugins: [require("prettier-plugin-tailwindcss")],
printWidth: 60,
tabWidth: 2,
useTabs: false,
semi: true,
+5 -4
View File
@@ -22,12 +22,12 @@
"googleapis": "^118.0.0",
"jsonwebtoken": "^9.0.0",
"mime-types": "^2.1.35",
"next": "13.3.0",
"next": "^13.4.3",
"next-seo": "^6.0.0",
"nextjs-progressbar": "^0.0.16",
"postcss": "8.4.22",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-h5-audio-player": "^3.8.6",
"react-icons": "^4.8.0",
"react-loading": "^2.0.3",
@@ -55,8 +55,9 @@
"@types/node": "18.15.11",
"@types/three": "^0.150.2",
"cypress": "^12.10.0",
"encoding": "^0.1.13",
"eslint": "8.38.0",
"eslint-config-next": "13.3.0",
"eslint-config-next": "^13.4.3",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.7",
"typescript": "5.0.4"
+89
View File
@@ -0,0 +1,89 @@
import { NextRequest, NextResponse } from "next/server";
import { ErrorResponse } from "types/googleapis";
import getSearchParams from "utils/getSearchParams";
import { ExtendedError } from "utils/driveHelper";
import driveClient from "utils/driveClient";
import { shortDecrypt } from "utils/encryptionHelper";
export async function GET(request: NextRequest) {
const _start = Date.now();
try {
const { id } = getSearchParams(request.url, ["id"]);
if (!id) {
throw new ExtendedError(
"No id provided",
400,
"badRequest",
);
}
const getMetadata = driveClient.files.get({
fileId: shortDecrypt(id),
fields: "id, name, mimeType",
});
const getStream = driveClient.files.get(
{
fileId: shortDecrypt(id),
alt: "media",
},
{ responseType: "stream" },
);
const [metadata, stream] = await Promise.all([
getMetadata,
getStream,
]);
const arrayBuffer = await new Promise<ArrayBuffer>(
(resolve, reject) => {
const chunks: Buffer[] = [];
stream.data.on("data", (chunk) =>
chunks.push(chunk),
);
stream.data.on("end", () => {
const buffer = Buffer.concat(chunks);
resolve(
buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
),
);
});
stream.data.on("error", reject);
},
);
return new NextResponse(arrayBuffer, {
status: 200,
headers: {
"Content-Type":
metadata.data.mimeType ||
"application/octet-stream",
"Cache-Control":
"public, max-age=31536000, immutable",
"Content-Disposition": `inline; filename="${metadata.data.name}"`,
},
});
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return NextResponse.json(payload, {
status: payload.code || 500,
});
}
}
+211
View File
@@ -0,0 +1,211 @@
import { NextRequest, NextResponse } from "next/server";
import { ErrorResponse } from "types/googleapis";
import { ExtendedError } from "utils/driveHelper";
import apiConfig from "config/api.config";
import driveClient from "utils/driveClient";
import {
shortDecrypt,
shortEncrypt,
} from "utils/encryptionHelper";
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } },
) {
const _start = Date.now();
try {
const pathArray = params.path;
const getRootId =
apiConfig.files.rootFolder !== "root"
? apiConfig.files.rootFolder
: driveClient.files
.get({
fileId: "root",
fields: "id",
})
.then((res) => res.data.id);
const getPathId = pathArray.map(async (path, index) => {
const query = [
"trashed = false",
"'me' in owners",
`name = '${path}'`,
];
const fetchFolderContents =
await driveClient.files.list({
q: `${query.join(" and ")}`,
fields: "files(id, name, mimeType, parents)",
});
if (!fetchFolderContents.data.files?.length) {
throw new ExtendedError(
`Path ${path} is not found`,
404,
"notFound",
);
}
return {
path,
data: fetchFolderContents.data.files.map(
(file) => ({
id: file.id,
parents: file.parents?.[0],
mimeType: file.mimeType,
}),
),
};
});
const [rootId, pathId] = await Promise.all([
getRootId,
Promise.all(getPathId),
]);
const selectedPath: string[] = [];
const mapIds: Record<string, string>[] = [];
// Check path validity
pathId.forEach((path, index) => {
if (index === 0) {
// Check if root folder inside data id
const checkRoot = path.data.find(
(item) => item.parents === rootId,
);
if (!checkRoot) {
throw new ExtendedError(
`Path "${path.path}" is either not found or not valid`,
400,
"badRequest",
);
}
selectedPath.push(checkRoot.id as string);
mapIds.push({
name: path.path,
id: shortEncrypt(checkRoot.id as string),
mimeType: checkRoot.mimeType as string,
});
} else {
const checkPath = path.data.find(
(item) =>
item.parents === selectedPath[index - 1],
);
if (!checkPath) {
throw new ExtendedError(
`Path "${path.path}" is either not found or not valid`,
400,
"badRequest",
);
}
selectedPath.push(checkPath.id as string);
mapIds.push({
name: path.path,
id: shortEncrypt(checkPath.id as string),
mimeType: checkPath.mimeType as string,
});
}
});
// Check for protected folder
const getPassword = mapIds.map(async (path) => {
if (
path.mimeType !==
"application/vnd.google-apps.folder"
)
return;
const query = [
"trashed = false",
"'me' in owners",
`'${shortDecrypt(path.id)}' in parents`,
`name = '${apiConfig.files.specialFile.password}'`,
];
const folderContents = await driveClient.files.list({
q: `${query.join(" and ")}`,
fields: "files(id, name, mimeType, parents)",
});
if (!folderContents.data.files?.length) return;
return {
path: path.name,
password: folderContents.data.files?.[0].id,
};
});
let password = await Promise.all(getPassword);
password = password.filter(
(item) => item !== undefined,
);
const lastProtectedFolder =
password[password.length - 1];
// Check password
if (lastProtectedFolder) {
const pass = request.headers.get(
"next-gdrive-password",
);
if (!pass) {
throw new ExtendedError(
`You are trying to access a protected folder, please provide a password`,
401,
"unauthorized",
);
}
const passwordFile = await driveClient.files.get(
{
fileId: lastProtectedFolder.password as string,
alt: "media",
},
{ responseType: "text" },
);
if (passwordFile.data !== pass) {
throw new ExtendedError(
`The password you entered is incorrect`,
401,
"unauthorized",
);
}
}
const payload = {
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: 200,
data: mapIds,
};
return NextResponse.json(payload, {
status: 200,
headers: {
"Cache-Control": apiConfig.cache,
},
});
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return NextResponse.json(payload, {
status: payload.code || 500,
});
}
}
@@ -0,0 +1,206 @@
import {
ErrorResponse,
FileResponse,
FilesResponse,
} from "types/googleapis";
import { NextRequest, NextResponse } from "next/server";
import getSearchParams from "utils/getSearchParams";
import {
shortDecrypt,
shortEncrypt,
} from "utils/encryptionHelper";
import driveClient from "utils/driveClient";
import apiConfig from "config/api.config";
import {
ExtendedError,
hiddenFiles,
} from "utils/driveHelper";
export async function GET(
request: NextRequest,
{ params }: { params: { encryptedId: string } },
) {
const _start = Date.now();
try {
const { pageToken, banner, thumbnail } =
getSearchParams(request.url, ["pageToken", "banner"]);
const id = shortDecrypt(params.encryptedId);
if (id === apiConfig.files.rootFolder) {
return NextResponse.redirect(
`${apiConfig.basePath}/api/files`,
{
status: 301,
},
);
}
const file = await driveClient.files.get({
fileId: id,
fields: apiConfig.files.field,
});
if (!file || file.data.trashed) {
const msg = file.data.trashed
? "File has been deleted"
: "File not found";
throw new ExtendedError(msg, 404, "notFound");
}
if (
file.data.mimeType !==
"application/vnd.google-apps.folder"
) {
if (thumbnail === "1") {
return NextResponse.redirect(
file.data.thumbnailLink as string,
{
status: 302,
},
);
}
const payload: FileResponse = {
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
file: {
...file.data,
id: shortEncrypt(file.data.id as string),
webContentLink:
shortEncrypt(
file.data.webContentLink as string,
) || undefined,
},
};
return NextResponse.json(payload, {
status: 200,
headers: {
"Cache-Control": apiConfig.cache,
},
});
}
const query = [
`'${id}' in parents`,
"trashed = false",
"'me' in owners",
];
const folderContents = await driveClient.files.list({
q: `${query.join(" and ")}`,
fields: `files(${apiConfig.files.field}), nextPageToken`,
orderBy: apiConfig.files.orderBy,
pageSize: apiConfig.files.itemsPerPage,
pageToken: pageToken || undefined,
});
const readmeFile = folderContents.data.files?.find(
(file) =>
file.name === apiConfig.files.specialFile.readme,
);
const bannerFile = folderContents.data.files?.find(
(file) =>
file.name?.startsWith(
apiConfig.files.specialFile.banner,
) && file.mimeType?.startsWith("image/"),
);
if (banner === "1") {
if (!bannerFile) {
throw new ExtendedError(
"Banner not found.",
404,
"notFound",
);
}
if (
Number(bannerFile.size) >
apiConfig.files.download.maxFileSize
) {
return NextResponse.redirect(
bannerFile.webContentLink as string,
{ status: 302 },
);
}
return NextResponse.redirect(
`${apiConfig.basePath}/api/banner?id=${shortEncrypt(
bannerFile.id as string,
)}`,
{
status: 302,
},
);
}
const folderList =
folderContents.data.files
?.filter(
(file) =>
file.mimeType ===
"application/vnd.google-apps.folder",
)
.map((file) => ({
...file,
id: shortEncrypt(file.id as string),
})) || [];
const fileList =
folderContents.data.files
?.filter(
(file) =>
file.mimeType !==
"application/vnd.google-apps.folder" &&
!hiddenFiles.some((hiddenFile) =>
file.name?.startsWith(hiddenFile),
),
)
.map((file) => ({
...file,
id: shortEncrypt(file.id as string),
webContentLink:
shortEncrypt(file.webContentLink as string) ||
undefined,
})) || [];
const payload: FilesResponse = {
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
folders: folderList,
files: fileList,
isReadmeExists: !!readmeFile,
isBannerExists: !!bannerFile,
nextPageToken:
folderContents.data.nextPageToken || undefined,
};
return NextResponse.json(payload, {
status: 200,
headers: {
"Cache-Control": apiConfig.cache,
},
});
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return NextResponse.json(payload, {
status: payload.code || 500,
});
}
}
+145
View File
@@ -0,0 +1,145 @@
import { NextRequest, NextResponse } from "next/server";
import apiConfig from "config/api.config";
import getSearchParams from "utils/getSearchParams";
import {
ErrorResponse,
FilesResponse,
} from "types/googleapis";
import driveClient from "utils/driveClient";
import {
ExtendedError,
hiddenFiles,
} from "utils/driveHelper";
import { shortEncrypt } from "utils/encryptionHelper";
export async function GET(request: NextRequest) {
const _start = Date.now();
try {
const { pageToken, banner } = getSearchParams(
request.url,
["pageToken", "banner"],
);
const query: string[] = [
"trashed = false",
"'me' in owners",
`parents = '${apiConfig.files.rootFolder}'`,
];
const fetchFolderContents =
await driveClient.files.list({
q: `${query.join(" and ")}`,
fields: `files(${apiConfig.files.field}), nextPageToken`,
orderBy: apiConfig.files.orderBy,
pageSize: apiConfig.files.itemsPerPage,
pageToken: pageToken || undefined,
});
const readmeFile = fetchFolderContents.data.files?.find(
(file) =>
file.name === apiConfig.files.specialFile.readme,
);
const bannerFile = fetchFolderContents.data.files?.find(
(file) =>
file.name?.startsWith(
apiConfig.files.specialFile.banner,
) && file.mimeType?.startsWith("image/"),
);
if (banner === "1") {
if (!bannerFile) {
throw new ExtendedError(
"Banner not found.",
404,
"notFound",
);
}
if (
Number(bannerFile.size) >
apiConfig.files.download.maxFileSize
) {
return NextResponse.redirect(
bannerFile.webContentLink as string,
{ status: 302 },
);
}
return NextResponse.redirect(
`${apiConfig.basePath}/api/banner?id=${shortEncrypt(
bannerFile.id as string,
)}`,
{
status: 302,
},
);
}
const folderList =
fetchFolderContents.data.files
?.filter(
(file) =>
file.mimeType ===
"application/vnd.google-apps.folder",
)
.map((file) => ({
...file,
id: shortEncrypt(file.id as string),
})) || [];
const fileList =
fetchFolderContents.data.files
?.filter(
(file) =>
file.mimeType !==
"application/vnd.google-apps.folder" &&
!hiddenFiles.some((hiddenFile) =>
file.name?.startsWith(hiddenFile),
),
)
.map((file) => ({
...file,
id: shortEncrypt(file.id as string),
webContentLink:
shortEncrypt(file.webContentLink as string) ||
undefined,
})) || [];
const payload: FilesResponse = {
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
folders: folderList,
files: fileList,
isReadmeExists: !!readmeFile,
isBannerExists: !!bannerFile,
nextPageToken:
fetchFolderContents.data.nextPageToken || undefined,
};
return NextResponse.json(payload, {
status: 200,
headers: {
"Cache-Control": apiConfig.cache,
},
});
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return NextResponse.json(payload, {
status: payload.code || 500,
});
}
}
+94
View File
@@ -0,0 +1,94 @@
import { NextResponse } from "next/server";
import { ErrorResponse } from "types/googleapis";
import driveClient from "utils/driveClient";
export async function GET() {
const _start = Date.now();
try {
const ids = [
"18h88Fit0MgIrHTGBEtpIbcHIZ_GEgpuP",
"1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j",
"1pctigJQKaF7GbDU1t8s0gT-0fPfNhl0B",
"16p09HGtImeuuvn06W6hDlDzSPqF4e80g",
];
const fields = "id, name, mimeType, parents";
const startFetch0 = Date.now();
const id0 = driveClient.files
.get({
fileId: ids[0],
fields,
})
.then((res) => res.data);
const startFetch1 = Date.now();
const id1 = driveClient.files.get({
fileId: ids[1],
fields,
});
const startFetch2 = Date.now();
const id2 = driveClient.files
.get({
fileId: ids[2],
fields,
})
.then((res) => res.data);
const startFetch3 = Date.now();
const id3 = driveClient.files.get({
fileId: ids[3],
fields,
});
const fetchStarted = Date.now();
const files = await Promise.all([id0, id1, id2, id3]);
const fetchFinished = Date.now();
// const files = await Promise.all(
// ids.map(async (id) => {
// const file = await driveClient.files.get({
// fileId: id,
// fields: "id, name, mimeType, parents",
// });
// return file.data;
// }),
// );
const payload = {
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: 200,
timing: {
fetch0: startFetch0,
fetch1: startFetch1,
fetch2: startFetch2,
fetch3: startFetch3,
fetchStarted,
fetchFinished,
duration: fetchFinished - startFetch0,
},
files,
};
return NextResponse.json(payload, {
status: 200,
});
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return NextResponse.json(payload, {
status: payload.code || 500,
});
}
}
+241
View File
@@ -0,0 +1,241 @@
import { NextRequest, NextResponse } from "next/server";
import {
ErrorResponse,
TPath,
ValidatePathResponse,
} from "types/googleapis";
import { ExtendedError } from "utils/driveHelper";
import apiConfig from "config/api.config";
import driveClient from "utils/driveClient";
import {
shortDecrypt,
shortEncrypt,
} from "utils/encryptionHelper";
import { cookies } from "next/headers";
export async function GET(request: NextRequest) {
const _start = Date.now();
try {
const path = new URL(request.url).searchParams.get(
"path",
);
if (!path) {
throw new ExtendedError(
"No path provided",
400,
"badRequest",
);
}
let pathArray: string[] = path.split(/[\\/]/);
pathArray = pathArray.filter((path) => path !== "");
const getRootId =
apiConfig.files.rootFolder !== "root"
? apiConfig.files.rootFolder
: driveClient.files
.get({
fileId: "root",
fields: "id",
})
.then((res) => res.data.id);
const getPathId = pathArray.map(async (path, index) => {
const query = [
"trashed = false",
"'me' in owners",
`name = '${path}'`,
];
const fetchFolderContents =
await driveClient.files.list({
q: `${query.join(" and ")}`,
fields: "files(id, name, mimeType, parents)",
});
if (!fetchFolderContents.data.files?.length) {
throw new ExtendedError(
`Path ${path} is not found`,
404,
"notFound",
);
}
return {
path,
data: fetchFolderContents.data.files.map(
(file) => ({
id: file.id,
parents: file.parents?.[0],
mimeType: file.mimeType,
}),
),
};
});
const [rootId, pathId] = await Promise.all([
getRootId,
Promise.all(getPathId),
]);
const selectedPath: string[] = [];
const mapIds: TPath[] = [];
// Check path validity
pathId.forEach((path, index) => {
if (index === 0) {
// Check if root folder inside data id
const checkRoot = path.data.find(
(item) => item.parents === rootId,
);
if (!checkRoot) {
throw new ExtendedError(
`Path "${path.path}" is either not found or not valid`,
400,
"badRequest",
);
}
selectedPath.push(checkRoot.id as string);
mapIds.push({
name: path.path,
id: shortEncrypt(checkRoot.id as string),
mimeType: checkRoot.mimeType as string,
});
} else {
const checkPath = path.data.find(
(item) =>
item.parents === selectedPath[index - 1],
);
if (!checkPath) {
throw new ExtendedError(
`Path "${path.path}" is either not found or not valid`,
400,
"badRequest",
);
}
selectedPath.push(checkPath.id as string);
mapIds.push({
name: path.path,
id: shortEncrypt(checkPath.id as string),
mimeType: checkPath.mimeType as string,
});
}
});
// Check for protected folder
const getPassword = mapIds.map(async (path) => {
if (
path.mimeType !==
"application/vnd.google-apps.folder"
)
return;
const query = [
"trashed = false",
"'me' in owners",
`'${shortDecrypt(path.id)}' in parents`,
`name = '${apiConfig.files.specialFile.password}'`,
];
const folderContents = await driveClient.files.list({
q: `${query.join(" and ")}`,
fields: "files(id, name, mimeType, parents)",
});
if (!folderContents.data.files?.length) return;
return {
path: path.name,
password: folderContents.data.files?.[0].id,
};
});
let password = await Promise.all(getPassword);
password = password.filter(
(item) => item !== undefined,
);
const lastProtectedFolder =
password[password.length - 1];
// Check password
if (lastProtectedFolder?.password) {
const fetchPassword = await driveClient.files.get(
{
fileId: lastProtectedFolder.password,
alt: "media",
},
{ responseType: "text" },
);
const userPassword = cookies().get(
`next-gdrive-password/${encodeURIComponent(
lastProtectedFolder.path,
)}`,
)?.value;
console.log(
`next-gdrive-password/${encodeURIComponent(
lastProtectedFolder.path,
)}`,
shortEncrypt("loremipsum"),
);
if (!userPassword) {
throw new ExtendedError(
`You are not authorized to access this folder`,
401,
"unauthorized",
);
}
console.log(
shortDecrypt(userPassword),
fetchPassword.data,
);
if (
shortDecrypt(userPassword) !== fetchPassword.data
) {
throw new ExtendedError(
`The password you entered is incorrect`,
401,
"unauthorized",
);
}
}
const payload: ValidatePathResponse = {
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
data: mapIds,
password: lastProtectedFolder?.password
? shortEncrypt(lastProtectedFolder.password)
: undefined,
};
return NextResponse.json(payload, {
status: 200,
headers: {
"Cache-Control": apiConfig.cache,
},
});
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return NextResponse.json(payload, {
status: payload.code || 500,
});
}
}
@@ -0,0 +1,3 @@
function SetupPage() {}
export default SetupPage;
@@ -0,0 +1,5 @@
function SetupFirstStep() {
return <div className={""}></div>;
}
export default SetupFirstStep;
+22
View File
@@ -0,0 +1,22 @@
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import { cookies } from "next/headers";
import axios from "axios";
import { ValidatePathResponse } from "types/googleapis";
async function _validatePath(path: string) {
const { data } = await axios.get<ValidatePathResponse>(
`/api/validate-path?path=${path}`,
);
return data;
}
type Props = {
params: {
path: string[];
};
};
async function ListIdPage({ params }: Props) {
const validatePath = await _validatePath(
params.path.join("/"),
);
const c = cookies();
const token = c.has("token") ? c.get("token")?.name : "";
return <div>lorem - {token}</div>;
}
export default ListIdPage;
+63
View File
@@ -0,0 +1,63 @@
"use client";
import { ThemeContext, TTheme } from "context/themeContext";
import { useEffect, useState } from "react";
import { TLayout } from "context/layoutContext";
type Props = {
children: React.ReactNode;
};
function ContextWrapper({ children }: Props) {
const [theme, setTheme] = useState<TTheme>("light");
const [layout, setLayout] = useState<TLayout>("grid");
useEffect(() => {
console.log("ContextWrapper useEffect");
if (typeof window === "undefined") return;
const lsTheme = localStorage.getItem("theme");
const lsLayout = localStorage.getItem("layout");
const lsSitePassword =
localStorage.getItem("sitePassword");
if (
lsTheme &&
(lsTheme === "dark" || lsTheme === "light")
) {
setTheme(lsTheme as TTheme);
}
if (
lsLayout &&
(lsLayout === "grid" || lsLayout === "list")
) {
setLayout(lsLayout as TLayout);
}
}, []);
useEffect(() => {
if (theme === "light") {
document.body.classList.remove("dark");
} else {
document.body.classList.add("dark");
}
}, [theme]);
return (
<>
<ThemeContext.Provider
value={{
theme,
setTheme: (theme) => {
if (typeof window !== "undefined") {
localStorage.setItem("theme", theme);
}
setTheme(theme);
},
}}
>
{children}
</ThemeContext.Provider>
</>
);
}
export default ContextWrapper;
+142
View File
@@ -0,0 +1,142 @@
import { ErrorResponse } from "types/googleapis";
import { NextRequest, NextResponse } from "next/server";
import driveClient from "utils/driveClient";
import apiConfig from "config/api.config";
import { ExtendedError } from "utils/driveHelper";
import axios from "axios";
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } },
) {
const _start = Date.now();
try {
const validatePath = await axios.get(
`${
apiConfig.basePath
}/api/validatePath?path=${params.path.join("/")}`,
);
return NextResponse.json(
{ data: validatePath.data },
{ status: 200 },
);
let id = "";
// const id = shortDecrypt(params.encryptedId);
// const { token } = getSearchParams(request.url, [
// "token",
// ]);
//
// if (!apiConfig.files.download.allowProtectedFile) {
// const decryptToken = shortDecrypt(token ? token : "");
// const parsedToken = JSON.parse(
// decryptToken || "{}",
// ) as DownloadToken;
// if (!parsedToken) {
// throw new ExtendedError(
// "Protected file not allowed.",
// 403,
// "forbidden",
// );
// }
// }
const getMetadata = driveClient.files.get({
fileId: id,
fields: apiConfig.files.field,
});
const getStream = driveClient.files.get(
{
fileId: id,
alt: "media",
},
{ responseType: "stream" },
);
const [metadata, stream] = await Promise.all([
getMetadata,
getStream,
]);
if (!metadata || metadata.data.trashed) {
const msg = metadata.data.trashed
? "File has been deleted"
: "File not found";
throw new ExtendedError(msg, 404, "notFound");
}
if (
metadata.data.mimeType ===
"application/vnd.google-apps.folder"
) {
throw new ExtendedError(
"Can't download folder",
400,
"badRequest",
);
}
if (
Number(metadata.data.size) >
apiConfig.files.download.maxFileSize
) {
return NextResponse.redirect(
metadata.data.webContentLink as string,
{ status: 302 },
);
}
const arrayBuffer = await new Promise<ArrayBuffer>(
(resolve, reject) => {
const chunks: Buffer[] = [];
stream.data.on("data", (chunk) =>
chunks.push(chunk),
);
stream.data.on("end", () => {
const buffer = Buffer.concat(chunks);
resolve(
buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
),
);
});
stream.data.on("error", reject);
},
);
return new NextResponse(arrayBuffer, {
status: 200,
headers: {
"Content-Type":
metadata.data.mimeType ||
"application/octet-stream",
"Cache-Control":
"public, max-age=31536000, immutable",
"Content-Disposition": `inline; filename="${metadata.data.name}"`,
},
});
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return NextResponse.json(payload, {
status: payload.code || 500,
});
}
}
+70
View File
@@ -0,0 +1,70 @@
import { Metadata } from "next";
import Navbar from "components/Navbar";
import Footer from "components/Footer";
import ContextWrapper from "./contextWrapper";
import {
Exo_2,
JetBrains_Mono,
Source_Sans_Pro,
} from "next/font/google";
import siteConfig from "config/site.config";
import "styles/globals.css";
const exo2 = Exo_2({
weight: ["300", "400", "600", "700"],
style: ["normal", "italic"],
display: "auto",
subsets: ["latin", "latin-ext"],
variable: "--font-exo2",
});
const sourceSansPro = Source_Sans_Pro({
weight: ["300", "400", "600", "700"],
style: ["normal", "italic"],
display: "auto",
subsets: ["latin", "latin-ext"],
variable: "--font-source-sans-pro",
});
const jetBrainsMono = JetBrains_Mono({
weight: ["300", "400", "600", "700"],
style: ["normal", "italic"],
display: "auto",
subsets: ["latin", "latin-ext"],
variable: "--font-jetbrains-mono",
});
export const metadata: Metadata = {
title: siteConfig.siteName,
description: siteConfig.siteDescription,
};
type Props = {
children: React.ReactNode;
};
function RootLayout({ children }: Props) {
return (
<html lang='en'>
<body
className={`${exo2.variable} ${sourceSansPro.variable} ${jetBrainsMono.variable} font-body`}
>
<ContextWrapper>
<main
className={
"text-dark-900 flex h-full min-h-dynamic w-dynamic flex-col bg-zinc-200 font-body dark:bg-zinc-800 dark:text-zinc-100"
}
>
<Navbar />
<div className={"flex-grow p-4"}>
{children}
</div>
<Footer />
</main>
</ContextWrapper>
</body>
</html>
);
}
export default RootLayout;
+22
View File
@@ -0,0 +1,22 @@
import driveClient from "utils/driveClient";
async function getRootFiles() {}
async function getPassword() {}
async function getReadme() {}
async function RootPage() {
const start = new Date().getTime();
const [files, password, readme] = await Promise.all([
getRootFiles(),
getPassword(),
getReadme(),
]);
const end = new Date().getTime();
return (
<div>
<h1>{end - start}</h1>
</div>
);
}
export default RootPage;
+31
View File
@@ -0,0 +1,31 @@
import siteConfig from "config/site.config";
import Link from "next/link";
function Footer() {
return (
<footer
className={
"flex w-full items-center justify-center gap-2 bg-zinc-100 py-1 dark:bg-zinc-900"
}
>
<span className={"text-xs font-semibold"}>
{siteConfig.footer.renderYear &&
new Date().getFullYear()}{" "}
{siteConfig.footer.text} - Powered by{" "}
<Link
role={"url"}
href={
"https://www.github.com/mbaharip/next-gdrive-index"
}
target={"_blank"}
rel={"noreferrer noopener"}
>
next-gdrive-index
</Link>{" "}
</span>
</footer>
);
}
export default Footer;
+186
View File
@@ -0,0 +1,186 @@
"use client";
import Link from "next/link";
import siteConfig from "config/site.config";
import {
MdClose,
MdDarkMode,
MdLightMode,
MdLogout,
MdMenu,
MdSearch,
} from "react-icons/md";
import { useContext, useState } from "react";
import {
ThemeContext,
TThemeContext,
} from "context/themeContext";
function Navbar() {
const { theme, setTheme } =
useContext<TThemeContext>(ThemeContext);
const [isMenuOpen, setIsMenuOpen] =
useState<boolean>(false);
return (
<>
<nav
className={
"sticky top-0 z-[1000] flex w-full items-center justify-between gap-2 bg-zinc-100 px-2 py-1 dark:bg-zinc-900 tablet:gap-4"
}
>
<Link
href={"/"}
title={siteConfig.siteName}
className={"flex items-center gap-2"}
>
<img
src={"/logo.svg"}
alt={siteConfig.siteName}
className={`h-8 w-8 ${
theme === "dark" && "hue-rotate-180 invert"
}`}
loading={"lazy"}
/>
<span
className={
"hidden text-lg font-bold tablet:block"
}
>
{siteConfig.navbar.title || siteConfig.siteName}
</span>
</Link>
<div className={"flex items-center gap-2"}>
<div
className={
"hidden items-center gap-2 tablet:flex"
}
>
{siteConfig.navbar.links.map((link) => (
<Link
key={`nav-link-${link.name}`}
href={link.href}
rel={
link.newTab ? "noopener noreferrer" : ""
}
target={link.newTab ? "_blank" : "_self"}
title={link.name}
className={
"flex items-center gap-1 whitespace-nowrap"
}
>
{link.icon && <link.icon />}
{link.name}
</Link>
))}
</div>
<div
id={"nav-search"}
title={"Search files"}
role={"button"}
className={
"interactive relative flex aspect-square h-6 w-6 items-center justify-center tablet:h-5 tablet:w-5"
}
>
<MdSearch
className={
"global-duration absolute h-full w-full"
}
/>
</div>
<div
id={"nav-theme"}
title={"Toggle Theme"}
role={"button"}
className={
"interactive relative flex aspect-square h-6 w-6 items-center justify-center tablet:h-5 tablet:w-5"
}
onClick={() =>
setTheme(theme === "dark" ? "light" : "dark")
}
>
<MdDarkMode
className={`global-duration absolute h-full w-full transition-all ${
theme === "dark"
? "pointer-events-none rotate-90 opacity-0"
: "pointer-events-auto rotate-0 opacity-100"
}`}
/>
<MdLightMode
className={`global-duration absolute h-full w-full transition-all ${
theme === "dark"
? "pointer-events-auto rotate-0 opacity-100"
: "pointer-events-none rotate-90 opacity-0"
}`}
/>
</div>
<div
id={"nav-logout"}
title={"Logout"}
role={"button"}
className={
"interactive relative flex aspect-square h-6 w-6 items-center justify-center tablet:h-5 tablet:w-5"
}
>
<MdLogout
className={
"global-duration absolute h-full w-full"
}
/>
</div>
<div
id={"nav-menu"}
title={"Menu"}
role={"button"}
className={
"interactive relative flex aspect-square h-6 w-6 items-center justify-center tablet:hidden"
}
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
<MdMenu
className={`global-duration absolute h-full w-full transition-all ${
isMenuOpen
? "pointer-events-none rotate-90 opacity-0"
: "pointer-events-auto rotate-0 opacity-100"
}`}
/>
<MdClose
className={`global-duration absolute h-full w-full transition-all ${
isMenuOpen
? "pointer-events-auto rotate-0 opacity-100"
: "pointer-events-none rotate-90 opacity-0"
}`}
/>
</div>
</div>
</nav>
<div
className={`global-duration fixed z-[999] flex h-dynamic w-full flex-col items-start justify-center gap-4 bg-zinc-100 px-8 text-xl transition-all dark:bg-zinc-900 tablet:hidden ${
isMenuOpen ? "top-0" : "-top-full"
}`}
>
{siteConfig.navbar.links.map((link, index) => (
<Link
key={`m-nav-link-${link.name}`}
href={link.href}
rel={link.newTab ? "noopener noreferrer" : ""}
target={link.newTab ? "_blank" : "_self"}
title={link.name}
className={`global-duration relative flex items-center gap-1 whitespace-nowrap transition-opacity delay-75 ${
isMenuOpen ? "opacity-100" : "opacity-0"
}`}
>
{link.icon && <link.icon />}
{link.name}
</Link>
))}
</div>
</>
);
}
export default Navbar;
-22
View File
@@ -1,22 +0,0 @@
import siteConfig from "config/site.config";
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<div className='flex w-full items-center justify-center gap-2 py-1'>
<span className='text-xs font-semibold'>
{currentYear} {siteConfig.footerText} - Powered by{" "}
<a
className={"link"}
href={"https://www.github.com/mbaharip/next-gdrive-index"}
target={"blank"}
rel={"noreferrer noopener"}
>
next-gdrive-index
</a>{" "}
</span>
</div>
);
}
+103 -38
View File
@@ -2,46 +2,111 @@ module.exports = {
// Client ID are safe to expose
client_id:
"126409166174-l2unckghm8d5m1gp3deu0uaps5son64f.apps.googleusercontent.com",
// Client secret and refresh token are encrypted
// Using encryption key
// https://drive.mbaharip.com/gudora-setup
client_secret:
"54f0505d11a8fe04d22ec642cdde4728:a6f911c70b0305a51160a2133a9bd2ada3c120567857002311f17bf29cc69e07d7f1a9ca20fd119d684191aee6e3866a",
refresh_token:
"209b4d2ff44bf877ce0636ddc81cdc9a:4a40e172ba5d6c1ee8daf50389841bd2c9d1153163196da0dab63c07b6f305bedafaa209a9e6adbb8d2377c74ba98cf818b6e73b8f57098686ed0b89bec93b0186047ccaab8804310a2ab4e46069d1cac8c322f0f1e206808ac62c18639610be07559eebad904905ba185cae8ba9fbe9",
dev_client_id:
"126409166174-l0f9hdblsrmhkt9jeue9m8o93skfs1sr.apps.googleusercontent.com",
files: {
// How many files to show per page
itemsPerPage: 25,
// Max number of files to show in search result
searchResult: 5,
// Starting point of the drive
// Use 'root' to use My Drive as starting point
// Or use folder id to use a specific folder as starting point
// TODO: Change when final
// rootFolder: "root",
rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j", // Test folder
// Limit breadcrumb to specific depth
// 0 = Unlimited
// 1 = Only show current folder
// 2 = Show current folder and its parent
// 3 = Show current folder and its parent and grandparent
// and so on.
// Warning: More parent = More API calls = Slower loading time
// There are no workaround for this yet, since Google Drive API v3 return only 1 parent
// Default: 2
breadcrumbDepth: 2,
/**
* Change this value to match your platform.
*/
basePath:
process.env.NODE_ENV === "production"
? `https://${process.env.VERCEL_URL}`
: "http://localhost:5000",
old: {
files: {
// How many files to show per page
itemsPerPage: 25,
// Max number of files to show in search result
searchResult: 5,
// Starting point of the drive
// Use 'root' to use My Drive as starting point
// Or use folder id to use a specific folder as starting point
// TODO: Change when final
// rootFolder: "root",
rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j", // Test folder
// Limit breadcrumb to specific depth
// 0 = Unlimited
// 1 = Only show current folder
// 2 = Show current folder and its parent
// 3 = Show current folder and its parent and grandparent
// and so on.
// Warning: More parent = More API calls = Slower loading time
// There are no workaround for this yet, since Google Drive API v3 return only 1 parent
// Default: 2
breadcrumbDepth: 2,
},
// Cache control header
// https://web.dev/uses-long-cache-ttl/
// Default: 1 minute
cache: "s-maxage=60, stale-while-revalidate",
// Max response body size
// If you're using Vercel, the max response size is 4.5MB
// https://vercel.com/docs/platform/limits#serverless-function-payload-size-limit
// If you're using another platform that has different limit,
// change this value to match your platform.
maxResponseSize: 4 * 1024 * 1024,
},
// Cache control header
// https://web.dev/uses-long-cache-ttl/
// Default: 1 minute
cache: "s-maxage=60, stale-while-revalidate",
files: {
field:
"id, name, mimeType, thumbnailLink, fileExtension, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink, trashed",
orderBy: "folder, name asc, modifiedTime desc",
/**
* Special file names that will be used for certain purposes
* These files will be ignored when searching for files
* and will be hidden from the file list
*/
specialFile: {
password: ".password",
readme: ".readme.md",
/**
* Banner are used for generating custom open graph image for folder
* By default, all folder will use the og.png inside public folder
*/
banner: ".banner",
},
itemsPerPage: 50,
searchResult: 10,
/**
* Starting point of the drive
* Use 'root' to use My Drive as starting point
* Or use folder id to use a specific folder as starting point
*/
rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j",
/**
* Limit breadcrumb to specific depth
* 0 = Unlimited
* 1 = Only show current file
* 2 = Show current file and its parent
* 3 = Show current file, its parent and grandparent
* and so on.
* Default: 2
*/
breadcrumbDepth: 2,
download: {
/**
* Allow user to download protected file without password
* If this set to false, the download link will have temporary token to download the files
* If this set to true, the download link will be permanent
* Default: false
*/
allowProtectedFile: false,
temporaryTokenDuration: 60 * 60, // 1 hour
/**
* If you're using Vercel, the max response size is 4.5MB
* https://vercel.com/docs/platform/limits#serverless-function-payload-size-limit
* If you're using another platform that has different limit,
* change this value to match your platform.
*/
maxFileSize: 4 * 1024 * 1024,
},
},
// Max response body size
// If you're using Vercel, the max response size is 4.5MB
// https://vercel.com/docs/platform/limits#serverless-function-payload-size-limit
// If you're using another platform that has different limit,
// change this value to match your platform.
maxResponseSize: 4 * 1024 * 1024,
/**
* https://web.dev/uses-long-cache-ttl/
*/
cache: "s-maxage=60, stale-while-revalidate",
};
+63 -3
View File
@@ -1,12 +1,18 @@
import { BsDiscord, BsGithub, BsPaypal } from "react-icons/bs";
import {
BsDiscord,
BsEnvelopeAt,
BsGithub,
BsPaypal,
} from "react-icons/bs";
const config = {
const oldConfig = {
/* Site MetaData */
// The name of the site
siteName: "mbahArip Stash",
// The description of the site
// Used in meta tags and document head
siteDescription: "Personal Stash of mbahArip, a place to store my files.",
siteDescription:
"Personal Stash of mbahArip, a place to store my files.",
// Fav icon of the site
// Also used as the logo of the site on the navbar
siteIcon: "/favicon.svg",
@@ -90,5 +96,59 @@ const config = {
pdfProvider: "mozilla",
},
};
const config = {
/* Site MetaData */
siteName: "mbahArip Stash",
siteDescription:
"Personal Stash of mbahArip, a place to store my files.",
/* General */
/**
* Site wide password protection
*/
privateIndex: true,
indexPassword:
"640e3e38dd31aec254f214ba38541a82ddb615e73ce9c6129f33ef549b154ab9",
navbar: {
/**
* Title beside the logo
* If not set, will be using `siteName` instead
*/
title: "",
links: [
{
icon: BsGithub,
name: "GitHub",
href: "https://www.github.com/mbaharip",
newTab: true,
},
{
icon: BsPaypal,
name: "Donate",
href: "https://www.paypal.me/mbaharip",
newTab: true,
},
{
icon: BsEnvelopeAt,
name: "Email",
href: "mailto:support@mbaharip.com",
},
{
name: "Main Website",
href: "https://www.mbaharip.com",
newTab: true,
},
],
},
footer: {
/**
* Footer text will be rendered as
* {year} {footerText} - Powered by next-gdrive-index ❤️
*/
text: "mbahArip Stash",
renderYear: true,
},
};
export default config;
+7 -1
View File
@@ -1,4 +1,10 @@
import React, { createContext, useState, useEffect } from "react";
"use client";
import React, {
createContext,
useState,
useEffect,
} from "react";
export type TTheme = "dark" | "light";
@@ -1,9 +1,12 @@
import initMiddleware from "utils/apiMiddleware";
import { NextApiRequest, NextApiResponse } from "next";
import { BannerResponse, ErrorResponse } from "types/googleapis";
import {
BannerResponse,
ErrorResponse,
} from "types/googleapis";
import { ExtendedError } from "utils/driveHelper";
import driveClient from "utils/driveClient";
import { urlEncrypt } from "utils/encryptionHelper";
import { shortEncrypt } from "utils/encryptionHelper";
export default initMiddleware(async function handler(
request: NextApiRequest,
@@ -14,7 +17,9 @@ export default initMiddleware(async function handler(
try {
const { folderId } = request.query;
const [name, partialId] = (folderId as string).split(":");
const [name, partialId] = (folderId as string).split(
":",
);
if (!name || !partialId || partialId.length !== 8) {
throw new ExtendedError(
@@ -45,7 +50,7 @@ export default initMiddleware(async function handler(
}
payload.banner = {
id: urlEncrypt(banner.id as string),
id: shortEncrypt(banner.id as string),
name: banner.name as string,
};
@@ -57,11 +62,19 @@ export default initMiddleware(async function handler(
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message: error.errors?.[0].message || error.message || "Unknown error",
reason: error.errors?.[0].reason || error.cause || "internalError",
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response.status(payload.code || 500).json(payload);
return response
.status(payload.code || 500)
.json(payload);
}
});
@@ -1,9 +1,12 @@
import { BannerResponse, ErrorResponse } from "types/googleapis";
import {
BannerResponse,
ErrorResponse,
} from "types/googleapis";
import { NextApiRequest, NextApiResponse } from "next";
import initMiddleware from "utils/apiMiddleware";
import apiConfig from "config/api.config";
import driveClient from "utils/driveClient";
import { urlEncrypt } from "utils/encryptionHelper";
import { shortEncrypt } from "utils/encryptionHelper";
export default initMiddleware(async function handler(
request: NextApiRequest,
@@ -31,7 +34,7 @@ export default initMiddleware(async function handler(
}
payload.banner = {
id: urlEncrypt(banner.id as string),
id: shortEncrypt(banner.id as string),
name: banner.name as string,
};
@@ -43,11 +46,19 @@ export default initMiddleware(async function handler(
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message: error.errors?.[0].message || error.message || "Unknown error",
reason: error.errors?.[0].reason || error.cause || "internalError",
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response.status(payload.code || 500).json(payload);
return response
.status(payload.code || 500)
.json(payload);
}
});
@@ -4,8 +4,8 @@ import { ExtendedError } from "utils/driveHelper";
import {
decrypt,
encrypt,
urlDecrypt,
urlEncrypt,
shortDecrypt,
shortEncrypt,
} from "utils/encryptionHelper";
export default function handler(
@@ -15,23 +15,36 @@ export default function handler(
const _start = Date.now();
try {
const { text, isUrl, isDecrypt } = request.query;
if (!text) throw new ExtendedError("Missing text", 400, "missingText");
if (!text)
throw new ExtendedError(
"Missing text",
400,
"missingText",
);
let encrypted;
let decrypted;
if (!isDecrypt) {
if (isUrl) {
const encodedText = encodeURIComponent(text as string);
encrypted = urlEncrypt(encodedText);
const encodedText = encodeURIComponent(
text as string,
);
encrypted = shortEncrypt(encodedText);
} else {
const encodedText = encodeURIComponent(text as string);
const encodedText = encodeURIComponent(
text as string,
);
encrypted = encrypt(encodedText);
}
} else {
if (isUrl) {
decrypted = decodeURIComponent(urlDecrypt(text as string));
decrypted = decodeURIComponent(
shortDecrypt(text as string),
);
} else {
decrypted = decodeURIComponent(decrypt(text as string));
decrypted = decodeURIComponent(
decrypt(text as string),
);
}
}
@@ -55,11 +68,19 @@ export default function handler(
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message: error.errors?.[0].message || error.message || "Unknown error",
reason: error.errors?.[0].reason || error.cause || "internalError",
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response.status(payload.code || 500).json(payload);
return response
.status(payload.code || 500)
.json(payload);
}
}
+58
View File
@@ -0,0 +1,58 @@
import { NextApiRequest, NextApiResponse } from "next";
import { ErrorResponse } from "types/googleapis";
import { ExtendedError } from "utils/driveHelper";
import {
decrypt,
encrypt,
shortDecrypt,
shortEncrypt,
} from "utils/encryptionHelper";
import { OAuth2Client } from "google-auth-library";
import apiConfig from "config/api.config";
export default async function handler(
request: NextApiRequest,
response: NextApiResponse,
) {
const _start = Date.now();
try {
const { code } = request.body;
if (!code)
throw new ExtendedError(
"Missing code",
400,
"missingCode",
);
const oauth2Client = new OAuth2Client(
"126409166174-l0f9hdblsrmhkt9jeue9m8o93skfs1sr.apps.googleusercontent.com",
"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",
"http://localhost",
);
const tokens = await oauth2Client.getToken(code);
return response.status(200).json(tokens.tokens);
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response
.status(payload.code || 500)
.json(payload);
}
}
+61
View File
@@ -0,0 +1,61 @@
import { NextApiRequest, NextApiResponse } from "next";
import { ErrorResponse } from "types/googleapis";
import { ExtendedError } from "utils/driveHelper";
import {
decrypt,
encrypt,
shortDecrypt,
shortEncrypt,
} from "utils/encryptionHelper";
import { OAuth2Client } from "google-auth-library";
import apiConfig from "config/api.config";
export default async function handler(
request: NextApiRequest,
response: NextApiResponse,
) {
const _start = Date.now();
try {
const { code } = request.body;
if (!code)
throw new ExtendedError(
"Missing code",
400,
"missingCode",
);
const oauth2Client = new OAuth2Client(
"126409166174-l0f9hdblsrmhkt9jeue9m8o93skfs1sr.apps.googleusercontent.com",
"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",
"http://localhost",
);
oauth2Client.setCredentials({
refresh_token: code,
});
const tokens = await oauth2Client.getAccessToken();
return response.status(200).json(tokens);
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response
.status(payload.code || 500)
.json(payload);
}
}
@@ -2,12 +2,19 @@
// [filename]:[partialId]
import { NextApiRequest, NextApiResponse } from "next";
import { ErrorResponse, FileResponse, FilesResponse } from "types/googleapis";
import { ExtendedError, hiddenFiles } from "utils/driveHelper";
import {
ErrorResponse,
FileResponse,
FilesResponse,
} from "types/googleapis";
import {
ExtendedError,
hiddenFiles,
} from "utils/driveHelper";
import driveClient from "utils/driveClient";
import apiConfig from "config/api.config";
import initMiddleware from "utils/apiMiddleware";
import { urlEncrypt } from "utils/encryptionHelper";
import { shortEncrypt } from "utils/encryptionHelper";
export default initMiddleware(async function handler(
request: NextApiRequest,
@@ -16,7 +23,8 @@ export default initMiddleware(async function handler(
const _start = Date.now();
try {
const { id, pageToken, download, thumbnail, banner } = request.query;
const { id, pageToken, download, thumbnail, banner } =
request.query;
const [name, partialId] = (id as string).split(":");
@@ -40,7 +48,11 @@ export default initMiddleware(async function handler(
(file.id as string).startsWith(partialId),
);
if (!file) {
throw new ExtendedError("File not found.", 404, "notFound");
throw new ExtendedError(
"File not found.",
404,
"notFound",
);
}
if (file.id === apiConfig.files.rootFolder) {
return response.status(301).redirect("/api/files");
@@ -48,14 +60,23 @@ export default initMiddleware(async function handler(
response.setHeader("Cache-Control", apiConfig.cache);
if (file.mimeType !== "application/vnd.google-apps.folder") {
if (
file.mimeType !== "application/vnd.google-apps.folder"
) {
if (thumbnail === "1") {
return response.status(301).redirect(file.thumbnailLink as string);
return response
.status(301)
.redirect(file.thumbnailLink as string);
}
if (download === "1") {
// Check size
if (Number(file.size as string) > apiConfig.maxResponseSize) {
return response.status(301).redirect(file.webContentLink as string);
if (
Number(file.size as string) >
apiConfig.maxResponseSize
) {
return response
.status(301)
.redirect(file.webContentLink as string);
}
const fileStream = await driveClient.files.get(
@@ -72,9 +93,14 @@ export default initMiddleware(async function handler(
);
response.setHeader(
"Content-Disposition",
`attachment; filename=${encodeURIComponent(file.name as string)}`,
`attachment; filename=${encodeURIComponent(
file.name as string,
)}`,
);
response.setHeader(
"Content-Length",
file.size as string,
);
response.setHeader("Content-Length", file.size as string);
return response.status(200).send(fileStream.data);
}
@@ -85,9 +111,9 @@ export default initMiddleware(async function handler(
responseTime: Date.now() - _start,
file: {
...file,
id: urlEncrypt(file.id as string),
id: shortEncrypt(file.id as string),
webContentLink: file.webContentLink
? urlEncrypt(file.webContentLink)
? shortEncrypt(file.webContentLink)
: undefined,
},
};
@@ -100,29 +126,37 @@ export default initMiddleware(async function handler(
"trashed = false",
"'me' in owners",
];
const fetchFolderContents = await driveClient.files.list({
q: `${query.join(" and ")}`,
fields:
"files(id, name, mimeType, thumbnailLink, fileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken",
orderBy: "folder, name asc, createdTime",
pageSize: apiConfig.files.itemsPerPage,
pageToken: (pageToken as string) || undefined,
});
const fetchFolderContents =
await driveClient.files.list({
q: `${query.join(" and ")}`,
fields:
"files(id, name, mimeType, thumbnailLink, fileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken",
orderBy: "folder, name asc, createdTime",
pageSize: apiConfig.files.itemsPerPage,
pageToken: (pageToken as string) || undefined,
});
const isReadmeExists = !!fetchFolderContents.data.files?.find(
(file) => file.name === ".readme.md",
);
const isBannerExists = !!fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
const isReadmeExists =
!!fetchFolderContents.data.files?.find(
(file) => file.name === ".readme.md",
);
const isBannerExists =
!!fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
if (banner === "1") {
if (!isBannerExists) {
throw new ExtendedError("Banner not found.", 404, "notFound");
throw new ExtendedError(
"Banner not found.",
404,
"notFound",
);
}
const bannerFile = fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
const bannerFile =
fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
const bannerFileStream = await driveClient.files.get(
{
fileId: bannerFile?.id as string,
@@ -140,19 +174,26 @@ export default initMiddleware(async function handler(
bannerFile?.name as string,
)}`,
);
response.setHeader("Content-Length", bannerFile?.size as string);
return response.status(200).send(bannerFileStream.data);
response.setHeader(
"Content-Length",
bannerFile?.size as string,
);
return response
.status(200)
.send(bannerFileStream.data);
}
// Get only folder, since we order the folder to be first, we can just get all the folder first
const folderList =
fetchFolderContents.data.files
?.filter(
(item) => item.mimeType === "application/vnd.google-apps.folder",
(item) =>
item.mimeType ===
"application/vnd.google-apps.folder",
)
.map((item) => ({
...item,
id: urlEncrypt(item.id as string),
id: shortEncrypt(item.id as string),
})) || [];
// Filter the files, excluding every google apps files and hidden files (.password and .readme.md)
// .password currently not used, but will probably be used in the future
@@ -160,7 +201,9 @@ export default initMiddleware(async function handler(
fetchFolderContents.data.files
?.filter(
(item) =>
!item.mimeType?.startsWith("application/vnd.google-apps") &&
!item.mimeType?.startsWith(
"application/vnd.google-apps",
) &&
// !hiddenFiles.includes(item.name as string),
!hiddenFiles.some((hiddenFile) =>
item.name?.startsWith(hiddenFile),
@@ -168,9 +211,9 @@ export default initMiddleware(async function handler(
)
.map((item) => ({
...item,
id: urlEncrypt(item.id as string),
id: shortEncrypt(item.id as string),
webContentLink: item.webContentLink
? urlEncrypt(item.webContentLink)
? shortEncrypt(item.webContentLink)
: undefined,
})) || [];
@@ -181,7 +224,8 @@ export default initMiddleware(async function handler(
isReadmeExists: isReadmeExists,
folders: folderList,
files: fileList,
nextPageToken: fetchFolderContents.data.nextPageToken || undefined,
nextPageToken:
fetchFolderContents.data.nextPageToken || undefined,
};
return response.status(200).json(payload);
@@ -192,11 +236,19 @@ export default initMiddleware(async function handler(
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message: error.errors?.[0].message || error.message || "Unknown error",
reason: error.errors?.[0].reason || error.cause || "internalError",
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response.status(payload.code || 500).json(payload);
return response
.status(payload.code || 500)
.json(payload);
}
});
@@ -1,10 +1,16 @@
import { NextApiRequest, NextApiResponse } from "next";
import driveClient from "utils/driveClient";
import apiConfig from "config/api.config";
import { ExtendedError, hiddenFiles } from "utils/driveHelper";
import {
ExtendedError,
hiddenFiles,
} from "utils/driveHelper";
import initMiddleware from "utils/apiMiddleware";
import { ErrorResponse, FilesResponse } from "types/googleapis";
import { urlEncrypt } from "utils/encryptionHelper";
import {
ErrorResponse,
FilesResponse,
} from "types/googleapis";
import { shortEncrypt } from "utils/encryptionHelper";
export default initMiddleware(async function handler(
request: NextApiRequest,
@@ -20,32 +26,41 @@ export default initMiddleware(async function handler(
"'me' in owners",
`parents = '${apiConfig.files.rootFolder}'`,
];
const fetchFolderContents = await driveClient.files.list({
q: `${query.join(" and ")}`,
fields:
"files(id, name, mimeType, thumbnailLink, fileExtension, fullFileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken",
orderBy: "folder, name asc, createdTime",
pageSize: apiConfig.files.itemsPerPage,
pageToken: (pageToken as string) || undefined,
});
const fetchFolderContents =
await driveClient.files.list({
q: `${query.join(" and ")}`,
fields:
"files(id, name, mimeType, thumbnailLink, fileExtension, fullFileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, webContentLink, iconLink), nextPageToken",
orderBy: "folder, name asc, createdTime",
pageSize: apiConfig.files.itemsPerPage,
pageToken: (pageToken as string) || undefined,
});
const isReadmeExists = fetchFolderContents.data.files?.find(
(file) => file.name === ".readme.md",
);
const isBannerExists = fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
const isPasswordExists = fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".password"),
);
const isReadmeExists =
fetchFolderContents.data.files?.find(
(file) => file.name === ".readme.md",
);
const isBannerExists =
fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
const isPasswordExists =
fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".password"),
);
if (banner === "1") {
if (!isBannerExists) {
throw new ExtendedError("Banner not found.", 404, "notFound");
throw new ExtendedError(
"Banner not found.",
404,
"notFound",
);
}
const bannerFile = fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
const bannerFile =
fetchFolderContents.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
const bannerFileStream = await driveClient.files.get(
{
fileId: bannerFile?.id as string,
@@ -63,24 +78,33 @@ export default initMiddleware(async function handler(
bannerFile?.name as string,
)}`,
);
response.setHeader("Content-Length", bannerFile?.size as string);
return response.status(200).send(bannerFileStream.data);
response.setHeader(
"Content-Length",
bannerFile?.size as string,
);
return response
.status(200)
.send(bannerFileStream.data);
}
const folderList =
fetchFolderContents.data.files
?.filter(
(item) => item.mimeType === "application/vnd.google-apps.folder",
(item) =>
item.mimeType ===
"application/vnd.google-apps.folder",
)
.map((item) => ({
...item,
id: urlEncrypt(item.id as string),
id: shortEncrypt(item.id as string),
})) || [];
const fileList =
fetchFolderContents.data.files
?.filter(
(item) =>
!item.mimeType?.startsWith("application/vnd.google-apps") &&
!item.mimeType?.startsWith(
"application/vnd.google-apps",
) &&
// !hiddenFiles.includes(item.name as string),
!hiddenFiles.some((hiddenFile) =>
item.name?.startsWith(hiddenFile),
@@ -88,9 +112,9 @@ export default initMiddleware(async function handler(
)
.map((item) => ({
...item,
id: urlEncrypt(item.id as string),
id: shortEncrypt(item.id as string),
webContentLink: item.webContentLink
? urlEncrypt(item.webContentLink)
? shortEncrypt(item.webContentLink)
: undefined,
})) || [];
@@ -103,7 +127,8 @@ export default initMiddleware(async function handler(
isReadmeExists: !!isReadmeExists,
isBannerExists: !!isBannerExists,
isPasswordExists: !!isPasswordExists,
nextPageToken: fetchFolderContents.data.nextPageToken || undefined,
nextPageToken:
fetchFolderContents.data.nextPageToken || undefined,
};
return response
@@ -117,11 +142,19 @@ export default initMiddleware(async function handler(
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message: error.errors?.[0].message || error.message || "Unknown error",
reason: error.errors?.[0].reason || error.cause || "internalError",
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response.status(payload.code || 500).json(payload);
return response
.status(payload.code || 500)
.json(payload);
}
});
@@ -3,27 +3,38 @@ import siteConfig from "config/site.config";
import { NextRequest } from "next/server";
export const config = {
runtime: "edge",
runtime: "experimental-edge",
};
const fontBold = fetch(
new URL("../../../public/fonts/Exo2-Bold.ttf", import.meta.url),
new URL(
"../../../public/fonts/Exo2-Bold.ttf",
import.meta.url,
),
).then((res) => res.arrayBuffer());
const fontRegular = fetch(
new URL("../../../public/fonts/Exo2-Regular.ttf", import.meta.url),
new URL(
"../../../public/fonts/Exo2-Regular.ttf",
import.meta.url,
),
).then((res) => res.arrayBuffer());
export default async function handler(request: NextRequest) {
export default async function handler(
request: NextRequest,
) {
try {
const exoFont = await fontRegular;
const exoFontBold = await fontBold;
const { searchParams } = new URL(request.url);
const siteName = searchParams.get("siteName") || siteConfig.siteName;
const siteName =
searchParams.get("siteName") || siteConfig.siteName;
const fileId = searchParams.get("fileId") || null;
const fileName =
searchParams.get("fileName") || fileId?.split(":")[0] || null;
searchParams.get("fileName") ||
fileId?.split(":")[0] ||
null;
if (!fileId) {
throw new Error("No file ID");
+24 -9
View File
@@ -3,32 +3,47 @@ import siteConfig from "config/site.config";
import { NextRequest } from "next/server";
export const config = {
runtime: "edge",
runtime: "experimental-edge",
};
const fontBold = fetch(
new URL("../../../public/fonts/Exo2-Bold.ttf", import.meta.url),
new URL(
"../../../public/fonts/Exo2-Bold.ttf",
import.meta.url,
),
).then((res) => res.arrayBuffer());
const fontRegular = fetch(
new URL("../../../public/fonts/Exo2-Regular.ttf", import.meta.url),
new URL(
"../../../public/fonts/Exo2-Regular.ttf",
import.meta.url,
),
).then((res) => res.arrayBuffer());
export default async function handler(request: NextRequest) {
export default async function handler(
request: NextRequest,
) {
try {
const exoFont = await fontRegular;
const exoFontBold = await fontBold;
const { searchParams } = new URL(request.url);
const siteName = searchParams.get("siteName") || siteConfig.siteName;
const siteName =
searchParams.get("siteName") || siteConfig.siteName;
const fileId = searchParams.get("fileId") || null;
const fileName =
searchParams.get("fileName") || fileId?.split(":")[0] || null;
searchParams.get("fileName") ||
fileId?.split(":")[0] ||
null;
const fileExt = fileName?.split(".").pop() || null;
const isImage = ["jpg", "jpeg", "png", "gif", "webp"].includes(
fileExt || "",
);
const isImage = [
"jpg",
"jpeg",
"png",
"gif",
"webp",
].includes(fileExt || "");
if (!fileId) {
throw new Error("No file ID");
@@ -1,8 +1,11 @@
import apiConfig from "config/api.config";
import { NextApiRequest, NextApiResponse } from "next";
import { ErrorResponse, SearchResponse } from "types/googleapis";
import {
ErrorResponse,
SearchResponse,
} from "types/googleapis";
import driveClient from "utils/driveClient";
import { urlEncrypt } from "utils/encryptionHelper";
import { shortEncrypt } from "utils/encryptionHelper";
import { hiddenFiles } from "utils/driveHelper";
export default async function handler(
@@ -43,12 +46,15 @@ export default async function handler(
!hiddenFiles.some((hiddenFile) =>
item.name?.startsWith(hiddenFile),
) &&
(!item.mimeType?.startsWith("application/vnd.google-apps") ||
item.mimeType === "application/vnd.google-apps.folder"),
(!item.mimeType?.startsWith(
"application/vnd.google-apps",
) ||
item.mimeType ===
"application/vnd.google-apps.folder"),
)
.map((item) => ({
...item,
id: urlEncrypt(item.id as string),
id: shortEncrypt(item.id as string),
})) || [],
};
@@ -63,11 +69,19 @@ export default async function handler(
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message: error.errors?.[0].message || error.message || "Unknown error",
reason: error.errors?.[0].reason || error.cause || "internalError",
message:
error.errors?.[0].message ||
error.message ||
"Unknown error",
reason:
error.errors?.[0].reason ||
error.cause ||
"internalError",
},
};
return response.status(payload.code || 500).json(payload);
return response
.status(payload.code || 500)
.json(payload);
}
}
+72 -35
View File
@@ -1,13 +1,19 @@
import { GetServerSideProps } from "next";
import axios from "axios";
import { ErrorResponse, FileResponse } from "types/googleapis";
import {
ErrorResponse,
FileResponse,
} from "types/googleapis";
import { useEffect, useState } from "react";
import useSWR from "swr";
import { urlDecrypt } from "utils/encryptionHelper";
import { shortDecrypt } from "utils/encryptionHelper";
import DefaultLayout from "components/layout/DefaultLayout";
import { NextSeo } from "next-seo";
import SWRLayout from "components/layout/SWRLayout";
import { getFilePreview, getFileType } from "utils/mimeTypesHelper";
import {
getFilePreview,
getFileType,
} from "utils/mimeTypesHelper";
import {
capitalize,
formatBytes,
@@ -27,7 +33,8 @@ type Metadata = {
};
export default function File({ id, fileName }: Props) {
const [data, setData] = useState<FileResponse>();
const [PreviewComponent, setPreviewComponent] = useState<JSX.Element>();
const [PreviewComponent, setPreviewComponent] =
useState<JSX.Element>();
const [metadata, setMetadata] = useState<Metadata[]>([]);
const copyLink = useCopyText();
@@ -41,7 +48,9 @@ export default function File({ id, fileName }: Props) {
data: swrData,
error,
isLoading,
} = useSWR<FileResponse, ErrorResponse>(`/api/files/${id}`);
} = useSWR<FileResponse, ErrorResponse>(
`/api/files/${id}`,
);
useEffect(() => {
if (swrData) {
@@ -49,8 +58,10 @@ export default function File({ id, fileName }: Props) {
...swrData,
file: {
...swrData.file,
id: urlDecrypt(swrData.file.id as string),
webContentLink: urlDecrypt(swrData.file.webContentLink as string),
id: shortDecrypt(swrData.file.id as string),
webContentLink: shortDecrypt(
swrData.file.webContentLink as string,
),
},
};
setData(decryptedData);
@@ -90,11 +101,15 @@ export default function File({ id, fileName }: Props) {
},
{
label: "Created",
value: formatDate(new Date(data.file.createdTime as string)),
value: formatDate(
new Date(data.file.createdTime as string),
),
},
{
label: "Modified",
value: formatDate(new Date(data.file.modifiedTime as string)),
value: formatDate(
new Date(data.file.modifiedTime as string),
),
},
];
if (data.file.imageMediaMetadata) {
@@ -107,7 +122,8 @@ export default function File({ id, fileName }: Props) {
defaultMetadata.push({
label: "Duration",
value: formatDuration(
data.file.videoMediaMetadata.durationMillis as string,
data.file.videoMediaMetadata
.durationMillis as string,
),
});
}
@@ -121,13 +137,19 @@ export default function File({ id, fileName }: Props) {
renderSwitchLayout={false}
>
<NextSeo
title={`Viewing ${fileName.split(".").slice(0, -1).join(".")}`}
title={`Viewing ${fileName
.split(".")
.slice(0, -1)
.join(".")}`}
openGraph={{
title: fileName.split(".").slice(0, -1).join("."),
description: `Viewing ${fileName.split(".").slice(0, -1).join(".")}`,
url: `${process.env.NEXT_PUBLIC_DOMAIN}/file/${encodeURIComponent(
id,
)}`,
description: `Viewing ${fileName
.split(".")
.slice(0, -1)
.join(".")}`,
url: `${
process.env.NEXT_PUBLIC_DOMAIN
}/file/${encodeURIComponent(id)}`,
images: [
{
url: `${
@@ -177,11 +199,19 @@ export default function File({ id, fileName }: Props) {
{metadata.map((item, index) => (
<div
key={`fileDetails-${index}`}
className={"mb-2 flex w-full flex-col justify-center"}
className={
"mb-2 flex w-full flex-col justify-center"
}
>
<span className={"font-bold text-inherit"}>{item.label}</span>
<span
className={"whitespace-pre-wrap break-words text-inherit"}
className={"font-bold text-inherit"}
>
{item.label}
</span>
<span
className={
"whitespace-pre-wrap break-words text-inherit"
}
>
{item.value}
</span>
@@ -195,14 +225,20 @@ export default function File({ id, fileName }: Props) {
<div className={"divider-horizontal"} />
<div className={"flex w-full flex-col justify-center gap-2"}>
<div
className={
"flex w-full flex-col justify-center gap-2"
}
>
<Link
href={`/api/files/${id}?download=1`}
className={"w-full"}
target={"_blank"}
rel={"noopener noreferrer"}
>
<button className={"primary w-full"}>Download</button>
<button className={"primary w-full"}>
Download
</button>
</Link>
<button
className={"secondary"}
@@ -223,22 +259,23 @@ export default function File({ id, fileName }: Props) {
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.query;
export const getServerSideProps: GetServerSideProps =
async (context) => {
const { id } = context.query;
const fetchFileMetadata = await axios.get<FileResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}`,
);
if (!fetchFileMetadata.data.success) {
return {
notFound: true,
};
}
const fetchFileMetadata = await axios.get<FileResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}`,
);
if (!fetchFileMetadata.data.success) {
return {
notFound: true,
props: {
id,
fileName: fetchFileMetadata.data.file.name,
},
};
}
return {
props: {
id,
fileName: fetchFileMetadata.data.file.name,
},
};
};
+58 -49
View File
@@ -1,7 +1,7 @@
import { GetServerSideProps } from "next";
import { FileResponse } from "types/googleapis";
import axios from "axios";
import { urlDecrypt } from "utils/encryptionHelper";
import { shortDecrypt } from "utils/encryptionHelper";
import { promisify } from "util";
import { pipeline } from "stream";
import driveClient from "utils/driveClient";
@@ -12,58 +12,67 @@ export default function Media() {
}
const pipelineAsync = promisify(pipeline);
export const getServerSideProps: GetServerSideProps = async (context) => {
const { fileId } = context.query;
export const getServerSideProps: GetServerSideProps =
async (context) => {
const { fileId } = context.query;
const searchFile = await axios.get<FileResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}`,
);
if (!searchFile.data.success) {
return {
notFound: true,
};
}
const searchFile = await axios.get<FileResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}`,
);
if (!searchFile.data.success) {
return {
notFound: true,
};
}
const id = urlDecrypt(searchFile.data.file.id as string);
const fileName = searchFile.data.file.name as string;
const mimeType = searchFile.data.file.mimeType as string;
const id = shortDecrypt(
searchFile.data.file.id as string,
);
const fileName = searchFile.data.file.name as string;
const mimeType = searchFile.data.file
.mimeType as string;
if (
!mimeType.startsWith("image/") &&
!mimeType.startsWith("video/") &&
!mimeType.startsWith("audio/")
) {
return {
notFound: true,
};
}
if (
!mimeType.startsWith("image/") &&
!mimeType.startsWith("video/") &&
!mimeType.startsWith("audio/")
) {
return {
notFound: true,
};
}
if (Number(searchFile.data.file.size as string) > apiConfig.maxResponseSize) {
return {
redirect: {
destination: urlDecrypt(searchFile.data.file.webContentLink as string),
permanent: false,
if (
Number(searchFile.data.file.size as string) >
apiConfig.maxResponseSize
) {
return {
redirect: {
destination: shortDecrypt(
searchFile.data.file.webContentLink as string,
),
permanent: false,
},
};
}
const getImageStream = await driveClient.files.get(
{
fileId: id,
alt: "media",
},
{ responseType: "stream" },
);
context.res.setHeader("Content-Type", mimeType);
context.res.setHeader(
"Content-Disposition",
`inline; filename="${fileName}"`,
);
await pipelineAsync(getImageStream.data, context.res);
return {
props: {},
};
}
const getImageStream = await driveClient.files.get(
{
fileId: id,
alt: "media",
},
{ responseType: "stream" },
);
context.res.setHeader("Content-Type", mimeType);
context.res.setHeader(
"Content-Disposition",
`inline; filename="${fileName}"`,
);
await pipelineAsync(getImageStream.data, context.res);
return {
props: {},
};
};
-33
View File
@@ -1,33 +0,0 @@
import { GetServerSideProps } from "next";
import driveClient from "utils/driveClient";
import { promisify } from "util";
import { pipeline } from "stream";
export default function Test() {
return <div />;
}
const pipelineAsync = promisify(pipeline);
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
const getImageMetadata = driveClient.files.get({
fileId: "1uAmdeBGJPAEkyEG_M_gYczjP0eVy6ncH",
fields: "name, mimeType",
});
const getImageStream = driveClient.files.get(
{
fileId: "1uAmdeBGJPAEkyEG_M_gYczjP0eVy6ncH",
alt: "media",
},
{ responseType: "stream" },
);
const [imageMetadata, imageStream] = await Promise.all([
getImageMetadata,
getImageStream,
]);
res.setHeader("Content-Type", imageMetadata.data.mimeType as string);
await pipelineAsync(imageStream.data, res);
return {
props: {},
};
};
+60 -246
View File
@@ -2,265 +2,79 @@
@tailwind components;
@tailwind utilities;
html, body {
margin: 0;
padding: 0;
box-sizing: border-box;
:root {
--animation-duration: 300ms;
--animation-timing: cubic-bezier(0.4, 0, 0.2, 1);
}
html, body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Base */
@layer base {
html {
@apply text-sm tablet:text-base;
}
/* Scrollbar */
::-webkit-scrollbar {
@apply w-1.5;
}
html ::-webkit-scrollbar-track {
@apply bg-zinc-100 rounded;
}
html ::-webkit-scrollbar-thumb {
@apply bg-zinc-400 rounded;
}
html ::-webkit-scrollbar-thumb:hover {
@apply bg-zinc-500;
}
html.dark ::-webkit-scrollbar-track {
@apply bg-zinc-500;
}
html.dark ::-webkit-scrollbar-thumb {
@apply bg-zinc-900;
}
html.dark ::-webkit-scrollbar-thumb:hover {
@apply bg-zinc-800;
}
main {
@apply flex flex-col items-center justify-start min-h-screen h-full w-full;
@apply bg-zinc-100 dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100;
}
/* Typography */
h1 {
@apply text-4xl leading-loose font-bold;
}
h2 {
@apply text-3xl leading-loose font-bold;
}
h3 {
@apply text-2xl leading-loose font-bold;
}
h4 {
@apply text-xl leading-relaxed font-semibold;
}
h5 {
@apply text-lg leading-relaxed font-semibold;
}
h6 {
@apply text-base leading-normal font-semibold;
}
p {
@apply text-base leading-relaxed whitespace-pre-wrap;
}
span {
@apply text-base leading-normal;
}
a {
@apply hover:opacity-75 transition duration-150;
@apply text-zinc-900 dark:text-zinc-100 hover:text-zinc-800 dark:hover:text-zinc-200;
}
a.file {
@apply opacity-90 hover:opacity-100 !important;
}
a.link {
@apply text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 underline underline-offset-2;
}
input, textarea, button, select {
@apply rounded-lg px-4 py-2 tablet:py-1;
@apply text-sm tablet:text-base;
@apply outline-none border;
@apply border-zinc-400 hover:border-blue-300 focus:border-blue-400;
@apply bg-zinc-200 hover:bg-zinc-300 focus:bg-zinc-50;
@apply dark:border-zinc-600 dark:hover:border-blue-400 dark:focus:border-blue-500;
@apply dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:focus:bg-zinc-950;
@apply disabled:border-zinc-300 dark:disabled:border-zinc-700;
@apply disabled:bg-zinc-400 dark:disabled:bg-zinc-800;
@apply disabled:text-zinc-500;
@apply transition duration-150;
}
input.error {
@apply border-red-400 hover:border-red-500 focus:border-red-600;
@apply bg-red-500 hover:bg-red-600 focus:bg-red-700 placeholder-zinc-100;
@apply dark:border-red-500 dark:hover:border-red-600 dark:focus:border-red-700 dark:placeholder-zinc-100;
@apply dark:bg-red-500 dark:hover:bg-red-600 dark:focus:bg-red-700;
}
button {
@apply cursor-pointer disabled:cursor-default;
}
button[disabled] {
@apply cursor-not-allowed !important;
@apply border-zinc-300 dark:border-zinc-700 !important;
@apply bg-zinc-400 dark:bg-zinc-800 !important;
@apply text-zinc-500 !important;
}
button.primary {
@apply text-zinc-100 dark:text-zinc-100;
@apply border-blue-400 hover:border-blue-500 active:border-blue-600;
@apply bg-blue-500 hover:bg-blue-700 active:bg-blue-400;
@apply dark:border-blue-500 dark:hover:border-blue-600 dark:active:border-blue-700;
@apply dark:bg-blue-500 dark:hover:bg-blue-600 dark:active:bg-blue-700;
}
button.secondary{
@apply text-zinc-100 dark:text-zinc-100;
@apply border-zinc-400 hover:border-zinc-500 active:border-zinc-600;
@apply bg-zinc-500 hover:bg-zinc-700 active:bg-zinc-400;
@apply dark:border-zinc-500 dark:hover:border-zinc-600 dark:active:border-zinc-700;
@apply dark:bg-zinc-500 dark:hover:bg-zinc-600 dark:active:bg-zinc-700;
}
button.danger {
@apply text-zinc-100 dark:text-zinc-100;
@apply border-red-400 hover:border-red-500 active:border-red-600;
@apply bg-red-500 hover:bg-red-700 active:bg-red-400;
@apply dark:border-red-500 dark:hover:border-red-600 dark:active:border-red-700;
@apply dark:bg-red-500 dark:hover:bg-red-600 dark:active:bg-red-700;
}
table {
@apply w-full border-collapse;
}
th, td {
@apply text-left;
html {
@apply text-sm tablet:text-base;
}
th {
@apply border-b border-zinc-400 dark:border-zinc-700 pt-1 pb-2;
}
td {
@apply border-b border-zinc-400 dark:border-zinc-700 py-1.5;
* {
@apply focus:outline focus:outline-blue-400 focus:rounded;
@apply dark:focus:outline-blue-600;
}
img.thumbnail {
@apply rounded-lg w-full max-w-screen-tablet mt-2 mb-4;
}
/* Scrollbar */
::-webkit-scrollbar {
@apply w-1.5;
}
html ::-webkit-scrollbar-track {
@apply bg-zinc-100 rounded;
}
html ::-webkit-scrollbar-thumb {
@apply bg-zinc-400 rounded;
}
html ::-webkit-scrollbar-thumb:hover {
@apply bg-zinc-500;
}
html.dark ::-webkit-scrollbar-track {
@apply bg-zinc-500;
}
html.dark ::-webkit-scrollbar-thumb {
@apply bg-zinc-900;
}
html.dark ::-webkit-scrollbar-thumb:hover {
@apply bg-zinc-800;
}
/* Text */
a, .interactive {
@apply opacity-90 transition-opacity duration-[var(--animation-duration)] ease-[var(--animation-timing)];
@apply hover:opacity-100;
}
a[role='url']{
@apply opacity-100 text-blue-600 transition-colors duration-[var(--animation-duration)] ease-[var(--animation-timing)];
@apply hover:text-blue-500;
@apply active:text-blue-700;
@apply focus:underline;
@apply dark:text-blue-500;
@apply dark:hover:text-blue-400;
@apply dark:active:text-blue-600;
}
}
@layer components {
div.card {
@apply rounded-lg p-2 tablet:p-4;
@apply border border-zinc-400 dark:border-zinc-700;
@apply drop-shadow;
@apply bg-zinc-100 dark:bg-zinc-900;
}
div.banner {
@apply rounded-lg p-2 tablet:p-4 my-2;
@apply border border-zinc-400 dark:border-zinc-700;
@apply drop-shadow;
@apply bg-zinc-100 dark:bg-zinc-900;
@apply flex gap-4 items-center;
}
div.banner.warning {
@apply border-yellow-400 dark:border-yellow-600;
@apply bg-yellow-100 dark:bg-yellow-900;
}
div.layoutDropdown {
@apply rounded-lg pr-2 pl-4 py-1 cursor-pointer;
@apply flex items-center justify-between gap-8;
@apply text-sm tablet:text-base;
@apply border;
@apply border-zinc-400 hover:border-blue-300 focus:border-blue-400;
@apply bg-zinc-100 hover:bg-zinc-300 focus:bg-zinc-50;
@apply dark:border-zinc-600 dark:hover:border-blue-400 dark:focus:border-blue-500;
@apply dark:bg-zinc-900 dark:hover:bg-zinc-800 dark:focus:bg-zinc-950;
@apply disabled:border-zinc-300 dark:disabled:border-zinc-700;
@apply disabled:bg-zinc-400 dark:disabled:bg-zinc-800;
@apply disabled:text-zinc-500;
@apply transition duration-150;
}
div.layoutOption {
@apply rounded-lg overflow-hidden;
@apply justify-center;
@apply text-sm tablet:text-base;
@apply border;
@apply border-blue-400;
@apply bg-zinc-200;
@apply dark:border-blue-500;
@apply dark:bg-zinc-800;
@apply transition duration-150 ease-in-out;
}
div.layoutItem {
@apply flex items-center justify-start gap-2 pr-2 pl-4 py-1 h-full w-full cursor-pointer;
@apply bg-zinc-100 hover:bg-blue-300/25 focus:bg-zinc-50;
@apply dark:bg-zinc-900 dark:hover:bg-blue-400/25 dark:focus:bg-zinc-950;
}
div.layoutItem[data-active="true"] {
@apply bg-blue-300 cursor-default;
@apply dark:bg-blue-400;
}
div.items {
@apply rounded-xl p-1 duration-75;
@apply bg-zinc-300/50 hover:bg-zinc-300;
@apply dark:bg-zinc-800/50 dark:hover:bg-zinc-800;
@apply text-zinc-900/80 hover:text-zinc-950;
@apply dark:text-zinc-100/80 dark:hover:text-zinc-50;
@apply transition duration-200;
}
div.loading > svg {
@apply fill-zinc-900 dark:fill-zinc-100;
}
div.preview-audio > div.rhap_container {
@apply rounded-lg;
@apply bg-zinc-50 dark:bg-zinc-800;
@apply text-zinc-900 dark:text-zinc-100;
}
div.preview-audio div.rhap_time {
@apply text-zinc-900 dark:text-zinc-100;
}
div.preview-audio button {
@apply bg-transparent hover:bg-transparent active:bg-transparent focus:bg-transparent;
}
}
/* Utility */
@layer utilities {
.react-icons {
}
.fillCard {
@apply min-h-[25vh]
}
.navbar a {
@apply text-inherit font-normal not-italic no-underline;
}
.interactive {
@apply hover:opacity-75 transition duration-150;
}
.center {
@apply top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
}
div.divider-horizontal {
@apply w-full h-px my-2 bg-zinc-400 dark:bg-zinc-500 tablet:my-2;
}
div.divider-vertical {
@apply h-full w-px mx-2 bg-zinc-400 dark:bg-zinc-500 tablet:mx-2;
}
.grid-auto-fit {
grid-template-columns: repeat(auto-fit, minmax(16px, 1fr));
}
.global-duration {
@apply duration-[var(--animation-duration)] ease-[var(--animation-timing)];
}
}
+16
View File
@@ -0,0 +1,16 @@
export interface API_Response<T = unknown> {
success: boolean;
timestamp: string;
responseTime?: number;
data?: T;
}
export interface API_Error {
success: boolean;
timestamp: string;
responseTime?: number;
code: number;
message: string;
category: string;
reason: string;
}
+5
View File
@@ -1,3 +1,8 @@
export interface ExtendedError extends Error {
code?: number;
}
export interface DownloadToken {
password: string;
exp: Date | string | number;
}
+9
View File
@@ -71,7 +71,16 @@ export type TFile = {
"text/plain": string;
};
};
export type TPath = {
name: string;
id: string;
mimeType: string;
};
export interface ValidatePathResponse extends APIResponse {
data: TPath[];
password?: string;
}
export interface FilesResponse extends APIResponse {
passwordRequired?: boolean;
passwordValidated?: boolean;
+44
View File
@@ -0,0 +1,44 @@
import axios, { AxiosError } from "axios";
import apiConfig from "config/api.config";
const fetch = axios.create({
baseURL: apiConfig.basePath,
maxRate: 5,
timeout: 10000,
});
fetch.interceptors.response.use(
(response) => response,
(error: AxiosError<API_ErrorResponse>) => {
if (error.response) {
const payload = new ExtendedError(
error.message,
error.response.data.code,
error.response.data.category,
error.response.data.reason,
);
// Terjadi ketika request berhasil dikirimkan, namun server memberikan response dengan status code di luar range 2xx.
return Promise.reject(JSON.stringify(payload));
} else if (error.request) {
const payload = new ExtendedError(
Constant["noResponse"],
500,
"noResponse",
error.message,
);
// Terjadi ketika request dikirimkan namun tidak menerima response dari server.
return Promise.reject(JSON.stringify(payload));
} else {
const payload = new ExtendedError(
Constant["badRequest"],
400,
"badRequest",
error.message,
);
// Terjadi ketika terjadi kesalahan saat melakukan request.
return Promise.reject(JSON.stringify(payload));
}
},
);
export default fetch;
+26 -11
View File
@@ -2,21 +2,36 @@ import apiConfig from "config/api.config";
import { drive_v3, google } from "googleapis";
import { decrypt } from "utils/encryptionHelper";
const decryptedSecret: string = decrypt(
apiConfig.client_secret,
process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string,
);
const decryptedRefreshToken: string = decrypt(
apiConfig.refresh_token,
process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string,
);
// const decryptedSecret: string = decrypt(
// apiConfig.client_secret,
// process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string,
// );
// const decryptedRefreshToken: string = decrypt(
// apiConfig.refresh_token,
// process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string,
// );
const config = {
client_id:
process.env.NODE_ENV === "development"
? apiConfig.dev_client_id
: apiConfig.client_id,
client_secret:
process.env.NODE_ENV === "development"
? process.env.DEV_DRIVE_CLIENT_SECRET
: process.env.DRIVE_CLIENT_SECRET,
refresh_token:
process.env.NODE_ENV === "development"
? process.env.DEV_DRIVE_REFRESH_TOKEN
: process.env.DRIVE_REFRESH_TOKEN,
};
const oauth2Client = new google.auth.OAuth2(
apiConfig.client_id,
process.env.DRIVE_CLIENT_SECRET as string,
config.client_id,
config.client_secret as string,
);
oauth2Client.setCredentials({
refresh_token: process.env.DRIVE_REFRESH_TOKEN as string,
refresh_token: config.refresh_token as string,
});
let gdriveInstance;
+17 -7
View File
@@ -1,7 +1,11 @@
import { drive_v3 } from "googleapis";
import { urlDecrypt } from "utils/encryptionHelper";
import { shortDecrypt } from "utils/encryptionHelper";
export const hiddenFiles = [".password", ".readme.md", ".banner"];
export const hiddenFiles = [
".password",
".readme.md",
".banner",
];
export function createFileId(
data: drive_v3.Schema$File,
encrypted: boolean = false,
@@ -9,17 +13,23 @@ export function createFileId(
if (process.env.ENCRYPTION_KEY) {
}
if (encrypted) {
return `${encodeURIComponent(data.name as string)}:${urlDecrypt(
data.id as string,
)?.slice(0, 8)}`;
return `${encodeURIComponent(
data.name as string,
)}:${shortDecrypt(data.id as string)?.slice(0, 8)}`;
}
return `${encodeURIComponent(data.name as string)}:${data.id?.slice(0, 8)}`;
return `${encodeURIComponent(
data.name as string,
)}:${data.id?.slice(0, 8)}`;
}
export class ExtendedError extends Error {
code?: number;
constructor(message?: string, code?: number, reason?: string) {
constructor(
message?: string,
code?: number,
reason?: string,
) {
super(message);
this.code = code;
this.cause = reason;
+35 -11
View File
@@ -11,7 +11,9 @@ export function generateRandomEncryptionKey(): Promise<string> {
});
}
export function createEncryptionKey(passphrase: string): Promise<string> {
export function createEncryptionKey(
passphrase: string,
): Promise<string> {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(16); // generate a random salt
const iterations = 100000; // number of PBKDF2 iterations
@@ -34,7 +36,8 @@ export function createEncryptionKey(passphrase: string): Promise<string> {
export function encrypt(
data: string,
encryptionKey: string = process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string,
encryptionKey: string = process.env
.NEXT_PUBLIC_ENCRYPTION_KEY as string,
) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(
@@ -44,14 +47,19 @@ export function encrypt(
);
let encrypted = cipher.update(data);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString("hex") + ":" + encrypted.toString("hex");
return (
iv.toString("hex") + ":" + encrypted.toString("hex")
);
}
export function decrypt(
encryptedData: string,
encryptionKey: string = process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string,
encryptionKey: string = process.env
.NEXT_PUBLIC_ENCRYPTION_KEY as string,
) {
const [ivString, encryptedString] = encryptedData.split(":");
if (!encryptedData) return "";
const [ivString, encryptedString] =
encryptedData.split(":");
const iv = Buffer.from(ivString, "hex");
const encrypted = Buffer.from(encryptedString, "hex");
const decipher = crypto.createDecipheriv(
@@ -65,19 +73,35 @@ export function decrypt(
}
const urlKey =
(process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string).slice(0, 16) || "";
(process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string).slice(
0,
16,
) || "";
const urlIV = Buffer.from(urlKey);
export function urlEncrypt(fileId: string): string {
const cipher = crypto.createCipheriv("aes-128-cbc", urlKey, urlIV);
export function shortEncrypt(fileId: string): string {
const cipher = crypto.createCipheriv(
"aes-128-cbc",
urlKey,
urlIV,
);
let cipherText = cipher.update(fileId, "utf8", "hex");
cipherText += cipher.final("hex");
return cipherText;
}
export function urlDecrypt(cipherText: string): string {
const decipher = crypto.createDecipheriv("aes-128-cbc", urlKey, urlIV);
let plainText = decipher.update(cipherText, "hex", "utf8");
export function shortDecrypt(cipherText: string): string {
if (!cipherText) return "";
const decipher = crypto.createDecipheriv(
"aes-128-cbc",
urlKey,
urlIV,
);
let plainText = decipher.update(
cipherText,
"hex",
"utf8",
);
plainText += decipher.final("utf8");
return plainText;
}
@@ -0,0 +1,26 @@
import crypto from "crypto";
function createEncryptionKey(
password: string,
): Promise<string> {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(16); // generate a random salt
const iterations = 100000; // number of PBKDF2 iterations
const keyLength = 32; // desired key length in bytes
crypto.pbkdf2(
password,
salt,
iterations,
keyLength,
"sha256",
(err, derivedKey) => {
if (err) throw reject(err);
const encryptionKey = derivedKey.toString("hex");
resolve(encryptionKey);
},
);
});
}
export default createEncryptionKey;
@@ -0,0 +1,14 @@
import crypto from "crypto";
function generateRandomEncryptionKey(): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(32, (err, buffer) => {
if (err) {
reject(err);
}
resolve(buffer.toString("hex"));
});
});
}
export default generateRandomEncryptionKey;
@@ -0,0 +1,52 @@
import crypto from "crypto";
function encrypt(
data: string,
key: string = process.env
.NEXT_PUBLIC_ENCRYPTION_KEY as string,
): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(
"aes-256-cbc",
Buffer.from(key, "hex"),
iv,
);
const encrypted = Buffer.concat([
cipher.update(data),
cipher.final(),
]);
return (
iv.toString("hex") + ":" + encrypted.toString("hex")
);
}
function decrypt(
encryptedData: string,
key: string = process.env
.NEXT_PUBLIC_ENCRYPTION_KEY as string,
): string {
if (!encryptedData) return "";
const [ivString, encryptedString] =
encryptedData.split(":");
const iv = Buffer.from(ivString, "hex");
const encrypted = Buffer.from(encryptedString, "hex");
const decipher = crypto.createDecipheriv(
"aes-256-cbc",
Buffer.from(key, "hex"),
iv,
);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
return decrypted.toString();
}
const longEncryption = {
encrypt,
decrypt,
};
export default longEncryption;
@@ -0,0 +1,44 @@
import crypto from "crypto";
const key =
(process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string).slice(
0,
16,
) || "";
const iv = Buffer.from(key);
function encrypt(data: string): string {
const cipher = crypto.createCipheriv(
"aes-128-cbc",
key,
iv,
);
const encrypted = Buffer.concat([
cipher.update(data, "utf-8"),
cipher.final(),
]);
return encrypted.toString("hex");
}
function decrypt(encryptedData: string): string {
if (!encryptedData) return "";
const decipher = crypto.createDecipheriv(
"aes-128-cbc",
key,
iv,
);
const decrypted = Buffer.concat([
decipher.update(encryptedData, "hex"),
decipher.final(),
]);
return decrypted.toString();
}
const shortEncryption = {
encrypt,
decrypt,
};
export default shortEncryption;
+21
View File
@@ -0,0 +1,21 @@
class ExtendedError extends Error {
public extendedMessage?: string;
public code?: number;
public category?: string;
public reason?: string;
constructor(
msg: string,
code: number,
category: string,
reason: string,
) {
super(msg);
this.extendedMessage = msg;
this.code = code;
this.category = category;
this.reason = reason;
}
}
export default ExtendedError;
+23
View File
@@ -0,0 +1,23 @@
/**
* Get search params from url string, and return it as object
* @param url - Request url
* @param key - Key to get from the url
* @returns Object containing the key and value
*/
function getSearchParams(
url: string,
key: string | string[],
): Record<string, string | null> {
const { searchParams } = new URL(url);
if (typeof key === "string") {
return { [key]: searchParams.get(key) };
} else {
const result: Record<string, string | null> = {};
key.forEach((k) => {
result[k] = searchParams.get(k);
});
return result;
}
}
export default getSearchParams;
+12
View File
@@ -10,6 +10,18 @@ module.exports = {
darkMode: "class",
theme: {
extend: {
minHeight: {
dynamic: "100vh",
},
minWidth: {
dynamic: "100vw",
},
height: {
dynamic: "100dvh",
},
width: {
dynamic: "100dvw",
},
fontFamily: {
body: ["var(--font-exo2)", ...defaultTheme.fontFamily.sans],
docs: ["var(--font-source-sans-pro", ...defaultTheme.fontFamily.sans],
+46 -13
View File
@@ -1,7 +1,12 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext", "es2019"],
"lib": [
"dom",
"dom.iterable",
"esnext",
"es2019"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -16,16 +21,44 @@
"incremental": true,
"baseUrl": "./src",
"paths": {
// "@/*": ["./src/*"],
"components/*": ["components/*"],
"config/*": ["config/*"],
"hooks/*": ["hooks/*"],
"styles/*": ["styles/*"],
"utils/*": ["utils/*"],
"types/*": ["types/*"],
"context/*": ["context/*"],
}
// "@/*": ["./src/*"],
"components/*": [
"components/*"
],
"config/*": [
"config/*"
],
"hooks/*": [
"hooks/*"
],
"styles/*": [
"styles/*"
],
"utils/*": [
"utils/*"
],
"types/*": [
"types/*"
],
"context/*": [
"context/*"
]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
"exclude": ["node_modules", ".next"]
}
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
".next"
]
}
+533 -338
View File
File diff suppressed because it is too large Load Diff