adds roles
This commit is contained in:
parent
d803aaaf1b
commit
84b45cd6b1
@ -6,6 +6,8 @@ import { AuthContext } from './utils/auth_context';
|
|||||||
import { useApi } from './utils/use_api';
|
import { useApi } from './utils/use_api';
|
||||||
import { useJwtRefresh } from './utils/use_jwt_refresh';
|
import { useJwtRefresh } from './utils/use_jwt_refresh';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
import { RolesContext } from './utils/roles_context';
|
||||||
|
import { parseJwt } from './utils/parse_jwt';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [authToken, setAuthToken] = useState(null);
|
const [authToken, setAuthToken] = useState(null);
|
||||||
@ -26,16 +28,21 @@ export const App = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// before displaying anything try getting a token using cookies,
|
const jwtPayload = parseJwt(authToken);
|
||||||
|
console.log(jwtPayload);
|
||||||
|
|
||||||
|
// don't display anything while trying to get user token
|
||||||
// can display a loading screen here if desired
|
// can display a loading screen here if desired
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={[authToken, setAuthToken]}>
|
<AuthContext.Provider value={[authToken, setAuthToken]}>
|
||||||
<ApiContext.Provider value={api}>
|
<ApiContext.Provider value={api}>
|
||||||
|
<RolesContext.Provider value={jwtPayload.roles}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Router />
|
<Router />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
</RolesContext.Provider>
|
||||||
</ApiContext.Provider>
|
</ApiContext.Provider>
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
21
client/components/admin/_admin.jsx
Normal file
21
client/components/admin/_admin.jsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useState, useContext, useEffect } from 'react';
|
||||||
|
import { ApiContext } from '../../utils/api_context';
|
||||||
|
|
||||||
|
export const Admin = () => {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const api = useContext(ApiContext);
|
||||||
|
|
||||||
|
useEffect(async () => {
|
||||||
|
const { users } = await api.get('/users');
|
||||||
|
setUsers(users);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-3xl">Users</h2>
|
||||||
|
{users.map((user) => (
|
||||||
|
<div>{user.name}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,10 +1,16 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { ApiContext } from '../../utils/api_context';
|
import { ApiContext } from '../../utils/api_context';
|
||||||
import { AuthContext } from '../../utils/auth_context';
|
import { AuthContext } from '../../utils/auth_context';
|
||||||
|
import { RolesContext } from '../../utils/roles_context';
|
||||||
|
import { Button } from '../common/button';
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const [, setAuthToken] = useContext(AuthContext);
|
const [, setAuthToken] = useContext(AuthContext);
|
||||||
const api = useContext(ApiContext);
|
const api = useContext(ApiContext);
|
||||||
|
const roles = useContext(RolesContext);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
@ -26,11 +32,16 @@ export const Home = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="p-4">
|
||||||
<h1>Welcome {user.name}</h1>
|
<h1>Welcome {user.name}</h1>
|
||||||
<button type="button" onClick={logout}>
|
<Button type="button" onClick={logout}>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</Button>
|
||||||
|
{roles.includes('admin') && (
|
||||||
|
<Button type="button" onClick={() => navigate('/admin')}>
|
||||||
|
Admin
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import { Home } from './home/_home';
|
|||||||
import { AuthContext } from '../utils/auth_context';
|
import { AuthContext } from '../utils/auth_context';
|
||||||
import { SignIn } from './sign_in/_sign_in';
|
import { SignIn } from './sign_in/_sign_in';
|
||||||
import { SignUp } from './sign_up/_sign_up';
|
import { SignUp } from './sign_up/_sign_up';
|
||||||
|
import { Admin } from './admin/_admin';
|
||||||
|
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
const [authToken] = useContext(AuthContext);
|
const [authToken] = useContext(AuthContext);
|
||||||
@ -14,6 +15,7 @@ export const Router = () => {
|
|||||||
path="/"
|
path="/"
|
||||||
element={authToken ? <Home /> : <Navigate replace to="signin" />} // no token means not logged in
|
element={authToken ? <Home /> : <Navigate replace to="signin" />} // no token means not logged in
|
||||||
/>
|
/>
|
||||||
|
<Route path="admin" element={<Admin />} />
|
||||||
<Route path="signin" element={<SignIn />} />
|
<Route path="signin" element={<SignIn />} />
|
||||||
<Route path="signup" element={<SignUp />} />
|
<Route path="signup" element={<SignUp />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
5
client/utils/parse_jwt.js
Normal file
5
client/utils/parse_jwt.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const parseJwt = (token) => {
|
||||||
|
if (!token) return {};
|
||||||
|
const jwtPayload = JSON.parse(window.atob(token.split('.')[1]));
|
||||||
|
return jwtPayload;
|
||||||
|
};
|
3
client/utils/roles_context.js
Normal file
3
client/utils/roles_context.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const RolesContext = createContext({});
|
@ -36,11 +36,13 @@
|
|||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
"@nestjs/serve-static": "^2.2.2",
|
"@nestjs/serve-static": "^2.2.2",
|
||||||
"@nestjs/typeorm": "^8.0.2",
|
"@nestjs/typeorm": "^8.0.2",
|
||||||
|
"@types/lodash": "^4.14.177",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"hbs": "^4.1.2",
|
"hbs": "^4.1.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Controller, Get, Render } from '@nestjs/common';
|
import { Controller, Get, Render } from '@nestjs/common';
|
||||||
|
import { Skip } from './decorators/skip.decorator';
|
||||||
|
import { AuthGuard } from './providers/guards/auth.guard';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
@Get()
|
@Get()
|
||||||
@Render('index')
|
@Render('index')
|
||||||
|
@Skip(AuthGuard)
|
||||||
index() {}
|
index() {}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,24 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { config } from './database/config';
|
import { config } from './database/config';
|
||||||
import { UsersModule } from './modules/users.module';
|
import { UsersModule } from './modules/users.module';
|
||||||
|
import { AuthGuard } from './providers/guards/auth.guard';
|
||||||
|
import { RolesGuard } from './providers/guards/roles.guard';
|
||||||
import { JwtService } from './providers/services/jwt.service';
|
import { JwtService } from './providers/services/jwt.service';
|
||||||
|
import { RolesService } from './providers/services/roles.service';
|
||||||
|
import { UsersService } from './providers/services/users.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forRoot(config), UsersModule],
|
imports: [TypeOrmModule.forRoot(config), UsersModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [JwtService],
|
providers: [
|
||||||
|
UsersService,
|
||||||
|
RolesService,
|
||||||
|
JwtService,
|
||||||
|
{ provide: APP_GUARD, useClass: AuthGuard }, // auth guard should come before roles guard
|
||||||
|
{ provide: APP_GUARD, useClass: RolesGuard }, // otherwise users won't be authenticated before roles check
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -4,14 +4,18 @@ import { UsersService } from 'server/providers/services/users.service';
|
|||||||
import { SignInDto } from 'server/dto/sign_in.dto';
|
import { SignInDto } from 'server/dto/sign_in.dto';
|
||||||
import { RefreshTokenBody } from 'server/dto/refresh_token_body.dto';
|
import { RefreshTokenBody } from 'server/dto/refresh_token_body.dto';
|
||||||
import { JwtService } from 'server/providers/services/jwt.service';
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
|
import { Skip } from 'server/decorators/skip.decorator';
|
||||||
|
import { AuthGuard } from 'server/providers/guards/auth.guard';
|
||||||
|
import { RolesService } from 'server/providers/services/roles.service';
|
||||||
|
|
||||||
// this is kind of a misnomer because we are doing token based auth
|
// this is kind of a misnomer because we are doing token based auth
|
||||||
// instead of session based auth
|
// instead of session based auth
|
||||||
@Controller()
|
@Controller()
|
||||||
export class RefreshTokensController {
|
export class RefreshTokensController {
|
||||||
constructor(private usersService: UsersService, private jwtService: JwtService) {}
|
constructor(private usersService: UsersService, private rolesService: RolesService, private jwtService: JwtService) {}
|
||||||
|
|
||||||
@Get('/refresh_token')
|
@Get('/refresh_token')
|
||||||
|
@Skip(AuthGuard)
|
||||||
async get(@Body() body: SignInDto, @Req() req: Request) {
|
async get(@Body() body: SignInDto, @Req() req: Request) {
|
||||||
const refreshToken: string = req.cookies['_refresh_token'];
|
const refreshToken: string = req.cookies['_refresh_token'];
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
@ -20,13 +24,15 @@ export class RefreshTokensController {
|
|||||||
|
|
||||||
const tokenBody = this.jwtService.parseRefreshToken(refreshToken) as RefreshTokenBody;
|
const tokenBody = this.jwtService.parseRefreshToken(refreshToken) as RefreshTokenBody;
|
||||||
|
|
||||||
const user = await this.usersService.find(tokenBody.userId, ['refreshTokens']);
|
const user = await this.usersService.find(tokenBody.userId, ['refreshTokens', 'userRoles']);
|
||||||
|
const userRoles = await this.rolesService.findByIds(user.userRoles.map((ur) => ur.roleId));
|
||||||
|
|
||||||
const userRefreshToken = user.refreshTokens.find((t) => t.id === tokenBody.id);
|
const userRefreshToken = user.refreshTokens.find((t) => t.id === tokenBody.id);
|
||||||
if (!userRefreshToken) {
|
if (!userRefreshToken) {
|
||||||
throw new HttpException('User refresh token not found', 401);
|
throw new HttpException('User refresh token not found', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = this.jwtService.issueToken({ userId: user.id });
|
const token = this.jwtService.issueToken({ userId: user.id, roles: userRoles.map((r) => r.key) });
|
||||||
return { token };
|
return { token };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,9 @@ import { SignInDto } from 'server/dto/sign_in.dto';
|
|||||||
import { JwtService } from 'server/providers/services/jwt.service';
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
import { RefreshTokensService } from 'server/providers/services/refresh_tokens.service';
|
import { RefreshTokensService } from 'server/providers/services/refresh_tokens.service';
|
||||||
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
||||||
|
import { Skip } from 'server/decorators/skip.decorator';
|
||||||
|
import { AuthGuard } from 'server/providers/guards/auth.guard';
|
||||||
|
import { RolesService } from 'server/providers/services/roles.service';
|
||||||
|
|
||||||
// this is kind of a misnomer because we are doing token based auth
|
// this is kind of a misnomer because we are doing token based auth
|
||||||
// instead of session based auth
|
// instead of session based auth
|
||||||
@ -13,10 +16,12 @@ export class SessionsController {
|
|||||||
constructor(
|
constructor(
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
|
private rolesService: RolesService,
|
||||||
private refreshTokenService: RefreshTokensService,
|
private refreshTokenService: RefreshTokensService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('/sessions')
|
@Post('/sessions')
|
||||||
|
@Skip(AuthGuard)
|
||||||
async create(@Body() body: SignInDto, @Res({ passthrough: true }) res: Response) {
|
async create(@Body() body: SignInDto, @Res({ passthrough: true }) res: Response) {
|
||||||
const { verified, user } = await this.usersService.verify(body.email, body.password);
|
const { verified, user } = await this.usersService.verify(body.email, body.password);
|
||||||
|
|
||||||
@ -32,8 +37,10 @@ export class SessionsController {
|
|||||||
// generate new refresh token
|
// generate new refresh token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userRoles = await this.rolesService.findByIds(user.userRoles.map((ur) => ur.roleId));
|
||||||
|
|
||||||
// JWT gets sent with response
|
// JWT gets sent with response
|
||||||
const token = this.jwtService.issueToken({ userId: user.id });
|
const token = this.jwtService.issueToken({ userId: user.id, roles: userRoles.map((r) => r.key) });
|
||||||
|
|
||||||
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
|
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
|
||||||
|
|
||||||
|
@ -2,36 +2,53 @@ import { Body, Controller, Get, HttpException, HttpStatus, Post, Res, UseGuards
|
|||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { JwtBody } from 'server/decorators/jwt_body.decorator';
|
import { JwtBody } from 'server/decorators/jwt_body.decorator';
|
||||||
|
import { Roles } from 'server/decorators/roles.decorator';
|
||||||
|
import { Skip } from 'server/decorators/skip.decorator';
|
||||||
import { CreateUserDto } from 'server/dto/create_user.dto';
|
import { CreateUserDto } from 'server/dto/create_user.dto';
|
||||||
import { JwtBodyDto } from 'server/dto/jwt_body.dto';
|
import { JwtBodyDto } from 'server/dto/jwt_body.dto';
|
||||||
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
||||||
|
import { RoleKey } from 'server/entities/role.entity';
|
||||||
import { User } from 'server/entities/user.entity';
|
import { User } from 'server/entities/user.entity';
|
||||||
|
import { UserRole } from 'server/entities/user_role.entity';
|
||||||
import { AuthGuard } from 'server/providers/guards/auth.guard';
|
import { AuthGuard } from 'server/providers/guards/auth.guard';
|
||||||
import { JwtService } from 'server/providers/services/jwt.service';
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
import { RefreshTokensService } from 'server/providers/services/refresh_tokens.service';
|
import { RefreshTokensService } from 'server/providers/services/refresh_tokens.service';
|
||||||
|
import { RolesService } from 'server/providers/services/roles.service';
|
||||||
import { UsersService } from 'server/providers/services/users.service';
|
import { UsersService } from 'server/providers/services/users.service';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(
|
constructor(
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
|
private rolesService: RolesService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private refreshTokenService: RefreshTokensService,
|
private refreshTokenService: RefreshTokensService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Get('/users')
|
||||||
|
@Roles(RoleKey.ADMIN)
|
||||||
|
async index() {
|
||||||
|
const users = await this.usersService.findAll();
|
||||||
|
return { users };
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/users/me')
|
@Get('/users/me')
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
async getCurrentUser(@JwtBody() jwtBody: JwtBodyDto) {
|
async getCurrentUser(@JwtBody() jwtBody: JwtBodyDto) {
|
||||||
const user = await this.usersService.find(jwtBody.userId);
|
const user = await this.usersService.find(jwtBody.userId);
|
||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/users')
|
@Post('/users')
|
||||||
|
@Skip(AuthGuard)
|
||||||
async create(@Body() userPayload: CreateUserDto, @Res({ passthrough: true }) res: Response) {
|
async create(@Body() userPayload: CreateUserDto, @Res({ passthrough: true }) res: Response) {
|
||||||
const newUser = new User();
|
const newUser = new User();
|
||||||
newUser.email = userPayload.email;
|
newUser.email = userPayload.email;
|
||||||
newUser.name = userPayload.name;
|
newUser.name = userPayload.name;
|
||||||
newUser.passwordHash = await bcrypt.hash(userPayload.password, 10);
|
newUser.passwordHash = await bcrypt.hash(userPayload.password, 10);
|
||||||
|
const [role] = await this.rolesService.findByKey(RoleKey.USER);
|
||||||
|
const userRole = new UserRole();
|
||||||
|
userRole.role = role;
|
||||||
|
newUser.userRoles = [userRole];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await this.usersService.create(newUser);
|
const user = await this.usersService.create(newUser);
|
||||||
@ -39,9 +56,11 @@ export class UsersController {
|
|||||||
const newRefreshToken = new RefreshToken();
|
const newRefreshToken = new RefreshToken();
|
||||||
newRefreshToken.user = user;
|
newRefreshToken.user = user;
|
||||||
const refreshToken = await this.refreshTokenService.create(newRefreshToken);
|
const refreshToken = await this.refreshTokenService.create(newRefreshToken);
|
||||||
|
|
||||||
// issue jwt and refreshJwtToken
|
// issue jwt and refreshJwtToken
|
||||||
const token = this.jwtService.issueToken({ userId: user.id });
|
// note the roles hard coded to just USER.
|
||||||
|
// If you want to allow users to sign up as different roles then
|
||||||
|
// you will need to update this here.
|
||||||
|
const token = this.jwtService.issueToken({ userId: user.id, roles: [RoleKey.USER] });
|
||||||
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
|
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
|
||||||
|
|
||||||
// only refresh token should go in the cookie
|
// only refresh token should go in the cookie
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Factory, Seeder } from 'typeorm-seeding';
|
import { Factory, Seeder } from 'typeorm-seeding';
|
||||||
import { Connection, Db } from 'typeorm';
|
import { Connection } from 'typeorm';
|
||||||
import { User } from '../entities/user.entity';
|
import { User } from '../entities/user.entity';
|
||||||
import { Role } from '../entities/role.entity';
|
import { Role, RoleKey } from '../entities/role.entity';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { UserRole } from '../entities/user_role.entity';
|
import { UserRole } from '../entities/user_role.entity';
|
||||||
@ -11,6 +11,7 @@ export default class Seeds implements Seeder {
|
|||||||
public async run(factory: Factory, connection: Connection): Promise<any> {
|
public async run(factory: Factory, connection: Connection): Promise<any> {
|
||||||
// CREATE ROLES
|
// CREATE ROLES
|
||||||
console.log('\nCreating Roles');
|
console.log('\nCreating Roles');
|
||||||
|
|
||||||
const roleObjects = Role.ROLES.map((key) => ({ key }));
|
const roleObjects = Role.ROLES.map((key) => ({ key }));
|
||||||
const roleRepository = connection.getRepository(Role);
|
const roleRepository = connection.getRepository(Role);
|
||||||
for (const roleObj of roleObjects) {
|
for (const roleObj of roleObjects) {
|
||||||
@ -26,10 +27,9 @@ export default class Seeds implements Seeder {
|
|||||||
|
|
||||||
// CREATE ADMIN USER
|
// CREATE ADMIN USER
|
||||||
const userRepository = connection.getRepository(User);
|
const userRepository = connection.getRepository(User);
|
||||||
const userRoleRepository = connection.getRepository(UserRole);
|
|
||||||
let adminUser = await userRepository.findOne({ email: process.env.ADMIN_EMAIL });
|
let adminUser = await userRepository.findOne({ email: process.env.ADMIN_EMAIL });
|
||||||
if (!adminUser) {
|
if (!adminUser) {
|
||||||
const adminRole = await roleRepository.findOne({ key: Role.ADMIN });
|
const adminRole = await roleRepository.findOne({ key: RoleKey.ADMIN });
|
||||||
console.log(`\nCreating Admin User with email ${process.env.ADMIN_EMAIL}`);
|
console.log(`\nCreating Admin User with email ${process.env.ADMIN_EMAIL}`);
|
||||||
console.log(adminRole);
|
console.log(adminRole);
|
||||||
const passwordHash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10);
|
const passwordHash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10);
|
||||||
|
5
server/decorators/roles.decorator.ts
Normal file
5
server/decorators/roles.decorator.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { RoleKey } from 'server/entities/role.entity';
|
||||||
|
|
||||||
|
export const ROLES_CONTEXT_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: RoleKey[]) => SetMetadata(ROLES_CONTEXT_KEY, roles);
|
5
server/decorators/skip.decorator.ts
Normal file
5
server/decorators/skip.decorator.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { CanActivate, SetMetadata } from '@nestjs/common';
|
||||||
|
import { Class } from 'server/dto/class.dto';
|
||||||
|
|
||||||
|
export const SKIP_KEY = 'skip';
|
||||||
|
export const Skip = (...guards: Class<CanActivate>[]) => SetMetadata(SKIP_KEY, guards);
|
1
server/dto/class.dto.ts
Normal file
1
server/dto/class.dto.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type Class<T> = { new (...args: any[]): T };
|
@ -1,3 +1,6 @@
|
|||||||
|
import { RoleKey } from 'server/entities/role.entity';
|
||||||
|
|
||||||
export interface JwtBodyDto {
|
export interface JwtBodyDto {
|
||||||
userId: number;
|
userId: number;
|
||||||
|
roles: RoleKey[];
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, OneToMany, Column } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, OneToMany, Column } from 'typeorm';
|
||||||
import { UserRole } from './user_role.entity';
|
import { UserRole } from './user_role.entity';
|
||||||
|
|
||||||
|
// Make sure to add aditional roles here then reseed
|
||||||
|
export enum RoleKey {
|
||||||
|
ADMIN = 'admin',
|
||||||
|
USER = 'user',
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Role {
|
export class Role {
|
||||||
static ADMIN = 'admin';
|
static ROLES = [RoleKey.ADMIN, RoleKey.USER];
|
||||||
static USER = 'user';
|
|
||||||
|
|
||||||
// make sure add additional roles to this arraylist as it
|
|
||||||
// will be used during seeds to initiallize all roles.
|
|
||||||
static ROLES = [Role.ADMIN, Role.USER];
|
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
key: string;
|
key: RoleKey;
|
||||||
|
|
||||||
@OneToMany(() => UserRole, (userRole) => userRole.role)
|
@OneToMany(() => UserRole, (userRole) => userRole.role)
|
||||||
userRoles: UserRole[];
|
userRoles: UserRole[];
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, ManyToOne, Column } from 'typeorm';
|
||||||
import { Role } from './role.entity';
|
import { Role } from './role.entity';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
|
||||||
@ -7,6 +7,12 @@ export class UserRole {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
roleId: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: number;
|
||||||
|
|
||||||
@ManyToOne(() => Role, (role) => role.userRoles)
|
@ManyToOne(() => Role, (role) => role.userRoles)
|
||||||
role: Role;
|
role: Role;
|
||||||
|
|
||||||
|
@ -8,10 +8,14 @@ import { RefreshTokensService } from '../providers/services/refresh_tokens.servi
|
|||||||
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
||||||
import { JwtService } from 'server/providers/services/jwt.service';
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
import { RefreshTokensController } from 'server/controllers/refresh_tokens.controller';
|
import { RefreshTokensController } from 'server/controllers/refresh_tokens.controller';
|
||||||
|
import { Role } from 'server/entities/role.entity';
|
||||||
|
import { RolesService } from 'server/providers/services/roles.service';
|
||||||
|
import { UserRole } from 'server/entities/user_role.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User, RefreshToken])],
|
imports: [TypeOrmModule.forFeature([User, RefreshToken, Role, UserRole])],
|
||||||
controllers: [SessionsController, UsersController, RefreshTokensController],
|
controllers: [SessionsController, UsersController, RefreshTokensController],
|
||||||
providers: [UsersService, RefreshTokensService, JwtService],
|
providers: [UsersService, RolesService, RefreshTokensService, JwtService],
|
||||||
|
exports: [TypeOrmModule],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
import { JwtService } from '../services/jwt.service';
|
import { JwtService } from '../services/jwt.service';
|
||||||
|
import { SKIP_KEY } from 'server/decorators/skip.decorator';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Class } from 'server/dto/class.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
constructor(private jwtService: JwtService) {}
|
constructor(private reflector: Reflector, private jwtService: JwtService) {}
|
||||||
|
|
||||||
canActivate(context: ExecutionContext) {
|
canActivate(context: ExecutionContext) {
|
||||||
|
const skippedGuards = this.reflector.getAllAndOverride<Class<CanActivate>[]>(SKIP_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (skippedGuards) {
|
||||||
|
const skippedGuard = skippedGuards.find((guard) => this instanceof guard);
|
||||||
|
if (skippedGuard) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader) return false;
|
||||||
|
|
||||||
const jwt = authHeader.split(' ')[1];
|
const jwt = authHeader.split(' ')[1];
|
||||||
try {
|
try {
|
||||||
req.jwtBody = this.jwtService.parseToken(jwt);
|
req.jwtBody = this.jwtService.parseToken(jwt);
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ROLES_CONTEXT_KEY } from 'server/decorators/roles.decorator';
|
||||||
|
import { JwtBodyDto } from 'server/dto/jwt_body.dto';
|
||||||
|
import { RoleKey } from 'server/entities/role.entity';
|
||||||
|
import { RolesService } from '../services/roles.service';
|
||||||
|
import { UsersService } from '../services/users.service';
|
||||||
|
import { some } from 'lodash';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector, private usersService: UsersService, private rolesService: RolesService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<RoleKey[]>(ROLES_CONTEXT_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
console.log(requiredRoles);
|
||||||
|
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwtBody: JwtBodyDto = context.switchToHttp().getRequest().jwtBody;
|
||||||
|
|
||||||
|
if (!jwtBody) return false; // unauthenticated users are not authorized
|
||||||
|
|
||||||
|
const user = await this.usersService.find(jwtBody.userId, ['userRoles']);
|
||||||
|
const roles = await this.rolesService.findByKey(...requiredRoles);
|
||||||
|
const roleMatches = user.userRoles.map((userRole) => {
|
||||||
|
return !!roles.find((role) => role.id === userRole.roleId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return some(roleMatches);
|
||||||
|
}
|
||||||
|
}
|
25
server/providers/services/roles.service.ts
Normal file
25
server/providers/services/roles.service.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { In, Repository } from 'typeorm';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { Role, RoleKey } from 'server/entities/role.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Role)
|
||||||
|
private rolesRepository: Repository<Role>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
findByKey(...keys: RoleKey[]) {
|
||||||
|
return this.rolesRepository.find({ where: { key: In(keys) } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findByIds(ids: number[]) {
|
||||||
|
return this.rolesRepository.findByIds(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
find(id: number, relations: string[] = []) {
|
||||||
|
return this.rolesRepository.findOne(id, { relations });
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,10 @@ export class UsersService {
|
|||||||
private usersRespository: Repository<User>,
|
private usersRespository: Repository<User>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
findAll(relations: string[] = []) {
|
||||||
|
return this.usersRespository.find({ relations });
|
||||||
|
}
|
||||||
|
|
||||||
findBy(options: Record<string, any>, relations: string[] = []) {
|
findBy(options: Record<string, any>, relations: string[] = []) {
|
||||||
return this.usersRespository.findOne(options, { relations });
|
return this.usersRespository.findOne(options, { relations });
|
||||||
}
|
}
|
||||||
@ -24,7 +28,7 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verify(email: string, password: string) {
|
async verify(email: string, password: string) {
|
||||||
const user = await this.usersRespository.findOne({ email }, { relations: ['refreshTokens'] });
|
const user = await this.usersRespository.findOne({ email }, { relations: ['refreshTokens', 'userRoles'] });
|
||||||
if (!user) return { verified: false, user: null };
|
if (!user) return { verified: false, user: null };
|
||||||
const verified: boolean = await bcrypt.compare(password, user.passwordHash);
|
const verified: boolean = await bcrypt.compare(password, user.passwordHash);
|
||||||
return { verified, user: verified ? user : null };
|
return { verified, user: verified ? user : null };
|
||||||
|
@ -16,6 +16,6 @@
|
|||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -896,6 +896,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||||
|
|
||||||
|
"@types/lodash@^4.14.177":
|
||||||
|
version "4.14.177"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.177.tgz#f70c0d19c30fab101cad46b52be60363c43c4578"
|
||||||
|
integrity sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==
|
||||||
|
|
||||||
"@types/mime@^1":
|
"@types/mime@^1":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||||
|
Loading…
Reference in New Issue
Block a user