Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/api/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { globalConfig } from '@gh/config';
import { PrismaService, User as PrismaUser } from '@gh/prisma';
import { LoginDto } from '@gh/shared/dtos';
import { AuthKeys } from '@gh/shared/models';
import { loggedMethod } from '@gh/shared/utils';
import { loggedMethod, measureTime } from '@gh/shared/utils';
import { Body, Controller, Get, HttpCode, HttpStatus, Inject, Post, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { ApiBody } from '@nestjs/swagger';
Expand All @@ -28,6 +28,7 @@ export class AuthController {
@HttpCode(HttpStatus.OK)
@ApiBody({ type: LoginDto })
@loggedMethod('Log in with user/password')
@measureTime()
async login(@Body() user: LoginDto, @Res() res: Response) {
const response = await this.authService.login(user as PrismaUser);

Expand Down
4 changes: 2 additions & 2 deletions apps/ui-e2e/src/poms/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export class LoginPage {
await this.page.goto('/login');
}

async fill(email: string, passord: string) {
async fill(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(passord);
await this.password.fill(password);
}

async login() {
Expand Down
62 changes: 32 additions & 30 deletions apps/ui/src/app/components/gh-users/gh-users.component.html
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
<div class="container-fluid" ghLoader [loading]="usersPageResource.isLoading()">
<div class="row header position-sticky top-0 bg-white z-2">
<div class="col">
<div class="d-flex flex-row align-items-center">
@if (!usersPageResource.error() && usersPageResource.hasValue() && !!userListRangeIds.first) {
<div class="p-2">Showing <span data-testid="usersInPage">{{ usersPageResource.value().length }}</span> Github users (ID #<span data-testid="firstUser">{{ userListRangeIds.first }}</span> to #<span data-testid="lastUser">{{ userListRangeIds.last }}</span>)</div>
}
@else if (!usersPageResource.isLoading()) {
<div class="p-2" data-testid="noUsersFound">No users found</div>
}
<div class="p-2">
<button ghTooltipTrigger class="btn btn-primary btn-sm" [disabled]="!usersPageResource.value().length || pseudoPageIndex() === 0" data-bs-toggle="tooltip" data-bs-title="Previous page" (click)="decrementPage()">
<i class="fa-solid fa-chevron-left"></i>
</button>
@if(!usersPageResource.error()) {
<div class="d-flex flex-row align-items-center">
@if (usersPageResource.hasValue() && !!userListRangeIds.first) {
<div class="p-2">Showing <span data-testid="usersInPage">{{ usersPageResource.value().length }}</span> Github users (ID #<span data-testid="firstUser">{{ userListRangeIds.first }}</span> to #<span data-testid="lastUser">{{ userListRangeIds.last }}</span>)</div>
}
@else if (!usersPageResource.isLoading()) {
<div class="p-2" data-testid="noUsersFound">No users found</div>
}
<div class="p-2">
<button ghTooltipTrigger class="btn btn-primary btn-sm" [disabled]="!usersPageResource.value().length || pseudoPageIndex() === 0" data-bs-toggle="tooltip" data-bs-title="Previous page" (click)="decrementPage()">
<i class="fa-solid fa-chevron-left"></i>
</button>
</div>
<div class="p-2">
<button ghTooltipTrigger class="btn btn-primary btn-sm" [disabled]="!usersPageResource.value().length" data-bs-toggle="tooltip" data-bs-title="Next page" (click)="incrementPage()">
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
<div class="p-2">
<button ghTooltipTrigger class="btn btn-primary btn-sm" [disabled]="!usersPageResource.value().length" data-bs-toggle="tooltip" data-bs-title="Show all avatars" (click)="flipUsersToFront()">
<i class="fa-regular fa-eye"></i>
</button>
</div>
<div class="p-2">
<form [formGroup]="searchForm">
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-magnifying-glass"></i></span>
<input type="text" class="form-control" placeholder="search" formControlName="userName">
</div>
</form>
</div>
</div>
<div class="p-2">
<button ghTooltipTrigger class="btn btn-primary btn-sm" [disabled]="!usersPageResource.value().length" data-bs-toggle="tooltip" data-bs-title="Next page" (click)="incrementPage()">
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
<div class="p-2">
<button ghTooltipTrigger class="btn btn-primary btn-sm" [disabled]="!usersPageResource.value().length" data-bs-toggle="tooltip" data-bs-title="Show all avatars" (click)="flipUsersToFront()">
<i class="fa-regular fa-eye"></i>
</button>
</div>
<div class="p-2">
<form [formGroup]="searchForm">
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-magnifying-glass"></i></span>
<input type="text" class="form-control" placeholder="search" formControlName="userName">
</div>
</form>
</div>
</div>
}
</div>
</div>
<section class="row">
Expand Down
9 changes: 4 additions & 5 deletions apps/ui/src/app/interceptors/auth.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpErrorResponse, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { AppRouter } from 'fw-extensions/app-router';
import { catchError, switchMap, tap, throwError } from 'rxjs';
import { catchError, from, switchMap, tap, throwError } from 'rxjs';
import { AuthService } from 'services/auth.service';
import { IS_PUBLIC_API, IS_REFRESH_API } from 'utils/api';

Expand All @@ -19,14 +19,13 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
console.error('*** authInterceptor error = ', error);
if (error.status === 401) {
if (req.context.get(IS_REFRESH_API)) {
router.navigateToTokenExpired();

return throwError(() => new Error(error.message));
return from(router.navigateToTokenExpired()).pipe(
switchMap(() => throwError(() => new Error(error.message))),
);
}
return authService.refresh(authService.refreshToken).pipe(
tap(() => authService.saveCredentials()),
switchMap(() => {
authService.saveCredentials();
authReq = addToken(req, authService.accessToken);

return next(authReq);
Expand Down
44 changes: 41 additions & 3 deletions libs/shared/src/lib/utils/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import chalk from 'chalk';

const loggedMethod = (message = '') => {
return (target: object, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;

// Now, over-write the original method
descriptor.value = function (...args: unknown[]) {
descriptor.value = function(...args: unknown[]) {
// Execute custom logic
console.log(`Called %c${propertyKey}(${args})`, 'color: green; font-size: 14px; font-style: italic; font-weight: bold;', message ? `| message = ${message}` : '');
console.log(chalk.green.bold(`🔊 Called ${propertyKey}(${args})`), message ? `| message = ${message}` : '');

// Call original function
return originalMethod.apply(this, args);
Expand All @@ -15,4 +17,40 @@ const loggedMethod = (message = '') => {
};
};

export { loggedMethod };
const measureTime = () => {
const logTime = (methodName: string, duration: number) => {
console.log(chalk.green.bold(`\u23f3 Method ${methodName} took ${duration.toFixed(2)}ms`));
};

return (target: object, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;

descriptor.value = function(...args: unknown[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);

// If result is a Promise (thenable), handle asynchronously
if (result && typeof (result as Promise<unknown>).then === 'function') {
return (result as Promise<unknown>)
.then((res) => {
const end = performance.now();

logTime(propertyKey, end - start);

return res;
});
}

// Synchronous result
const end = performance.now();

logTime(propertyKey, end - start);

return result;
};

return descriptor;
}
};

export { loggedMethod, measureTime };
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@
"@nestjs/microservices": "^11.1.11",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.11",
"@nestjs/swagger": "^11.2.3",
"@nestjs/swagger": "^11.2.4",
"@ngrx/signals": "^21.0.1",
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"bootstrap": "^5.3.8",
"chalk": "^5.6.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"cookie-parser": "^1.4.7",
Expand Down Expand Up @@ -118,14 +119,14 @@
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/pg": "^8.16.0",
"@typescript-eslint/eslint-plugin": "^8.51.0",
"@typescript-eslint/parser": "^8.51.0",
"@typescript-eslint/eslint-plugin": "^8.52.0",
"@typescript-eslint/parser": "^8.52.0",
"copy-files-from-to": "^4.0.0",
"esbuild": "^0.27.2",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-playwright": "^2.4.0",
"globals": "^16.5.0",
"globals": "^17.0.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"jest-environment-node": "^30.2.0",
Expand Down
3 changes: 2 additions & 1 deletion services/users-service/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CreateUserDto, UpdateUserDto } from '@gh/shared';
import { loggedMethod } from '@gh/shared/utils';
import { loggedMethod, measureTime } from '@gh/shared/utils';
import { Controller, Get } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { UsersService } from './users.service';
Expand Down Expand Up @@ -28,6 +28,7 @@ export class UsersController {

@MessagePattern('get_user_by_email')
@loggedMethod('Users microservice: Get user by email')
@measureTime()
getUserByEmail(email: string) {
return this.usersService.getUserByEmail(email);
}
Expand Down
Loading
Loading