diff --git a/@server/@api-auth/passport/strategies/github.strategy.ts b/@server/@api-auth/passport/strategies/github.strategy.ts index e7157fd..163ac30 100644 --- a/@server/@api-auth/passport/strategies/github.strategy.ts +++ b/@server/@api-auth/passport/strategies/github.strategy.ts @@ -4,6 +4,8 @@ import { UserModel as User } from '@server/@api-user/user.model'; import { DoneCallback } from 'passport'; import { badRequestErr } from '@lib/errors/Errors'; import { success } from '@lib/helpers'; +import { TokenModel as Token, TokenIssuer, TokenType } from '@server/api-token/token.model'; +import { revokeGithubAccessToken } from '@api-external/github.service'; import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; @@ -16,7 +18,7 @@ export const githubStrategy = new Strategy( clientID: process.env.GITHUB_CLIENT_ID as string, clientSecret: process.env.GITHUB_CLIENT_SECRET as string, callbackURL: `${process.env.BACKEND_URL as string}/auth/github/callback`, - scope: ['user'] + scope: ['user', 'repo'] }, async (accessToken: string, refreshToken: string, profile: Profile, done: DoneCallback)=>{ try { @@ -33,9 +35,25 @@ export const githubStrategy = new Strategy( if (user) { if (!user.email_verified) { user.email_verified = true; - user = await user.save() } - success(`${user_email} just logged in`); + const token = await Token.findOne({user:user, issuer: TokenIssuer.Github, type: TokenType.Access}).exec(); + if (token){ + revokeGithubAccessToken(token.token); + token.token = accessToken; + await token.save(); + } + else { + const createToken = new Token({ + token: accessToken, + type: TokenType.Access, + issuer: TokenIssuer.Github, + user: user, + }); + const new_token = await createToken.save(); + user.tokens.push(new_token); + } + user = await user.save(); + success(`${user_email} just logged in via GitHub`); } else { const createUser = new User({ @@ -43,8 +61,20 @@ export const githubStrategy = new Strategy( email_verified: true, password: 12345, // default password (it can be changed later) }); - user = await createUser.save(); - success(`${user_email} just signed up`); + const new_user = await createUser.save(); + + const createToken = new Token({ + token: accessToken, + type: TokenType.Access, + issuer: TokenIssuer.Github, + user: new_user, + }); + const token = await createToken.save(); + new_user.tokens.push(token); + + user = await new_user.save(); + + success(`${user_email} just signed up via GitHub`); } return done(null, user); diff --git a/@server/@api-external/github.controller.ts b/@server/@api-external/github.controller.ts index 8faa675..226b29e 100644 --- a/@server/@api-external/github.controller.ts +++ b/@server/@api-external/github.controller.ts @@ -1,6 +1,7 @@ -import { Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; import { success } from '@lib/helpers'; -import { createIssueService, getIssuesService, getPullRequestsService, getRepositoriesService,getIssueTemplatesService, getIssueTemplatesContentService } from '@api-external/github.service'; +import { createIssueService, getIssuesService, getPullRequestsService, getRepositoriesService,getIssueTemplatesService, getIssueTemplatesContentService, revokeGithubAccessTokenService } from '@api-external/github.service'; +import { ReqUser } from '@ts-types/index'; let response: { [key: string]: unknown } = {}; const message = { @@ -24,18 +25,22 @@ export const getIssuesController = async (req: Request, res: Response) => { return res.status(200).json(response); } -export const createIssueController = async (req: Request, res: Response) => { - const docs = await createIssueService(req); - response = { - success: true, - message: message.success.issues.submitted, - data: { - url: docs.html_url, - number: docs.number, - }, +export const createIssueController = async (req: ReqUser, res: Response, next: NextFunction) => { + try { + const docs = await createIssueService(req); + response = { + success: true, + message: message.success.issues.submitted, + data: { + url: docs.html_url, + number: docs.number, + }, + } + success(message.success.issues.submitted); + return res.status(201).json(response); + } catch (err) { + next(err); } - success(message.success.issues.submitted); - return res.status(201).json(response); } export const getPullRequestsController = async (req: Request, res: Response) => { @@ -84,4 +89,21 @@ export const getIssueTemplatesContentController = async (req: Request, res: Resp }; success(message.success.get); return res.status(200).json(response); -}; \ No newline at end of file +}; + +export const revokeGithubAccessTokenController = async (req: ReqUser, res: Response, next: NextFunction) => { + try { + const docs = await revokeGithubAccessTokenService(req.user._id); + response = { + success: true, + message: "github access token successfully revoked", + data: { + docs + }, + } + success("github access token successfully revoked"); + return res.status(201).json(response); + } catch (err) { + next(err); + } +} \ No newline at end of file diff --git a/@server/@api-external/github.route.ts b/@server/@api-external/github.route.ts index 70ec4b1..a403c0f 100644 --- a/@server/@api-external/github.route.ts +++ b/@server/@api-external/github.route.ts @@ -1,15 +1,26 @@ import express, { IRouter } from 'express'; -import { createIssueController, getIssuesController, getPullRequestsController, getRepositoriesController,getIssueTemplatesController, getIssueTemplatesContentController } from '@api-external/github.controller'; +import { createIssueController, getIssuesController, getPullRequestsController, getRepositoriesController,getIssueTemplatesController, getIssueTemplatesContentController, revokeGithubAccessTokenController } from '@api-external/github.controller'; +import { authenticateUserWithJWT, authorizeByUserRoles } from '@auth/middlewares/auth.middleware'; +import { UserRole } from '@user/user.model'; const router: IRouter = express.Router(); router.get('/issues', getIssuesController); //------------------------------------------- -router.post('/issues', createIssueController); +router.post('/issues', + authenticateUserWithJWT, + authorizeByUserRoles([UserRole.Admin, UserRole.User]), + createIssueController); +router.post('/issues-unauthenticated', createIssueController); //------------------------------------------- router.get('/issue-templates', getIssueTemplatesController); router.get('/pull-requests', getPullRequestsController); router.get('/repositories', getRepositoriesController); router.get('/templates/issues', getIssueTemplatesContentController); +//------------------------------------------- +router.delete('/revoke-token', + authenticateUserWithJWT, + authorizeByUserRoles([UserRole.Admin, UserRole.User]), + revokeGithubAccessTokenController); export { router }; diff --git a/@server/@api-external/github.service.ts b/@server/@api-external/github.service.ts index 5b62a05..e9d77ba 100644 --- a/@server/@api-external/github.service.ts +++ b/@server/@api-external/github.service.ts @@ -1,5 +1,7 @@ -import { Request } from 'express'; -import { unAuthorizedErr } from '@lib/errors/Errors'; +import { badRequestErr, notFoundErr, unAuthorizedErr } from '@lib/errors/Errors'; +import { UserModel as User } from '@server/@api-user/user.model'; +import { TokenModel as Token, TokenIssuer, TokenType } from '@server/api-token/token.model'; +import { ReqUser } from '@ts-types/index'; export const getIssuesService = async () => { const response = await fetch(`${process.env.REPO_API_URL}/issues`, { @@ -14,13 +16,16 @@ export const getIssuesService = async () => { return data; } -export const createIssueService = async (req: Request) => { +export const createIssueService = async (req: ReqUser) => { + const user = await User.findById(req?.user?._id).exec(); + const token = await Token.findOne({user:user, issuer: TokenIssuer.Github, type: TokenType.Access}).exec(); + const { title, body } = req.body; const response = await fetch(`${process.env.REPO_API_URL}/issues`, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`, + Authorization: token ? `Bearer ${token.token}` : `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`, }, body: JSON.stringify({ title: `[GitHubSync] ${title}`, @@ -107,4 +112,42 @@ export const getIssueTemplatesContentService = async () => { ); return data; -}; \ No newline at end of file +}; + +export const revokeGithubAccessToken = async (github_access_token: string) => { + + const response = await fetch(`https://api.github.com/applications/${process.env.GITHUB_CLIENT_ID as string}/token`, { + method: 'DELETE', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': 'Basic ' + Buffer.from(`${process.env.GITHUB_CLIENT_ID as string}:${process.env.GITHUB_CLIENT_SECRET as string}`).toString('base64'), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + access_token: github_access_token, + }), + }); + + if (response.status === 401) { + unAuthorizedErr("Unauthorized: Can't access this resource"); + } + if (!response.ok) { + badRequestErr("Failed to Revoke Token") + } +} + +export const revokeGithubAccessTokenService = async (user_id: string) => { + const user = await User.findById(user_id).exec(); + if(!user){ + notFoundErr('User not found'); + } + const token = await Token.findOne({user:user, issuer: TokenIssuer.Github, type: TokenType.Access}).exec(); + + if (!token){ + badRequestErr("user has no github access token to revoke"); + } + + revokeGithubAccessToken(token.token); + + await Token.deleteOne({user:user, issuer: TokenIssuer.Github, type: TokenType.Access}); +} \ No newline at end of file diff --git a/@server/@api-user/user.model.ts b/@server/@api-user/user.model.ts index ebfaf72..1eab7d0 100644 --- a/@server/@api-user/user.model.ts +++ b/@server/@api-user/user.model.ts @@ -1,6 +1,7 @@ import mongoose from 'mongoose'; import bcrypt from "bcrypt"; import { CollabocateInstanceDocument } from '@collabocate/instance.model'; +import { TokenDocument } from '@server/api-token/token.model'; export enum UserRole { Admin = 'admin', @@ -16,6 +17,7 @@ export interface UserDocument extends mongoose.Document { createdAt?: Date; updatedAt?: Date; instance: CollabocateInstanceDocument[]; + tokens: TokenDocument[]; } const collectionName = 'user'; @@ -32,6 +34,9 @@ const UserSchema = new mongoose.Schema({ role: { type: String, required: true, default: UserRole.User }, instance: [ { type: mongoose.Schema.Types.ObjectId, ref:'collabocate-instance' } + ], + tokens: [ + { type: mongoose.Schema.Types.ObjectId, ref:'token' } ] }, { diff --git a/@server/@api-user/user.service.ts b/@server/@api-user/user.service.ts index 2272390..fac263b 100644 --- a/@server/@api-user/user.service.ts +++ b/@server/@api-user/user.service.ts @@ -1,6 +1,7 @@ import { badRequestErr, notFoundErr } from '@lib/errors/Errors'; import { UserDocument, UserModel as User, UserRole } from '@server/@api-user/user.model'; import { CollabocateInstanceModel as CollabocateInstance } from '@collabocate/instance.model'; +import { TokenModel as Token } from '@server/api-token/token.model'; export const getAllUsersService = async () => { @@ -22,6 +23,7 @@ export const deleteOneUserService = async (paramsId: string) => { notFoundErr('Operation not allowed'); } await CollabocateInstance.deleteMany({ user: user }).exec(); + await Token.deleteMany({ user: user }).exec(); const query = await User.deleteOne({ _id: paramsId }).exec(); return query; } @@ -74,6 +76,7 @@ export const createAdminUserService = async () => { export const deleteAllUserService = async () => { const adminUser = await User.findOne({ role: UserRole.Admin }).exec(); await CollabocateInstance.deleteMany({ user: {$ne: adminUser} }).exec(); + await Token.deleteMany({ user: {$ne: adminUser} }).exec(); const query = await User.deleteMany({role:{$ne: UserRole.Admin}}).exec(); return query; } diff --git a/@server/api-token/token.controller.ts b/@server/api-token/token.controller.ts new file mode 100644 index 0000000..506c376 --- /dev/null +++ b/@server/api-token/token.controller.ts @@ -0,0 +1,103 @@ +import { NextFunction, Response } from 'express'; +import { + createTokenService, + getAllTokenService, + getOneTokenService, + getTokenService, +} from '@token/token.service'; +import { success } from '@lib/helpers'; +import { ReqUser } from '@ts-types/index'; +// import { error } from '@lib/helpers'; + +// const routeName = 'token'; +// const item = `${routeName}`; + +let response: { [key: string]: unknown } = {}; + + +export const createTokenController = async (req: ReqUser, res: Response, next: NextFunction) => { + try { + const token = await createTokenService(req.user._id, req.body); + response = { + success: true, + message: `SUCCESS: All items succesfully retrieved`, + data: { + _id: token._id, + token: token.token, + issuer: token.issuer, + user_id: token.user._id + } + }; + success(`SUCCESS: All tokens succesfully retrieved`); + return res.status(200).json(response); + } catch (err) { + next(err); + } +} + +export const getAllTokenController = async (req: ReqUser, res: Response, next: NextFunction) => { + try { + const tokens = await getAllTokenService(); + response = { + success: true, + message: `SUCCESS: All tokens succesfully retrieved`, + count: tokens.length, + data: tokens.map((token)=>{ + return { + _id: token._id, + token: token.token, + issuer: token.issuer, + user_id: token.user._id + } + }) + }; + success(`SUCCESS: All tokens succesfully retrieved`); + return res.status(200).json(response); + } catch (err) { + next(err); + } +} + +export const getTokenController = async (req: ReqUser, res: Response, next: NextFunction) => { + try { + const tokens = await getTokenService(req.user._id); + response = { + success: true, + message: `SUCCESS: All tokens succesfully retrieved`, + count: tokens.length, + data: tokens.map((token)=>{ + return { + _id: token._id, + token: token.token, + issuer: token.issuer, + user_id: token.user._id + } + }) + }; + success(`SUCCESS: All tokens succesfully retrieved for user: ${req.user._id}`); + return res.status(200).json(response); + } catch (err) { + next(err); + } +} + +export const getOneTokenController = async (req: ReqUser, res: Response, next: NextFunction) => { + try { + const token = await getOneTokenService(req.user._id, req.params.id); + response = { + success: true, + message: `SUCCESS: token succesfully retrieved`, + data: { + _id: token._id, + token: token.token, + issuer: token.issuer, + user_id: token.user._id + } + }; + success(`SUCCESS: token succesfully retrieved`); + return res.status(200).json(response); + + } catch (err) { + next(err); + } +} \ No newline at end of file diff --git a/@server/api-token/token.model.ts b/@server/api-token/token.model.ts new file mode 100644 index 0000000..47f16ae --- /dev/null +++ b/@server/api-token/token.model.ts @@ -0,0 +1,38 @@ +import { UserDocument } from '@user/user.model'; +import mongoose from 'mongoose'; + +export enum TokenType { + Access = 'access', + Refresh = 'refresh', +} + +export enum TokenIssuer { + Server = 'server', + Github = 'github', + Google = 'google', +} + +export interface TokenDocument extends mongoose.Document { + _id?: string; + token: string; + type: string; + issuer: string; + createdAt?: Date; + user: UserDocument; +} + +const collectionName = 'token'; + +const TokenSchema = new mongoose.Schema({ + token: { type: String, required: true }, + type: { type: String, required: true }, + issuer: { type: String, required: true }, + user: { type: mongoose.Schema.Types.ObjectId, ref:"user", required: true } +}, +{ + timestamps: true, +}); + +const TokenModel = mongoose.model(collectionName, TokenSchema, collectionName); //declare collection name a second time to prevent mongoose from pluralizing or adding 's' to the collection name + +export { TokenModel }; \ No newline at end of file diff --git a/@server/api-token/token.route.ts b/@server/api-token/token.route.ts new file mode 100644 index 0000000..ad2a0df --- /dev/null +++ b/@server/api-token/token.route.ts @@ -0,0 +1,17 @@ +import express, { IRouter } from 'express'; +import { + createTokenController, + getAllTokenController, + getOneTokenController, + getTokenController, +} from '@server/api-token/token.controller'; +import { authenticateUserWithJWT, authorizeByUserRoles } from '@auth/middlewares/auth.middleware'; +import { UserRole } from '@user/user.model'; + +const router: IRouter = express.Router(); + +router.get('/all', authenticateUserWithJWT, authorizeByUserRoles([UserRole.Admin]), getAllTokenController); +router.get('/', authenticateUserWithJWT, authorizeByUserRoles([UserRole.Admin, UserRole.User]), getTokenController); +router.post('/', authenticateUserWithJWT, authorizeByUserRoles([UserRole.Admin, UserRole.User]), createTokenController); +router.get('/:id', authenticateUserWithJWT, authorizeByUserRoles([UserRole.Admin, UserRole.User]), getOneTokenController); +export { router }; diff --git a/@server/api-token/token.service.ts b/@server/api-token/token.service.ts new file mode 100644 index 0000000..a508706 --- /dev/null +++ b/@server/api-token/token.service.ts @@ -0,0 +1,42 @@ +import { notFoundErr } from '@lib/errors/Errors'; +import { TokenModel as Token, TokenDocument } from '@token/token.model'; +import { UserModel as User } from '@user/user.model'; + +export const createTokenService = async (user_id: string, requestBody: TokenDocument): Promise => { + const user = await User.findById(user_id).exec(); + if(!user){ + notFoundErr('No record found for provided User ID'); + } + + const createToken = new Token({ + token: requestBody.token, + type: requestBody.type, + issuer: requestBody.issuer, + user: user + }); + + const token = await createToken.save(); + + user.tokens.push(token) + await user.save() + + return token; +}; + +export const getAllTokenService = async () => { + const query = await Token.find().exec(); + return query; +} + +export const getTokenService = async (user_id: string) => { + const query = await Token.find({user:{_id: user_id}}).exec(); + return query; +} + +export const getOneTokenService = async (user_id: string, paramsId: string) => { + const query = await Token.findOne({ _id: paramsId, user: user_id }).exec(); + if(!query){ + notFoundErr('No record found for provided ID'); + } + return query; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 381c5aa..e3fc7a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,6 +35,7 @@ //------------------------------ "baseUrl": "./", // This must be specified if "paths" is. "paths": { + "@token/*": ["@server/@api-token/*"], "@trash/*": ["@server/@api-trash/*"], "@collabocate/*": ["@server/@api-manager_collabocate/*"], "@user/*": ["@server/@api-user/*"],