diff --git a/admin/src/App.js b/admin/src/App.js
index ea89762..3dc711d 100644
--- a/admin/src/App.js
+++ b/admin/src/App.js
@@ -15,12 +15,16 @@ const App = () => (
layout={OpenGardenAdminLayout}
authProvider={AuthProvider}
dataProvider={Provider(BASE_URL, HttpClient)}>
-
+ {permissions => (
+ <>
+
+ >
+ )}
);
diff --git a/admin/src/configs/AuthProvider.js b/admin/src/configs/AuthProvider.js
index 0200b84..3344b6b 100644
--- a/admin/src/configs/AuthProvider.js
+++ b/admin/src/configs/AuthProvider.js
@@ -1,10 +1,11 @@
import axios from 'axios';
import { BASE_URL } from './BaseUrl';
import jwt_decode from "jwt-decode";
+import { GetPermissions } from '../helpers/GetPermissions';
export const AuthProvider = {
login: (params) => new Promise((resolve, reject) => {
- axios.post(BASE_URL + '/account/login', {
+ return axios.post(BASE_URL + '/account/login', {
email: params.username,
password: params.password
}, {
@@ -12,21 +13,25 @@ export const AuthProvider = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
- })
- .then(request => {
- localStorage.setItem('auth', JSON.stringify(request.data.access_token));
- return resolve();
- })
- .catch(exception => {
- return reject(exception);
- })
+ }).then(request => {
+ localStorage.setItem('access_token', JSON.stringify(request.data.access_token));
+ localStorage.setItem('user', JSON.stringify(jwt_decode(request.data.access_token)));
+ return resolve();
+ }).catch(() => {
+ return reject();
+ });
}),
logout: () => {
- localStorage.removeItem('auth');
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('user');
return Promise.resolve();
},
checkAuth: () => new Promise((resolve, reject) => {
- const ticket = jwt_decode(localStorage.getItem('auth'));
+ if (!localStorage.getItem('access_token') || !localStorage.getItem('user')) {
+ return reject();
+ }
+
+ const ticket = JSON.parse(localStorage.getItem('user'));
const expiration = Math.floor((Date.now() / 1000) - (1000 * 60 * 5));
if (ticket.exp < expiration) {
@@ -37,15 +42,23 @@ export const AuthProvider = {
checkError: (error) => {
const status = error.status;
if (status === 401 || status === 403) {
- localStorage.removeItem('auth');
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('user');
return Promise.reject();
}
return Promise.resolve();
},
getIdentity: () => {
- const user = jwt_decode(localStorage.getItem('auth'));
+ const user = JSON.parse(localStorage.getItem('user'));
user.fullName = user.username;
- return user;
+ return Promise.resolve(user);
+ },
+ getPermissions: () => {
+ try {
+ return Promise.resolve(GetPermissions());
+ } catch (exception) {
+ console.log('getPermissions error', exception)
+ return Promise.resolve([]);
+ }
},
- getPermissions: () => Promise.resolve(''),
}
diff --git a/admin/src/configs/HttpClient.js b/admin/src/configs/HttpClient.js
index 9c1f89e..5fe63f2 100644
--- a/admin/src/configs/HttpClient.js
+++ b/admin/src/configs/HttpClient.js
@@ -4,7 +4,7 @@ export const HttpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: "application/json" });
}
- const access_token = JSON.parse(localStorage.getItem("auth"));
+ const access_token = JSON.parse(localStorage.getItem("access_token"));
options.headers.set("Authorization", `Bearer ${access_token}`);
return fetchUtils.fetchJson(url, options);
};
diff --git a/admin/src/configs/Provider.js b/admin/src/configs/Provider.js
index 79b0db3..6d99ba3 100644
--- a/admin/src/configs/Provider.js
+++ b/admin/src/configs/Provider.js
@@ -5,7 +5,6 @@ import { fetchUtils, DataProvider } from 'ra-core';
export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
getList: (resource, params) => {
const { page, perPage } = params.pagination;
- // const { field, order } = params.sort;
const query = {
...fetchUtils.flattenObject(params.filter),
offset: (page - 1) * perPage,
diff --git a/admin/src/helpers/GetPermissions.js b/admin/src/helpers/GetPermissions.js
new file mode 100644
index 0000000..8ae72d3
--- /dev/null
+++ b/admin/src/helpers/GetPermissions.js
@@ -0,0 +1,5 @@
+export const GetPermissions = () => {
+ const user = JSON.parse(localStorage.getItem('user'));
+ const permissions = user.roles || [];
+ return permissions;
+}
diff --git a/admin/src/views/plants.js b/admin/src/views/plants.js
index 162e799..d949fdd 100644
--- a/admin/src/views/plants.js
+++ b/admin/src/views/plants.js
@@ -7,8 +7,8 @@ import {
Show,
RichTextField,
ReferenceOneField,
- EditButton,
ShowButton,
+ EditButton,
Create,
TextInput,
SimpleFormIterator,
@@ -24,9 +24,10 @@ import {
} from "react-admin";
import { RichTextInput } from 'ra-input-rich-text';
import { StringToLabelObject } from '../helpers/StringToLabelObject';
+import { GetPermissions } from "../helpers/GetPermissions";
-export const PlantCreate = () => (
-
+export const PlantCreate = (props) => (
+
@@ -49,8 +50,8 @@ export const PlantCreate = () => (
);
-export const PlantEdit = () => (
-
+export const PlantEdit = (props) => (
+
@@ -73,24 +74,27 @@ export const PlantEdit = () => (
);
-export const PlantsList = (props) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
+export const PlantsList = (props) => {
+ const permissions = GetPermissions();
+ return (
+
+ permissions.includes('ADMIN')}>
+
+
+
+
+
+
+
+ {permissions.includes('ADMIN') && }
+
+
+
+ );
+};
export const PlantShow = (props) => (
-
+
diff --git a/core/src/app.module.ts b/core/src/app.module.ts
index 23a996e..04d05fa 100644
--- a/core/src/app.module.ts
+++ b/core/src/app.module.ts
@@ -9,6 +9,7 @@ import { JwtGuard } from './auth/jwt.guard';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { ControllersModule } from './controllers/controllers.module';
import { EntitiesModule } from './entities/entities.module';
+import { RolesGuard } from './auth/roles/roles.guard';
@Module({
imports: [
@@ -44,6 +45,10 @@ import { EntitiesModule } from './entities/entities.module';
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
+ {
+ provide: APP_GUARD,
+ useClass: RolesGuard,
+ },
],
})
export class AppModule {}
diff --git a/core/src/auth/auth.service.ts b/core/src/auth/auth.service.ts
index 91307a5..5d52738 100644
--- a/core/src/auth/auth.service.ts
+++ b/core/src/auth/auth.service.ts
@@ -32,6 +32,7 @@ export class AuthService {
sub: user._id.toString(),
email: user.email,
username: user.username,
+ roles: user.roles,
};
return {
access_token: this.jwtService.sign(payload),
diff --git a/core/src/auth/models/jwt.content.ts b/core/src/auth/models/jwt.content.ts
index ff30e7e..8c691fa 100644
--- a/core/src/auth/models/jwt.content.ts
+++ b/core/src/auth/models/jwt.content.ts
@@ -1,3 +1,5 @@
+import { Role } from '../roles/role.enum';
+
export interface JwtContent {
/**
* User ID
@@ -5,4 +7,5 @@ export interface JwtContent {
sub: string;
username: string;
email: string;
+ roles: Role[];
}
diff --git a/core/src/auth/roles/role.enum.ts b/core/src/auth/roles/role.enum.ts
new file mode 100644
index 0000000..7c99d21
--- /dev/null
+++ b/core/src/auth/roles/role.enum.ts
@@ -0,0 +1,4 @@
+export enum Role {
+ USER = 'USER',
+ ADMIN = 'ADMIN',
+}
diff --git a/core/src/auth/roles/roles.decorator.ts b/core/src/auth/roles/roles.decorator.ts
new file mode 100644
index 0000000..b687140
--- /dev/null
+++ b/core/src/auth/roles/roles.decorator.ts
@@ -0,0 +1,5 @@
+import { SetMetadata } from '@nestjs/common';
+import { Role } from './role.enum';
+
+export const ROLES_KEY = 'roles';
+export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
diff --git a/core/src/auth/roles/roles.guard.ts b/core/src/auth/roles/roles.guard.ts
new file mode 100644
index 0000000..a6abc07
--- /dev/null
+++ b/core/src/auth/roles/roles.guard.ts
@@ -0,0 +1,21 @@
+import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { Role } from './role.enum';
+import { ROLES_KEY } from './roles.decorator';
+
+@Injectable()
+export class RolesGuard implements CanActivate {
+ constructor(private reflector: Reflector) {}
+
+ canActivate(context: ExecutionContext): boolean {
+ const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
+ context.getHandler(),
+ context.getClass(),
+ ]);
+ if (!requiredRoles) {
+ return true;
+ }
+ const { user } = context.switchToHttp().getRequest();
+ return requiredRoles.some((role) => user.roles?.includes(role));
+ }
+}
diff --git a/core/src/controllers/account/account.controller.ts b/core/src/controllers/account/account.controller.ts
index 3bf0865..0fcb3c7 100644
--- a/core/src/controllers/account/account.controller.ts
+++ b/core/src/controllers/account/account.controller.ts
@@ -13,6 +13,7 @@ import { UsersService } from '../../users/users.service';
import { RegisterRequestBody } from './models/register.request.body';
import { HashService } from '../../auth/hash.service';
import { ErrorsRequestBody } from '../models/errors.response.body';
+import { Role } from '../../auth/roles/role.enum';
@ApiTags('Account')
@Controller('/account')
@@ -53,6 +54,7 @@ export class AccountController {
const createUser: User = {
...registerRequestBody,
_id: null,
+ roles: [Role.USER],
password: this.hashService.hash(registerRequestBody.password),
};
const user = await this.usersService.create(createUser);
diff --git a/core/src/controllers/floors/floors.controller.ts b/core/src/controllers/floors/floors.controller.ts
index 7f6ddd7..cc9e3ef 100644
--- a/core/src/controllers/floors/floors.controller.ts
+++ b/core/src/controllers/floors/floors.controller.ts
@@ -20,6 +20,8 @@ import { CreateFloorRequestBody } from './models/floor.request.body';
import { FloorsSearchRequestQuery } from './models/floor.request.query';
import { ErrorsRequestBody } from '../models/errors.response.body';
import { Response as Res } from 'express';
+import { Roles } from '../../auth/roles/roles.decorator';
+import { Role } from '../../auth/roles/role.enum';
@ApiBearerAuth()
@ApiTags('Floors')
@@ -30,6 +32,8 @@ export class FloorsController {
constructor(private floorsService: FloorsService, @InjectMapper() private mapper: Mapper) {}
@Post()
+ @Roles(Role.ADMIN)
+ @ApiResponse({ status: 403, description: 'Forbidden' })
@ApiResponse({ status: 201, type: FloorResponseBody })
@ApiResponse({
status: 400,
diff --git a/core/src/controllers/plants/plants.controller.ts b/core/src/controllers/plants/plants.controller.ts
index c95d9b1..99342a6 100644
--- a/core/src/controllers/plants/plants.controller.ts
+++ b/core/src/controllers/plants/plants.controller.ts
@@ -20,6 +20,8 @@ import { CreatePlantRequestBody } from './models/plant.request.body';
import { PlantsSearchRequestQuery } from './models/plant.request.query';
import { ErrorsRequestBody } from '../models/errors.response.body';
import { Response as Res } from 'express';
+import { Roles } from '../../auth/roles/roles.decorator';
+import { Role } from '../../auth/roles/role.enum';
@ApiBearerAuth()
@ApiTags('Plants')
@@ -30,6 +32,8 @@ export class PlantsController {
constructor(private plantsService: PlantsService, @InjectMapper() private mapper: Mapper) {}
@Post()
+ @Roles(Role.ADMIN)
+ @ApiResponse({ status: 403, description: 'Forbidden' })
@ApiResponse({ status: 201, type: PlantResponseBody })
@ApiResponse({
status: 400,
diff --git a/core/src/controllers/profiles/models/mapper.profiles.ts b/core/src/controllers/profiles/models/mapper.profiles.ts
index 12d2185..aa53f28 100644
--- a/core/src/controllers/profiles/models/mapper.profiles.ts
+++ b/core/src/controllers/profiles/models/mapper.profiles.ts
@@ -24,6 +24,10 @@ export class ProfileMapperProfiles extends AutomapperProfile {
(d) => d.username,
mapFrom((s) => s.username),
),
+ forMember(
+ (d) => d.roles,
+ mapFrom((s) => s.roles),
+ ),
);
};
}
diff --git a/core/src/controllers/profiles/models/profile.response.body.ts b/core/src/controllers/profiles/models/profile.response.body.ts
index 56eb3a6..b57552d 100644
--- a/core/src/controllers/profiles/models/profile.response.body.ts
+++ b/core/src/controllers/profiles/models/profile.response.body.ts
@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
+import { Role } from '../../../auth/roles/role.enum';
export class ProfileResponseBody {
@ApiProperty()
@@ -6,4 +7,7 @@ export class ProfileResponseBody {
@ApiProperty()
username: string;
+
+ @ApiProperty({ isArray: true, enum: Role })
+ roles: Role[];
}
diff --git a/core/src/controllers/varieties/varieties.controller.ts b/core/src/controllers/varieties/varieties.controller.ts
index 2dd2356..24c9f07 100644
--- a/core/src/controllers/varieties/varieties.controller.ts
+++ b/core/src/controllers/varieties/varieties.controller.ts
@@ -10,6 +10,8 @@ import { CreateVarietyRequestBody } from './models/variety.request.body';
import { VarietiesSearchRequestQuery } from './models/variety.request.query';
import mongoose from 'mongoose';
import { ErrorsRequestBody } from '../models/errors.response.body';
+import { Roles } from '../../auth/roles/roles.decorator';
+import { Role } from '../../auth/roles/role.enum';
@ApiBearerAuth()
@ApiTags('Varieties')
@@ -20,6 +22,8 @@ export class VarietiesController {
constructor(private plantsService: VarietiesService, @InjectMapper() private mapper: Mapper) {}
@Post()
+ @Roles(Role.ADMIN)
+ @ApiResponse({ status: 403, description: 'Forbidden' })
@ApiResponse({ status: 201, type: VarietyResponseBody })
@ApiResponse({
status: 400,
diff --git a/core/src/users/models/user.entity.ts b/core/src/users/models/user.entity.ts
index cccc701..e26503e 100644
--- a/core/src/users/models/user.entity.ts
+++ b/core/src/users/models/user.entity.ts
@@ -1,5 +1,6 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
+import { Role } from '../../auth/roles/role.enum';
export type UserDocument = User & Document;
@@ -15,6 +16,9 @@ export class User {
@Prop({ required: true })
password: string;
+
+ @Prop({ required: true })
+ roles: Role[];
}
export const UserSchema = SchemaFactory.createForClass(User);