diff --git a/src/main/middlewares/datasetAuthInjector.ts b/src/main/middlewares/datasetAuthInjector.ts new file mode 100644 index 00000000..964db5b0 --- /dev/null +++ b/src/main/middlewares/datasetAuthInjector.ts @@ -0,0 +1,20 @@ +import { NextFunction, Request, Response } from "express"; +import appConfig from "../../shared/resources/appConfig"; +import { datasetServiceHttpInstance } from "../services/dataset"; + +const authenticationType = appConfig.AUTHENTICATION_TYPE; + +export default { + name: 'datasetAuthInjector', + handler: () => (request: any, response: Response, next: NextFunction) => { + if (authenticationType === 'keycloak') { + const keycloakToken = JSON.parse(request?.session['keycloak-token']); + const access_token: string = keycloakToken.access_token; + datasetServiceHttpInstance.defaults.headers['Authorization'] = `Bearer ${access_token}`; + } else if (authenticationType === 'basic') { + const jwtToken: string = request?.session?.token; + datasetServiceHttpInstance.defaults.headers['Authorization'] = `Bearer ${jwtToken}`; + } + next(); + } +}; \ No newline at end of file diff --git a/src/main/resources/routesConfig.ts b/src/main/resources/routesConfig.ts index 769babe6..bd8437c6 100644 --- a/src/main/resources/routesConfig.ts +++ b/src/main/resources/routesConfig.ts @@ -9,6 +9,7 @@ import passportAuthenticateCallback from '../middlewares/passportAuthenticate'; import setContext from '../middlewares/setContext'; import authorizationMiddleware from '../middlewares/authorization'; import { permissions } from '../middlewares/authorization'; +import datasetAuthInjector from '../middlewares/datasetAuthInjector'; const baseURL = appConfig.BASE_URL; export default [ @@ -146,6 +147,8 @@ export default [ path: 'state/:datasetId', method: 'GET', middlewares: [ + ensureLoggedInMiddleware, + datasetAuthInjector.handler(), controllers.get('dataset:state')?.handler({}) ] }, @@ -154,6 +157,7 @@ export default [ method: 'GET', middlewares: [ ensureLoggedInMiddleware, + datasetAuthInjector.handler(), controllers.get('dataset:diff')?.handler({}) ] }, @@ -162,6 +166,7 @@ export default [ method: 'GET', middlewares: [ ensureLoggedInMiddleware, + datasetAuthInjector.handler(), controllers.get('dataset:exists')?.handler({}) ] } @@ -174,6 +179,8 @@ export default [ path: 'generate-fields/:dataset_id', method: 'GET', middlewares: [ + ensureLoggedInMiddleware, + datasetAuthInjector.handler(), controllers.get('get:all:fields')?.handler({}) ] } diff --git a/src/main/services/dataset.ts b/src/main/services/dataset.ts index 5ba5ff40..87c00e1b 100644 --- a/src/main/services/dataset.ts +++ b/src/main/services/dataset.ts @@ -4,7 +4,7 @@ import appConfig from '../../shared/resources/appConfig' import { fieldsByStatus } from '../controllers/dataset_diff'; type Payload = Record; -const datasetServiceHttpInstance = axios.create({ baseURL: appConfig.OBS_API.URL}); +export const datasetServiceHttpInstance = axios.create({ baseURL: appConfig.OBS_API.URL}); const transform = (response: any) => _.get(response, 'data.result') diff --git a/src/main/services/keycloak.ts b/src/main/services/keycloak.ts index e7a5d84a..98dcc719 100644 --- a/src/main/services/keycloak.ts +++ b/src/main/services/keycloak.ts @@ -18,13 +18,15 @@ export const authenticated = async (request: any) => { request.session.preferred_username = preferred_username; const user = await userService.find({ id: userId?.[0] }); - request.session.roles = user?.roles; + request.session.userDetails = _.pick(user, ['id', 'user_name', 'email_address', 'roles', 'is_owner']); + request.session.roles = _.get(user, ['roles']); } catch (err) { console.log('user not authenticated', request?.kauth?.grant?.access_token?.content?.sub, err); } }; export const deauthenticated = function (request: any) { + delete request?.session?.userDetails; delete request?.session?.roles; delete request?.session?.userId; delete request?.session?.email_address; @@ -44,6 +46,8 @@ export const userCreate = async (access_token: any, userRequest: any) => { const payload = { email: email_address, username: user_name, + firstName: userRequest?.first_name, + lastName: userRequest?.last_name, enabled: true, credentials: [ { @@ -63,7 +67,6 @@ export const userCreate = async (access_token: any, userRequest: any) => { .then((response) => { const location = _.get(response, 'headers.location'); const userId = location ? _.last(location.split('/')) : null; - console.log('keyuser', userId); if (!userId) { throw new Error('UserId not found'); } diff --git a/src/main/services/keycloakAuthProvider.ts b/src/main/services/keycloakAuthProvider.ts index 48e746b1..a425f023 100644 --- a/src/main/services/keycloakAuthProvider.ts +++ b/src/main/services/keycloakAuthProvider.ts @@ -15,8 +15,21 @@ export class KeycloakAuthProvider implements BaseAuthProvider { return this.keycloak.middleware(); } - authenticate(): (req: Request, res: Response, next: NextFunction) => void { - return this.keycloak.protect(); + authenticate(): (req: any, res: Response, next: NextFunction) => void { + const protect = this.keycloak.protect(); + + return async (req: any, res: Response, next: NextFunction) => { + protect(req, res, async () => { + try { + if (req.kauth?.grant) { + await authenticated(req); + } + next(); + } catch (error) { + next(error); + } + }); + }; } async logout(req: Request, res: Response): Promise { diff --git a/web-console-v2/src/pages/UserManagement/AddUser.tsx b/web-console-v2/src/pages/UserManagement/AddUser.tsx index 7ce09c6d..f715c01e 100644 --- a/web-console-v2/src/pages/UserManagement/AddUser.tsx +++ b/web-console-v2/src/pages/UserManagement/AddUser.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, MenuItem, Select, InputLabel, FormControl, Button, SelectChangeEvent } from '@mui/material'; +import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, MenuItem, Select, InputLabel, FormControl, Button, SelectChangeEvent, InputAdornment, IconButton } from '@mui/material'; import { UserRequest } from './UserManagement'; import { useUserList } from 'services/user'; import { User } from './UserManagement'; import { useAlert } from 'contexts/AlertContextProvider'; import Alert from '@mui/material/Alert'; +import { Visibility, VisibilityOff } from '@mui/icons-material'; interface AddUserProps { open: boolean; @@ -37,12 +38,20 @@ const AddUser: React.FC = ({ open, onClose, onSubmit, currentUser const { data: users } = useUserList(); const { showAlert } = useAlert(); const [error, setError] = useState(null); + const [passwordRequirements, setPasswordRequirements] = useState({ + length: false, + uppercase: false, + lowercase: false, + number: false, + specialChar: false, + }); + const [showPassword, setShowPassword] = useState(false); useEffect(() => { const userName = newUser?.user_name.replace(/\s+/g, '_'); const emailAddress = newUser?.email_address; if (userName || emailAddress) { - const usernameExists = users?.data?.some((user: { user_name: string; }) => user.user_name === userName); + const usernameExists = users?.data?.some((user: { user_name: string; }) => user.user_name.toLowerCase() === userName.toLowerCase()); setIsUsernameTaken(usernameExists || false); } else { setIsUsernameTaken(null); @@ -58,10 +67,53 @@ const AddUser: React.FC = ({ open, onClose, onSubmit, currentUser const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setNewUser({ - ...newUser, - [name]: value, - }); + + setNewUser(prev => ({ + ...prev, + [name]: name === 'user_name' ? value.toLowerCase() : value + })); + + if (name === 'password') { + validatePassword(value); + } + }; + + const validatePassword = (password: string) => { + const updatedRequirements = { + length: password.length >= 8 && password.length <= 15, + uppercase: /[A-Z]/.test(password), + lowercase: /[a-z]/.test(password), + number: /\d/.test(password), + specialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password), + }; + + setPasswordRequirements(updatedRequirements); + }; + + const getPasswordHelperText = () => { + const requirements = []; + + if (!passwordRequirements.length) { + requirements.push('8-15 characters'); + } + if (!passwordRequirements.uppercase) { + requirements.push('at least one uppercase letter'); + } + if (!passwordRequirements.lowercase) { + requirements.push('at least one lowercase letter'); + } + if (!passwordRequirements.number) { + requirements.push('at least one number'); + } + if (!passwordRequirements.specialChar) { + requirements.push('at least one special character'); + } + + if (requirements.length > 0) { + return `Password must contain: ${requirements.join(', ')}`; + } else { + return ''; + } }; const handleRoleChange = (e: SelectChangeEvent) => { @@ -74,15 +126,14 @@ const AddUser: React.FC = ({ open, onClose, onSubmit, currentUser const handleSubmit = () => { onSubmit(newUser) .then(() => { - onClose(); - resetForm(); + onClose(); + resetForm(); }) .catch(() => { showAlert('Failed to create user', 'error'); setError(true); }); }; - const resetForm = () => { setNewUser({ @@ -93,18 +144,20 @@ const AddUser: React.FC = ({ open, onClose, onSubmit, currentUser }); }; + const isUserNameValid = newUser?.user_name && newUser?.user_name.length >= 3; const isEmailValid = newUser?.email_address ? emailRegex.test(newUser?.email_address) : true; - const isFirstNameValid = !newUser.first_name || newUser.first_name.length >= 3; - const isLastNameValid = !newUser.last_name || newUser.last_name.length >= 3; + const isFirstNameValid = !newUser?.first_name || newUser?.first_name.length >= 3; + const isLastNameValid = !newUser?.last_name || newUser?.last_name.length >= 3; + const isPasswordValid = newUser?.password && getPasswordHelperText() === ''; const isFormValid = - newUser?.user_name && newUser?.email_address && - newUser?.password && + isPasswordValid && newUser?.roles.length > 0 && isUsernameTaken === false && isEmailValid && isFirstNameValid && - isLastNameValid; + isLastNameValid && + isUserNameValid; const availableRoles = currentUser?.is_owner ? rolesOptions : rolesOptions.filter(role => role.value !== 'admin'); @@ -137,8 +190,8 @@ const AddUser: React.FC = ({ open, onClose, onSubmit, currentUser onChange={handleChange} required margin="normal" - error={isUsernameTaken === true} - helperText={isUsernameTaken ? 'Username already exists' : ''} + error={isUsernameTaken === true || isUserNameValid === false} + helperText={isUsernameTaken ? 'Username already exists' : (isUserNameValid === false) ? 'Username must be at least 3 characters' : ''} /> = ({ open, onClose, onSubmit, currentUser value={newUser.first_name} onChange={handleChange} margin="normal" - error={ !isFirstNameValid} + error={!isFirstNameValid} helperText={!isFirstNameValid ? 'If provided, first name must be at least 3 characters' : ''} /> = ({ open, onClose, onSubmit, currentUser + setShowPassword(!showPassword)} + edge="end" + > + {showPassword ? : } + + + ), + }} /> Role