Revoked token implementation and GraphQL on frontend
This commit is contained in:
parent
ff5cdb8e8a
commit
8471825e5d
24 changed files with 7734 additions and 1032 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -48,3 +48,5 @@ testem.log
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
.firebase/
|
.firebase/
|
||||||
|
frontend/src/app/services/graphql.ts
|
||||||
|
frontend/graphql.schema.json
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { arg, authorized, GraphQLBindings, Int, mutation, query, resolver, ResolverData } from '@loopback/graphql';
|
import { arg, authorized, GraphQLBindings, 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 { UserRepository } from '../repositories';
|
import { 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 @@ import { SzakdolgozatBindings } from '../bindings';
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
constructor(
|
constructor(
|
||||||
@repository('UserRepository') private readonly userRepository: UserRepository,
|
@repository('UserRepository') private readonly userRepository: UserRepository,
|
||||||
|
@repository('RevTokenRepository') private readonly revTokenRepo: RevTokenRepository,
|
||||||
@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,
|
||||||
|
@ -38,11 +39,6 @@ export class UserResolver {
|
||||||
return this.userRepository.create(user);
|
return this.userRepository.create(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@query(returns => User)
|
|
||||||
async test(request: User): Promise<User> {
|
|
||||||
return (await this.userRepository.find())[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
@mutation(returns => LoginResult)
|
@mutation(returns => LoginResult)
|
||||||
async login(@arg('email') email: string, @arg('password') password: string): Promise<LoginResult> {
|
async login(@arg('email') email: string, @arg('password') password: string): Promise<LoginResult> {
|
||||||
// ensure the user exists, and the password is correct
|
// ensure the user exists, and the password is correct
|
||||||
|
@ -59,10 +55,8 @@ export class UserResolver {
|
||||||
@mutation(returns => Boolean)
|
@mutation(returns => Boolean)
|
||||||
async logout(): Promise<boolean> {
|
async logout(): Promise<boolean> {
|
||||||
const token = await this.context.get(SzakdolgozatBindings.AUTH_TOKEN);
|
const token = await this.context.get(SzakdolgozatBindings.AUTH_TOKEN);
|
||||||
if (this.jwtService.revokeToken) {
|
if (token && !(await this.revTokenRepo.count({token})).count) {
|
||||||
await this.jwtService.revokeToken(token);
|
await this.revTokenRepo.create({token, created: new Date()});
|
||||||
} else {
|
|
||||||
console.error('Cannot revoke token');
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,12 @@ export class RevToken extends Entity {
|
||||||
})
|
})
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Date,
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
created: Date;
|
||||||
|
|
||||||
|
|
||||||
constructor(data?: Partial<RevToken>) {
|
constructor(data?: Partial<RevToken>) {
|
||||||
super(data);
|
super(data);
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { AuthChecker, ExpressContext } from '@loopback/graphql';
|
||||||
import { Context, Getter, inject, ValueOrPromise } from '@loopback/core';
|
import { Context, Getter, inject, ValueOrPromise } from '@loopback/core';
|
||||||
import { JWTAuthenticationStrategy } from '@loopback/authentication-jwt';
|
import { JWTAuthenticationStrategy } from '@loopback/authentication-jwt';
|
||||||
import { SzakdolgozatBindings } from './bindings';
|
import { SzakdolgozatBindings } from './bindings';
|
||||||
|
import { repository } from '@loopback/repository';
|
||||||
|
import { RevTokenRepository } from './repositories';
|
||||||
|
|
||||||
export class SzakdolgozatAuthChecker {
|
export class SzakdolgozatAuthChecker {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -12,13 +14,21 @@ export class SzakdolgozatAuthChecker {
|
||||||
static value(@inject(AuthenticationBindings.AUTH_ACTION) authenticate: AuthenticateFn,
|
static value(@inject(AuthenticationBindings.AUTH_ACTION) authenticate: AuthenticateFn,
|
||||||
@inject.getter(AuthenticationBindings.STRATEGY)
|
@inject.getter(AuthenticationBindings.STRATEGY)
|
||||||
getStrategies: Getter<AuthenticationStrategy | AuthenticationStrategy[] | undefined>,
|
getStrategies: Getter<AuthenticationStrategy | AuthenticationStrategy[] | undefined>,
|
||||||
@inject.context() context: Context): ValueOrPromise<AuthChecker> {
|
@inject.context() context: Context,
|
||||||
|
@repository('RevTokenRepository') revTokenRepo: RevTokenRepository): ValueOrPromise<AuthChecker> {
|
||||||
return async (resolverData, roles) => {
|
return async (resolverData, roles) => {
|
||||||
const econtext = (<ExpressContext> resolverData.context);
|
const econtext = (<ExpressContext> resolverData.context);
|
||||||
const res = await authenticate(econtext.req);
|
const res = await authenticate(econtext.req);
|
||||||
const strat = <JWTAuthenticationStrategy> await getStrategies();
|
const strat = <JWTAuthenticationStrategy> await getStrategies();
|
||||||
// Itt már biztosan van érvényes token
|
// Itt már biztosan van érvényes token
|
||||||
context.bind(SzakdolgozatBindings.AUTH_TOKEN).to(strat.extractCredentials(econtext.req));
|
const token = strat.extractCredentials(econtext.req);
|
||||||
|
const date = new Date();
|
||||||
|
date.setMonth(date.getMonth() - 1);
|
||||||
|
await revTokenRepo.deleteAll({created: {lt: date}});
|
||||||
|
if ((await revTokenRepo.count({token})).count) {
|
||||||
|
throw new Error('Session expired. Please sign in again.');
|
||||||
|
}
|
||||||
|
context.bind(SzakdolgozatBindings.AUTH_TOKEN).to(token);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
15
frontend/.graphqlconfig
Normal file
15
frontend/.graphqlconfig
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "Untitled GraphQL Schema",
|
||||||
|
"schemaPath": "./graphql.schema.json",
|
||||||
|
"extensions": {
|
||||||
|
"endpoints": {
|
||||||
|
"Default GraphQL Endpoint": {
|
||||||
|
"url": "http://localhost:8019/graphql",
|
||||||
|
"headers": {
|
||||||
|
"user-agent": "JS GraphQL"
|
||||||
|
},
|
||||||
|
"introspect": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
frontend/graphql.config.js
Normal file
20
frontend/graphql.config.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
module.exports = {
|
||||||
|
schema: 'http://backend:3000/graphql',
|
||||||
|
documents: "src/app/graphql/**/*.graphql",
|
||||||
|
extensions: {
|
||||||
|
codegen: {
|
||||||
|
generates: {
|
||||||
|
"src/app/services/graphql.ts": {
|
||||||
|
plugins: [
|
||||||
|
"typescript",
|
||||||
|
"typescript-operations",
|
||||||
|
"typescript-apollo-angular"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"./graphql.schema.json": {
|
||||||
|
plugins: ["introspection"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8543
frontend/package-lock.json
generated
8543
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,8 @@
|
||||||
"buildProd": "ng build --prod",
|
"buildProd": "ng build --prod",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e"
|
"e2e": "ng e2e",
|
||||||
|
"gql": "graphql-codegen --config graphql.config.js"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -23,7 +24,16 @@
|
||||||
"@angular/platform-browser": "^11.0.4",
|
"@angular/platform-browser": "^11.0.4",
|
||||||
"@angular/platform-browser-dynamic": "^11.0.4",
|
"@angular/platform-browser-dynamic": "^11.0.4",
|
||||||
"@angular/router": "^11.0.4",
|
"@angular/router": "^11.0.4",
|
||||||
|
"@apollo/client": "^3.0.0",
|
||||||
|
"@graphql-codegen/cli": "^2.6.2",
|
||||||
|
"@graphql-codegen/introspection": "^2.1.1",
|
||||||
|
"@graphql-codegen/typescript": "^2.4.8",
|
||||||
|
"@graphql-codegen/typescript-apollo-angular": "^3.4.7",
|
||||||
|
"@graphql-codegen/typescript-operations": "^2.3.5",
|
||||||
|
"@types/react": "^18.0.8",
|
||||||
|
"apollo-angular": "^2.6.0",
|
||||||
"firebase": "^8.2.0",
|
"firebase": "^8.2.0",
|
||||||
|
"graphql": "^15.0.0",
|
||||||
"rxjs": "~6.6.0",
|
"rxjs": "~6.6.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
|
|
|
@ -4,8 +4,7 @@ import { Observable } from 'rxjs';
|
||||||
import { filter, map, shareReplay } from 'rxjs/operators';
|
import { filter, map, shareReplay } from 'rxjs/operators';
|
||||||
import { LoginService } from './auth/login.service';
|
import { LoginService } from './auth/login.service';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Route, Router, Routes } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Route, Router, Routes } from '@angular/router';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './services/api.service';
|
||||||
import { UserRole } from './model/user.model';
|
|
||||||
import { RouteData } from './app-routing.module';
|
import { RouteData } from './app-routing.module';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@ -138,7 +137,7 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MenuItem = { path: string, requiredRole: UserRole | 'admin', title?: string };
|
type MenuItem = { path: string, requiredRole: 'admin', title?: string }; // TODO: Role
|
||||||
type RouteSegment = { title: string, url: string };
|
type RouteSegment = { title: string, url: string };
|
||||||
|
|
||||||
export interface CustomTitleComponent {
|
export interface CustomTitleComponent {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { MatInputModule } from '@angular/material/input';
|
||||||
import { LoginService } from './auth/login.service';
|
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';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -37,7 +38,8 @@ import { AuthCheck } from './auth-check';
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
HttpClientModule
|
HttpClientModule,
|
||||||
|
GraphQLModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LoginService,
|
LoginService,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { User } from '../model/user.model';
|
import { LoginGQL, UserResult } from '../services/graphql';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -9,17 +9,17 @@ import { User } from '../model/user.model';
|
||||||
export class LoginService {
|
export class LoginService {
|
||||||
|
|
||||||
private tokenP: string;
|
private tokenP: string;
|
||||||
private userP: User;
|
private userP: UserResult;
|
||||||
|
|
||||||
get token(): string {
|
get token(): string {
|
||||||
return this.tokenP;
|
return this.tokenP;
|
||||||
}
|
}
|
||||||
|
|
||||||
get user(): User {
|
get user(): UserResult {
|
||||||
return this.userP;
|
return this.userP;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient, private loginService: LoginGQL) {
|
||||||
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'));
|
||||||
}
|
}
|
||||||
|
@ -30,14 +30,11 @@ export class LoginService {
|
||||||
|
|
||||||
async login(email: string, password: string): Promise<boolean> {
|
async login(email: string, password: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const resp = await this.http.post<{ token: string, user: User }>(environment.backendUrl + '/users/login', {
|
const resp = await this.loginService.mutate({email, password}).toPromise();
|
||||||
email,
|
this.tokenP = resp.data.login.token;
|
||||||
password
|
this.userP = resp.data.login.user;
|
||||||
}).toPromise();
|
window.localStorage.setItem('token', this.tokenP);
|
||||||
this.tokenP = resp.token;
|
window.localStorage.setItem('user', JSON.stringify(this.userP));
|
||||||
this.userP = resp.user;
|
|
||||||
window.localStorage.setItem('token', resp.token);
|
|
||||||
window.localStorage.setItem('user', JSON.stringify(resp.user));
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401 || e.status === 422) {
|
if (e.status === 401 || e.status === 422) {
|
||||||
|
|
25
frontend/src/app/graphql.module.ts
Normal file
25
frontend/src/app/graphql.module.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { APOLLO_OPTIONS } from 'apollo-angular';
|
||||||
|
import { ApolloClientOptions, InMemoryCache } from '@apollo/client/core';
|
||||||
|
import { HttpLink } from 'apollo-angular/http';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
|
const uri = environment.backendUrl + '/graphql'; // <-- add the URL of the GraphQL server here
|
||||||
|
export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> {
|
||||||
|
return {
|
||||||
|
link: httpLink.create({uri}),
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APOLLO_OPTIONS,
|
||||||
|
useFactory: createApollo,
|
||||||
|
deps: [HttpLink],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class GraphQLModule {
|
||||||
|
}
|
11
frontend/src/app/graphql/user.graphql
Normal file
11
frontend/src/app/graphql/user.graphql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
mutation Login($email: String!, $password: String!) {
|
||||||
|
login(email: $email, password: $password) {
|
||||||
|
token
|
||||||
|
user {
|
||||||
|
email
|
||||||
|
id
|
||||||
|
isAdmin
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
import { Model } from './model';
|
|
||||||
|
|
||||||
export class Course extends Model {
|
|
||||||
semester: string;
|
|
||||||
subjectId: number;
|
|
||||||
alias: string;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export class Model {
|
|
||||||
id: number;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { Model } from './model';
|
|
||||||
|
|
||||||
export class Subject extends Model {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { Model } from './model';
|
|
||||||
|
|
||||||
export class User extends Model {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
isAdmin = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UserRole = 'teacher' | 'student';
|
|
||||||
|
|
|
@ -1,22 +1,27 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { LoginService } from '../auth/login.service';
|
||||||
import { environment } from '../environments/environment';
|
|
||||||
import { LoginService } from './auth/login.service';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { Apollo } from 'apollo-angular';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ApiService {
|
export class ApiService {
|
||||||
|
|
||||||
constructor(private http: HttpClient, private loginService: LoginService, private router: Router) {
|
constructor(private apollo: Apollo, private loginService: LoginService, private router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
request(method: 'post' | 'get' | 'delete' | 'patch', url: string, body: any): Promise<any> {
|
request(method: 'post' | 'get' | 'delete' | 'patch', url: string, body: any): Promise<any> {
|
||||||
return this.http.request(method, environment.backendUrl + url, {
|
const asd = this.apollo.query({
|
||||||
body,
|
query: gql`
|
||||||
headers: {Authorization: 'Bearer ' + this.loginService.token}
|
{
|
||||||
}).toPromise().catch(e => {
|
__typename
|
||||||
|
}
|
||||||
|
`
|
||||||
|
// headers: {Authorization: 'Bearer ' + this.loginService.token}
|
||||||
|
}).toPromise().then(res => res);
|
||||||
|
return asd.catch(e => {
|
||||||
if (e.status === 401) {
|
if (e.status === 401) {
|
||||||
this.loginService.deleteToken();
|
this.loginService.deleteToken();
|
||||||
return this.router.navigateByUrl('/auth/login');
|
return this.router.navigateByUrl('/auth/login');
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, Input, OnInit, Type } from '@angular/core';
|
import { Component, Input, OnInit, Type } from '@angular/core';
|
||||||
import { ApiService } from '../../api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Model } from '../../model/model';
|
import { Model } from '../../model/model';
|
||||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { Component, Input, OnInit, Type } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { PageEvent } from '@angular/material/paginator';
|
import { PageEvent } from '@angular/material/paginator';
|
||||||
import { PaginationData } from '../../utility/pagination-data';
|
import { PaginationData } from '../../utility/pagination-data';
|
||||||
import { ApiService } from '../../api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { Model } from '../../model/model';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -10,10 +9,10 @@ import { Router } from '@angular/router';
|
||||||
templateUrl: './list.component.html',
|
templateUrl: './list.component.html',
|
||||||
styleUrls: ['./list.component.css']
|
styleUrls: ['./list.component.css']
|
||||||
})
|
})
|
||||||
export class ListComponent<T extends Model> implements OnInit {
|
export class ListComponent<T extends { id: number }> implements OnInit {
|
||||||
|
|
||||||
@Input() apiPath: string;
|
@Input() apiPath: string;
|
||||||
@Input() itemType: Type<T>;
|
@Input() itemType: T;
|
||||||
@Input() columns: { title: string, prop: keyof T }[];
|
@Input() columns: { title: string, prop: keyof T }[];
|
||||||
@Input() allowNew = false;
|
@Input() allowNew = false;
|
||||||
@Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = [];
|
@Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = [];
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Course } from '../../../../model/course.model';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -8,7 +7,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||||
styleUrls: ['./course-edit.component.css']
|
styleUrls: ['./course-edit.component.css']
|
||||||
})
|
})
|
||||||
export class CourseEditComponent implements OnInit {
|
export class CourseEditComponent implements OnInit {
|
||||||
itemType = Course;
|
itemType: Course;
|
||||||
beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId});
|
beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId});
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {
|
constructor(private route: ActivatedRoute) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { User } from '../../model/user.model';
|
import { UserResult } from '../../services/graphql';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-list',
|
selector: 'app-user-list',
|
||||||
|
@ -7,7 +7,7 @@ import { User } from '../../model/user.model';
|
||||||
styleUrls: ['./user-list.component.css']
|
styleUrls: ['./user-list.component.css']
|
||||||
})
|
})
|
||||||
export class UserListComponent implements OnInit {
|
export class UserListComponent implements OnInit {
|
||||||
itemType = User;
|
itemType: UserResult;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
|
||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
@ -14,7 +13,9 @@
|
||||||
"module": "es2020",
|
"module": "es2020",
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2018",
|
"es2018",
|
||||||
"dom"
|
"dom",
|
||||||
]
|
"esnext.asynciterable"
|
||||||
|
],
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue