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);