continued

implemented HTMX
implemented ORM (sequelize)
This commit is contained in:
Kai Waggeling 2025-11-29 21:56:21 +01:00
parent 2a9bd4e81b
commit d756a192e4
71 changed files with 3822 additions and 694 deletions

View file

@ -1,36 +0,0 @@
import {
getUsers,
getGroups
} from "../../lib/mysql.mjs";
export const get = async function (request, response) {
// if (!request.isLoginCompleted()) {
// response.redirect('/login');
// return;
// }
switch (request.params.page) {
case 'users':
response.render(`ui/admin.njk`, {
page: 'users',
users: await getUsers()
});
break;
case 'groups':
response.render(`ui/admin.njk`, {
page: 'groups',
groups: await getGroups()
});
break;
default:
response.redirect('/admin/users');
break;
}
}
export const post = async function (request, response) {
console.log(request.body);
response.redirect("/login");
}

17
routes/admin/groups.mjs Normal file
View file

@ -0,0 +1,17 @@
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}
let currUser = await request.getUser();
response.render(`views/admin/groups.njk`, {
user: {
firstName: currUser.givenname,
lastName: currUser.sn,
mail: currUser.mail,
}
});
}

17
routes/admin/users.mjs Normal file
View file

@ -0,0 +1,17 @@
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}
let currUser = await request.getUser();
response.render(`views/admin/users.njk`, {
user: {
firstName: currUser.givenname,
lastName: currUser.sn,
mail: currUser.mail,
}
});
}

View file

@ -0,0 +1,59 @@
import {
Group,
User
} from "../../../../lib/database/connect.mjs";
import {
getConfig
} from "../../../../lib/config.mjs";
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.set('HX-Redirect', '/login').status(401).end();
return;
}
const dbGroup = await Group.findByPk(request.params.groupid);
const config = await getConfig();
const userList = await User.findAll({
attributes: [
'id',
'uidnumber',
'name',
'othergroups',
'primarygroup'
]
}).filter((user) => {
// check if users primary group is the group being edited
if (user.primarygroup == dbGroup.gidnumber) {
return true;
}
// check if users other groups include the group being edited
if (user.othergroups.split(',').map(gidnumber => parseInt(gidnumber)).includes(dbGroup.gidnumber)) {
return true;
}
// else exclude user
return false;
}).map((user) => {
return {
id: user.id,
name: user.name,
uidnumber: user.uidnumber,
};
});
response.render(`views/htmx/admin/editGroup.njk`, {
group: {
id: dbGroup.id,
name: dbGroup.name,
gidnumber: dbGroup.gidnumber,
},
ldap: {
baseDN: config.ldap.baseDN,
},
userList: userList
});
}

View file

@ -0,0 +1,18 @@
import {
Group
} from "../../../../lib/database/connect.mjs";
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.set('HX-Redirect', '/login').status(401).end();
return;
}
let groupList = await Group.findAll();
response.render(`views/htmx/admin/groupTable.njk`, {
groupList: groupList
});
}

View file

@ -0,0 +1,53 @@
import {
Group,
User
} from "../../../../lib/database/connect.mjs";
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.set('HX-Redirect', '/login').status(401).end();
return;
}
const dbUser = await User.findByPk(request.params.userid);
if (!dbUser) {
response.status(404).end('User not found');
return;
}
const userGroups = await (async () => {
if (!dbUser.othergroups || dbUser.othergroups.length == 0) {
return [];
} else {
return dbUser.othergroups.split(',').map(gid => parseInt(gid));
}
})();
const groupList = (await Group.findAll()).map((dbGroup) => {
return {
id: dbGroup.id,
gidnumber: dbGroup.gidnumber,
name: dbGroup.name,
isPrimaryGroup: dbUser.primarygroup == dbGroup.gidnumber,
isOtherGroup: userGroups.includes(dbGroup.gidnumber)
};
});
console.log(groupList);
response.render(`views/htmx/admin/editUser.njk`, {
user: {
id: dbUser.id,
username: dbUser.name,
uidnumber: dbUser.uidnumber,
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
disabled: dbUser.disabled,
},
groupList: groupList
});
}

View file

@ -0,0 +1,18 @@
import {
User
} from "../../../../lib/database/connect.mjs";
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.set('HX-Redirect', '/login').status(401).end();
return;
}
let userList = await User.findAll();
response.render(`views/htmx/admin/userTable.njk`, {
userList: userList
});
}

75
routes/htmx/authForm.mjs Normal file
View file

