feat: start working on google drive storage

This commit is contained in:
charlesgauthereau
2026-01-24 00:26:05 +01:00
parent 797b287899
commit 657586ddff
18 changed files with 1038 additions and 7 deletions
+31
View File
@@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { google } from "googleapis";
export async function GET(request: Request) {
try {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const clientId = url.searchParams.get("clientId");
const clientSecret = url.searchParams.get("clientSecret");
const redirectUri = url.searchParams.get("redirectUri");
if (!code || !clientId || !clientSecret || !redirectUri) {
return NextResponse.json({ error: "Missing parameters" }, { status: 400 });
}
try {
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
const { tokens } = await oauth2Client.getToken(code);
// Save tokens.refresh_token securely in your DB here
return NextResponse.json(tokens);
} catch (err: any) {
console.error("Error exchanging code:", err);
return NextResponse.json({ error: "Failed to exchange code" }, { status: 500 });
}
} catch (error) {
console.error("Error in GET handler:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
+2 -1
View File
@@ -62,6 +62,7 @@
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"embla-carousel-react": "^8.6.0",
"googleapis": "^170.1.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.553.0",
"minio": "^8.0.5",
@@ -122,5 +123,5 @@
"typescript": "^5.8.3",
"zenstack": "2.14.2"
},
"packageManager": "pnpm@10.27.0"
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
}
+259
View File
@@ -152,6 +152,9 @@ importers:
embla-carousel-react:
specifier: ^8.6.0
version: 8.6.0(react@19.2.3)
googleapis:
specifier: ^170.1.0
version: 170.1.0
input-otp:
specifier: ^1.4.2
version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -1735,6 +1738,10 @@ packages:
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@prisma/client@6.7.0':
resolution: {integrity: sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==}
engines: {node: '>=18.18'}
@@ -3430,6 +3437,10 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
@@ -3630,6 +3641,9 @@ packages:
zod:
optional: true
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -3668,6 +3682,9 @@ packages:
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
engines: {node: '>=8.0.0'}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -3924,6 +3941,10 @@ packages:
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -4161,6 +4182,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
effect@3.18.4:
resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==}
@@ -4429,6 +4453,9 @@ packages:
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
fast-check@3.23.2:
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
engines: {node: '>=8.0.0'}
@@ -4483,6 +4510,10 @@ packages:
picomatch:
optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -4522,6 +4553,10 @@ packages:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -4575,6 +4610,14 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
gaxios@7.1.3:
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
engines: {node: '>=18'}
gcp-metadata@8.1.2:
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
engines: {node: '>=18'}
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
@@ -4625,6 +4668,10 @@ packages:
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
hasBin: true
glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22}
@@ -4646,6 +4693,22 @@ packages:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'}
google-auth-library@10.5.0:
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
engines: {node: '>=18'}
google-logging-utils@1.1.3:
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
engines: {node: '>=14'}
googleapis-common@8.0.1:
resolution: {integrity: sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==}
engines: {node: '>=18.0.0'}
googleapis@170.1.0:
resolution: {integrity: sha512-RLbc7yG6qzZqvAmGcgjvNIoZ7wpcCFxtc+HN+46etxDrlO4a8l5Cb7NxNQGhV91oRmL7mt56VoRoypAtEQEIKg==}
engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -4653,6 +4716,10 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
gtoken@8.0.0:
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
engines: {node: '>=18'}
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -4707,6 +4774,10 @@ packages:
resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -4900,6 +4971,9 @@ packages:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
@@ -4935,6 +5009,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -4966,6 +5043,12 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
jwa@2.0.1:
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -5124,6 +5207,9 @@ packages:
lowlight@1.17.0:
resolution: {integrity: sha512-vmtBgYKD+QVNy7tIa7ulz5d//Il9R4MooOVh4nkOf9R9Cb/Dk5TXMSTieg/vDulkBkIWj59/BIlyFQxT9X1oAQ==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.4:
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
engines: {node: 20 || >=22}
@@ -5364,6 +5450,11 @@ packages:
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
engines: {node: '>=6.0.0'}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
@@ -5376,6 +5467,10 @@ packages:
encoding:
optional: true
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-forge@1.3.3:
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
engines: {node: '>= 6.13.0'}
@@ -5530,6 +5625,10 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-scurry@2.0.1:
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
engines: {node: 20 || >=22}
@@ -5728,6 +5827,10 @@ packages:
qr.js@0.0.0:
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
qs@6.14.1:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
query-string@7.1.3:
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
engines: {node: '>=6'}
@@ -5928,6 +6031,10 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rimraf@5.0.10:
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
hasBin: true
rou3@0.7.12:
resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
@@ -6440,6 +6547,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-template@2.0.8:
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
@@ -6557,6 +6667,10 @@ packages:
web-encoding@1.1.5:
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -8031,6 +8145,9 @@ snapshots:
'@phc/format@1.0.0': {}
'@pkgjs/parseargs@0.11.0':
optional: true
'@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3)':
optionalDependencies:
prisma: 6.7.0(typescript@5.9.3)
@@ -10433,6 +10550,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
agent-base@7.1.4: {}
ajv-formats@2.1.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
@@ -10640,6 +10759,8 @@ snapshots:
optionalDependencies:
zod: 4.3.5
bignumber.js@9.3.1: {}
binary-extensions@2.3.0: {}
bl@4.1.0:
@@ -10681,6 +10802,8 @@ snapshots:
buffer-crc32@1.0.0: {}
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {}
buffer@5.7.1:
@@ -10960,6 +11083,8 @@ snapshots:
damerau-levenshtein@1.0.8: {}
data-uri-to-buffer@4.0.1: {}
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -11119,6 +11244,10 @@ snapshots:
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
effect@3.18.4:
dependencies:
'@standard-schema/spec': 1.1.0
@@ -11650,6 +11779,8 @@ snapshots:
exsolve@1.0.8: {}
extend@3.0.2: {}
fast-check@3.23.2:
dependencies:
pure-rand: 6.1.0
@@ -11702,6 +11833,11 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -11739,6 +11875,10 @@ snapshots:
format@0.2.2: {}
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
fraction.js@4.3.7: {}
framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
@@ -11783,6 +11923,23 @@ snapshots:
functions-have-names@1.2.3: {}
gaxios@7.1.3:
dependencies:
extend: 3.0.2
https-proxy-agent: 7.0.6
node-fetch: 3.3.2
rimraf: 5.0.10
transitivePeerDependencies:
- supports-color
gcp-metadata@8.1.2:
dependencies:
gaxios: 7.1.3
google-logging-utils: 1.1.3
json-bigint: 1.0.0
transitivePeerDependencies:
- supports-color
generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {}
@@ -11840,6 +11997,15 @@ snapshots:
glob-to-regexp@0.4.1: {}
glob@10.5.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
glob@11.1.0:
dependencies:
foreground-child: 3.3.1
@@ -11860,10 +12026,48 @@ snapshots:
define-properties: 1.2.1
gopd: 1.2.0
google-auth-library@10.5.0:
dependencies:
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
gaxios: 7.1.3
gcp-metadata: 8.1.2
google-logging-utils: 1.1.3
gtoken: 8.0.0
jws: 4.0.1
transitivePeerDependencies:
- supports-color
google-logging-utils@1.1.3: {}
googleapis-common@8.0.1:
dependencies:
extend: 3.0.2
gaxios: 7.1.3
google-auth-library: 10.5.0
qs: 6.14.1
url-template: 2.0.8
transitivePeerDependencies:
- supports-color
googleapis@170.1.0:
dependencies:
google-auth-library: 10.5.0
googleapis-common: 8.0.1
transitivePeerDependencies:
- supports-color
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
gtoken@8.0.0:
dependencies:
gaxios: 7.1.3
jws: 4.0.1
transitivePeerDependencies:
- supports-color
has-bigints@1.1.0: {}
has-flag@4.0.0: {}
@@ -11923,6 +12127,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
ieee754@1.2.1: {}
ignore@5.3.2: {}
@@ -12103,6 +12314,12 @@ snapshots:
has-symbols: 1.1.0
set-function-name: 2.0.2
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
@@ -12129,6 +12346,10 @@ snapshots:
jsesc@3.1.0: {}
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.1
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1: {}
@@ -12158,6 +12379,17 @@ snapshots:
object.assign: 4.1.7
object.values: 1.2.1
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@4.0.1:
dependencies:
jwa: 2.0.1
safe-buffer: 5.2.1
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -12292,6 +12524,8 @@ snapshots:
fault: 1.0.4
highlight.js: 10.4.1
lru-cache@10.4.3: {}
lru-cache@11.2.4: {}
lru-cache@5.1.1:
@@ -12494,12 +12728,20 @@ snapshots:
node-cron@4.2.1: {}
node-domexception@1.0.0: {}
node-fetch-native@1.6.7: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-forge@1.3.3: {}
node-gyp-build@4.8.4: {}
@@ -12680,6 +12922,11 @@ snapshots:
path-parse@1.0.7: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
path-scurry@2.0.1:
dependencies:
lru-cache: 11.2.4
@@ -12867,6 +13114,10 @@ snapshots:
qr.js@0.0.0: {}
qs@6.14.1:
dependencies:
side-channel: 1.1.0
query-string@7.1.3:
dependencies:
decode-uri-component: 0.2.2
@@ -13131,6 +13382,10 @@ snapshots:
reusify@1.1.0: {}
rimraf@5.0.10:
dependencies:
glob: 10.5.0
rou3@0.7.12: {}
run-parallel@1.2.0:
@@ -13840,6 +14095,8 @@ snapshots:
dependencies:
punycode: 2.3.1
url-template@2.0.8: {}
use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0):
dependencies:
react: 19.0.0
@@ -13977,6 +14234,8 @@ snapshots:
optionalDependencies:
'@zxing/text-encoding': 0.9.0
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
webpack-sources@3.3.3: {}
+11
View File
@@ -1,2 +1,13 @@
onlyBuiltDependencies:
- '@prisma/client'
- '@prisma/engines'
- argon2
- bcrypt
- cpu-features
- esbuild
- prisma
- protobufjs
- sharp
- ssh2
- unrs-resolver
- zenstack
+1
View File
@@ -59,6 +59,7 @@ function checkRouteExists(pathname: string) {
/^\/api\/events\/?$/,
/^\/api\/init\/?$/,
/^\/api\/config\/?$/,
/^\/api\/google\/drive\/callback\/?$/,
];
return routePatterns.some((pattern) => pattern.test(pathname));
}
@@ -19,7 +19,7 @@ export const NotificationChannelFormSchema = ChannelFormSchema.extend({
});
export const StorageChannelFormSchema = ChannelFormSchema.extend({
provider: z.enum(["local", "s3"], {required_error: "Provider is required"}),
provider: z.enum(["local", "s3", "google-drive"], {required_error: "Provider is required"}),
});
@@ -46,6 +46,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
if (result.success) {
toast.success("Successfully connected to storage channel");
} else {
console.error(result);
toast.error("An error occurred while testing the storage channel, check your configuration");
}
} else {
@@ -0,0 +1,370 @@
// import {UseFormReturn} from "react-hook-form";
// import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
// import {Input} from "@/components/ui/input";
// import {Separator} from "@/components/ui/separator";
//
//
// type StorageGoogleDriveFormProps = {
// form: UseFormReturn<any, any, any>
// }
//
//
// export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
// return (
// <>
// <Separator className="my-1"/>
// <FormField
// control={form.control}
// name="config.clientEmail"
// render={({field}) => (
// <FormItem>
// <FormLabel>Client Email</FormLabel>
// <FormControl>
// <Input {...field} placeholder="xxx@xxx.iam.gserviceaccount.com"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="config.privateKey"
// render={({field}) => (
// <FormItem>
// <FormLabel>Private Key</FormLabel>
// <FormControl>
// <Input {...field} placeholder="xxxxxxxxxxxx"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="config.folderId"
// render={({field}) => (
// <FormItem>
// <FormLabel>Folder Id</FormLabel>
// <FormControl>
// <Input {...field} placeholder="xxxxxxxxxxxx"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
//
// </>
// )
// }
// "use client";
//
// import {UseFormReturn} from "react-hook-form";
// import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
// import {Input} from "@/components/ui/input";
// import {Separator} from "@/components/ui/separator";
// import {Button} from "@/components/ui/button";
// import {getServerUrl} from "@/utils/get-server-url";
//
// type StorageGoogleDriveFormProps = {
// form: UseFormReturn<any, any, any>
// };
//
// export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
// const handleConnect = async () => {
// const clientId = form.getValues("config.clientId");
// // const baseUrl = getServerUrl();
// // const redirectUri = form.getValues("config.redirectUri") || "http://localhost:3000/oauth2callback";
// const redirectUri = "http://localhost:8887/dashboard/storages/channels";
//
// if (!clientId) {
// alert("Please fill in Client ID first");
// return;
// }
//
// // Construct Google OAuth URL
// const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file");
// const url = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&access_type=offline&prompt=consent`;
//
// // Open consent screen in a new tab
// window.open(url, "_blank");
// };
//
// return (
// <>
// <Separator className="my-1"/>
//
// {/* Client ID */}
// <FormField
// control={form.control}
// name="config.clientId"
// render={({field}) => (
// <FormItem>
// <FormLabel>Client ID</FormLabel>
// <FormControl>
// <Input {...field} placeholder="XXXX.apps.googleusercontent.com"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
//
// {/* Client Secret */}
// <FormField
// control={form.control}
// name="config.clientSecret"
// render={({field}) => (
// <FormItem>
// <FormLabel>Client Secret</FormLabel>
// <FormControl>
// <Input {...field} placeholder="XXXXXXXXXXXX"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
//
// {/* Redirect URI */}
// <FormField
// control={form.control}
// name="config.redirectUri"
// render={({field}) => (
// <FormItem>
// <FormLabel>Redirect URI</FormLabel>
// <FormControl>
// <Input {...field} placeholder="http://localhost:3000/oauth2callback"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
//
// {/* Refresh Token */}
// <FormField
// control={form.control}
// name="config.refreshToken"
// render={({field}) => (
// <FormItem>
// <FormLabel>Refresh Token</FormLabel>
// <FormControl>
// <Input {...field} placeholder="Paste refresh token here"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
//
// {/* Folder ID */}
// <FormField
// control={form.control}
// name="config.folderId"
// render={({field}) => (
// <FormItem>
// <FormLabel>Folder ID</FormLabel>
// <FormControl>
// <Input {...field} placeholder="XXXXXXXXXXXXXXXXXXXX"/>
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
//
// <FormField
// control={form.control}
// name="oauth.connect"
// render={() => (
// <FormItem>
// <FormLabel>OAuth2 Connect</FormLabel>
// <FormControl>
// <Button type="button" onClick={handleConnect}>
// Connect Google Drive
// </Button>
// </FormControl>
// <FormMessage>
// Clicking this will open Googles consent screen. After granting access, copy the authorization code and exchange it for a refresh token in your backend.
// </FormMessage>
// </FormItem>
// )}
// />
// </>
// );
// };
//
"use client";
import {UseFormReturn} from "react-hook-form";
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator";
import {Button} from "@/components/ui/button";
import {
googleDriveRefreshTokenAction
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers";
type StorageGoogleDriveFormProps = {
form: UseFormReturn<any, any, any>;
};
export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
const handleConnect = () => {
const clientId = form.getValues("config.clientId");
const redirectUri = form.getValues("config.redirectUri") || `${window.location.origin}/api/google/drive/callback`;
if (!clientId) {
alert("Please fill in Client ID first");
return;
}
const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file");
const oauthUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(
redirectUri
)}&response_type=code&scope=${scope}&access_type=offline&prompt=consent`;
// Open OAuth in a new tab
const oauthWindow = window.open(oauthUrl, "_blank", "width=500,height=600");
// Poll the window for the refresh token (after the redirect)
const interval = setInterval(async () => {
try {
if (!oauthWindow || oauthWindow.closed) {
clearInterval(interval);
return;
}
// Check if redirected to your server callback
if (oauthWindow.location.href.startsWith(`${window.location.origin}/api/google/drive/callback`)) {
const params = new URL(oauthWindow.location.href).searchParams;
const code = params.get("code");
console.log(code)
if (code) {
const clientId = form.getValues("config.clientId");
const clientSecret = form.getValues("config.clientSecret");
const redirectUri =
form.getValues("config.redirectUri") ||
`${window.location.origin}/api/google/drive/callback`;
// Exchange code for refresh token
const data = await googleDriveRefreshTokenAction({
code: code,
clientId: clientId,
clientSecret: clientSecret,
redirectUri: redirectUri,
})
if (data.data.refreshToken) {
console.log(data.data.refreshToken)
form.setValue("config.refreshToken", data.data.refreshToken); // auto-update the field
}
oauthWindow.close();
clearInterval(interval);
}
}
} catch (err) {
// ignore cross-origin errors until redirect happens
}
}, 500);
};
return (
<>
<Separator className="my-1"/>
{/* Client ID */}
<FormField
control={form.control}
name="config.clientId"
render={({field}) => (
<FormItem>
<FormLabel>Client ID</FormLabel>
<FormControl>
<Input {...field} placeholder="XXXX.apps.googleusercontent.com"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
{/* Client Secret */}
<FormField
control={form.control}
name="config.clientSecret"
render={({field}) => (
<FormItem>
<FormLabel>Client Secret</FormLabel>
<FormControl>
<Input {...field} placeholder="XXXXXXXXXXXX"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
{/* Redirect URI */}
<FormField
control={form.control}
name="config.redirectUri"
render={({field}) => (
<FormItem>
<FormLabel>Redirect URI</FormLabel>
<FormControl>
<Input {...field} placeholder="http://localhost:3000/api/google-drive/callback"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
{/* Refresh Token */}
{/*<FormField*/}
{/* control={form.control}*/}
{/* name="config.refreshToken"*/}
{/* render={({field}) => (*/}
{/* <FormItem>*/}
{/* <FormLabel>Refresh Token</FormLabel>*/}
{/* <FormControl>*/}
{/* <Input {...field} placeholder="Will be filled after OAuth flow"/>*/}
{/* </FormControl>*/}
{/* <FormMessage/>*/}
{/* </FormItem>*/}
{/* )}*/}
{/*/>*/}
{/* Folder ID */}
<FormField
control={form.control}
name="config.folderId"
render={({field}) => (
<FormItem>
<FormLabel>Folder ID</FormLabel>
<FormControl>
<Input {...field} placeholder="XXXXXXXXXXXXXXXXXXXX"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.refreshToken"
render={({ field }) => (
<FormItem>
<FormLabel>Refresh Token</FormLabel>
<FormControl>
<Input {...field} placeholder="Will be filled after OAuth flow" readOnly />
</FormControl>
<FormMessage>
The refresh token will automatically appear here after connecting.
</FormMessage>
</FormItem>
)}
/>
<Button type="button" onClick={handleConnect}> Connect Google Drive </Button>
</>
);
};
@@ -0,0 +1,39 @@
"use server"
import {userAction} from "@/lib/safe-actions/actions";
import {string, z} from "zod";
import {google} from "googleapis";
export const googleDriveRefreshTokenAction = userAction.schema(
z.object({
code: z.string(),
clientId: z.string(),
clientSecret: z.string(),
redirectUri: z.string(),
})).action(async ({parsedInput}) => {
// google.auth.OAuth2
// const oauth2Client = new google.auth.OAuth2(parsedInput.clientId,parsedInput.clientSecret, parsedInput.redirectUri);
// const { tokens } = await oauth2Client.getToken(parsedInput.code);
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: parsedInput.clientId,
client_secret: parsedInput.clientSecret,
code: parsedInput.code,
grant_type: "authorization_code",
redirect_uri: parsedInput.redirectUri,
}),
});
console.log(tokenRes);
const tokens = await tokenRes.json();
console.log(tokens.refresh_token);
return {
refreshToken: tokens.refresh_token,
};
});
@@ -29,6 +29,9 @@ import {LucideProps} from "lucide-react";
import {
StorageS3Form
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form";
import {
StorageGoogleDriveForm
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive.form";
export type ChannelKind = "notification" | "storage";
@@ -86,6 +89,8 @@ export const renderChannelForm = (provider: string | undefined, form: UseFormRet
return <NotifierWebhookForm form={form}/>;
case "s3":
return <StorageS3Form form={form}/>
case "google-drive":
return <StorageGoogleDriveForm form={form}/>
case "local":
return <></>
default:
@@ -5,6 +5,7 @@ import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/
export const storageProviders: ProviderIconTypes[] = [
{value: "local", label: "Local", icon: Server},
{value: "s3", label: "S3", icon: S3Icon},
{value: "google-drive", label: "Google Drive", icon: GoogleDriveIcon},
{value: "blob", label: "Azure Blob Storage", icon: BlobIcon, preview: true},
{value: "gcs", label: "Google Cloud Storage", icon: GCSIcon, preview: true},
]
@@ -18,12 +19,15 @@ export function S3Icon(props: SVGProps<SVGSVGElement>) {
);
}
export function GoogleDriveIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} {...props} viewBox="0 0 256 229"><path fill="#0066da" d="m19.354 196.034l11.29 19.5c2.346 4.106 5.718 7.332 9.677 9.678q17.009-21.591 23.68-33.137q6.77-11.717 16.641-36.655q-26.604-3.502-40.32-3.502q-13.165 0-40.322 3.502c0 4.545 1.173 9.09 3.519 13.196z"/><path fill="#ea4335" d="M215.681 225.212c3.96-2.346 7.332-5.572 9.677-9.677l4.692-8.064l22.434-38.855a26.57 26.57 0 0 0 3.518-13.196q-27.315-3.502-40.247-3.502q-13.899 0-40.248 3.502q9.754 25.075 16.422 36.655q6.724 11.683 23.752 33.137"/><path fill="#00832d" d="M128.001 73.311q19.68-23.768 27.125-36.655q5.996-10.377 13.196-33.137C164.363 1.173 159.818 0 155.126 0h-54.25C96.184 0 91.64 1.32 87.68 3.519q9.16 26.103 15.544 37.154q7.056 12.213 24.777 32.638"/><path fill="#2684fc" d="M175.36 155.42H80.642l-40.32 69.792c3.958 2.346 8.503 3.519 13.195 3.519h148.968c4.692 0 9.238-1.32 13.196-3.52z"/><path fill="#00ac47" d="M128.001 73.311L87.681 3.52c-3.96 2.346-7.332 5.571-9.678 9.677L3.519 142.224A26.57 26.57 0 0 0 0 155.42h80.642z"/><path fill="#ffba00" d="m215.242 77.71l-37.243-64.514c-2.345-4.106-5.718-7.331-9.677-9.677l-40.32 69.792l47.358 82.109h80.496c0-4.546-1.173-9.09-3.519-13.196z"/></svg>
)
}
export function BlobIcon(props: SVGProps<SVGSVGElement>) {
return (
// <svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} {...props} viewBox="0 0 24 24">
// <path fill="currentColor"
// d="M13.207.006a2.16 2.16 0 0 0-1.62.582a2.15 2.15 0 0 0-.095 3.035l3.408 3.55a3.042 3.042 0 0 1-.663 4.688l-.463.239V7.285a15.42 15.42 0 0 0-8.018 10.487v.017l6.549-3.328v7.621L13.779 24V13.682l.897-.463a4.443 4.443 0 0 0 1.22-7.03l-3.37-3.525a.75.75 0 0 1 .037-1.055a.75.75 0 0 1 1.056.038l.467.486l-.006.006l4.07 4.244a.057.057 0 0 0 .082 0a.06.06 0 0 0 0-.07l-3.14-5.143l-.149.143l.149-.145C14.494.393 13.829.054 13.207.006m-.902 9.865v2.994l-4.152 2.149a14 14 0 0 1 2.767-3.928a14 14 0 0 1 1.385-1.215"/>
// </svg>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width={256} height={199} {...props} viewBox="0 0 48 48">
<path fill="#2979ff"
d="M32.66,7H15.34c-0.715,0-1.375,0.381-1.732,1l-8.66,15c-0.357,0.619-0.357,1.381,0,2l8.66,15c0.357,0.619,1.018,1,1.732,1H32.66c0.715,0,1.375-0.381,1.732-1l8.66-15c0.357-0.619,0.357-1.381,0-2l-8.66-15C34.035,7.381,33.375,7,32.66,7z"></path>
+7
View File
@@ -225,6 +225,13 @@
"when": 1768931603846,
"tag": "0031_chemical_edwin_jarvis",
"breakpoints": true
},
{
"idx": 32,
"version": "7",
"when": 1769203863305,
"tag": "0032_sparkling_thunderbolt_ross",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -7,7 +7,7 @@ import {z} from "zod";
import {database} from "@/db/schema/07_database";
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3']);
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive']);
export const storageChannel = pgTable('storage_channel', {
id: uuid("id").defaultRandom().primaryKey(),
@@ -0,0 +1,73 @@
// import {drive_v3, google} from "googleapis";
// import Drive = drive_v3.Drive;
// import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
//
//
// export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<Drive> {
// const auth = new google.auth.JWT({
// email: config.clientEmail,
// key: config.privateKey?.replace(/\\n/g, "\n"),
// scopes: ["https://www.googleapis.com/auth/drive"],
// });
//
// return google.drive({
// version: "v3",
// auth,
// });
// }
//
//
// export async function findFileByName(
// drive: any,
// name: string,
// folderId: string
// ): Promise<string | null> {
// const res = await drive.files.list({
// q: `name='${name}' and '${folderId}' in parents and trashed=false`,
// fields: "files(id)",
// pageSize: 1,
// });
//
// return res.data.files?.[0]?.id ?? null;
// }
import {drive_v3, google} from "googleapis";
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
import Drive = drive_v3.Drive;
import {getServerUrl} from "@/utils/get-server-url";
export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<Drive> {
const baseUrl = getServerUrl();
const oauth2Client = new google.auth.OAuth2(
config.clientId,
config.clientSecret,
// config.redirectUri
baseUrl
);
oauth2Client.setCredentials({
refresh_token: config.refreshToken
});
return google.drive({
version: "v3",
auth: oauth2Client
});
}
export async function findFileByName(
drive: drive_v3.Drive,
name: string,
folderId: string
): Promise<string | null> {
const res = await drive.files.list({
q: `name='${name}' and '${folderId}' in parents and trashed=false`,
fields: "files(id)",
pageSize: 1,
supportsAllDrives: true,
includeItemsFromAllDrives: true,
});
return res.data.files?.[0]?.id ?? null;
}
@@ -0,0 +1,201 @@
"use server"
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from '../../types';
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
import {findFileByName, getGoogleDriveClient} from "@/features/storages/providers/google-drive/helpers";
import {Readable} from "node:stream";
// export async function uploadGoogleDrive(
// config: GoogleDriveConfig,
// input: { data: StorageUploadInput }
// ): Promise<StorageResult> {
// const client = await getGoogleDriveClient(config);
//
// const name = input.data.path;
//
// const existing = await findFileByName(client, name, config.folderId);
// if (existing) {
// return {
// success: false,
// provider: "google-drive",
// error: "File already exists"
// };
// }
//
// await client.files.create({
// requestBody: {
// name,
// parents: [config.folderId],
// },
// media: {
// body: input.data.file as Buffer,
// },
// });
//
//
// return {
// success: true,
// provider: 'google-drive',
// };
// }
//
// export async function getGoogleDrive(
// config: GoogleDriveConfig,
// input: { data: StorageGetInput }
// ): Promise<StorageResult> {
// const client = await getGoogleDriveClient(config);
// const name = input.data.path;
//
// const fileId = await findFileByName(client, name, config.folderId);
// if (!fileId) {
// return {success: false, provider: "google-drive", error: "File not found"};
// }
//
// const res = await client.files.get(
// {fileId, alt: "media"},
// {responseType: "arraybuffer"}
// );
//
// return {
// success: true,
// provider: "google-drive",
// file: Buffer.from(res.data as ArrayBuffer),
// };
// }
//
//
// export async function deleteGoogleDrive(
// config: GoogleDriveConfig,
// input: { data: StorageDeleteInput }
// ): Promise<StorageResult> {
// const client = await getGoogleDriveClient(config);
// const name = input.data.path;
//
// const fileId = await findFileByName(client, name, config.folderId);
// if (!fileId) {
// return {success: false, provider: "google-drive", error: "File not found"};
// }
//
// await client.files.delete({fileId});
//
// return {
// success: true,
// provider: "google-drive"
// };
// }
//
//
// export async function pingGoogleDrive(config: GoogleDriveConfig): Promise<StorageResult> {
// try {
// const drive = await getGoogleDriveClient(config);
// const name = `ping-${Date.now()}.txt`;
//
// const buffer = Buffer.from("ping");
//
// const file = await drive.files.create({
// requestBody: {
// name,
// parents: [config.folderId],
// },
// media: {
// mimeType: "text/plain",
// body: Readable.from(buffer),
// },
// fields: "id",
// });
// console.log(file)
//
// await drive.files.get({fileId: file.data.id!});
// await drive.files.delete({fileId: file.data.id!});
//
// return {
// success: true,
// provider: "google-drive",
// response: "Google Drive storage OK",
// };
// } catch (err: any) {
// return {
// success: false,
// provider: "google-drive",
// response: err.message,
// };
// }
// }
export async function uploadGoogleDrive(
config: GoogleDriveConfig,
input: { data: StorageUploadInput }
): Promise<StorageResult> {
const client = await getGoogleDriveClient(config);
const name = input.data.path;
const existing = await findFileByName(client, name, config.folderId);
if (existing) return {success: false, provider: "google-drive", error: "File already exists"};
await client.files.create({
requestBody: {name, parents: [config.folderId]},
media: {body: Readable.from(input.data.file as Buffer)},
fields: "id",
supportsAllDrives: true,
});
return {success: true, provider: 'google-drive'};
}
export async function getGoogleDrive(
config: GoogleDriveConfig,
input: { data: StorageGetInput }
): Promise<StorageResult> {
const client = await getGoogleDriveClient(config);
const name = input.data.path;
const fileId = await findFileByName(client, name, config.folderId);
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
const res = await client.files.get(
{fileId, alt: "media", supportsAllDrives: true},
{responseType: "arraybuffer"}
);
return {
success: true,
provider: "google-drive",
file: Buffer.from(res.data as ArrayBuffer),
};
}
export async function deleteGoogleDrive(
config: GoogleDriveConfig,
input: { data: StorageDeleteInput }
): Promise<StorageResult> {
const client = await getGoogleDriveClient(config);
const name = input.data.path;
const fileId = await findFileByName(client, name, config.folderId);
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
await client.files.delete({fileId, supportsAllDrives: true});
return {success: true, provider: "google-drive"};
}
export async function pingGoogleDrive(config: GoogleDriveConfig): Promise<StorageResult> {
try {
const drive = await getGoogleDriveClient(config);
const name = `ping-${Date.now()}.txt`;
const buffer = Buffer.from("ping");
const file = await drive.files.create({
requestBody: {name, parents: [config.folderId]},
media: {mimeType: "text/plain", body: Readable.from(buffer)},
fields: "id",
supportsAllDrives: true,
});
await drive.files.get({fileId: file.data.id!, supportsAllDrives: true});
// await drive.files.delete({fileId: file.data.id!, supportsAllDrives: true});
return {success: true, provider: "google-drive", response: "Google Drive storage OK"};
} catch (err: any) {
return {success: false, provider: "google-drive", response: err.message};
}
}
@@ -0,0 +1,15 @@
// export type GoogleDriveConfig = {
// clientEmail: string;
// privateKey: string;
// folderId: string;
// };
export type GoogleDriveConfig = {
clientId: string;
clientSecret: string;
refreshToken: string; // from OAuth flow
// redirectUri: string; // e.g., http://localhost:3000/oauth2callback
folderId: string; // target folder in your Drive
};
+12
View File
@@ -6,6 +6,12 @@ import type {
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
import {deleteS3, getS3, pingS3, uploadS3} from "@/features/storages/providers/s3";
import {
deleteGoogleDrive,
getGoogleDrive,
pingGoogleDrive,
uploadGoogleDrive
} from "@/features/storages/providers/google-drive";
type ProviderHandler = {
upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>;
@@ -27,6 +33,12 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
delete: deleteS3,
ping: pingS3,
},
"google-drive": {
upload: uploadGoogleDrive,
get: getGoogleDrive,
delete: deleteGoogleDrive,
ping: pingGoogleDrive,
}
// gcs: null as any,
// azure: null as any,
};
+1
View File
@@ -1,6 +1,7 @@
export type StorageProviderKind =
| 'local'
| 's3'
| 'google-drive'
;
export type StorageAction =