diff --git a/backend/docker.sh b/backend/docker.sh index cade815..8ccc494 100644 --- a/backend/docker.sh +++ b/backend/docker.sh @@ -18,6 +18,6 @@ fi echo "Installing packages" npm install echo "Running application" -npm run rebuild +npm run clean wait-for-it database:3306 -t 0 npm run start:watch diff --git a/backend/src/graphql-resolvers/user.resolver.ts b/backend/src/graphql-resolvers/user.resolver.ts index aaf231c..7275fb8 100644 --- a/backend/src/graphql-resolvers/user.resolver.ts +++ b/backend/src/graphql-resolvers/user.resolver.ts @@ -1,4 +1,4 @@ -import { arg, authorized, GraphQLBindings, ID, 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 { repository } from '@loopback/repository'; import { RevTokenRepository, UserRepository } from '../repositories'; @@ -10,9 +10,10 @@ import { SecurityBindings, UserProfile } from '@loopback/security'; import { genSalt, hash } from 'bcryptjs'; import { UserRegisterInput } from '../graphql-types/input/user-register.input'; import { validated } from '../helpers'; -import { LoginResult } from '../graphql-types/user'; +import { LoginResult, UserList } from '../graphql-types/user'; import { UserUpdateInput } from '../graphql-types/input/user-update.input'; import { SzakdolgozatBindings } from '../bindings'; +import { listResponse } from '../graphql-types/list'; @resolver(of => User) export class UserResolver { @@ -69,20 +70,26 @@ export class UserResolver { @authorized() @query(returns => User, {name: 'user'}) - async findById(@arg('id') id: number): Promise { + async findById(@arg('id', returns => ID) id: number): Promise { return this.userRepository.findById(id); } + @authorized() + @query(returns => [User]) + async users(@arg('limit', returns => Int) limit: number, @arg('offset', returns => Int) offset: number) { + return await listResponse(this.userRepository, offset, limit, UserList); + } + @authorized() @mutation(returns => Boolean) - async userUpdate(@arg('id', returns => ID) id: number, @arg('user') user: UserUpdateInput): Promise { - if (id === +this.user?.id) { //TODO: this.user + async userUpdate(@arg('user') user: UserUpdateInput): Promise { + if (user.id === +this.user?.id) { //TODO: this.user const loggedInUser = await this.userService.findUserById(this.user.id); if (user.isAdmin !== undefined && loggedInUser.isAdmin !== user.isAdmin) { throw new Error('Cannot change admin status of self'); } } - await this.userRepository.updateById(id, user); + await this.userRepository.updateById(user.id, user); return true; } diff --git a/backend/src/graphql-types/input/user-update.input.ts b/backend/src/graphql-types/input/user-update.input.ts index ae2386d..bcd246c 100644 --- a/backend/src/graphql-types/input/user-update.input.ts +++ b/backend/src/graphql-types/input/user-update.input.ts @@ -1,10 +1,12 @@ import { User } from '../../models'; -import { field, inputType } from '@loopback/graphql'; +import { field, ID, inputType } from '@loopback/graphql'; import { UserProperties } from '../user'; import { property } from '@loopback/repository'; @inputType() -export class UserUpdateInput implements Partial> { +export class UserUpdateInput implements Partial> { + @field(returns => ID) + id: number; @field({nullable: true}) @property(UserProperties.email) email?: string; diff --git a/backend/src/graphql-types/list.ts b/backend/src/graphql-types/list.ts index ba22e1e..a0ffa8c 100644 --- a/backend/src/graphql-types/list.ts +++ b/backend/src/graphql-types/list.ts @@ -14,7 +14,7 @@ export interface ListResponse { list: T[]; } -export async function listResponse>(repo: DefaultCrudRepository, offset: number, limit: number, listType: ClassType) { +export async function listResponse>>(repo: DefaultCrudRepository, offset: number, limit: number, listType: ClassType) { const list = new listType(); list.list = await repo.find({offset, limit}); list.count = (await repo.count()).count; diff --git a/backend/src/graphql-types/user.ts b/backend/src/graphql-types/user.ts index 33e27fe..b208c58 100644 --- a/backend/src/graphql-types/user.ts +++ b/backend/src/graphql-types/user.ts @@ -1,5 +1,6 @@ import { User } from '../models'; -import { field, objectType } from '@loopback/graphql'; +import { field, Int, objectType } from '@loopback/graphql'; +import { ListResponse } from './list'; @objectType() export class UserResult implements Pick { @@ -21,6 +22,14 @@ export class LoginResult { user: UserResult; } +@objectType() +export class UserList implements ListResponse { + @field(returns => Int) + count: number; + @field(returns => [UserResult]) + list: UserResult[]; +} + export const UserProperties = { id: { type: 'number', diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index f1e3d01..3ca12ba 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -4,7 +4,6 @@ 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 './services/api.service'; import { RouteData } from './app-routing.module'; import { Title } from '@angular/platform-browser'; @@ -26,13 +25,11 @@ export class AppComponent implements OnInit { {path: 'subjects', requiredRole: 'admin'} ]; - routeSegments: RouteSegment[]; - pageTitle: string; private activeRouteTitle: string; - constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService, private api: ApiService, + constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService, private router: Router, private activeRoute: ActivatedRoute, private title: Title) { } diff --git a/frontend/src/app/graphql.module.ts b/frontend/src/app/graphql.module.ts index 816f185..7f9b7e2 100644 --- a/frontend/src/app/graphql.module.ts +++ b/frontend/src/app/graphql.module.ts @@ -1,13 +1,51 @@ -import { NgModule } from '@angular/core'; +import { Injector, NgModule } from '@angular/core'; import { APOLLO_OPTIONS } from 'apollo-angular'; -import { ApolloClientOptions, InMemoryCache } from '@apollo/client/core'; +import { ApolloClientOptions, ApolloLink, InMemoryCache } from '@apollo/client/core'; import { HttpLink } from 'apollo-angular/http'; import { environment } from '../environments/environment'; +import { setContext } from '@apollo/client/link/context'; +import { onError } from '@apollo/client/link/error'; +import { Router } from '@angular/router'; +import { LoginService } from './auth/login.service'; const uri = environment.backendUrl + '/graphql'; // <-- add the URL of the GraphQL server here -export function createApollo(httpLink: HttpLink): ApolloClientOptions { +export function createApollo(httpLink: HttpLink, routerService: Router, injector: Injector): ApolloClientOptions { + const auth = setContext((operation, context) => { + const token = injector.get(LoginService).token; + + if (token === null) { + return {}; + } else { + return { + headers: { + Authorization: `Bearer ${token}` + } + }; + } + }); + + const errorLink = onError(({graphQLErrors, networkError}) => { + if (graphQLErrors) { + graphQLErrors.map((graphqlError) => { + console.log(graphqlError); + if (graphqlError.message.startsWith('Error verifying token')) { + injector.get(LoginService).deleteToken(); + routerService.navigateByUrl('/'); + } else { + alert(graphqlError.message); + } + }); + } + + if (networkError) { + const errorMessage = networkError.message; + console.log(errorMessage); + alert(errorMessage); + } + }); + return { - link: httpLink.create({uri}), + link: ApolloLink.from([auth, errorLink, httpLink.create({uri})]), cache: new InMemoryCache(), }; } @@ -17,7 +55,7 @@ export function createApollo(httpLink: HttpLink): ApolloClientOptions { { provide: APOLLO_OPTIONS, useFactory: createApollo, - deps: [HttpLink], + deps: [HttpLink, Router, Injector], }, ], }) diff --git a/frontend/src/app/graphql/subject.graphql b/frontend/src/app/graphql/subject.graphql index 9e661f2..3af3f0b 100644 --- a/frontend/src/app/graphql/subject.graphql +++ b/frontend/src/app/graphql/subject.graphql @@ -16,3 +16,7 @@ query Subject($id: ID!) { description } } + +mutation EditSubject($input: SubjectUpdateInput!) { + subjectUpdate(subject: $input) +} diff --git a/frontend/src/app/graphql/user.graphql b/frontend/src/app/graphql/user.graphql index 0b4f7e9..608fae3 100644 --- a/frontend/src/app/graphql/user.graphql +++ b/frontend/src/app/graphql/user.graphql @@ -13,3 +13,25 @@ mutation Login($email: String!, $password: String!) { mutation Logout { logout } + +query UserList($limit: Int!, $offset: Int!) { + users(limit: $limit, offset: $offset) { + id + name + email + isAdmin + } +} + +query User($id: ID!) { + user(id: $id) { + id + isAdmin + email + name + } +} + +mutation EditUser($input: UserUpdateInput!) { + userUpdate(user: $input) +} diff --git a/frontend/src/app/services/api.service.spec.ts b/frontend/src/app/services/api.service.spec.ts deleted file mode 100644 index c0310ae..0000000 --- a/frontend/src/app/services/api.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { ApiService } from './api.service'; - -describe('ApiService', () => { - let service: ApiService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(ApiService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts deleted file mode 100644 index f520e2c..0000000 --- a/frontend/src/app/services/api.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from '@angular/core'; -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 apollo: Apollo, private loginService: LoginService, private router: Router) { - } - - request(method: 'post' | 'get' | 'delete' | 'patch', url: string, body: any): Promise { - 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'); - } else { - throw e; - } - }); - } - - requestPage(url: string, limit: number, page: number): Promise { - const c = url.indexOf('?') === -1 ? '?' : '&'; - return this.request('get', url + c + 'filter=' + encodeURI(JSON.stringify({ - limit, - offset: (page - 1) * limit - })), {}); - } - - requestItemCount(url: string): Promise { - return this.request('get', url + '/count', {}).then(count => count.count); - } -} diff --git a/frontend/src/app/shared-components/edit/edit.component.ts b/frontend/src/app/shared-components/edit/edit.component.ts index b093211..2bb911f 100644 --- a/frontend/src/app/shared-components/edit/edit.component.ts +++ b/frontend/src/app/shared-components/edit/edit.component.ts @@ -1,21 +1,25 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ApiService } from '../../services/api.service'; import { ActivatedRoute, Router } from '@angular/router'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; -import { Query } from 'apollo-angular'; +import { Mutation, Query } from 'apollo-angular'; +import { noop } from 'rxjs'; +import { HasID, MutationInput, QueryResult } from '../../utility/types'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.css'] }) -export class EditComponent implements OnInit { +export class EditComponent, UT extends QueryResult, CT extends QueryResult, MI extends Partial> + implements OnInit { item?: T; creating = false; isLoading = true; - @Input() gql: Query; + @Input() gql: Query; + @Input() updateMutation: Mutation>; + @Input() createMutation: Mutation>; @Input() fields: { title: string, name: keyof T, readonly?: (item: T) => boolean }[]; @Input() itemType: T; /** @@ -24,15 +28,20 @@ export class EditComponent implements OnInit { @Input() beforeSubmit: (item: T) => Partial; formGroup: FormGroup; - constructor(private api: ApiService, private router: Router, private fb: FormBuilder, private route: ActivatedRoute) { + private key: string; + private id: string; + + constructor(private router: Router, private fb: FormBuilder, private route: ActivatedRoute) { } async ngOnInit(): Promise { - this.item = JSON.parse(window.localStorage.getItem(this.router.url)); window.localStorage.removeItem(this.router.url); const url = this.route.snapshot.url; + this.id = this.route.snapshot.url[this.route.snapshot.url.length - 1] + ''; if (!this.item && url[url.length - 1].path !== 'new') { - //this.item = await this.api.request('get', this.apiPath + '/' + this.route.snapshot.url[this.route.snapshot.url.length - 1], {}); - TODO + const data = (await this.gql.fetch({id: this.id}).toPromise()).data; + this.key = Object.keys(data).filter(k => k !== '__typename')[0]; + this.item = data[this.key]; } this.formGroup = this.fb.group(this.fields.reduce((pv, cv) => { const control = new FormControl(); @@ -52,17 +61,17 @@ export class EditComponent implements OnInit { async submit(): Promise { this.isLoading = true; - const value = Object.assign({}, this.formGroup.value, this.beforeSubmit(this.item) ?? {}); + const input = Object.assign({}, this.formGroup.value, (this.beforeSubmit ?? noop)(this.item) ?? {}, {id: this.id}) as MI; try { if (this.item && !this.creating) { - //await this.api.request('patch', this.apiPath + '/' + this.item.id, value); - TODO + await this.updateMutation.mutate({input}).toPromise(); } else { - //await this.api.request('post', this.apiPath, value); - TODO + await this.createMutation.mutate({input}).toPromise(); } await this.router.navigate(['..'], {relativeTo: this.route}); } catch (e) { alert(e.message); - } + } // TODO: Clear/update cache this.isLoading = false; } diff --git a/frontend/src/app/shared-components/list/list.component.ts b/frontend/src/app/shared-components/list/list.component.ts index 076d570..f5830fa 100644 --- a/frontend/src/app/shared-components/list/list.component.ts +++ b/frontend/src/app/shared-components/list/list.component.ts @@ -1,7 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { PaginationData } from '../../utility/pagination-data'; -import { ApiService } from '../../services/api.service'; import { Router } from '@angular/router'; import { Query } from 'apollo-angular'; @@ -23,7 +22,7 @@ export class ListComponent +]" [updateMutation]="updateGQL"> diff --git a/frontend/src/app/subjects/subject-edit/subject-edit.component.ts b/frontend/src/app/subjects/subject-edit/subject-edit.component.ts index 218035b..4cd8402 100644 --- a/frontend/src/app/subjects/subject-edit/subject-edit.component.ts +++ b/frontend/src/app/subjects/subject-edit/subject-edit.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { Subject, SubjectGQL } from '../../services/graphql'; +import { EditSubjectGQL, Subject, SubjectGQL } from '../../services/graphql'; @Component({ selector: 'app-subject-edit', @@ -9,7 +9,7 @@ import { Subject, SubjectGQL } from '../../services/graphql'; export class SubjectEditComponent implements OnInit { itemType: Subject; - constructor(public itemGQL: SubjectGQL) { + constructor(public itemGQL: SubjectGQL, public updateGQL: EditSubjectGQL) { } ngOnInit(): void { diff --git a/frontend/src/app/users/user-edit/user-edit.component.html b/frontend/src/app/users/user-edit/user-edit.component.html index d019870..6f513f1 100644 --- a/frontend/src/app/users/user-edit/user-edit.component.html +++ b/frontend/src/app/users/user-edit/user-edit.component.html @@ -1,4 +1,4 @@ - diff --git a/frontend/src/app/users/user-list/user-list.component.ts b/frontend/src/app/users/user-list/user-list.component.ts index cd94404..7837a76 100644 --- a/frontend/src/app/users/user-list/user-list.component.ts +++ b/frontend/src/app/users/user-list/user-list.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { UserResult } from '../../services/graphql'; +import { UserListGQL, UserResult } from '../../services/graphql'; @Component({ selector: 'app-user-list', @@ -9,7 +9,7 @@ import { UserResult } from '../../services/graphql'; export class UserListComponent implements OnInit { itemType: UserResult; - constructor() { + constructor(public listGQL: UserListGQL) { } ngOnInit(): void { diff --git a/frontend/src/app/utility/types.ts b/frontend/src/app/utility/types.ts new file mode 100644 index 0000000..eeae322 --- /dev/null +++ b/frontend/src/app/utility/types.ts @@ -0,0 +1,5 @@ +import { Scalars } from '../services/graphql'; + +export type HasID = { id: Scalars['ID'] }; +export type QueryResult = { [entityName: string]: T }; +export type MutationInput, U extends HasID> = { input: T };