@ -0,0 +1,75 @@
import crypto from "crypto";
import {
User
} from "../../lib/database/connect.mjs";
function sendAuthForm(response, errors=[]) {
response.render(`views/htmx/authForm.njk`, {
errors: errors
});
}
export const get = async function(request, response) {
sendAuthForm(response);
// response.set('HX-Redirect', '/profile').status(200).end();
}
export const post = async function(request, response) {
if (!request.body.username || !request.body.password) {
sendAuthForm(response, [{
title: 'Username and password are required',
detail: 'Username or Password was not received.'
}]);
return;
}
if (typeof request.body.username != 'string' || typeof request.body.password != 'string') {
sendAuthForm(response, [{
title: 'Invalid input types',
detail: 'Username and Password must be strings.'
}]);
return;
}
let username = request.body.username;
let password = crypto.createHash('sha256').update(request.body.password).digest('hex')
// let loginResult = await login(username, password)
let loginUser = await User.findOne({
where: {
name: username,
passsha256: password
}
});
if (loginUser == null) {
sendAuthForm(response, [{
title: 'Login failed',
detail: 'Invalid Username or Password.'
}]);
return;
}
if (loginUser.disabled == 1) {
sendAuthForm(response, [{
title: 'User disabled',
detail: 'This user account is disabled.'
}]);
return;
}
request.session.userid = loginUser.id;
request.session.save();
if (loginUser.otpsecret == '' || loginUser.otpsecret == null) {
request.setAuthState('authenticated');
response.set('HX-Redirect', '/profile').status(200).end();
} else {
request.setAuthState('totp-verfication');
response.redirect('/htmx/totpForm');
}
}

View file

@ -0,0 +1,27 @@
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.set('HX-Redirect', '/login').status(401).end();
return;
}
let dbUser = await request.getUser();
response.render(`views/htmx/profile/editData.njk`, {
user: {
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
},
data: {
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
}
});
}
export const post = async function (request, response) {
console.log(request.body);
response.redirect("/login");
}

View file

@ -0,0 +1,29 @@
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.set('HX-Redirect', '/login').status(401).end();
return;
}
let dbUser = await request.getUser();
response.render(`views/htmx/profile/showData.njk`, {
user: {
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
},
data: {
uidnumber: dbUser.uidnumber,
username: dbUser.name,
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
}
});
}
export const post = async function (request, response) {
console.log(request.body);
response.redirect("/login");
}

View file

@ -0,0 +1,30 @@
import {
generateOTPQRCode
} from "../../../../lib/otp.mjs";
export const get = async function (request, response) {
if (request.getAuthState() != 'authenticated') {
response.set('HX-Redirect', '/login').status(401).end();
return;
}
let dbUser = await request.getUser();
response.render(`views/htmx/profile/showMFA.njk`, {
user: {
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
},
data: {
otpsecret: dbUser.otpsecret,
yubikey: dbUser.yubikey,
qrcode: await generateOTPQRCode(dbUser.mail, dbUser.otpsecret)
}
});
}
export const post = async function (request, response) {
console.log(request.body);
response.redirect("/login");
}

53
routes/htmx/totpForm.mjs Normal file
View file

@ -0,0 +1,53 @@
import {
validateOTPCode
} from "../../lib/otp.mjs";
function sendTOTPForm(response, errors=[]) {
response.render(`views/htmx/totpForm.njk`, {
errors: errors
});
}
export const get = async function(request, response) {
if (request.getAuthState() != 'totp-verfication') {
response.redirect('/htmx/authForm');
return;
}
sendTOTPForm(response);
}
export const post = async function(request, response) {
// redirect if not in TOTP verification state
if (request.getAuthState() != 'totp-verfication') {
response.redirect('/htmx/authForm');
return;
}
// validate input
if (!request.body.otpToken || typeof request.body.otpToken != 'string') {
sendTOTPForm(response, [{
title: 'OTP token is required',
detail: 'no OTP token was received.'
}]);
return;
}
let otpToken = request.body.otpToken;
let dbUser = await request.getUser();
let validationResult = await validateOTPCode(dbUser.mail, dbUser.otpsecret, otpToken);
if (validationResult != null) {
request.setAuthState('authenticated');
response.set('HX-Redirect', '/profile').status(200).end();
} else {
sendTOTPForm(response, [{
title: 'OTP validation failed',
detail: 'the provided OTP token is invalid. Please try again.'
}]);
}
}

4
routes/index.mjs Normal file
View file

@ -0,0 +1,4 @@
export const get = async function (request, response) {
response.redirect(`/profile`);
}

View file

@ -1,78 +1,8 @@
import crypto from "crypto";
import {
login,
getUser,
getUserMFA
} from "../lib/mysql.mjs";
import {
validateOTPCode
} from "../lib/otp.mjs";
export const get = async function(request, response) {
if (typeof request.session.userid != 'string') {
response.render(`ui/login.njk`, {
step: 'login'
});
return;
if (request.getAuthState() == 'authenticated') {
response.redirect('/profile');
}
if (request.session.otpVerified != true) {
response.render(`ui/login.njk`, {
step: 'otp'
});
return;
}
}
export const post = async function(request, response) {
if (typeof request.body.username == 'string' && typeof request.body.password == 'string') {
let username = request.body.username;
let password = crypto.createHash('sha256').update(request.body.password).digest('hex')
let loginResult = await login(username, password)
if (loginResult == null) {
response.render(`ui/login.njk`, {
step: 'login',
error: 'login failed'
});
return;
}
request.session.userid = loginResult.id;
request.session.login = {
completed: false,
otpVerified: false
}
request.session.save();
if (loginResult.otpsecret != '' && loginResult.yubikey != '') {
response.render(`ui/login.njk`, {
step: 'otp'
});
return;
} else {
request.session.login.completed = true;
response.redirect('/profile')
}
} else if (typeof request.body.otpToken == 'string') {
let otpToken = request.body.otpToken;
let userData = await getUser(request.session.userid);
let mfaData = await getUserMFA(request.session.userid);
let validationResult = await validateOTPCode(userData.mail, mfaData.otpsecret, otpToken);
if (validationResult != null) {
request.session.login.completed = true;
response.redirect('/profile');
} else {
request.session.destroy();
response.render(`ui/login.njk`, {
step: 'login',
error: 'otp failed'
});
}
}
}
response.render(`views/login.njk`);
}

View file

@ -1,5 +1,5 @@
export const get = async function(request, response) {
request.session.destroy();
response.render(`ui/logout.njk`);
response.render(`views/logout.njk`);
}

View file

@ -1,49 +1,58 @@
import {
getUser,
getUserMFA
} from "../../lib/mysql.mjs";
import {
generateOTPQRCode
} from "../../lib/otp.mjs";
export const get = async function(request, response) {
if (!request.isLoginCompleted()) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}
let userData = await getUser(request.session.userid);
let mfaData = await getUserMFA(request.session.userid);
let dbUser = await request.getUser();
switch (request.params.page) {
case 'personal':
response.render(`ui/profile.njk`, {
response.render(`views/profile.njk`, {
page: 'profile/personal',
user: {
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
},
data: {
firstName: userData.givenname,
lastName: userData.sn,
mail: userData.mail,
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
}
});
break;
case 'security':
response.render(`ui/profile.njk`, {
response.render(`views/profile.njk`, {
page: 'profile/security',
user: {
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
},
otp: {
active: mfaData.otpsecret != '' ? true : false,
qrcode: await generateOTPQRCode(userData.mail, mfaData.otpsecret)
active: dbUser.otpsecret != '' ? true : false,
qrcode: await generateOTPQRCode(dbUser.mail, dbUser.otpsecret)
}
});
break;
case 'createOTPSecret':
response.render(`ui/profile.njk`, {
response.render(`views/profile.njk`, {
page: 'profile/createOTPSecret',
user: {
firstName: dbUser.givenname,
lastName: dbUser.sn,
mail: dbUser.mail,
},
otp: {
active: mfaData.otpsecret != '' ? true : false,
qrcode: await generateOTPQRCode(userData.mail, mfaData.otpsecret)
qrcode: await generateOTPQRCode(dbUser.mail, dbUser.otpsecret)
}
});
break;

View file

@ -1,6 +1,6 @@
export const get = async function (request, response) {
if (!request.isLoginCompleted()) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}

View file

@ -11,7 +11,7 @@ import {
} from "../../../lib/mysql.mjs";
export const get = async function (request, response) {
if (!request.isLoginCompleted()) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}
@ -38,7 +38,7 @@ export const get = async function (request, response) {
export const post = async function (request, response) {
if (!request.isLoginCompleted()) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}

View file

@ -11,7 +11,7 @@ import {
} from "../../../lib/mysql.mjs";
export const get = async function (request, response) {
if (!request.isLoginCompleted()) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}
@ -38,7 +38,7 @@ export const get = async function (request, response) {
export const post = async function (request, response) {
if (!request.isLoginCompleted()) {
if (request.getAuthState() != 'authenticated') {
response.redirect('/login');
return;
}