diff --git a/app/api/google/drive/callback/route.ts b/app/api/google/drive/callback/route.ts new file mode 100644 index 00000000..96c3d090 --- /dev/null +++ b/app/api/google/drive/callback/route.ts @@ -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 }); + } +} diff --git a/package.json b/package.json index 329a0042..01efce9e 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35d2d5b5..f049ce17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1075896b..a8169ceb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,13 @@ onlyBuiltDependencies: + - '@prisma/client' + - '@prisma/engines' + - argon2 + - bcrypt + - cpu-features + - esbuild + - prisma + - protobufjs + - sharp + - ssh2 + - unrs-resolver - zenstack diff --git a/proxy.ts b/proxy.ts index ac68b7eb..89cf607b 100644 --- a/proxy.ts +++ b/proxy.ts @@ -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)); } diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema.ts b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema.ts index b88d9c74..f9359c40 100644 --- a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema.ts +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema.ts @@ -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"}), }); diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx index e5a0f212..f553d3ea 100644 --- a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx @@ -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 { diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive.form.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive.form.tsx new file mode 100644 index 00000000..bd2f568f --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive.form.tsx @@ -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 +// } +// +// +// export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => { +// return ( +// <> +// +// ( +// +// Client Email +// +// +// +// +// +// )} +// /> +// ( +// +// Private Key +// +// +// +// +// +// )} +// /> +// ( +// +// Folder Id +// +// +// +// +// +// )} +// /> +// +// +// ) +// } + +// "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 +// }; +// +// 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 ( +// <> +// +// +// {/* Client ID */} +// ( +// +// Client ID +// +// +// +// +// +// )} +// /> +// +// {/* Client Secret */} +// ( +// +// Client Secret +// +// +// +// +// +// )} +// /> +// +// {/* Redirect URI */} +// ( +// +// Redirect URI +// +// +// +// +// +// )} +// /> +// +// {/* Refresh Token */} +// ( +// +// Refresh Token +// +// +// +// +// +// )} +// /> +// +// {/* Folder ID */} +// ( +// +// Folder ID +// +// +// +// +// +// )} +// /> +// +// ( +// +// OAuth2 Connect +// +// +// +// +// Clicking this will open Google’s consent screen. After granting access, copy the authorization code and exchange it for a refresh token in your backend. +// +// +// )} +// /> +// +// ); +// }; +// +"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; +}; + +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 ( + <> + + + {/* Client ID */} + ( + + Client ID + + + + + + )} + /> + + {/* Client Secret */} + ( + + Client Secret + + + + + + )} + /> + + {/* Redirect URI */} + ( + + Redirect URI + + + + + + )} + /> + + {/* Refresh Token */} + + {/* (*/} + {/* */} + {/* Refresh Token*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* )}*/} + {/*/>*/} + + {/* Folder ID */} + ( + + Folder ID + + + + + + )} + /> + + ( + + Refresh Token + + + + + The refresh token will automatically appear here after connecting. + + + )} + /> + + + + ); +}; diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers.ts b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers.ts new file mode 100644 index 00000000..a9daac80 --- /dev/null +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers.ts @@ -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, + }; +}); diff --git a/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx b/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx index 47bd0285..0f1094f5 100644 --- a/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx +++ b/src/components/wrappers/dashboard/admin/channels/helpers/common.tsx @@ -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 ; case "s3": return + case "google-drive": + return case "local": return <> default: diff --git a/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx b/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx index 00753c9e..ad13ec9a 100644 --- a/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx +++ b/src/components/wrappers/dashboard/admin/channels/helpers/storage.tsx @@ -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) { ); } +export function GoogleDriveIcon(props: SVGProps) { + return ( + + ) +} + + export function BlobIcon(props: SVGProps) { return ( - // - // - // diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 8dc95b73..36017a8f 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/src/db/schema/12_storage-channel.ts b/src/db/schema/12_storage-channel.ts index eb51320f..c41229f8 100644 --- a/src/db/schema/12_storage-channel.ts +++ b/src/db/schema/12_storage-channel.ts @@ -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(), diff --git a/src/features/storages/providers/google-drive/helpers.ts b/src/features/storages/providers/google-drive/helpers.ts new file mode 100644 index 00000000..0a3468c2 --- /dev/null +++ b/src/features/storages/providers/google-drive/helpers.ts @@ -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 { +// 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 { +// 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 { + 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 { + 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; +} \ No newline at end of file diff --git a/src/features/storages/providers/google-drive/index.ts b/src/features/storages/providers/google-drive/index.ts new file mode 100644 index 00000000..9a3c4a3a --- /dev/null +++ b/src/features/storages/providers/google-drive/index.ts @@ -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 { +// 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 { +// 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 { +// 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 { +// 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 { + 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 { + 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 { + 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 { + 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}; + } +} \ No newline at end of file diff --git a/src/features/storages/providers/google-drive/types.ts b/src/features/storages/providers/google-drive/types.ts new file mode 100644 index 00000000..33136be7 --- /dev/null +++ b/src/features/storages/providers/google-drive/types.ts @@ -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 +}; \ No newline at end of file diff --git a/src/features/storages/providers/index.ts b/src/features/storages/providers/index.ts index bb4acdff..5aeffeec 100644 --- a/src/features/storages/providers/index.ts +++ b/src/features/storages/providers/index.ts @@ -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; @@ -27,6 +33,12 @@ const handlers: Record = { delete: deleteS3, ping: pingS3, }, + "google-drive": { + upload: uploadGoogleDrive, + get: getGoogleDrive, + delete: deleteGoogleDrive, + ping: pingGoogleDrive, + } // gcs: null as any, // azure: null as any, }; diff --git a/src/features/storages/types.ts b/src/features/storages/types.ts index 9adf827e..df05e968 100644 --- a/src/features/storages/types.ts +++ b/src/features/storages/types.ts @@ -1,6 +1,7 @@ export type StorageProviderKind = | 'local' | 's3' + | 'google-drive' ; export type StorageAction =