Commit 2a69d860 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '80-admin-user-management' into 'master'

Resolve "Admin: User management"

Closes #80

See merge request genesys-pgr/genesys-ui!78
parents bc92eb56 2c76f6ee
......@@ -80,6 +80,20 @@
"Confirm password": "Confirm password"
}
}
},
"admin": {
"users": {
"list": "Registered user accounts",
"profile": {
"information": "User profile information",
"edit": {
"title": "Update user profile",
"fullName": "Full name",
"email": "E-mail address",
"roles": "User roles"
}
}
}
}
},
"stats": {
......@@ -188,6 +202,14 @@
"country": {
"iso3": "Country code"
}
},
"users": {
"role": "Role",
"enabled": "Active",
"expired": "Exptired",
"locked": "Locked",
"email": "E-mail address",
"uuid": "UUID"
}
},
"subset": {
......
/*
* Defined in Swagger as '#/definitions/UserFilter'
*/
import DateFilter from 'model/filter/DateFilter';
import StringFilter from 'model/filter/StringFilter';
class UserFilter {
public NOT: UserFilter;
public NOTNULL: string[];
public NULL: string[];
public active: boolean;
public createdBy: number[];
public createdDate: DateFilter;
public email: StringFilter;
public enabled: boolean;
public expired: boolean;
public id: number[];
public lastModifiedBy: number[];
public lastModifiedDate: DateFilter;
public locked: boolean;
public role: string[];
public uuid: string[];
public version: number[];
}
export default UserFilter;
import { UuidModel } from 'model/common.model';
class User extends UuidModel {
public static USERROLES: { [key: string]: any; } = {
USER: 'User',
ADMINISTRATOR: 'Administrator',
EVERYONE: 'Everyone',
VALIDATEDUSER: 'Validated user',
VETTEDUSER: 'Vetted user',
CONTENTMANAGER: 'Content manager',
};
public clazz: string = 'org.genesys2.server.model.impl.User';
public accountExpired: boolean;
public accountLocked: boolean;
......@@ -14,6 +23,7 @@ class User extends UuidModel {
public accountExpires: Date;
public lockedUntil: Date;
public passwordExpires: Date;
public lastLogin: Date;
public constructor(obj?) {
super(obj);
......@@ -24,12 +34,17 @@ enum UserRole {
USER = 'USER',
ADMINISTRATOR = 'ADMINISTRATOR',
EVERYONE = 'EVERYONE',
VALIDATEDUSER = 'VALIDATEDUSER',
VETTEDUSER = 'VETTEDUSER',
CONTENTMANAGER = 'CONTENTMANAGER',
}
enum AccountType {
LOCAL = 'LOCAL',
GOOGLE = 'GOOGLE',
SYSTEM = 'SYSTEM',
DELETED = 'DELETED',
LDAP = 'LDAP',
}
export { User, UserRole, AccountType };
import { log } from 'utilities/debug';
import { axiosBackend } from 'utilities/requestUtils';
import { User } from 'model/user/User';
const REGISTER_URL = `/api/v1/user/register`;
const GET_USER_PROFILE_URL = `/api/v0/me/profile`;
const CHANGE_USER_PASSWORD_URL = `/api/v0/me/password`;
export class UserProfileService {
// Get current user profile
public static getProfile(): Promise<User> {
log('Loading current user profile.');
return axiosBackend.request({
url: `${GET_USER_PROFILE_URL}`,
method: 'GET',
}).then(({data}) => new User(data));
}
// Change user password
public static changePassword(newPassword: string, oldPassword: string): Promise<any> {
log('Changing user password');
const data = new FormData();
data.append('old', oldPassword);
data.append('new', newPassword);
return axiosBackend.request({
url: `${CHANGE_USER_PASSWORD_URL}`,
method: 'POST',
data,
});
}
public static register(email: string, password: string, fullName: string, captcha: any) {
const form = new FormData();
form.append('email', email);
form.append('pass', password);
form.append('fullName', fullName);
form.append('g-recaptcha-response', captcha);
return axiosBackend.post(REGISTER_URL, form)
.then(({data}) => data);
}
}
import { log } from 'utilities/debug';
import { axiosBackend } from 'utilities/requestUtils';
import * as UrlTemplate from 'url-template';
import { User } from 'model/user/User';
import * as QueryString from 'query-string';
import UserFilter from 'model/UserFilter';
import FilteredPage, {IPageRequest} from 'model/FilteredPage';
const REGISTER_URL = `/api/v1/user/register`;
const GET_USER_PROFILE_URL = `/api/v0/me/profile`;
const CHANGE_USER_PASSWORD_URL = `/api/v0/me/password`;
const URL_LIST_USERS = `/api/v1/user/list`;
const URL_GET_USER = UrlTemplate.parse(`/api/v1/user/u/{uuid}`);
const URL_DISABLE_ACCOUNT = UrlTemplate.parse(`/api/v1/user/u/{uuid}/disable`);
const URL_ENABLE_ACCOUNT = UrlTemplate.parse(`/api/v1/user/u/{uuid}/enable`);
const URL_LOCK_ACCOUNT = UrlTemplate.parse(`/api/v1/user/u/{uuid}/lock`);
const URL_UNLOCK_ACCOUNT = UrlTemplate.parse(`/api/v1/user/u/{uuid}/unlock`);
const URL_ARCHIVE_ACCOUNT = UrlTemplate.parse(`/api/v1/user/u/{uuid}/archive`);
const URL_SEND_EMAIL = UrlTemplate.parse(`/api/v1/user/u/{uuid}/email-verification`);
const URL_UPDATE_USER = `/api/v1/user/user`;
export class UserService {
// Get current user profile
public static getProfile(): Promise<User> {
log('Loading current user profile.');
return axiosBackend.request({
url: `${GET_USER_PROFILE_URL}`,
method: 'GET',
}).then(({data}) => new User(data));
}
// Change user password
public static changePassword(newPassword: string, oldPassword: string): Promise<any> {
log('Changing user password');
const data = new FormData();
data.append('old', oldPassword);
data.append('new', newPassword);
return axiosBackend.request({
url: `${CHANGE_USER_PASSWORD_URL}`,
method: 'POST',
data,
});
}
public static register(email: string, password: string, fullName: string, captcha: any) {
const form = new FormData();
form.append('email', email);
form.append('pass', password);
form.append('fullName', fullName);
form.append('g-recaptcha-response', captcha);
return axiosBackend.post(REGISTER_URL, form)
.then(({data}) => data);
}
/**
* listUsers at /api/v1/user/list
*
* @param filter the user filter
* @param page the page
*/
public static listUsers(filter: string | UserFilter, page: IPageRequest): Promise<FilteredPage<User>> {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
p: page.page || undefined,
l: page.size || 100,
d: page.direction && page.direction.length && page.direction || undefined,
s: page.properties && page.properties.length && page.properties || undefined,
}, {});
const apiUrl = URL_LIST_USERS + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { data: typeof filter === 'string' ? null : { ...filter } };
return axiosBackend({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as FilteredPage<User>);
}
/**
* getUser at /api/v1/user/u/{uuid}
*
* @param uuid uuid
*/
public static getUser(uuid: string): Promise<User> {
const apiUrl = URL_GET_USER.expand({uuid});
return axiosBackend({
url: apiUrl,
method: 'GET',
}).then(({ data }) => data as User);
}
/**
* disableAccount at /api/v1/user/u/{uuid}/disable
*
* @param uuid uuid
*/
public static disableAccount(uuid: string): Promise<User> {
const apiUrl = URL_DISABLE_ACCOUNT.expand({uuid});
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as User);
}
/**
* enableAccount at /api/v1/user/u/{uuid}/enable
*
* @param uuid uuid
*/
public static enableAccount(uuid: string): Promise<User> {
const apiUrl = URL_ENABLE_ACCOUNT.expand({uuid});
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as User);
}
/**
* lockAccount at /api/v1/user/u/{uuid}/lock
*
* @param uuid uuid
*/
public static lockAccount(uuid: string): Promise<User> {
const apiUrl = URL_LOCK_ACCOUNT.expand({uuid});
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as User);
}
/**
* unlockAccount at /api/v1/user/u/{uuid}/unlock
*
* @param uuid uuid
*/
public static unlockAccount(uuid: string): Promise<User> {
const apiUrl = URL_UNLOCK_ACCOUNT.expand({uuid});
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as User);
}
/**
* archiveAccount at /api/v1/user/u/{uuid}/archive
*
* @param uuid uuid
*/
public static archiveAccount(uuid: string): Promise<User> {
const apiUrl = URL_ARCHIVE_ACCOUNT.expand({uuid});
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as User);
}
/**
* sendEmail at /api/v1/user/u/{uuid}/email-verification
*
* @param uuid uuid
*/
public static sendEmail(uuid: string): Promise<boolean> {
const apiUrl = URL_SEND_EMAIL.expand({uuid});
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as boolean);
}
/**
* updateUser at /api/v1/user/user
*
* @param user user
*/
public static updateUser(user: User): Promise<User> {
const apiUrl = URL_UPDATE_USER;
const content = { data: user };
return axiosBackend({
url: apiUrl,
method: 'PUT',
...content,
}).then(({ data }) => data as User);
}
}
import * as React from 'react';
import Button from '@material-ui/core/Button';
interface IBackButtonProps extends React.ClassAttributes<any> {
interface IActionButtonProps extends React.ClassAttributes<any> {
variant?: 'text' | 'flat' | 'outlined' | 'contained' | 'raised' | 'fab' | 'extendedFab';
title: string;
action: any;
style?: any;
}
class ActionButton extends React.Component<IBackButtonProps, any> {
class ActionButton extends React.Component<IActionButtonProps, any> {
public render() {
const { title, action, variant = 'raised', style = { margin: '0 8px'} } = this.props;
const { title, action, variant = 'raised', style } = this.props;
return (
<Button style={ style } variant={ variant } onClick={ action }>
{ title }
......
......@@ -12,6 +12,7 @@ interface IBackButtonProps extends React.ClassAttributes<any> {
lastLocation: string;
preferDefaultTarget?: boolean;
navigateTo: (path: string) => void;
style?: any;
}
class BackButton extends React.Component<IBackButtonProps, any> {
......@@ -34,9 +35,9 @@ class BackButton extends React.Component<IBackButtonProps, any> {
}
public render() {
const { defaultBackText} = this.props;
const { defaultBackText, style } = this.props;
return (
<Button variant="raised" onClick={ this.onGoBack }>
<Button variant="raised" style={ style } onClick={ this.onGoBack }>
{ defaultBackText || 'BACK' }
</Button>
);
......
......@@ -11,6 +11,7 @@ import * as flattenjs from 'flattenjs';
import * as _ from 'lodash';
import { cleanFilters } from 'utilities';
import Accession from 'model/accession/Accession';
import {User} from 'model/user/User';
import PrettyDate from 'ui/common/time/PrettyDate';
/**
......@@ -182,6 +183,7 @@ const mapStateToProps = (state, ownProps) => ({
category: '',
sampStat: Accession.SAMPSTAT,
storage: Accession.STORAGE,
role: User.USERROLES,
accessions: {
true: 'Yes',
false: 'No',
......
......@@ -2,17 +2,49 @@ import * as React from 'react';
import {withStyles} from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
const styles = {
/*tslint:disable*/
const styles = (theme) => ({
subHeader: {
display: 'flex',
backgroundColor: '#D4D2C7',
padding: '10px 20px',
borderBottom: '1px solid #B2AFA6',
'& > div': {
[theme.breakpoints.down('sm')]: {
display: 'flex' as 'flex',
width: '100%',
},
},
},
flexGrow: {
flex: '1 1 auto',
display: 'flex' as 'flex',
alignItems: 'center' as 'center',
position: 'sticky' as 'sticky',
overflow: 'hidden' as 'hidden',
height: '48px',
left: '100%',
padding: '0 16px',
'& > * > *': {
margin: '8px',
[theme.breakpoints.down('sm')]: {
width: '100%',
margin: '8px 0',
},
},
[theme.breakpoints.down('sm')]: {
position: 'initial' as 'initial',
overflow: 'initial' as 'initial',
display: 'initial' as 'initial',
padding: '0',
height: 'auto' as 'auto',
width: '100%',
'& > *': {
width: '100%',
},
},
},
};
});
/*tslint:enable*/
interface IContentHeaderWithButtonProps extends React.ClassAttributes<any> {
classes: any;
......@@ -27,16 +59,16 @@ class ContentHeaderWithButton extends React.Component<IContentHeaderWithButtonPr
}
public render() {
const {classes, title, buttons} = this.props;
return (
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } className={ classes.subHeader }>
<Grid container spacing={ 0 } item xs={ 12 } className={ classes.subHeader }>
<Grid item>
<h3 className="lh-35 m-0">
{ title }
</h3>
<div className={ classes.flexGrow }/>
</Grid>
<Grid item className={ `float-right ${classes.flexGrow}` }>
{ buttons && buttons }
</Grid>
</Grid>
......
......@@ -7,6 +7,7 @@ import Accession from 'model/accession/Accession';
import FaoInstitute from 'model/genesys/FaoInstitute';
import MaterialRequest from 'model/request/MaterialRequest';
import Country from 'model/geo/Country';
import {User} from 'model/user/User';
function SubsetLink({ to: subset, edit = false, children = null }
: { to: Subset, edit?: boolean, children?: any }) {
......@@ -72,6 +73,18 @@ function RequestLink({ to: request, edit = false, children = null }
}
}
function UserLink({ to: user, edit = false, children = null }: { to: User, edit?: boolean, children?: any }) {
if (user) {
return (
<Link to={ `/admin/users/${user.uuid}` }>
{ children || user.uuid }
</Link>
);
} else {
return null;
}
}
const CountryLink = ({ country, noflag, children }: { country: Country, noflag?: boolean, children?: any }) => {
if (! country) {
return null;
......@@ -99,4 +112,4 @@ const DatasetLink = ({ to: dataset, edit = false, children = null }
}
};
export { SubsetLink, AccessionLink, InstituteLink, RequestLink, CountryLink, DatasetLink};
export { SubsetLink, AccessionLink, InstituteLink, RequestLink, CountryLink, DatasetLink, UserLink};
......@@ -11,6 +11,7 @@ interface IBrowsePageProps<T> extends React.ClassAttributes<any> {
loadDataPage: any;
updateRoute: any;
currentTab: any;
t?: any;
}
class BrowsePage<T> extends React.Component<IBrowsePageProps<T>, any> {
......
import {UserService} from 'service/UserService';
import FilteredPage, {IPageRequest} from 'model/FilteredPage';
import {User} from 'model/user/User';
import {ADMIN_APPEND_USERS, ADMIN_RECEIVE_USER, ADMIN_RECEIVE_USERS} from 'user/ui/constants';
import {showSnackbar} from 'actions/snackbar';
import navigateTo from 'actions/navigation';
import {log} from 'utilities/debug';
import * as _ from 'lodash';
import UserFilter from 'model/UserFilter';
const receiveUsers = (paged: FilteredPage<User>, error = null) => ({
type: ADMIN_RECEIVE_USERS,
payload: { paged, error },
});
const appendUsers = (paged: FilteredPage<User>, error = null) => ({
type: ADMIN_APPEND_USERS,
payload: { paged, error },
});
const receiveUser = (user: User, error = null) => ({
type: ADMIN_RECEIVE_USER,
payload: { user, error },
});
export const loadUser = (uuid: string) => (dispatch) => {
return UserService.getUser(uuid)
.then((user) => {
dispatch(receiveUser(user));
})
.catch((error) => {
dispatch(receiveUser(null, error));
});
};
export const enableAccount = (uuid: string, enable: boolean = true) => (dispatch) => {
const updateAccountStatus = enable ? UserService.enableAccount : UserService.disableAccount;
return updateAccountStatus(uuid)
.then((user) => {
dispatch(receiveUser(user));
dispatch(showSnackbar(`Account is ${enable ? 'enabled' : 'disabled'}`));
})
.catch((error) => {
const data = _.get(error, 'response.data');
log('Error', data.error);
dispatch(showSnackbar(data.error || 'Error...'));
});
};
export const lockAccount = (uuid: string, lock: boolean = true) => (dispatch) => {
const updateAccountStatus = lock ? UserService.lockAccount : UserService.unlockAccount;