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
.gitignore
backend/src
frontend
.graphqlconfiggraphql.config.jspackage-lock.jsonpackage.json
src/app
app.component.tsapp.module.ts
tsconfig.jsonauth
graphql.module.tsgraphql
model
services
shared-components
subjects/subject-edit/courses/course-edit
users/user-list
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -48,3 +48,5 @@ testem.log
|
|||
Thumbs.db
|
||||
|
||||
.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 { User } from '../models';
|
||||
import { repository } from '@loopback/repository';
|
||||
import { UserRepository } from '../repositories';
|
||||
import { RevTokenRepository, UserRepository } from '../repositories';
|
||||
import { Context, inject } from '@loopback/core';
|
||||
import { SzakdolgozatUserService } from '../services';
|
||||
import { TokenServiceBindings, UserServiceBindings } from '@loopback/authentication-jwt';
|
||||
|
@ -18,6 +18,7 @@ import { SzakdolgozatBindings } from '../bindings';
|
|||
export class UserResolver {
|
||||
constructor(
|
||||
@repository('UserRepository') private readonly userRepository: UserRepository,
|
||||
@repository('RevTokenRepository') private readonly revTokenRepo: RevTokenRepository,
|
||||
@inject(UserServiceBindings.USER_SERVICE) private readonly userService: SzakdolgozatUserService,
|
||||
@inject(GraphQLBindings.RESOLVER_DATA) private readonly resolverData: ResolverData,
|
||||
@inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService,
|
||||
|
@ -38,11 +39,6 @@ export class UserResolver {
|
|||
return this.userRepository.create(user);
|
||||
}
|
||||
|
||||
@query(returns => User)
|
||||
async test(request: User): Promise<User> {
|
||||
return (await this.userRepository.find())[0];
|
||||
}
|
||||
|
||||
@mutation(returns => LoginResult)
|
||||
async login(@arg('email') email: string, @arg('password') password: string): Promise<LoginResult> {
|
||||
// ensure the user exists, and the password is correct
|
||||
|
@ -59,10 +55,8 @@ export class UserResolver {
|
|||
@mutation(returns => Boolean)
|
||||
async logout(): Promise<boolean> {
|
||||
const token = await this.context.get(SzakdolgozatBindings.AUTH_TOKEN);
|
||||
if (this.jwtService.revokeToken) {
|
||||
await this.jwtService.revokeToken(token);
|
||||
} else {
|
||||
console.error('Cannot revoke token');
|
||||
if (token && !(await this.revTokenRepo.count({token})).count) {
|
||||
await this.revTokenRepo.create({token, created: new Date()});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,12 @@ export class RevToken extends Entity {
|
|||
})
|
||||
token: string;
|
||||
|
||||
@property({
|
||||
type: Date,
|
||||
required: true
|
||||
})
|
||||
created: Date;
|
||||
|
||||
|
||||
constructor(data?: Partial<RevToken>) {
|
||||
super(data);
|
||||
|
|
|
@ -3,6 +3,8 @@ import { AuthChecker, ExpressContext } from '@loopback/graphql';
|
|||
import { Context, Getter, inject, ValueOrPromise } from '@loopback/core';
|
||||
import { JWTAuthenticationStrategy } from '@loopback/authentication-jwt';
|
||||
import { SzakdolgozatBindings } from './bindings';
|
||||
import { repository } from '@loopback/repository';
|
||||
import { RevTokenRepository } from './repositories';
|
||||
|
||||
export class SzakdolgozatAuthChecker {
|
||||
constructor() {
|
||||
|
@ -12,13 +14,21 @@ export class SzakdolgozatAuthChecker {
|
|||
static value(@inject(AuthenticationBindings.AUTH_ACTION) authenticate: AuthenticateFn,
|
||||
@inject.getter(AuthenticationBindings.STRATEGY)
|
||||
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) => {
|
||||
const econtext = (<ExpressContext> resolverData.context);
|
||||
const res = await authenticate(econtext.req);
|
||||
const strat = <JWTAuthenticationStrategy> await getStrategies();
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
|
|
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",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"e2e": "ng e2e",
|
||||
"gql": "graphql-codegen --config graphql.config.js"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
@ -23,7 +24,16 @@
|
|||
"@angular/platform-browser": "^11.0.4",
|
||||
"@angular/platform-browser-dynamic": "^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",
|
||||
"graphql": "^15.0.0",
|
||||
"rxjs": "~6.6.0",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.2"
|
||||
|
|
|
@ -4,8 +4,7 @@ import { Observable } from 'rxjs';
|
|||
import { filter, map, shareReplay } from 'rxjs/operators';
|
||||
import { LoginService } from './auth/login.service';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Route, Router, Routes } from '@angular/router';
|
||||
import { ApiService } from './api.service';
|
||||
import { UserRole } from './model/user.model';
|
||||
import { ApiService } from './services/api.service';
|
||||
import { RouteData } from './app-routing.module';
|
||||
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 };
|
||||
|
||||
export interface CustomTitleComponent {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { MatInputModule } from '@angular/material/input';
|
|||
import { LoginService } from './auth/login.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { AuthCheck } from './auth-check';
|
||||
import { GraphQLModule } from './graphql.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -37,7 +38,8 @@ import { AuthCheck } from './auth-check';
|
|||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientModule
|
||||
HttpClientModule,
|
||||
GraphQLModule
|
||||
],
|
||||
providers: [
|
||||
LoginService,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { User } from '../model/user.model';
|
||||
import { LoginGQL, UserResult } from '../services/graphql';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -9,17 +9,17 @@ import { User } from '../model/user.model';
|
|||
export class LoginService {
|
||||
|
||||
private tokenP: string;
|
||||
private userP: User;
|
||||
private userP: UserResult;
|
||||
|
||||
get token(): string {
|
||||
return this.tokenP;
|
||||
}
|
||||
|
||||
get user(): User {
|
||||
get user(): UserResult {
|
||||
return this.userP;
|
||||
}
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
constructor(private http: HttpClient, private loginService: LoginGQL) {
|
||||
this.tokenP = window.localStorage.getItem('token');
|
||||
this.userP = JSON.parse(window.localStorage.getItem('user'));
|
||||
}
|
||||
|
@ -30,14 +30,11 @@ export class LoginService {
|
|||
|
||||
async login(email: string, password: string): Promise<boolean> {
|
||||
try {
|
||||
const resp = await this.http.post<{ token: string, user: User }>(environment.backendUrl + '/users/login', {
|
||||
email,
|
||||
password
|
||||
}).toPromise();
|
||||
this.tokenP = resp.token;
|
||||
this.userP = resp.user;
|
||||
window.localStorage.setItem('token', resp.token);
|
||||
window.localStorage.setItem('user', JSON.stringify(resp.user));
|
||||
const resp = await this.loginService.mutate({email, password}).toPromise();
|
||||
this.tokenP = resp.data.login.token;
|
||||
this.userP = resp.data.login.user;
|
||||
window.localStorage.setItem('token', this.tokenP);
|
||||
window.localStorage.setItem('user', JSON.stringify(this.userP));
|
||||
return true;
|
||||
} catch (e) {
|
||||
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 { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../environments/environment';
|
||||
import { LoginService } from './auth/login.service';
|
||||
import { LoginService } from '../auth/login.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { Apollo } from 'apollo-angular';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
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> {
|
||||
return this.http.request(method, environment.backendUrl + url, {
|
||||
body,
|
||||
headers: {Authorization: 'Bearer ' + this.loginService.token}
|
||||
}).toPromise().catch(e => {
|
||||
const asd = this.apollo.query({
|
||||
query: gql`
|
||||
{
|
||||
__typename
|
||||
}
|
||||
`
|
||||
// headers: {Authorization: 'Bearer ' + this.loginService.token}
|
||||
}).toPromise().then(res => res);
|
||||
return asd.catch(e => {
|
||||
if (e.status === 401) {
|
||||
this.loginService.deleteToken();
|
||||
return this.router.navigateByUrl('/auth/login');
|
|
@ -1,5 +1,5 @@
|
|||
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 { Model } from '../../model/model';
|
||||
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 { PaginationData } from '../../utility/pagination-data';
|
||||
import { ApiService } from '../../api.service';
|
||||
import { Model } from '../../model/model';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
|
@ -10,10 +9,10 @@ import { Router } from '@angular/router';
|
|||
templateUrl: './list.component.html',
|
||||
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() itemType: Type<T>;
|
||||
@Input() itemType: T;
|
||||
@Input() columns: { title: string, prop: keyof T }[];
|
||||
@Input() allowNew = false;
|
||||
@Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = [];
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Course } from '../../../../model/course.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
|
@ -8,7 +7,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||
styleUrls: ['./course-edit.component.css']
|
||||
})
|
||||
export class CourseEditComponent implements OnInit {
|
||||
itemType = Course;
|
||||
itemType: Course;
|
||||
beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId});
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { User } from '../../model/user.model';
|
||||
import { UserResult } from '../../services/graphql';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-list',
|
||||
|
@ -7,7 +7,7 @@ import { User } from '../../model/user.model';
|
|||
styleUrls: ['./user-list.component.css']
|
||||
})
|
||||
export class UserListComponent implements OnInit {
|
||||
itemType = User;
|
||||
itemType: UserResult;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
|
@ -14,7 +13,9 @@
|
|||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
]
|
||||
"dom",
|
||||
"esnext.asynciterable"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue