Student dashboard, automatic redirection for home page
This commit is contained in:
parent
ef5e2f6cfb
commit
441b6ce68d
19 changed files with 219 additions and 14 deletions
|
@ -1,7 +1,7 @@
|
||||||
import { arg, authorized, GraphQLBindings, ID, Int, mutation, query, resolver, ResolverData } from '@loopback/graphql';
|
import { arg, authorized, GraphQLBindings, ID, Int, mutation, query, resolver, ResolverData } from '@loopback/graphql';
|
||||||
import { User } from '../models';
|
import { User } from '../models';
|
||||||
import { repository } from '@loopback/repository';
|
import { repository } from '@loopback/repository';
|
||||||
import { RevTokenRepository, UserRepository } from '../repositories';
|
import { CourseUserRepository, RevTokenRepository, UserRepository } from '../repositories';
|
||||||
import { Context, inject } from '@loopback/core';
|
import { Context, inject } from '@loopback/core';
|
||||||
import { SzakdolgozatUserService } from '../services';
|
import { SzakdolgozatUserService } from '../services';
|
||||||
import { TokenServiceBindings, UserServiceBindings } from '@loopback/authentication-jwt';
|
import { TokenServiceBindings, UserServiceBindings } from '@loopback/authentication-jwt';
|
||||||
|
@ -18,6 +18,7 @@ export class UserResolver {
|
||||||
constructor(
|
constructor(
|
||||||
@repository('UserRepository') private readonly userRepository: UserRepository,
|
@repository('UserRepository') private readonly userRepository: UserRepository,
|
||||||
@repository('RevTokenRepository') private readonly revTokenRepo: RevTokenRepository,
|
@repository('RevTokenRepository') private readonly revTokenRepo: RevTokenRepository,
|
||||||
|
@repository('CourseUserRepository') private readonly courseUserRepo: CourseUserRepository,
|
||||||
@inject(UserServiceBindings.USER_SERVICE) private readonly userService: SzakdolgozatUserService,
|
@inject(UserServiceBindings.USER_SERVICE) private readonly userService: SzakdolgozatUserService,
|
||||||
@inject(GraphQLBindings.RESOLVER_DATA) private readonly resolverData: ResolverData,
|
@inject(GraphQLBindings.RESOLVER_DATA) private readonly resolverData: ResolverData,
|
||||||
@inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService,
|
@inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService,
|
||||||
|
@ -50,7 +51,17 @@ export class UserResolver {
|
||||||
|
|
||||||
// create a JSON Web Token based on the user profile
|
// create a JSON Web Token based on the user profile
|
||||||
const token = await this.jwtService.generateToken(userProfile);
|
const token = await this.jwtService.generateToken(userProfile);
|
||||||
return {token, user};
|
let roles = [];
|
||||||
|
if (user.isAdmin) {
|
||||||
|
roles.push('admin');
|
||||||
|
}
|
||||||
|
if ((await this.courseUserRepo.count({userId: user.id, role: 'teacher'})).count) {
|
||||||
|
roles.push('teacher');
|
||||||
|
}
|
||||||
|
if ((await this.courseUserRepo.count({userId: user.id, role: 'student'})).count) {
|
||||||
|
roles.push('student');
|
||||||
|
}
|
||||||
|
return {token, user, roles};
|
||||||
}
|
}
|
||||||
|
|
||||||
@authorized()
|
@authorized()
|
||||||
|
|
|
@ -21,6 +21,8 @@ export class LoginResult {
|
||||||
token: string;
|
token: string;
|
||||||
@field()
|
@field()
|
||||||
user: UserResult;
|
user: UserResult;
|
||||||
|
@field(returns => [String])
|
||||||
|
roles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@objectType()
|
@objectType()
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { UserRepository } from '../repositories';
|
||||||
import { User } from '../models';
|
import { User } from '../models';
|
||||||
import { Mock, MockFactory } from 'mockingbird';
|
import { Mock, MockFactory } from 'mockingbird';
|
||||||
import { genSalt, hash } from 'bcryptjs';
|
import { genSalt, hash } from 'bcryptjs';
|
||||||
|
// noinspection ES6UnusedImports
|
||||||
|
import { repository } from '@loopback/repository';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will be bound to the application as a `LifeCycleObserver` during
|
* This class will be bound to the application as a `LifeCycleObserver` during
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { AuthCheck } from './auth-check';
|
import { AuthCheck } from './auth-check';
|
||||||
|
import { StartComponent } from './start.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
children: [
|
|
||||||
{path: 'auth', loadChildren: async () => (await import('./auth/auth.module')).AuthModule}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
canActivate: [AuthCheck],
|
canActivate: [AuthCheck],
|
||||||
|
@ -22,8 +17,25 @@ const routes: Routes = [
|
||||||
path: 'subjects',
|
path: 'subjects',
|
||||||
loadChildren: async () => (await import('./subjects/subjects.module')).SubjectsModule,
|
loadChildren: async () => (await import('./subjects/subjects.module')).SubjectsModule,
|
||||||
data: {title: 'Tárgyak'}
|
data: {title: 'Tárgyak'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'student',
|
||||||
|
loadChildren: async () => (await import('./students/students.module')).StudentsModule,
|
||||||
|
data: {title: 'Hallagtói kezdőlap'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
children: [
|
||||||
|
{path: '', component: StartComponent}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
children: [
|
||||||
|
{path: 'auth', loadChildren: async () => (await import('./auth/auth.module')).AuthModule}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<mat-toolbar>Menü</mat-toolbar>
|
<mat-toolbar>Menü</mat-toolbar>
|
||||||
<mat-nav-list>
|
<mat-nav-list>
|
||||||
<a mat-list-item routerLink="/">Főoldal</a>
|
<a mat-list-item routerLink="/">Főoldal</a>
|
||||||
<a mat-list-item *ngFor="let item of getMenuItems()" [routerLink]="'/' + item.path">{{ item.title }}</a>
|
<a mat-list-item *ngFor="let item of getMenuItems() | async" [routerLink]="'/' + item.path">{{ item.title }}</a>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content>
|
<mat-sidenav-content>
|
||||||
|
|
|
@ -22,7 +22,8 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
menu: MenuItem[] = [
|
menu: MenuItem[] = [
|
||||||
{path: 'users', requiredRole: 'admin'},
|
{path: 'users', requiredRole: 'admin'},
|
||||||
{path: 'subjects', requiredRole: 'admin'}
|
{path: 'subjects', requiredRole: 'admin'},
|
||||||
|
{path: 'student', requiredRole: 'student'}
|
||||||
];
|
];
|
||||||
|
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
|
@ -129,8 +130,11 @@ export class AppComponent implements OnInit {
|
||||||
await this.router.navigate(['/']);
|
await this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMenuItems(): MenuItem[] {
|
getMenuItems(): Observable<MenuItem[]> {
|
||||||
return this.menu.filter(item => item.requiredRole === 'admin' ? this.loginService.user?.isAdmin : true); // TODO: Roles
|
return this.loginService.rolesChanged.pipe(map(roles => {
|
||||||
|
const menu = this.menu.filter(({requiredRole}) => roles && requiredRole ? roles.includes(requiredRole) : !requiredRole);
|
||||||
|
return menu;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async routeActivated($event: any): Promise<void> {
|
async routeActivated($event: any): Promise<void> {
|
||||||
|
@ -149,7 +153,7 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MenuItem = { path: string, requiredRole: 'admin', title?: string }; // TODO: Role
|
type MenuItem = { path: string, requiredRole: 'admin' | 'teacher' | 'student', title?: string };
|
||||||
type RouteSegment = { title: string, url: string, params: Params };
|
type RouteSegment = { title: string, url: string, params: Params };
|
||||||
|
|
||||||
export interface CustomTitleComponent {
|
export interface CustomTitleComponent {
|
||||||
|
|
|
@ -18,10 +18,12 @@ import { LoginService } from './auth/login.service';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { AuthCheck } from './auth-check';
|
import { AuthCheck } from './auth-check';
|
||||||
import { GraphQLModule } from './graphql.module';
|
import { GraphQLModule } from './graphql.module';
|
||||||
|
import { StartComponent } from './start.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent,
|
||||||
|
StartComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { LoginGQL, LogoutGQL, RegisterGQL, UserResult } from '../services/graphql';
|
import { LoginGQL, LogoutGQL, RegisterGQL, UserResult } from '../services/graphql';
|
||||||
|
import { ReplaySubject } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -8,6 +9,8 @@ export class LoginService {
|
||||||
|
|
||||||
private tokenP: string;
|
private tokenP: string;
|
||||||
private userP: UserResult;
|
private userP: UserResult;
|
||||||
|
private rolesP: string[];
|
||||||
|
private rolesChangedP = new ReplaySubject<string[]>(1);
|
||||||
|
|
||||||
get token(): string {
|
get token(): string {
|
||||||
return this.tokenP;
|
return this.tokenP;
|
||||||
|
@ -17,9 +20,17 @@ export class LoginService {
|
||||||
return this.userP;
|
return this.userP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get roles(): string[] {
|
||||||
|
return this.rolesP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly rolesChanged = this.rolesChangedP.asObservable();
|
||||||
|
|
||||||
constructor(private loginGQL: LoginGQL, private logoutGQL: LogoutGQL, private registerGQL: RegisterGQL) {
|
constructor(private loginGQL: LoginGQL, private logoutGQL: LogoutGQL, private registerGQL: RegisterGQL) {
|
||||||
this.tokenP = window.localStorage.getItem('token');
|
this.tokenP = window.localStorage.getItem('token');
|
||||||
this.userP = JSON.parse(window.localStorage.getItem('user'));
|
this.userP = JSON.parse(window.localStorage.getItem('user'));
|
||||||
|
this.rolesP = JSON.parse(window.localStorage.getItem('roles'));
|
||||||
|
this.rolesChangedP.next(this.rolesP);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(email: string, password: string, name: string): Promise<void> {
|
async createUser(email: string, password: string, name: string): Promise<void> {
|
||||||
|
@ -31,8 +42,11 @@ export class LoginService {
|
||||||
const resp = await this.loginGQL.mutate({email, password}).toPromise();
|
const resp = await this.loginGQL.mutate({email, password}).toPromise();
|
||||||
this.tokenP = resp.data.login.token;
|
this.tokenP = resp.data.login.token;
|
||||||
this.userP = resp.data.login.user;
|
this.userP = resp.data.login.user;
|
||||||
|
this.rolesP = resp.data.login.roles;
|
||||||
window.localStorage.setItem('token', this.tokenP);
|
window.localStorage.setItem('token', this.tokenP);
|
||||||
window.localStorage.setItem('user', JSON.stringify(this.userP));
|
window.localStorage.setItem('user', JSON.stringify(this.userP));
|
||||||
|
window.localStorage.setItem('roles', JSON.stringify(this.rolesP));
|
||||||
|
this.rolesChangedP.next(this.rolesP);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401 || e.status === 422) {
|
if (e.status === 401 || e.status === 422) {
|
||||||
|
@ -47,6 +61,8 @@ export class LoginService {
|
||||||
this.userP = null;
|
this.userP = null;
|
||||||
window.localStorage.removeItem('token');
|
window.localStorage.removeItem('token');
|
||||||
window.localStorage.removeItem('user');
|
window.localStorage.removeItem('user');
|
||||||
|
window.localStorage.removeItem('roles');
|
||||||
|
this.rolesChangedP.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
|
|
|
@ -7,6 +7,7 @@ mutation Login($email: String!, $password: String!) {
|
||||||
isAdmin
|
isAdmin
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
roles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
41
frontend/src/app/start.component.ts
Normal file
41
frontend/src/app/start.component.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { LoginService } from './auth/login.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '<h1>Üdv, {{ loginService.user?.name }}!</h1>'
|
||||||
|
})
|
||||||
|
export class StartComponent implements OnInit, OnDestroy {
|
||||||
|
sub: Subscription;
|
||||||
|
|
||||||
|
constructor(private router: Router, public loginService: LoginService) {
|
||||||
|
console.log('Wtf');
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// tslint:disable-next-line:no-shadowed-variable
|
||||||
|
this.sub = this.loginService.rolesChanged.subscribe(roles => {
|
||||||
|
if (!roles) {
|
||||||
|
this.router.navigate(['auth', 'login']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const roles = this.loginService.roles;
|
||||||
|
console.log('Roles', roles);
|
||||||
|
if (roles.includes('admin')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (roles.includes('teacher') && !roles.includes('student')) {
|
||||||
|
this.router.navigate(['teacher']);
|
||||||
|
} else if (roles.includes('student') && !roles.includes('teacher')) {
|
||||||
|
this.router.navigate(['student']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
console.log('Unsubscribing');
|
||||||
|
this.sub?.unsubscribe();
|
||||||
|
this.sub = null;
|
||||||
|
}
|
||||||
|
}
|
0
frontend/src/app/students/courses/courses.component.css
Normal file
0
frontend/src/app/students/courses/courses.component.css
Normal file
1
frontend/src/app/students/courses/courses.component.html
Normal file
1
frontend/src/app/students/courses/courses.component.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<p>courses works!</p>
|
25
frontend/src/app/students/courses/courses.component.spec.ts
Normal file
25
frontend/src/app/students/courses/courses.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CoursesComponent } from './courses.component';
|
||||||
|
|
||||||
|
describe('CoursesComponent', () => {
|
||||||
|
let component: CoursesComponent;
|
||||||
|
let fixture: ComponentFixture<CoursesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [CoursesComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CoursesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
16
frontend/src/app/students/courses/courses.component.ts
Normal file
16
frontend/src/app/students/courses/courses.component.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-courses',
|
||||||
|
templateUrl: './courses.component.html',
|
||||||
|
styleUrls: ['./courses.component.css']
|
||||||
|
})
|
||||||
|
export class CoursesComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-header><h2>Teljesitett tárgyak</h2></mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-progress-bar [value]="50"></mat-progress-bar>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [DashboardComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
16
frontend/src/app/students/dashboard/dashboard.component.ts
Normal file
16
frontend/src/app/students/dashboard/dashboard.component.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrls: ['./dashboard.component.css']
|
||||||
|
})
|
||||||
|
export class DashboardComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
frontend/src/app/students/students.module.ts
Normal file
25
frontend/src/app/students/students.module.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
import { CoursesComponent } from './courses/courses.component';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { RouteData } from '../app-routing.module';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{path: '', component: DashboardComponent, data: {title: 'Kezdőlap'} as RouteData},
|
||||||
|
{path: 'courses', component: CoursesComponent, data: {title: 'Kurzusok'}}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [DashboardComponent, CoursesComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
RouterModule.forChild(routes)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class StudentsModule {
|
||||||
|
}
|
Loading…
Reference in a new issue