mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
Start migration, and re-organizing files.
This commit is contained in:
Generated
+1
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+20
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
@@ -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)];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
export interface ExtendedError extends Error {
|
||||
code?: number;
|
||||
}
|
||||
|
||||
export interface DownloadToken {
|
||||
password: string;
|
||||
exp: Date | string | number;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user