Skip to content

Fix: Allow issues to be submitted using the user's GitHub username in Collabocate #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions @server/@api-auth/passport/strategies/github.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -33,18 +35,46 @@ 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({
email: user_email,
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);

Expand Down
50 changes: 36 additions & 14 deletions @server/@api-external/github.controller.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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) => {
Expand Down Expand Up @@ -84,4 +89,21 @@ export const getIssueTemplatesContentController = async (req: Request, res: Resp
};
success(message.success.get);
return res.status(200).json(response);
};
};

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);
}
}
15 changes: 13 additions & 2 deletions @server/@api-external/github.route.ts
Original file line number Diff line number Diff line change
@@ -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 };
53 changes: 48 additions & 5 deletions @server/@api-external/github.service.ts
Original file line number Diff line number Diff line change
@@ -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`, {
Expand All @@ -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}`,
Expand Down Expand Up @@ -107,4 +112,42 @@ export const getIssueTemplatesContentService = async () => {
);

return data;
};
};

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});
}
5 changes: 5 additions & 0 deletions @server/@api-user/user.model.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -16,6 +17,7 @@ export interface UserDocument extends mongoose.Document {
createdAt?: Date;
updatedAt?: Date;
instance: CollabocateInstanceDocument[];
tokens: TokenDocument[];
}

const collectionName = 'user';
Expand All @@ -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' }
]
},
{
Expand Down
3 changes: 3 additions & 0 deletions @server/@api-user/user.service.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
103 changes: 103 additions & 0 deletions @server/api-token/token.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading