Skip to content

Commit 1c3d593

Browse files
committed
Merge branch 'main' into role-based-authorization-spa
2 parents 0e8e918 + fa522fd commit 1c3d593

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3544
-1107
lines changed

.env.sample

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ QUESTION_DB_USERNAME=user
88
QUESTION_DB_PASSWORD=password
99

1010
# User Service
11-
USER_SERVICE_CLOUD_URI=mongodb+srv://admin:<db_password>@cluster0.uo0vu.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
12-
USER_SERVICE_LOCAL_URI=mongodb://127.0.0.1:27017/peerprepUserServiceDB
13-
14-
# Will use cloud MongoDB Atlas database
15-
ENV=PROD
11+
USER_DB_CLOUD_URI=<FILL-THIS-IN>
12+
USER_DB_LOCAL_URI=mongodb://user-db:27017/user
13+
USER_DB_USERNAME=user
14+
USER_DB_PASSWORD=password
1615

1716
# Secret for creating JWT signature
1817
JWT_SECRET=you-can-replace-this-with-your-own-secret

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
strategy:
2424
fail-fast: false
2525
matrix:
26-
service: [frontend, services/question]
26+
service: [frontend, services/question, services/user]
2727
steps:
2828
- uses: actions/checkout@v4
2929
- name: Use Node.js

compose.dev.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ services:
66

77
question:
88
command: npm run dev
9+
ports:
10+
- 8081:8081
911
volumes:
1012
- /app/node_modules
1113
- ./services/question:/app
@@ -16,6 +18,12 @@ services:
1618

1719
user:
1820
command: npm run dev
21+
ports:
22+
- 8082:8082
1923
volumes:
2024
- /app/node_modules
21-
- ./services/user:/app
25+
- ./services/user:/app
26+
27+
user-db:
28+
ports:
29+
- 27018:27017

compose.yml

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@ services:
99
- 4200:4200
1010
restart: always
1111

12+
gateway:
13+
container_name: gateway
14+
image: nginx:1.27
15+
ports:
16+
- 8080:8080
17+
volumes:
18+
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
19+
networks:
20+
- gateway-network
21+
1222
question:
1323
container_name: question
1424
image: question
1525
build:
1626
context: services/question
1727
dockerfile: Dockerfile
18-
ports:
19-
- 8081:8081
2028
environment:
2129
DB_CLOUD_URI: ${QUESTION_DB_CLOUD_URI}
2230
DB_LOCAL_URI: ${QUESTION_DB_LOCAL_URI}
2331
DB_USERNAME: ${QUESTION_DB_USERNAME}
2432
DB_PASSWORD: ${QUESTION_DB_PASSWORD}
2533
networks:
34+
- gateway-network
2635
- question-db-network
2736
restart: always
2837

@@ -46,18 +55,38 @@ services:
4655
build:
4756
context: services/user
4857
dockerfile: Dockerfile
49-
ports:
50-
- 8082:8082
5158
environment:
52-
USER_SERVICE_CLOUD_URI: ${USER_SERVICE_CLOUD_URI}
53-
USER_SERVICE_LOCAL_URI: ${USER_SERVICE_LOCAL_URI}
54-
ENV: ${ENV}
59+
DB_CLOUD_URI: ${USER_DB_CLOUD_URI}
60+
DB_LOCAL_URI: ${USER_DB_LOCAL_URI}
61+
DB_USERNAME: ${USER_DB_USERNAME}
62+
DB_PASSWORD: ${USER_DB_PASSWORD}
5563
JWT_SECRET: ${JWT_SECRET}
64+
networks:
65+
- gateway-network
66+
- user-db-network
67+
restart: always
68+
69+
user-db:
70+
container_name: user-db
71+
image: mongo:7.0.14
72+
environment:
73+
MONGO_INITDB_ROOT_USERNAME: ${USER_DB_USERNAME}
74+
MONGO_INITDB_ROOT_PASSWORD: ${USER_DB_PASSWORD}
75+
volumes:
76+
- user-db:/data/db
77+
networks:
78+
- user-db-network
79+
command: --quiet
5680
restart: always
5781

5882
volumes:
5983
question-db:
84+
user-db:
6085

6186
networks:
87+
gateway-network:
88+
driver: bridge
6289
question-db-network:
6390
driver: bridge
91+
user-db-network:
92+
driver: bridge
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
3+
import { Observable, throwError } from 'rxjs';
4+
import { catchError } from 'rxjs/operators';
5+
6+
@Injectable()
7+
export class ErrorInterceptor implements HttpInterceptor {
8+
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
9+
return next.handle(request).pipe(
10+
catchError(err => {
11+
console.error(err);
12+
13+
const errorMessage = err.error.message;
14+
return throwError(() => new Error(errorMessage, { cause: err }));
15+
}),
16+
);
17+
}
18+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
3+
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
4+
import { JwtInterceptor } from './jwt.interceptor';
5+
import { AuthenticationService } from '../_services/authentication.service';
6+
import { API_CONFIG } from '../app/api.config';
7+
8+
describe('JwtInterceptor', () => {
9+
let httpMock: HttpTestingController;
10+
let httpClient: HttpClient;
11+
let mockAuthService: jasmine.SpyObj<AuthenticationService>;
12+
13+
beforeEach(() => {
14+
mockAuthService = jasmine.createSpyObj('AuthenticationService', ['userValue'], {
15+
userValue: { accessToken: 'fake-jwt-token' },
16+
});
17+
18+
TestBed.configureTestingModule({
19+
imports: [HttpClient],
20+
providers: [
21+
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
22+
{ provide: AuthenticationService, useValue: mockAuthService },
23+
provideHttpClientTesting(),
24+
],
25+
});
26+
27+
httpMock = TestBed.inject(HttpTestingController);
28+
httpClient = TestBed.inject(HttpClient);
29+
});
30+
31+
afterEach(() => {
32+
// Check if all Http requests were handled
33+
httpMock.verify();
34+
});
35+
36+
it('should add an Authorization header', () => {
37+
httpClient.get(`${API_CONFIG.baseUrl}/user/test`).subscribe();
38+
39+
const httpRequest = httpMock.expectOne(`${API_CONFIG.baseUrl}/user/test`);
40+
41+
expect(httpRequest.request.headers.has('Authorization')).toBeTruthy();
42+
expect(httpRequest.request.headers.get('Authorization')).toBe('Bearer fake-jwt-token');
43+
});
44+
45+
it('should not add an Authorization header if the user is not logged in', () => {
46+
mockAuthService = jasmine.createSpyObj('AuthenticationService', ['userValue'], {
47+
userValue: {},
48+
});
49+
50+
httpClient.get(`${API_CONFIG.baseUrl}/user/test`).subscribe();
51+
52+
const httpRequest = httpMock.expectOne(`${API_CONFIG.baseUrl}/user/test`);
53+
54+
expect(httpRequest.request.headers.has('Authorization')).toBeFalsy();
55+
});
56+
57+
it('should not add an Authorization header for non-API URLs', () => {
58+
httpClient.get('https://example.com/test').subscribe();
59+
60+
const httpRequest = httpMock.expectOne('https://example.com/test');
61+
62+
expect(httpRequest.request.headers.has('Authorization')).toBeFalsy();
63+
});
64+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
3+
import { Observable } from 'rxjs';
4+
import { AuthenticationService } from '../_services/authentication.service';
5+
6+
@Injectable()
7+
export class JwtInterceptor implements HttpInterceptor {
8+
constructor(private authenticationService: AuthenticationService) {}
9+
10+
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
11+
// add auth header with jwt if user is logged in and request is to the api url
12+
const currentUser = this.authenticationService.userValue;
13+
if (currentUser) {
14+
const accessToken = currentUser.accessToken;
15+
request = request.clone({
16+
headers: request.headers.set('Authorization', `Bearer ${accessToken}`),
17+
});
18+
}
19+
return next.handle(request);
20+
}
21+
}

frontend/src/_models/user.model.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
export class User {
2-
id?: string;
3-
username?: string;
4-
email?: string;
5-
isAdmin?: boolean;
6-
createdAt?: string;
7-
accessToken?: string;
1+
export interface User {
2+
id: string;
3+
username: string;
4+
email: string;
5+
isAdmin: boolean;
6+
createdAt: string;
7+
accessToken: string;
88
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { User } from './user.model';
2+
3+
export interface BaseResponse {
4+
status: string;
5+
message: string;
6+
}
7+
8+
export interface UServRes extends BaseResponse {
9+
message: string;
10+
data: User;
11+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { API_CONFIG } from '../app/api.config';
2+
3+
/**
4+
* Abstract class that serves as a base for API services.
5+
*/
6+
export abstract class ApiService {
7+
/**
8+
* The path for the specific resource, e.g. 'user', 'question', etc.
9+
* This property must be implemented by subclasses to specify the
10+
* endpoint path for the API resource they represent.
11+
*/
12+
protected abstract apiPath: string;
13+
14+
/**
15+
* Returns the full URL for the API endpoint based on
16+
* the specified apiPath.
17+
*/
18+
get apiUrl(): string {
19+
return API_CONFIG.baseUrl + this.apiPath;
20+
}
21+
}

0 commit comments

Comments
 (0)