MEAN Stack - Mongoose роли и разрешения для администратора приложения - PullRequest
0 голосов
/ 16 апреля 2019

Я создаю приложение MEAN Stack с аутентификацией jsonwebtoken.Я сомневаюсь, и я не уверен, как организовать роль пользователя для администратора, потому что он будет иметь определенную роль и привилегии.

Я хочу назначить права и разрешения администратору приложения, поэтому только администратор будет иметь права видеть опцию регистрации пользователя и входить в систему нового пользователя, а все остальные пользователи не смогут видеть опции регистрации.

В настоящее время каждый зарегистрированный пользователь может видеть опцию регистрации, поэтому я создал следующих пользователей из регистрационной формы, которая впоследствии должна быть видна только администратору приложения.

Я уже три пользователя в коллекции пользователей какследует

{"_id":{"$oid":"5ca6f82d46f3f33fd80069ad"},"email":"admin@edoctor.com","password":"$2b$10$5Ht6/YH.1bzZ9XPWYqDRF.gMb0bYgI3i/fB3YRY/fBX.MeuWrgtqy","isAdmin":true,"__v":{"$numberInt":"0"}}
{"_id":{"$oid":"5ca6f8cc46f3f33fd80069ae"},"email":"doctor1@edoctor.com","password":"$2b$10$jU4KOQ.n5Jm66TnV4U1T1ep1/3sU1Xq7eyndXooBDNVUBePg/BKlC","__v":{"$numberInt":"0"}}
{"_id":{"$oid":"5ca6f8dd46f3f33fd80069af"},"email":"doctor2@edoctor.com","password":"$2b$10$A9wmcSyVBCjPR6LV887gSeg5c5lnzyMQUPBVN61CMHhScsy14C01a","__v":{"$numberInt":"0"}}
{"_id":{"$oid":"5ca6f8e846f3f33fd80069b0"},"email":"doctor3@edoctor.com","password":"$2b$10$KS/HSXOibALmdTGGyWBgX.qxoDMwyk0PoMmULsem2twZgjg3UDkru","__v":{"$numberInt":"0"}}

Что является лучшим решением для моего сценария?

  1. Назначить вручную из свойства MongoDB Compass isAdmin: true конкретному пользователю, а затем *ngIf="user.isAdmin"и на основании этого условия будет доступна или не доступна опция регистрации:
  2. Использовать контроль доступа на основе ролей, например:
    { resource: { db: "hospital", collection: "users" }, actions: [ "update", "insert" ] }?

Код:

/ backend / middleware / check-auth.js

/**
 * Check whether the user is authenticated or not 
 */

const jwt = require('jsonwebtoken');

// Configuration file that hold environment variables
const config = require('../config/config');

module.exports = (req, res, next) => {
    try {
        // If user is authenticated, get a token from the incoming request
        const token = req.headers.authorization.split(" ")[1];

        // Decoded Token
        const decodedToken = jwt.verify(token, config.jwt.key);

        req.userData = { email: decodedToken.email, userId: decodedToken.userId };

        next();

    } catch (error) {
        // If user is not authenticated
        res.status(401).json({
            message: 'Authentication failed!'
        })
    }
}

/ backend / controllers / user.js

// Package that offers encryption functionalities (password hashing)
const bcrypt = require('bcrypt');

// Json Web Token package
const jwt = require('jsonwebtoken');

// Require User model
const User = require('../models/user');

// Configuration file that hold environment variables
const config = require('../config/config');

// User signup
exports.userSignup = (req, res, next) => { 
    // Hash method takes an input password and that is the value I want to hash
    bcrypt.hash(req.body.password, 10)
        .then(hash => {
            // Create a new user
            const user = new User({
                email: req.body.email,
                password: hash
            });

            // Save the user
            user.save()
                .then(result => {
                    // Status 201 - The request has been fulfilled and has resulted in one or more new resources being created
                    res.status(201).json({
                        message: 'User created',
                        result: result
                    })
                })
                .catch(err => {
                    /**
                     * Status 500 - Internal server error
                     * The server encountered an unexpected condition that prevented it from fulfilling the request
                     */
                    res.status(500).json({
                        error: err
                    })
                });
        })
}

// User login
exports.userLogin = (req, res, next) => {
    const email = req.body.email;
    const password = req.body.password;
    /**
     * Validate whether the credentials are valid, and if it is, 
     * I want to create validation JSON Web token (validate the user) and logged in the user
     * 
     * First, I want look for a user where the email address in the database matches the email
     * address which I attached to the request (entered in the email field)
     */
    let fetchedUser;

    User.findOne({ email: email })
        .then(user => {
            // If user does not exist
            if (!user) {
                // Authentication is denied
                return res.status(401).json({
                    message: 'Authentication failed.'
                })
            } 
            fetchedUser = user;

            /**
             * If user exist, compare the password that the user entered into the login form 
             * with the password stored in the database
             */
            return bcrypt.compare(password, user.password)
        })
        /**
         * Get back the result of compare operation
         * 
         * The result will be true if we did successfully compare, or false if we failed
         */
        .then(result => {

            if(!result) {
                return res.status(401).json({
                    message: 'Authentication failed.'
                });
            }

            /**
             * If result is true, then we know what we have a valid password send by the user, then I will
             * create a new Json Web token
             */
            const token = jwt.sign(
                { email: fetchedUser.email, userId: fetchedUser._id }, 
                config.jwt.key,
                { expiresIn: "1h" }
            );
            
            // Successfuly authenticated
            res.status(200).json({
                // Return token for user
                token: token,
                expiresIn: 3600, // 3600 seconds - 1h

                isAdmin: fetchedUser.isAdmin,

                // Return userId
                userId: fetchedUser._id
            })
        }) 
        .catch(err => { 
            return res.status(401).json({
                message: 'Authentication failed.'
            })
        })
}

/ backend / models / user

const mongoose = require('mongoose');

// Models are defined through the Schema interface.
const Schema = mongoose.Schema;

/**
 * Defining User schema
 * 
 * Mongoose schema is configurator object for a Mongoose model.
 * A SchemaType is then a configuration object for an individual property 
 * 
 * Each schema maps to a MongoDB collection and defines the shape of the documents within that collection
 */ 
const userSchema = new Schema({
    email: { type: String, required: true },
    password: { type: String, required: true }
});

// Compiling Schema into a model
module.exports = mongoose.model('User', userSchema);

/ backend / маршруты / user.js

const express = require('express');

// User controller
const UserController = require('../controllers/user');

// Express router 
const router = express.Router();

// Signup route which handle POST request
router.post("/registracija", UserController.userSignup);

// Login route which handle POST request
router.post('/prijava', UserController.userLogin);

// Exports the user model
module.exports = router;

/ angular / auth / auth.service.ts

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Router } from "@angular/router";
import { Subject } from "rxjs";

// Authentication Data model
import { AuthData } from "./auth-data.model";


@Injectable({ providedIn: "root" })
export class AuthService {
  private isAuthenticated = false;
  private token: string;
  private tokenTimer: any;
  private userId: string;
  private isAdmin: boolean;

  /**
   * I will use Subject from 'rxjs' to push Authentication information
   * to components which are interested, because I don't need the token in my other components (except in interceptor),
   * I just want to know if user is authneticated or not (true or false)
   */
  private authStatusListener = new Subject<boolean>();

  constructor(
    private http: HttpClient,
    private router: Router
  ) {}

  getToken() {
    return this.token;
  }

  getIsAuth() {
    return this.isAuthenticated;
  }

  getUserId() {
    return this.userId;
  }

  getIsAdmin() {
    return this.isAdmin;
  }

  // Return the observable part of authentication status listener, so I can emit new value from other components
  getAuthStatusListener() {
    return this.authStatusListener.asObservable();
  }

  // Create a new User
  createUser(email: string, password: string) {
    const authData: AuthData = { email: email, password: password };

    // Use http client to send post request
    this.http
      .post("http://localhost:3000/api/user/registracija", authData)
      .subscribe(
        response => console.log(response),
        err => console.log(err)
      );
  }

  // Login
  login(email: string, password: string ) {
    const authData: AuthData = { email: email, password: password };
    this.http
      .post<{ token: string; expiresIn: number; userId: string, isAdmin: boolean }>(
        "http://localhost:3000/api/user/prijava",
        authData
      )
      .subscribe(response => {
        /**
         * Use JWT token and store it to the future requests (CRUD operations)
         *
         * - Extract the token from the response
         * - Store the token in the service
         * - Authentication status is set to true after user is logged in
         */
        const token = response.token;
        this.token = token;

        // If we have a valid token
        if (token) {
          const expiresInDuration = response.expiresIn;

          this.setAuthTimer(expiresInDuration);

          // When user logged in, authentication status is true, otherwise it's false
          this.isAuthenticated = true;
          this.userId = response.userId;

          this.isAdmin = response.isAdmin;

          this.authStatusListener.next(true);

          // Today
          const now = new Date();

          // Token expiration date in Local Storage
          const expirationDate = new Date(
            now.getTime() + expiresInDuration * 1000
          );

          // Save token in Local Storage after user successfuly logged in
          this.saveAuthData(token, expirationDate, this.userId);

          // After user logged in , redirect user to the home page (list of patients)
          this.router.navigate(["/"]);
        }
      });
  }

  // Automatically authenticate the user if I got the information for it in Local Storage
  autoAuthUser() {
    const authInformation = this.getAuthData();

    if (!authInformation) {
      return;
    }

    const now = new Date();
    const expiresIn = authInformation.expirationDate.getTime() - now.getTime();

    // If token expiration date is in the future ( expiration time is greather than current time )
    if (expiresIn > 0) {
      this.token = authInformation.token;
      this.isAuthenticated = true;
      this.userId = authInformation.userId;
      this.setAuthTimer(expiresIn / 1000);
      this.authStatusListener.next(true);
    }
  }

  // Logout
  logout() {
    
    // Clear the JWT token
    this.token = null;

    // Set authentication status to false
    this.isAuthenticated = false;

    // Push a new authentication value after user logout (false)
    this.authStatusListener.next(false);
    
    // When user logout, reset userId
    this.userId = null;

    // Clear token expiration timer after logout
    clearTimeout(this.tokenTimer);

    // Clear the Local Storage
    this.clearAuthData();

    // After logout, redirect the user to the login page
    this.router.navigate(["/prijava"]);
  }

  // Set token expiration timer
  setAuthTimer(duration: number) {
    this.tokenTimer = setTimeout(() => {
      this.logout();
    }, duration * 1000);
  }

  // Save the Token in the Local Storage once the user is authenticated
  private saveAuthData(token: string, expirationDate: Date, userId: string) {
    localStorage.setItem("token", token);
    localStorage.setItem("expiration", expirationDate.toISOString());
    localStorage.setItem('userId', userId);
  }

  // Get the token and expiration date from Local Storage
  private getAuthData() {
    const token = localStorage.getItem("token");
    const expirationDate = localStorage.getItem("expiration");
    const userId = localStorage.getItem("userId");

    // If we don't have token or expiration date
    if (!token || !expirationDate) {
      return;
    }

    return {
      token: token,
      expirationDate: new Date(expirationDate),
      userId: userId
    };
  }

  // Clear Local Storage Token and expiration date
  private clearAuthData() {
    localStorage.removeItem("token");
    localStorage.removeItem("expiration");
    localStorage.removeItem("userId");
  }
}

/ angular / auth / auth.guard.ts

import {
    CanActivate,
    ActivatedRouteSnapshot,
    RouterStateSnapshot,
    Router
  } from "@angular/router";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
  
import { AuthService } from "./auth.service";
  
@Injectable()

export class AuthGuard implements CanActivate {

    constructor(private authService: AuthService, private router: Router) {}

    canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): boolean | Observable<boolean> | Promise<boolean> {

        // Information whether the user is authenticated or not
        const isAuth = this.authService.getIsAuth();

        // If user is not authenticated
        if (!isAuth) {

            // Redirect to the login page 
            this.router.navigate(['/prijava']);
        }

        // If user is authenticated
        return isAuth;
    }
}
  

/ angular / auth / auth-data.model.ts

/**
 * Defining how Authentication Data should looks like
 * 
 * I will use AuthData for submitting the data to the backend
 */
export interface AuthData {
    email: string;
    password: string;
}

/ angular / auth / login / login.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { NgForm } from '@angular/forms';

import { Subscription } from 'rxjs';

// Authentication service
import { AuthService } from '../auth.service';

@Component({
  selector: "app-login",
  templateUrl: "./login.component.html",
  styleUrls: ["./login.component.scss"]
})

export class LoginComponent implements OnInit, OnDestroy {

  // Spinner is loading
  public loading = false;

  private authStatusSubscription: Subscription;

  constructor(
    private authService: AuthService
  ) {}

  ngOnInit() {
    this.authStatusSubscription = this.authService.getAuthStatusListener().subscribe(
      authStatus => {
        this.loading = false;
      }
    )
  }

  // When user click on Login button
  onLogin(form: NgForm) {
     
    // If form is invalid
    if (form.invalid) {
      return;
    }

    // Start the spinner
    this.loading = true;

    // Login the user
    this.authService.login(form.value.email, form.value.password);
  }

  ngOnDestroy() {
    this.authStatusSubscription.unsubscribe();
  }
}

/ angular / auth / signup / signup.component.ts

import { Component, OnInit, OnDestroy } from "@angular/core";
import { NgForm } from "@angular/forms";

import { Subscription } from "rxjs";

// Authentication service
import { AuthService } from "../auth.service";

@Component({
  templateUrl: "./signup.component.html",
  styleUrls: ["./signup.component.scss"]
})

export class SignupComponent implements OnInit, OnDestroy {
  
  // Spinner is loading
  public loading = false;
  private authStatusSubscription: Subscription;
  
  constructor(
    public authService: AuthService
  ) {}

  ngOnInit() {
    this.authStatusSubscription = this.authService.getAuthStatusListener().subscribe(
      authStatus => {
        this.loading = false;
      }
    );
  }

  // When user click on Register button
  onSignup(form: NgForm) {

    // Check whether the signup form is invalid (make sure that the user enters an email and password)
    if (form.invalid) {
      return;
    }

    this.loading = true;
    // If form is not invalid, create a new user
    this.authService.createUser(form.value.email, form.value.password);
  }

  ngOnDestroy() {
    this.authStatusSubscription.unsubscribe();
  }
}
...