Skip to content
This repository has been archived by the owner on Dec 10, 2022. It is now read-only.

feat(close #18): Add oauth by gitlab #25

Open
wants to merge 4 commits into
base: master
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
18 changes: 18 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
APP_NAME=gitlab-one-board

SESSION_SECRET=gitlab-one-board

HOST=localhost
PORT=9000

GITLAB_DOMAIN=gitlab.com
GITLAB_TOKEN=

GITLAB_BASE_URL=
GITLAB_CLIENT_ID=
GITLAB_SECRET=
GITLAB_CALLBACK_URL=http://localhost:9000/api/auth/gitlab/callback
GITLAB_SCOPE=read_user

REDIRECT_URL=http://localhost:3000

# mongodb config
MONGO_URL=mongodb://localhost:27017/gitlab

# auth config
AUTH_GITLAB_ENABLE=true
9 changes: 8 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@
"license": "MIT",
"dependencies": {
"body-parser": "^1.18.2",
"connect-mongo": "^2.0.1",
"dotenv": "^5.0.1",
"express": "^4.16.2",
"express-session": "^1.15.6",
"express-validator": "^5.1.2",
"http-status-codes": "^1.3.0",
"node-fetch": "^2.1.2"
"mongoose": "4.6.5",
"morgan": "^1.9.0",
"node-fetch": "^2.1.2",
"passport": "^0.4.0",
"passport-gitlab2": "^3.0.0"
},
"devDependencies": {
"chai": "^4.1.2",
Expand Down Expand Up @@ -56,6 +62,7 @@
"never"
],
"no-await-in-loop": 0,
"global-require": 0,
"no-restricted-syntax": [
"error",
"WithStatement",
Expand Down
31 changes: 31 additions & 0 deletions server/src/auth/gitlab/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const Router = require('express').Router;
const Passport = require('passport');
const GitlabStrategy = require('passport-gitlab2').Strategy;

const { passportGeneralCallback } = require('../utils');

const { GITLAB_BASE_URL, GITLAB_CLIENT_ID, GITLAB_SECRET,
GITLAB_SCOPE, GITLAB_CALLBACK_URL } = process.env;

const gitlabAuth = Router();

Passport.use(new GitlabStrategy({
baseURL: GITLAB_BASE_URL,
clientID: GITLAB_CLIENT_ID,
clientSecret: GITLAB_SECRET,
scope: [GITLAB_SCOPE],
callbackURL: GITLAB_CALLBACK_URL,
}, passportGeneralCallback));

gitlabAuth.get('/auth/gitlab', Passport.authenticate('gitlab'));

// gitlab auth callback
gitlabAuth.get('/auth/gitlab/callback',
Passport.authenticate('gitlab', {
failureRedirect: process.env.REDIRECT_URL,
}), (req, res) => {
// Successful authentication, redirect home.
res.redirect(process.env.REDIRECT_URL);
});

module.exports = gitlabAuth;
53 changes: 53 additions & 0 deletions server/src/auth/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const Router = require('express').Router;
const Passport = require('passport');
const { FORBIDDEN } = require('http-status-codes');

const { User } = require('../models');

const { AUTH_GITLAB_ENABLE } = process.env;
const authRouter = Router();

// serialize and deserialize
Passport.serializeUser((user, cb) => cb(null, user.id));

Passport.deserializeUser(async (id, cb) => {
try {
const query = {
_id: id,
};
const user = await User.get(query);

return cb(null, user);
} catch (err) {
return cb(err, null);
}
});

if (AUTH_GITLAB_ENABLE) authRouter.use(require('./gitlab'));

// logout
authRouter.get('/logout', (req, res) => {
req.logout();

res.json({
message: 'Done',
});
});

// me
authRouter.get('/me', async (req, res) => {
if (req.isAuthenticated()) {
const query = {
_id: req.user.id,
};
const user = await User.get(query);

res.json(user);
} else {
res.send(FORBIDDEN, {
message: 'Forbidden',
});
}
});

module.exports = authRouter;
40 changes: 40 additions & 0 deletions server/src/auth/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const { User } = require('../models');

const passportGeneralCallback = async (accessToken, refreshToken, profile, cb) => {
const { id } = profile;

try {
const query = {
profileId: id,
};

let user = await User.get({
query,
});

if (user) {
await User.update(query, {
profile,
accessToken,
refreshToken,
});
} else {
user = new User({
profile,
accessToken,
refreshToken,
profileId: id.toString(),
});

await user.save();
}

return cb(null, user);
} catch (err) {
return cb(err, null);
}
};

module.exports = {
passportGeneralCallback,
};
103 changes: 77 additions & 26 deletions server/src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
require('dotenv').config();

const express = require('express');
const expressValidator = require('express-validator');
const bodyParser = require('body-parser');
const path = require('path');
const Express = require('express');
const ExpressValidator = require('express-validator');
const BodyParser = require('body-parser');
const Path = require('path');
const Logger = require('morgan');
const Mongoose = require('mongoose');
const Passport = require('passport');
const ConnectMongo = require('connect-mongo');
const ExpressSession = require('express-session');
const { NOT_FOUND } = require('http-status-codes');

const authRoute = require('./auth');
const issuesRoute = require('./issues');
const usersRoute = require('./users');
const projectsRoute = require('./projects');
Expand All @@ -16,38 +22,83 @@ const { fetchIssues, fetchUsers, fetchProjects } = require('./utils/gitlab-api')
// Constants
const PORT = process.env.PORT || 9000;
const HOST = process.env.HOST || 'localhost';
const CLIENT_BUILD_PATH = path.join(__dirname, '../../client/build');
const CLIENT_BUILD_PATH = Path.join(__dirname, '../../client/build');

// App
const app = express();
const app = Express();

// Static files
app.use(express.static(CLIENT_BUILD_PATH));
// Mongoose config
Mongoose.Promise = global.Promise;

app.use(expressValidator());
app.use(bodyParser.json());
// Connect mongo
const mongoUrl = `${process.env.MONGO_URL}?reconnectTries=10&reconnectInterval=3000`;

app.use('/api', [
issuesRoute,
usersRoute,
projectsRoute,
labelsRoute,
]);
Mongoose.connect(mongoUrl);
Mongoose.connection.on('open', () => {
// Static files
app.use(Express.static(CLIENT_BUILD_PATH));

const notFoundError = (req, res) => res.status(NOT_FOUND).json(sendNotFound());
app.use(Logger('combined'));
app.use(ExpressValidator());
app.use(BodyParser.json());

// Index request return the React app, so it can handle routing.
app.get('/', (request, response) => {
response.sendFile(path.join(CLIENT_BUILD_PATH, 'index.html'));
});

app.all('*', notFoundError);
// Session
app.use(ExpressSession({
name: process.env.APP_NAME,
secret: process.env.SESSION_SECRET,
cookie: {
path: '/',
httpOnly: true,
secure: false,
maxAge: 86400000,
},
resave: false,
saveUninitialized: true,
rolling: true,
store: new (ConnectMongo(ExpressSession))({
mongooseConnection: Mongoose.connection,
}),
}));

// Use passport
app.use(Passport.initialize());
app.use(Passport.session());

app.use('/api', [
authRoute,
issuesRoute,
usersRoute,
projectsRoute,
labelsRoute,
]);

const notFoundError = (req, res) => res.status(NOT_FOUND).json(sendNotFound());

// Index request return the React app, so it can handle routing.
app.get('/', (request, response) => {
response.sendFile(Path.join(CLIENT_BUILD_PATH, 'index.html'));
});

app.listen(PORT, HOST, async () => {
// fetch issues, users, projects for the first time
await Promise.all(fetchIssues(), fetchUsers(), fetchProjects());
app.all('*', notFoundError);

app.listen(PORT, HOST, async () => {
// fetch issues, users, projects for the first time
await Promise.all(fetchIssues(), fetchUsers(), fetchProjects());
});

console.log(`Running on http://${HOST}:${PORT}`); // eslint-disable-line no-console
});

console.log(`Running on http://${HOST}:${PORT}`); // eslint-disable-line no-console
// Mongoose connection error handler
Mongoose.connection.on('error', (err) => {
console.log('Mongoose failed to connect', err); // eslint-disable-line no-console
Mongoose.disconnect();
});

// Mongoose connection close handler
Mongoose.connection.on('close', () => {
console.log('Mongoose connection closed'); // eslint-disable-line no-console
});

module.exports = app; // for testing
5 changes: 5 additions & 0 deletions server/src/models/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const User = require('./user');

module.exports = {
User,
};
63 changes: 63 additions & 0 deletions server/src/models/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const Mongoose = require('mongoose');

const Schema = Mongoose.Schema;
const SELECT_DEFAULT = 'email isActive profileId profile accessToken refreshToken';

const schema = new Schema({
profileId: {
type: String,
required: true,
},
profile: {
type: Schema.Types.Mixed,
required: true,
},
email: {
type: String,
trim: true,
},
accessToken: {
type: String,
trim: true,
},
refreshToken: {
type: String,
trim: true,
},
isDeleted: {
type: Boolean,
default: false,
},
});

schema.method({
securedInfo() {
const { _id, email, isActive, profileId, profile, accessToken, refreshToken } = this;

return {
id: _id,
email,
isActive,
profileId,
profile,
accessToken,
refreshToken,
};
},
});

schema.statics = {
get({ select, query }) {
return this.findOne(query)
.select(select || SELECT_DEFAULT);
},
list({ query, page, sort, limit, select }) {
return this.find(query || {})
.sort(sort || '-created.at')
.select(select || SELECT_DEFAULT)
.skip((limit || 0) * (page || 0))
.limit(limit || 0);
},
};

module.exports = Mongoose.model('User', schema);
Loading