Commit 307ae233 authored by Rosanny Sihombing's avatar Rosanny Sihombing
Browse files

Merge branch 'MLAB-87' into 'testing'

Mlab 87

See merge request !83
parents 2fda31a1 957b531b
Pipeline #3641 passed with stage
in 13 seconds
const gitlab = require('../routes/gitlab') const gitlab = require('../functions/gitlab')
//const axios = require('axios') //const axios = require('axios')
//jest.mock('axios') //jest.mock('axios')
......
const methods = require('../routes/methods') const methods = require('../functions/methods')
describe("DB methohds test", () => { describe("DB methohds test", () => {
it('returns a user from DB by email', done => { it("returns a user from DB by email", async() => {
methods.getUserByEmail('litehon958@whipjoy.com', function(resp, err){ const user = await methods.getUserByEmail('litehon958@whipjoy.com')
try { expect(user).not.toBeNull()
expect(resp).not.toBeNull()
expect(err).toBeNull()
done()
} catch (error) {
done(error)
}
})
})
it("returns a user from DB by ID", done => {
methods.getUserById(10, function(resp, err){
try {
expect(resp).not.toBeNull()
expect(err).toBeNull()
done()
} catch (error) {
done(error)
}
})
})
it("checks user email", done => {
methods.checkUserEmail("test@email.de", function(err, resp){
try {
expect(resp).not.toBeNull()
expect(err).toBeNull()
done()
} catch (error) {
done(error)
}
})
})
it("returns a user by token", done => {
methods.checkUserEmail("1abc0qwerty", function(err, resp){ // token = any alphanumeric
try {
expect(resp).not.toBeNull()
expect(err).toBeNull()
done()
} catch (error) {
done(error)
}
})
}) })
it("returns a null user", async() => {
const user = await methods.getUserByEmail('jondoe@nowhere.com') // a non-exist user
expect(user).toBeNull()
})
it("returns a user's email", async() => {
const email = await methods.getUserEmailById(1)
expect(email).not.toBeNull()
})
it("returns null instead of a user's email", async() => {
const email = await methods.getUserEmailById(1005) // no user has this ID
expect(email).toBeNull()
})
it("returns null from DB by token", async() => {
const user = await methods.getUserByToken('12345678') // unvalid token
expect(user).toBeNull() // for valid token = expect(user).not.toBeNull()
})
it("returns a user's verification token, if any", async() => {
const token = await methods.getVerificationTokenByUserId(1)
expect(token).toBeNull()
})
it("returns a user's ID, if any", async() => {
const token = await methods.getUserIdByVerificationToken('12345678') // unvalid token
expect(token).toBeNull() // for valid token = expect(user).not.toBeNull()
})
it("returns a user's GitLab_ID, if any", async() => {
const id = await methods.getGitlabId(1)
expect(id).not.toBeNull()
})
it("checks user email", async() => {
const user = await methods.checkUserEmail('litehon958@whipjoy.com')
expect(user).not.toBeNull()
})
it("checks user email and return null", async() => {
const user = await methods.checkUserEmail('jondoe@nowhere.com') // a non-exist user
expect(user).toBeNull()
})
}) })
...@@ -68,49 +68,51 @@ var methods = { ...@@ -68,49 +68,51 @@ var methods = {
getUserByEmail: async function(email) { getUserByEmail: async function(email) {
try { try {
let rows = await dbconn.user.promise().query('SELECT id, verificationStatus, salutation, title, firstname, lastname, industry, organisation, speciality, m4lab_idp FROM user WHERE email = "' +email+'"') let rows = await dbconn.user.promise().query('SELECT id, verificationStatus, salutation, title, firstname, lastname, industry, organisation, speciality, m4lab_idp FROM user WHERE email = "' +email+'"')
return rows[0][0] if (rows[0][0]) {
return rows[0][0]
}
else { return null }
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return err
} }
return null
}, },
getUserById: function(userId, callback) { getUserEmailById: async function(userId) {
dbconn_OBSOLETE.user.query('SELECT verificationStatus, email, salutation, title, firstname, lastname, industry, organisation, speciality FROM user WHERE id = ' +userId, function (err, rows, fields) { try {
let user let rows = await dbconn.user.promise().query('SELECT email FROM user WHERE id = ' +userId)
if (err) { throw err } if (rows[0][0]) {
else { return rows[0][0].email
if ( rows.length > 0) {
user = rows[0];
}
} }
callback(user, err); else { return null }
}); } catch (err) {
console.error(err)
}
return null
}, },
checkUserEmail: function(email, callback) { checkUserEmail: async function(email) {
let user try {
dbconn_OBSOLETE.user.query('SELECT id, email FROM user WHERE email = "' +email+'"', function (err, rows) { let rows = await dbconn.user.promise().query('SELECT id, email FROM user WHERE email = "' +email+'"')
if (err) { throw err } if (rows[0][0]) {
else { return rows[0][0]
if ( rows.length > 0) {
user = rows[0];
}
} }
callback(err, user) else { return null }
}); } catch (err) {
console.error(err)
}
return null
}, },
getUserByToken: function(token, callback) { getUserByToken: async function(token) {
let user try {
dbconn_OBSOLETE.user.query('SELECT t1.user_id, t2.email FROM userdb.credential AS t1 INNER JOIN userdb.user AS t2 ON t1.user_id = t2.id AND t1.resetPasswordToken = "' let rows = await dbconn.user.promise().query('SELECT t1.user_id, t2.email FROM userdb.credential AS t1 INNER JOIN userdb.user AS t2 ON t1.user_id = t2.id AND t1.resetPasswordToken = "'
+token+'" and resetPasswordExpires > '+Date.now(), function (err, rows, fields) { +token+'" and resetPasswordExpires > '+Date.now())
if (err) { throw err } if (rows[0][0]) {
else { return rows[0][0]
if ( rows.length > 0) {
user = rows[0]
}
}
callback(err, user)
} }
) else { return null }
} catch (err) {
console.error(err)
}
return null
}, },
updateUserById: function(userData, callback) { updateUserById: function(userData, callback) {
dbconn_OBSOLETE.user.query('UPDATE user SET ? WHERE id = ' +userData.id, userData, function (err, rows, fields) { dbconn_OBSOLETE.user.query('UPDATE user SET ? WHERE id = ' +userData.id, userData, function (err, rows, fields) {
...@@ -124,7 +126,7 @@ var methods = { ...@@ -124,7 +126,7 @@ var methods = {
callback(err) callback(err)
}) })
}, },
getUserIdByEmail: function(email, callback) { getUserIdByEmail_OBSOLETE: function(email, callback) {
let userId let userId
dbconn_OBSOLETE.user.query('SELECT id FROM user WHERE email = "' +email+'"', function (err, rows, fields) { dbconn_OBSOLETE.user.query('SELECT id FROM user WHERE email = "' +email+'"', function (err, rows, fields) {
if (err) { if (err) {
...@@ -138,7 +140,7 @@ var methods = { ...@@ -138,7 +140,7 @@ var methods = {
callback(userId, err) callback(userId, err)
}); });
}, },
getUserProjectRole: function(userId, callback) { getUserProjectRole_OBSOLETE: function(userId, callback) {
dbconn_OBSOLETE.user.query('SELECT project_id, role_id FROM user_project_role WHERE user_id = "' +userId+'"', function (err, rows, fields) { dbconn_OBSOLETE.user.query('SELECT project_id, role_id FROM user_project_role WHERE user_id = "' +userId+'"', function (err, rows, fields) {
if (err) throw err if (err) throw err
callback(rows, err) callback(rows, err)
...@@ -150,31 +152,31 @@ var methods = { ...@@ -150,31 +152,31 @@ var methods = {
callback(err) callback(err)
}) })
}, },
getVerificationTokenByUserId: function(userId, callback) { getVerificationTokenByUserId: async function(userId) {
let token try {
dbconn_OBSOLETE.user.query('SELECT token FROM verification WHERE user_id = "' +userId+'"', function (err, rows, fields) { let rows = await dbconn.user.promise().query('SELECT token FROM verification WHERE user_id = "' +userId+'"')
if (err) { if (rows[0][0]) {
throw err return rows[0][0].token
}
else {
if (rows.length > 0) {
token = rows[0].token
}
} }
callback(token, err) else { return null }
}) } catch (err) {
console.error(err)
}
return null
}, },
getUserIdByVerificationToken: function(token, callback) { getUserIdByVerificationToken: async function(token) {
let userId try {
dbconn_OBSOLETE.user.query('SELECT user_id FROM verification WHERE token = "' +token+'"', function (err, rows, fields) { let rows = await dbconn.user.promise().query('SELECT user_id FROM verification WHERE token = "' +token+'"')
if (err) { if (rows[0][0]) {
throw err return rows[0][0].user_id
} }
else if(rows[0]) { else {
userId = rows[0].user_id return null
} }
callback(userId, err) } catch (err) {
}) console.error(err)
}
return null
}, },
verifyUserAccount: function(userData, callback) { verifyUserAccount: function(userData, callback) {
dbconn_OBSOLETE.user.beginTransaction(function(err) { // START TRANSACTION dbconn_OBSOLETE.user.beginTransaction(function(err) { // START TRANSACTION
......
...@@ -89,16 +89,20 @@ module.exports = function (app, config, passport, lang) { ...@@ -89,16 +89,20 @@ module.exports = function (app, config, passport, lang) {
async function getLoggedInUserData(email) { async function getLoggedInUserData(email) {
let user = await methods.getUserByEmail(email) let user = await methods.getUserByEmail(email)
let loggedInUser = new portalUser( if (!user) {
user.id, email, user.salutation, user.title, user.firstname, user.lastname, user.industry, user.organisation, user.speciality, user.m4lab_idp, null, user.verificationStatus console.log('no user found')
) return null
} else {
let userGitlabId = await methods.getGitlabId(loggedInUser.id) let loggedInUser = new portalUser(
if (userGitlabId) { user.id, email, user.salutation, user.title, user.firstname, user.lastname, user.industry, user.organisation, user.speciality, user.m4lab_idp, null, user.verificationStatus
loggedInUser.setGitlabUserId(userGitlabId) )
let userGitlabId = await methods.getGitlabId(loggedInUser.id)
if (userGitlabId) {
loggedInUser.setGitlabUserId(userGitlabId)
}
return loggedInUser
} }
return loggedInUser
} }
app.get('/', async function (req, res) { app.get('/', async function (req, res) {
...@@ -330,59 +334,39 @@ module.exports = function (app, config, passport, lang) { ...@@ -330,59 +334,39 @@ module.exports = function (app, config, passport, lang) {
} }
}); });
app.get("/resendVerificationEmail", function(req, res){ app.get('/resendVerificationEmail', async function(req, res){
if (req.isAuthenticated()) { if (!req.isAuthenticated) {
var emailAddress = req.user.email res.redirect('/login')
} else {
methods.getUserIdByEmail(req.user.email, function(userId, err) { let loggedInUser = await getLoggedInUserData(req.user.email)
if (!err) { if (!loggedInUser) {
// get token res.redirect('/login')
methods.getVerificationTokenByUserId(userId, function(token, err){ } else {
if (!err) { let token = await methods.getVerificationTokenByUserId(loggedInUser.id)
if (token) { if (!token) {
// send email res.send(false)
var emailSubject = "Bitte bestätigen Sie Ihr M4_LAB Benutzerkonto" } else {
var emailContent = '<div>Lieber Nutzer,<br/><br/>' + // send email
'<p>vielen Dank für Ihre Anmeldung am Transferportal der HFT Stuttgart. <br/>' + var emailSubject = "Bitte bestätigen Sie Ihr M4_LAB Benutzerkonto"
'Um Ihre Anmeldung zu bestätigen, klicken Sie bitte diesen Link: ' + config.app.host + '/verifyAccount?token=' + token + var emailContent = '<div>Lieber Nutzer,<br/><br/>' +
'<br/><br/>' + '<p>vielen Dank für Ihre Anmeldung am Transferportal der HFT Stuttgart. <br/>' +
'Ohne Bestätigung Ihres Kontos müssen wir Ihr Konto leider nach 7 Tagen löschen.</p><br/>' + constants.mailSignature + 'Um Ihre Anmeldung zu bestätigen, klicken Sie bitte diesen Link: ' + config.app.host + '/verifyAccount?token=' + token +
'</div>'; '<br/><br/>' +
mailer.options.to = emailAddress; 'Ohne Bestätigung Ihres Kontos müssen wir Ihr Konto leider nach 7 Tagen löschen.</p><br/>' + constants.mailSignature +
mailer.options.subject = emailSubject; '</div>';
mailer.options.html = emailContent; mailer.options.to = loggedInUser.email;
mailer.transport.sendMail(mailer.options, function(err) { mailer.options.subject = emailSubject;
if (err) { mailer.options.html = emailContent;
console.log('cannot send email') mailer.transport.sendMail(mailer.options, function(err) {
throw err if (err) {
} console.log('cannot send email')
}) throw err
res.send(true)
}
else {
res.send(false)
}
}
else {
console.log(err)
} }
}) })
}
})
}
})
app.get('/email/:email', function(req, res) {
methods.checkUserEmail(req.params.email, function(err, user){
if (!err) {
if (user) {
res.send(false)
}
else {
res.send(true) res.send(true)
} }
} }
}) }
}) })
// ============= NEW GITLAB PAGES =========================== // ============= NEW GITLAB PAGES ===========================
......
...@@ -98,56 +98,55 @@ module.exports = function (app, config, lang) { ...@@ -98,56 +98,55 @@ module.exports = function (app, config, lang) {
// =================== USERS VERIFICATION ========================= // =================== USERS VERIFICATION =========================
app.get("/verifyAccount", function(req, res){ app.get("/verifyAccount", async function(req, res){
methods.getUserIdByVerificationToken(req.query.token, function(userId, err){ let userId = await methods.getUserIdByVerificationToken(req.query.token)
if (userId) { if (!userId) {
let userData = { // no user found
id: userId, res.render(lang+'/account/verification', {
verificationStatus: 1 status: null
} })
methods.verifyUserAccount(userData, function(err){ } else {
if (err) { // a user found, verify the account
console.log("Error: "+err) let userData = {
id: userId,
verificationStatus: 1
}
methods.verifyUserAccount(userData, async function(err){
if (err) {
console.log("Error: "+err)
res.render(lang+'/account/verification', {
status: false
});
} else {
// send welcome email after successful account verification
let userEmail = await methods.getUserEmailById(userId)
if (!userEmail) {
res.render(lang+'/account/verification', { res.render(lang+'/account/verification', {
status: false status: false
});
}
else {
// send welcome email after successful account verification
methods.getUserById(userId, function(data, err){
if (err) {
console.log("Error: "+err)
}
else {
// send email
var emailSubject = "Herzlich willkommen"
var emailContent = '<div>Lieber Nutzer,<br/><br/>' +
'<p>herzlich willkommen beim Transferportal der HFT Stuttgart!<br/>' +
'Sie können nun alle Dienste des Portals nutzen.<p/><br/>' + constants.mailSignature;
mailer.options.to = data.email;
mailer.options.subject = emailSubject;
mailer.options.html = emailContent;
mailer.transport.sendMail(mailer.options, function(err) {
if (err) {
console.log('cannot send email')
throw err
}
})
}
}) })
} else {
res.render(lang+'/account/verification', { // send email
status: true var emailSubject = "Herzlich willkommen"
}); var emailContent = '<div>Lieber Nutzer,<br/><br/>' +
'<p>herzlich willkommen beim Transferportal der HFT Stuttgart!<br/>' +
'Sie können nun alle Dienste des Portals nutzen.<p/><br/>' + constants.mailSignature;
mailer.options.to = userEmail
mailer.options.subject = emailSubject
mailer.options.html = emailContent
mailer.transport.sendMail(mailer.options, function(err) {
if (err) {
console.log('cannot send email')
throw err
}
})
res.render(lang+'/account/verification', {
status: true
})
} }
}) }
} })
else { }
res.render(lang+'/account/verification', {
status: null
});
}
})
}) })
// ==================== FORGOT PASSWORD =========================== // ==================== FORGOT PASSWORD ===========================
...@@ -157,7 +156,7 @@ module.exports = function (app, config, lang) { ...@@ -157,7 +156,7 @@ module.exports = function (app, config, lang) {
user: req.user user: req.user
}) })
}) })
app.post('/forgotPwd', function(req, res, next) { app.post('/forgotPwd', function(req, res) {
let emailAddress = req.body.inputEmail let emailAddress = req.body.inputEmail
async.waterfall([ async.waterfall([
function(done) { function(done) {
...@@ -166,36 +165,34 @@ module.exports = function (app, config, lang) { ...@@ -166,36 +165,34 @@ module.exports = function (app, config, lang) {
done(err, token) done(err, token)
}) })
}, },
function(token, done) { async function(token) {
methods.checkUserEmail(emailAddress, function(err, user){ let user = await methods.checkUserEmail(emailAddress)
if (user) { if (!user) {
var emailSubject = "Ihre Passwort-Anfrage an das Transferportal der HFT Stuttgart"; console.log('no user found')
var emailContent = '<div>Lieber Nutzer,<br/><br/>' + } else {
'<p>wir haben Ihre Anfrage zur Erneuerung Ihres Passwortes erhalten. Falls Sie diese Anfrage nicht gesendet haben, ignorieren Sie bitte diese E-Mail.<br/><br/>' + var emailSubject = "Ihre Passwort-Anfrage an das Transferportal der HFT Stuttgart";
'Sie können Ihr Passwort mit dem Klick auf diesen Link ändern: '+config.app.host+'/reset/' + token + '<br/>' + var emailContent = '<div>Lieber Nutzer,<br/><br/>' +
'Dieser Link ist aus Sicherheitsgründen nur für 1 Stunde gültig.<br/></p>' + constants.mailSignature + '</div>' '<p>wir haben Ihre Anfrage zur Erneuerung Ihres Passwortes erhalten. Falls Sie diese Anfrage nicht gesendet haben, ignorieren Sie bitte diese E-Mail.<br/><br/>' +
'Sie können Ihr Passwort mit dem Klick auf diesen Link ändern: '+config.app.host+'/reset/' + token + '<br/>' +
'Dieser Link ist aus Sicherheitsgründen nur für 1 Stunde gültig.<br/></p>' + constants.mailSignature + '</div>'
var credentialData = { var credentialData = {
user_id: user.id, user_id: user.id,
resetPasswordToken: token, resetPasswordToken: token,
resetPasswordExpires: Date.now() + 3600000 // 1 hour resetPasswordExpires: Date.now() + 3600000 // 1 hour
}
methods.updateCredential(credentialData, function(err) {
done(err, token, user);
})
// send email
mailer.options.to = emailAddress
mailer.options.subject = emailSubject
mailer.options.html = emailContent
mailer.transport.sendMail(mailer.options, function(err) {
done(err, 'done')
});
}
else {
done(err, 'no user found')
} }
}); methods.updateCredential(credentialData, function(err) {
if (err) { console.error(err) }
})
// send email
mailer.options.to = emailAddress
mailer.options.subject = emailSubject
mailer.options.html = emailContent
mailer.transport.sendMail(mailer.options, function(err) {
if (err) { console.error(err) }
})
}
} }
], function(err) { ], function(err) {
if (err) { if (err) {
...@@ -205,60 +202,57 @@ module.exports = function (app, config, lang) { ...@@ -205,60 +202,57 @@ module.exports = function (app, config, lang) {
res.flash('success', 'Wenn Ihre E-Mail-Adresse registriert ist, wurde eine E-Mail mit dem weiteren Vorgehen an ' + emailAddress + ' versendet.') res.flash('success', 'Wenn Ihre E-Mail-Adresse registriert ist, wurde eine E-Mail mit dem weiteren Vorgehen an ' + emailAddress + ' versendet.')
} }
res.redirect('/account/forgotPwd') res.redirect('/account/forgotPwd')
}); })
}) })
// reset // reset
app.get('/reset/:token', function(req, res) { app.get('/reset/:token', async function(req, res) {
methods.getUserByToken(req.params.token, function(err, user){ let user = await methods.getUserByToken(req.params.token)
if (!user) { if (!user) {
res.flash('error', 'Der Schlüssel zum zurücksetzen des Passworts ist ungültig oder abgelaufen.') res.flash('error', 'Der Schlüssel zum zurücksetzen des Passworts ist ungültig oder abgelaufen.')
res.redirect('/account/forgotPwd') res.redirect('/account/forgotPwd')
} else { } else {
res.render(lang+'/account/reset') res.render(lang+'/account/reset')
} }
})
}) })
app.post('/reset/:token', function(req, res) { app.post('/reset/:token', async function(req, res) {
var newPwd = req.body.inputNewPwd var newPwd = req.body.inputNewPwd
methods.getUserByToken(req.params.token, function(err, user){
if (user) { let user = await methods.getUserByToken(req.params.token)
// encrypt password if (!user) {
bcrypt.genSalt(saltRounds, function(err, salt) { res.flash('error', "User not found.")
bcrypt.hash(newPwd, salt, function(err, hash) { res.redirect('/login')
var credentialData = { } else {
password: hash, // encrypt password
user_id: user.user_id bcrypt.genSalt(saltRounds, function(err, salt) {
bcrypt.hash(newPwd, salt, function(err, hash) {
var credentialData = {
password: hash,
user_id: user.user_id
}
// update password
methods.updateCredential(credentialData, function(err){
if (err) {
res.flash('error', "Datenbankfehler: Passwort kann nicht geändert werden.")
throw err
} else {
res.flash('success', "Passwort aktualisiert!")
// send notifiaction email
mailer.options.to = user.email
mailer.options.subject = constants.updatePasswordMailSubject
mailer.options.html = constants.updatePasswordMailContent+'<div>'+constants.mailSignature+'</div>'
mailer.transport.sendMail(mailer.options, function(err) {
if (err) { console.log(err) }
})
res.redirect('/login')
} }
// update password })
methods.updateCredential(credentialData, function(err){
if (err) {
res.flash('error', "Datenbankfehler: Passwort kann nicht geändert werden.")
throw err
}
else {
res.flash('success', "Passwort aktualisiert!")
// send notifiaction email
mailer.options.to = user.email
mailer.options.subject = constants.updatePasswordMailSubject
mailer.options.html = constants.updatePasswordMailContent+'<div>'+constants.mailSignature+'</div>'
mailer.transport.sendMail(mailer.options, function(err) {
if (err) {
console.log(err)
}
});
// redirect to login page
res.redirect('/login')
}
})
});
}); });
} });
else { }
res.flash('error', "User not found.")
res.redirect('/login')
}
})
}) })
// ======================= CONTACT FORM =========================== // ======================= CONTACT FORM ===========================
......
...@@ -60,13 +60,8 @@ html(lang="de") ...@@ -60,13 +60,8 @@ html(lang="de")
function verify() { function verify() {
$(".spinner-border").show() $(".spinner-border").show()
$.get( "/resendVerificationEmail", function( data ) { $.get( "/resendVerificationEmail", function( data ) {
console.log(data) if (data) { alert( "Email sent!" ) }
if (data) { else { alert("Please contact support-transfer@hft-stuttgart.de to verify your account.") }
alert( "Email sent!" )
}
else {
alert("Please contact support-transfer@hft-stuttgart.de to verify your account.")
}
}) })
.fail(function() { .fail(function() {
alert( "Something went wrong. Please try again." ) // todo: to DE alert( "Something went wrong. Please try again." ) // todo: to DE
......
...@@ -22,15 +22,15 @@ html(lang="de") ...@@ -22,15 +22,15 @@ html(lang="de")
body body
div(class="container") div(class="container")
div(class="center", align="center") div(class="center", align="center")
a(href="https://m4lab.hft-stuttgart.de") a(href="https://transfer.hft-stuttgart.de")
img(src="https://transfer.hft-stuttgart.de/images/demo/m4lab_logo.jpg", class="img-responsive center-block", width="185", height="192") img(src="https://transfer.hft-stuttgart.de/images/demo/m4lab_logo.jpg", class="img-responsive center-block", width="185", height="192")
br br
br br
if status == true if status == true
p(class="h5") Ihr Benutzerkonto wurde bestätigt. Bitte <a href="https://m4lab.hft-stuttgart.de/account/">melden Sie sich an</a>. p(class="h5") Ihr Benutzerkonto wurde bestätigt. Bitte <a href="https://transfer.hft-stuttgart.de/account/">melden Sie sich an</a>.
else if status == false else if status == false
p(class="h5") Ihr Benutzerkonto konnte nicht bestätigt werden, bitte versuchen Sie es erneut. p(class="h5") Ihr Benutzerkonto konnte nicht bestätigt werden, bitte versuchen Sie es erneut.
else else
p(class="h5") Ihr Benutzerkonto wude nicht gefunden. p(class="h5") Ihr Benutzerkonto wurde nicht gefunden.
// Bootstrap // Bootstrap
script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous") script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous")
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment