@@ -13,9 +13,153 @@ import {
1313import validateRequest from "../middleware/validateRequest.js" ;
1414import { registerSchema , loginSchema } from "../validators/authSchemas.js" ;
1515import { authIpLimiter , otpIpLimiter } from "../middleware/rateLimiters.js" ;
16+ import protect from "../middleware/isAuth.js" ;
17+ import { encrypt , decrypt } from "../utils/crypto.js" ;
18+ import axios from "axios" ;
19+ import jwt from "jsonwebtoken" ;
20+ import User from "../model/userModel.js" ;
21+ import RefreshToken from "../model/RefreshToken.js" ;
22+ import bcrypt from "bcryptjs" ;
1623
1724const authRoutes = express . Router ( ) ;
1825
26+ authRoutes . get ( "/github" , ( req , res ) => {
27+ const { token } = req . cookies ;
28+ if ( ! token ) {
29+ return res . status ( 401 ) . json ( { message : "Unauthorized: Please log in first to connect GitHub" } ) ;
30+ }
31+ try {
32+ jwt . verify ( token , process . env . JWT_SECRET ) ;
33+ const clientId = process . env . GITHUB_CLIENT_ID ;
34+ const redirectUri = process . env . GITHUB_CALLBACK_URL ;
35+ const scope = "repo,read:user" ;
36+ const githubUrl = `https://github.com/login/oauth/authorize?client_id=${ clientId } &redirect_uri=${ encodeURIComponent ( redirectUri ) } &scope=${ scope } &state=${ token } ` ;
37+ return res . redirect ( githubUrl ) ;
38+ } catch ( _err ) {
39+ return res . status ( 401 ) . json ( { message : "Invalid or expired token" } ) ;
40+ }
41+ } ) ;
42+
43+ authRoutes . get ( "/github/callback" , async ( req , res ) => {
44+ const { code, state } = req . query ;
45+ if ( ! code ) {
46+ return res . status ( 400 ) . json ( { message : "Authorization code missing" } ) ;
47+ }
48+
49+ try {
50+ let userId ;
51+ try {
52+ const decoded = jwt . verify ( state , process . env . JWT_SECRET ) ;
53+ userId = decoded . userId ;
54+ } catch ( _err ) {
55+ return res . status ( 401 ) . json ( { message : "Invalid OAuth state / session expired" } ) ;
56+ }
57+
58+ const tokenResponse = await axios . post (
59+ "https://github.com/login/oauth/access_token" ,
60+ {
61+ client_id : process . env . GITHUB_CLIENT_ID ,
62+ client_secret : process . env . GITHUB_CLIENT_SECRET ,
63+ code,
64+ redirect_uri : process . env . GITHUB_CALLBACK_URL ,
65+ } ,
66+ {
67+ headers : {
68+ Accept : "application/json" ,
69+ } ,
70+ }
71+ ) ;
72+
73+ const { access_token } = tokenResponse . data ;
74+ if ( ! access_token ) {
75+ return res . status ( 400 ) . json ( { message : "Failed to retrieve access token from GitHub" } ) ;
76+ }
77+
78+ await axios . get ( "https://api.github.com/user" , {
79+ headers : {
80+ Authorization : `Bearer ${ access_token } ` ,
81+ "User-Agent" : "RIVETO-App" ,
82+ } ,
83+ } ) ;
84+
85+ const encryptedToken = encrypt ( access_token ) ;
86+ const user = await User . findByIdAndUpdate (
87+ userId ,
88+ { githubAccessToken : encryptedToken } ,
89+ { new : true }
90+ ) ;
91+
92+ if ( ! user ) {
93+ return res . status ( 404 ) . json ( { message : "User not found" } ) ;
94+ }
95+
96+ const newAccessToken = jwt . sign ( { userId : user . _id } , process . env . JWT_SECRET , { expiresIn : "15m" } ) ;
97+ const newRefreshToken = jwt . sign ( { userId : user . _id } , process . env . JWT_SECRET , { expiresIn : "7d" } ) ;
98+
99+ await RefreshToken . findOneAndUpdate (
100+ { userId : user . _id } ,
101+ {
102+ tokenHash : await bcrypt . hash ( newRefreshToken , 10 ) ,
103+ expiresAt : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) ,
104+ } ,
105+ { upsert : true }
106+ ) ;
107+
108+ res . cookie ( "token" , newAccessToken , {
109+ httpOnly : true ,
110+ secure : process . env . NODE_ENV === "production" ,
111+ sameSite : process . env . NODE_ENV === "production" ? "none" : "lax" ,
112+ maxAge : 15 * 60 * 1000 ,
113+ } ) ;
114+
115+ res . cookie ( "refreshToken" , newRefreshToken , {
116+ httpOnly : true ,
117+ secure : process . env . NODE_ENV === "production" ,
118+ sameSite : process . env . NODE_ENV === "production" ? "none" : "lax" ,
119+ maxAge : 7 * 24 * 60 * 60 * 1000 ,
120+ } ) ;
121+
122+ const frontendUrl = process . env . FRONTEND_URL || "http://localhost:5173" ;
123+ return res . redirect ( `${ frontendUrl . replace ( / \/ + $ / , "" ) } /recommendations` ) ;
124+ } catch ( error ) {
125+ console . error ( "GitHub OAuth Callback error:" , error ) ;
126+ const frontendUrl = process . env . FRONTEND_URL || "http://localhost:5173" ;
127+ return res . redirect ( `${ frontendUrl . replace ( / \/ + $ / , "" ) } /recommendations?error=oauth_failed` ) ;
128+ }
129+ } ) ;
130+
131+ authRoutes . get ( "/github/profile" , protect , async ( req , res ) => {
132+ try {
133+ const user = await User . findById ( req . userId ) . select ( "+githubAccessToken" ) ;
134+ if ( ! user || ! user . githubAccessToken ) {
135+ return res . json ( { connected : false } ) ;
136+ }
137+
138+ const decryptedToken = decrypt ( user . githubAccessToken ) ;
139+ if ( ! decryptedToken ) {
140+ return res . json ( { connected : false } ) ;
141+ }
142+
143+ const profileResponse = await axios . get ( "https://api.github.com/user" , {
144+ headers : {
145+ Authorization : `Bearer ${ decryptedToken } ` ,
146+ "User-Agent" : "RIVETO-App" ,
147+ } ,
148+ } ) ;
149+
150+ const { avatar_url, login, public_repos } = profileResponse . data ;
151+ return res . json ( {
152+ connected : true ,
153+ avatar_url,
154+ login,
155+ public_repos,
156+ } ) ;
157+ } catch ( error ) {
158+ console . error ( "Failed to fetch GitHub profile:" , error . message ) ;
159+ return res . status ( 500 ) . json ( { error : "Failed to fetch GitHub profile" } ) ;
160+ }
161+ } ) ;
162+
19163authRoutes . post ( "/send-otp" , otpIpLimiter , validateRequest ( registerSchema ) , sendOTP ) ;
20164authRoutes . post ( "/verify-otp" , otpIpLimiter , verifyOTP ) ;
21165
@@ -42,17 +186,24 @@ authRoutes.post("/verify-otp", otpIpLimiter, verifyOTP);
42186 * responses:
43187 * 200:
44188 * description: Login successful
189+ * headers:
190+ * Set-Cookie:
191+ * description: Contains httpOnly 'token' and 'refreshToken' cookies
192+ * schema:
193+ * type: string
45194 * content:
46195 * application/json:
47196 * schema:
48197 * type: object
49198 * properties:
50- * token:
199+ * _id:
200+ * type: string
201+ * name:
51202 * type: string
52- * example: "eyJhbGciOiJIUzI1Ni..."
53- * refreshToken:
203+ * email:
204+ * type: string
205+ * authProvider:
54206 * type: string
55- * example: "eyJhbGciOiJIUzI1Ni..."
56207 */
57208authRoutes . post ( "/login" , authIpLimiter , validateRequest ( loginSchema ) , login ) ;
58209
@@ -66,7 +217,7 @@ authRoutes.post("/login", authIpLimiter, validateRequest(loginSchema), login);
66217 * 200:
67218 * description: Successfully logged out
68219 */
69- authRoutes . post ( "/logout" , logOut ) ;
220+ authRoutes . post ( "/logout" , protect , logOut ) ;
70221
71222/**
72223 * @swagger
@@ -77,17 +228,24 @@ authRoutes.post("/logout", logOut);
77228 * responses:
78229 * 200:
79230 * description: Login successful
231+ * headers:
232+ * Set-Cookie:
233+ * description: Contains httpOnly 'token' and 'refreshToken' cookies
234+ * schema:
235+ * type: string
80236 * content:
81237 * application/json:
82238 * schema:
83239 * type: object
84240 * properties:
85- * token:
241+ * _id:
242+ * type: string
243+ * name:
86244 * type: string
87- * example: "eyJhbGciOiJIUzI1Ni..."
88- * refreshToken:
245+ * email:
246+ * type: string
247+ * authProvider:
89248 * type: string
90- * example: "eyJhbGciOiJIUzI1Ni..."
91249 */
92250authRoutes . post ( "/googlelogin" , authIpLimiter , googleLogin ) ;
93251
@@ -100,17 +258,19 @@ authRoutes.post("/googlelogin", authIpLimiter, googleLogin);
100258 * responses:
101259 * 200:
102260 * description: Admin login successful
261+ * headers:
262+ * Set-Cookie:
263+ * description: Contains httpOnly 'adminToken' and 'adminRefreshToken' cookies
264+ * schema:
265+ * type: string
103266 * content:
104267 * application/json:
105268 * schema:
106269 * type: object
107270 * properties:
108- * token:
109- * type: string
110- * example: "eyJhbGciOiJIUzI1Ni..."
111- * refreshToken:
271+ * message:
112272 * type: string
113- * example: "eyJhbGciOiJIUzI1Ni... "
273+ * example: "Admin logged in successfully "
114274 */
115275authRoutes . post ( "/adminlogin" , authIpLimiter , adminLogin ) ;
116276
0 commit comments