From ff8a4863baefd86025376575baf74e7dd9830ba6 Mon Sep 17 00:00:00 2001 From: adjoly Date: Fri, 18 Jul 2025 17:28:47 +0200 Subject: [PATCH] =?UTF-8?q?=E3=80=8C=E2=9C=A8=E3=80=8D=20feat:=20added=20g?= =?UTF-8?q?oogle=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 190 ++++++++++++++++++++++++++++++ src/api/auth/default.js | 16 ++- src/api/auth/gLogCallback.js | 55 +++++++++ src/api/auth/gRedir.js | 23 ++++ src/api/auth/gRegisterCallback.js | 64 ++++++++++ src/api/auth/login.js | 5 +- src/api/auth/register.js | 6 +- src/utils/authDB.js | 35 ++++-- src/utils/authUtils.js | 13 -- 10 files changed, 381 insertions(+), 27 deletions(-) create mode 100644 src/api/auth/gLogCallback.js create mode 100644 src/api/auth/gRedir.js create mode 100644 src/api/auth/gRegisterCallback.js diff --git a/package.json b/package.json index 242fcb8..63b55d8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@fastify/cookie": "^11.0.2", "@fastify/env": "^5.0.2", "@fastify/jwt": "^9.1.0", + "axios": "^1.10.0", "bcrypt": "^6.0.0", "better-sqlite3": "^12.2.0", "fastify": "^5.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99e43ba..656f368 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@fastify/jwt': specifier: ^9.1.0 version: 9.1.0 + axios: + specifier: ^1.10.0 + version: 1.10.0 bcrypt: specifier: ^6.0.0 version: 6.0.0 @@ -480,6 +483,9 @@ packages: asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -487,6 +493,9 @@ packages: avvio@9.1.0: resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -516,6 +525,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -544,6 +557,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commist@3.2.0: resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==} @@ -575,6 +592,10 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -591,6 +612,10 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -604,6 +629,22 @@ packages: env-schema@6.0.1: resolution: {integrity: sha512-WRD40Q25pP4NUbI3g3CNU5PPzcaiX7YYcPwiCZlfR4qGsKmTlckRixgHww0/fOXiXSNKA87pwshzq0ULTK/48A==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.6: resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} engines: {node: '>=18'} @@ -691,6 +732,19 @@ packages: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -703,6 +757,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.1.1: resolution: {integrity: sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==} engines: {node: '>=18'} @@ -715,6 +772,14 @@ packages: resolution: {integrity: sha512-b4cVhbPfbgbCZtK0dcUc1lASitXGEAIqukV5DDAyWm25fomWnV+C+a1yXvqikcRZXHN2j0pSDyj3cTfzq8pC7Q==} hasBin: true + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -726,6 +791,10 @@ packages: resolution: {integrity: sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==} engines: {node: '>=14'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -737,6 +806,18 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -866,6 +947,18 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -993,6 +1086,9 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -1516,6 +1612,8 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} avvio@9.1.0: @@ -1523,6 +1621,14 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.19.1 + axios@1.10.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + base64-js@1.5.1: {} bcrypt@6.0.0: @@ -1556,6 +1662,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -1579,6 +1690,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commist@3.2.0: {} cookie@1.0.2: {} @@ -1597,6 +1712,8 @@ snapshots: deep-extend@0.6.0: {} + delayed-stream@1.0.0: {} + dequal@2.0.3: {} detect-libc@2.0.4: {} @@ -1605,6 +1722,12 @@ snapshots: dotenv@16.6.1: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -1624,6 +1747,21 @@ snapshots: dotenv: 16.6.1 dotenv-expand: 10.0.0 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.25.6: optionalDependencies: '@esbuild/aix-ppc64': 0.25.6 @@ -1767,6 +1905,16 @@ snapshots: dependencies: locate-path: 3.0.0 + follow-redirects@1.15.9: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -1776,6 +1924,8 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gaxios@7.1.1: dependencies: extend: 3.0.2 @@ -1799,6 +1949,24 @@ snapshots: split2: 3.2.2 walker: 1.0.8 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + github-from-package@0.0.0: {} google-auth-library@10.1.0: @@ -1815,6 +1983,8 @@ snapshots: google-logging-utils@1.1.1: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} gtoken@8.0.0: @@ -1826,6 +1996,16 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + help-me@5.0.0: {} https-proxy-agent@7.0.6: @@ -1936,6 +2116,14 @@ snapshots: dependencies: tmpl: 1.0.5 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-response@3.1.0: {} minimalistic-assert@1.0.1: {} @@ -2067,6 +2255,8 @@ snapshots: process-warning@5.0.0: {} + proxy-from-env@1.1.0: {} + pump@3.0.3: dependencies: end-of-stream: 1.4.5 diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 042245d..c6f8256 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -3,7 +3,10 @@ import fastifyCookie from '@fastify/cookie'; import { register } from './register.js'; import { login } from './login.js'; +import { gRedir } from './gRedir.js'; import authDB from '../../utils/authDB.js' +import { gLogCallback } from './gLogCallback.js'; +import { gRegisterCallback } from './gRegisterCallback.js'; const saltRounds = 10; @@ -36,7 +39,18 @@ export default async function(fastify, options) { }); // GOOGLE sign in - + fastify.get('/login/google', async (request, reply) => { + return gRedir(request, reply, fastify, '/login/google/callback'); + }); + fastify.get('/register/google', async (request, reply) => { + return gRedir(request, reply, fastify, '/register/google/callback'); + }); + fastify.get('/login/google/callback', async (request, reply) => { + return gLogCallback(request, reply, fastify); + }) + fastify.get('/register/google/callback', async (request, reply) => { + return gRegisterCallback(request, reply, fastify); + }) fastify.post('/login', { schema: { diff --git a/src/api/auth/gLogCallback.js b/src/api/auth/gLogCallback.js new file mode 100644 index 0000000..975b7d6 --- /dev/null +++ b/src/api/auth/gLogCallback.js @@ -0,0 +1,55 @@ +import axios from 'axios' +import authDB from '../../utils/authDB.js'; + +var env = process.env.NODE_ENV || 'development'; + +/** + * @param {import("fastify").FastifyRequest} request + * @param {import("fastify").FastifyReply} reply + * @param {import("fastify").FastifyInstance} fastify + * + * @returns {import('fastify').FastifyReply} + */ +export async function gLogCallback(request, reply, fastify) { + const { code } = request.query; + + try { + const response = await axios.post('https://oauth2.googleapis.com/token', { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_CALLBACK_URL + '/login/google/callback', + grant_type: 'authorization_code', + }); + + const { access_token } = response.data; + + const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const userProfile = userInfoResponse.data; + const user = { + username: userProfile.email, // Assuming email is used as the username + }; + + if (!authDB.checkUser(user.username) || authDB.RESERVED_USERNAMES.includes(user.username)) { + return reply.code(400).send({ error: "User does not exist" }); + } + + const token = fastify.jwt.sign(user); + + return reply + .setCookie('token', token, { + httpOnly: true, + path: '/', + secure: env !== 'development', + sameSite: 'lax', + }) + .code(200) + .send({ msg: "Login successful" }); + } catch (error) { + fastify.log.error(error); + reply.code(500).send({ error: 'Internal server error' }); + } +} diff --git a/src/api/auth/gRedir.js b/src/api/auth/gRedir.js new file mode 100644 index 0000000..ca4b33b --- /dev/null +++ b/src/api/auth/gRedir.js @@ -0,0 +1,23 @@ +/** + * @param {import("fastify").FastifyRequest} request + * @param {import("fastify").FastifyReply} reply + * @param {import("fastify").FastifyInstance} fastify + * @param {string} callbackRoute + * + * @returns {import('fastify').FastifyReply} + */ +export async function gRedir(request, reply, fastify, callbackRoute) { + try { + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` + + `client_id=${process.env.GOOGLE_CLIENT_ID}&` + + `redirect_uri=${encodeURIComponent(process.env.GOOGLE_CALLBACK_URL + callbackRoute)}&` + + `response_type=code&` + + `scope=email profile&` + + `access_type=offline`; + + return reply.redirect(authUrl); + } catch (error) { + fastify.log.error(error); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/auth/gRegisterCallback.js b/src/api/auth/gRegisterCallback.js new file mode 100644 index 0000000..f79542f --- /dev/null +++ b/src/api/auth/gRegisterCallback.js @@ -0,0 +1,64 @@ +import axios from 'axios' + +import authDB from '../../utils/authDB.js'; + +var env = process.env.NODE_ENV || 'development'; + +/** + * @param {import("fastify").FastifyRequest} request + * @param {import("fastify").FastifyReply} reply + * @param {import("fastify").FastifyInstance} fastify + * + * @returns {import('fastify').FastifyReply} + */ +export async function gRegisterCallback(request, reply, fastify) { + const { code } = request.query; + + try { + const response = await axios.post('https://oauth2.googleapis.com/token', { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_CALLBACK_URL + '/register/google/callback', + grant_type: 'authorization_code', + }, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + + const { access_token } = response.data; + + const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + const userProfile = userInfoResponse.data; + const user = { + username: userProfile.email, // Assuming email is used as the username + }; + + if (authDB.RESERVED_USERNAMES.includes(user)) { + return reply.code(400).send({ error: 'Reserved username' }); + } + if (authDB.checkUser(user.username) === true) { + return reply.code(400).send({ error: "User already exist" }); + } + + authDB.addUser(user.username, ''); + + const token = fastify.jwt.sign(user); + + return reply + .setCookie('token', token, { + httpOnly: true, + path: '/', + secure: env !== 'development', + sameSite: 'lax', + }) + .code(200) + .send({ msg: "Register successful" }); + } catch (error) { + fastify.log.error(error); + reply.code(500).send({ error: 'Internal server error' }); + } +} diff --git a/src/api/auth/login.js b/src/api/auth/login.js index 133dfee..9bb8380 100644 --- a/src/api/auth/login.js +++ b/src/api/auth/login.js @@ -1,6 +1,5 @@ import bcrypt from 'bcrypt'; -import { checkUser } from '../../utils/authUtils.js'; import authDB from '../../utils/authDB.js'; var env = process.env.NODE_ENV || 'development'; @@ -18,11 +17,11 @@ export async function login(request, reply, fastify) { /** @type {{ user: string, password: string }} */ const { user, password } = request.body; - if (!checkUser(user) || user === 'admin') { + if (!authDB.checkUser(user) || authDB.RESERVED_USERNAMES.includes(user)) { return reply.code(400).send({ error: "User does not exist" }); } - const query = authDB.passwordQuery.get(user); + const query = authDB.passwordQuery(user); const hash = query?.passwordHash; if (!hash) { diff --git a/src/api/auth/register.js b/src/api/auth/register.js index a75013a..986c8cd 100644 --- a/src/api/auth/register.js +++ b/src/api/auth/register.js @@ -1,6 +1,6 @@ import bcrypt from 'bcrypt'; -import { isValidString, checkUser } from '../../utils/authUtils.js'; +import { isValidString } from '../../utils/authUtils.js'; import authDB from '../../utils/authDB.js'; var env = process.env.NODE_ENV || 'development'; @@ -25,7 +25,7 @@ export async function register(request, reply, saltRounds, fastify) { if (!isValidString(user) || !isValidString(password)) { return reply.code(400).send({ error: 'Invalid username or password' }); - } else if (checkUser(user) === true) { + } else if (authDB.checkUser(user) === true) { return reply.code(400).send({ error: "User already exist" }); } else if (password.length <= 8) { return reply.code(400).send({ error: "Password too short" }); @@ -34,7 +34,7 @@ export async function register(request, reply, saltRounds, fastify) { } const hash = await bcrypt.hash(password, saltRounds); - authDB.userAdd.run(user, hash); + authDB.addUser(user, hash); const token = fastify.jwt.sign({ user }); diff --git a/src/utils/authDB.js b/src/utils/authDB.js index 40d84f5..140be8b 100644 --- a/src/utils/authDB.js +++ b/src/utils/authDB.js @@ -3,7 +3,6 @@ import Database from 'better-sqlite3'; var env = process.env.NODE_ENV || 'development'; let database; const RESERVED_USERNAMES = ['admin']; -let userCheck, passwordQuery, userAdd; if (!env || env === 'development') { database = new Database(":memory:", { verbose: console.log }); @@ -22,17 +21,39 @@ function prepareDB() { passwordHash TEXT ) STRICT `); - userCheck = database.prepare('SELECT EXISTS (SELECT 1 FROM credentials WHERE username = ?);'); - passwordQuery = database.prepare('SELECT passwordHash FROM credentials WHERE username = ?;'); - userAdd = database.prepare('INSERT INTO credentials (username, passwordHash) VALUES (?, ?)'); } +/** + * @param {string} name + * + * @returns {boolean} + */ +function checkUser(name) { + /** + * @type: {import('better-sqlite3').Statement} + */ + let userCheck = database.prepare('SELECT EXISTS (SELECT 1 FROM credentials WHERE username = ?);'); + const result = userCheck.get(name); + const key = Object.keys(result)[0]; + + return result[key] === 1; +} + +function addUser(name, pass) { + let userAdd = database.prepare('INSERT INTO credentials (username, passwordHash) VALUES (?, ?)'); + userAdd.run(name, pass); +} + +function passwordQuery(user) { + let passwordQuery = database.prepare('SELECT passwordHash FROM credentials WHERE username = ?;'); + return passwordQuery.get(user) +} const authDB = { prepareDB, - get userCheck() { return userCheck; }, - get userAdd() { return userAdd; }, - get passwordQuery() { return passwordQuery; }, + checkUser, + addUser, + passwordQuery, RESERVED_USERNAMES }; diff --git a/src/utils/authUtils.js b/src/utils/authUtils.js index d5062a1..775e824 100644 --- a/src/utils/authUtils.js +++ b/src/utils/authUtils.js @@ -8,16 +8,3 @@ import authDB from './authDB.js'; export function isValidString(value) { return typeof value === 'string' && value.trim() !== ''; } - -/** - * @param {string} name - * @param {import('better-sqlite3').Statement} userCheck - * - * @returns {boolean} - */ -export function checkUser(name, userCheck) { - const result = authDB.userCheck.get(name); - const key = Object.keys(result)[0]; - - return result[key] === 1; -}