diff --git a/admin/src/app/app.routes.ts b/admin/src/app/app.routes.ts
index 2b9ed87..aca0b8f 100644
--- a/admin/src/app/app.routes.ts
+++ b/admin/src/app/app.routes.ts
@@ -17,8 +17,53 @@ import { EventFormComponent } from './features/events/components/event-form/even
import { EventDetailsComponent } from './features/events/components/event-details/event-details.component';
import { EventTableComponent } from './features/events/components/event-table/event-table.component';
import { EventListComponent } from './features/events/components/event-list/event-list.component';
+import { UserFormComponent } from './features/user/components/user-form/user-form.component';
+import { UserDetailsComponent } from './features/user/components/user-details/user-details.component';
+import { UserTableComponent } from './features/user/components/user-table/user-table.component';
+import { UserListComponent } from './features/user/components/user-list/user-list.component';
export const routes: Routes = [
+ {
+ path: 'user/new',
+ component: UserFormComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user',
+ component: UserListComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user/table',
+ component: UserTableComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user/:id',
+ component: UserDetailsComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+ {
+ path: 'user/:id/edit',
+ component: UserFormComponent,
+ canActivate: [AuthGuard],
+ data: {
+ roles: ['admin'],
+ },
+ },
+
{
path: 'events/new',
component: EventFormComponent,
diff --git a/admin/src/app/app.ts b/admin/src/app/app.ts
index 8aec2f2..6773b62 100644
--- a/admin/src/app/app.ts
+++ b/admin/src/app/app.ts
@@ -35,6 +35,14 @@ export class App {
svgIcon: `
+`
+ },
+ {
+ menuText: 'Felhasználók',
+ targetUrl: '/user/table',
+ svgIcon: `
`
}
diff --git a/admin/src/app/features/user/components/user-details/user-details.component.html b/admin/src/app/features/user/components/user-details/user-details.component.html
new file mode 100644
index 0000000..4a46f14
--- /dev/null
+++ b/admin/src/app/features/user/components/user-details/user-details.component.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
User Details
+
+
+
+
+
+ | id |
+ {{ user.id }} |
+
+
+ | username |
+ {{ user.username }} |
+
+
+ | email |
+ {{ user.email }} |
+
+
+ | password |
+ {{ user.password }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/app/features/user/components/user-details/user-details.component.ts b/admin/src/app/features/user/components/user-details/user-details.component.ts
new file mode 100644
index 0000000..e2b9cd4
--- /dev/null
+++ b/admin/src/app/features/user/components/user-details/user-details.component.ts
@@ -0,0 +1,34 @@
+// dvbooking-cli/src/templates/angular/details.component.ts.tpl
+
+// Generated by the CLI
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ActivatedRoute, RouterModule } from '@angular/router';
+import { Observable } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+import { User } from '../../models/user.model';
+import { UserService } from '../../services/user.service';
+
+@Component({
+ selector: 'app-user-details',
+ templateUrl: './user-details.component.html',
+ standalone: true,
+ imports: [CommonModule, RouterModule],
+})
+export class UserDetailsComponent implements OnInit {
+ user$!: Observable;
+
+ constructor(
+ private route: ActivatedRoute,
+ private userService: UserService
+ ) {}
+
+ ngOnInit(): void {
+ this.user$ = this.route.params.pipe(
+ switchMap(params => {
+ const id = params['id'];
+ return this.userService.findOne(id);
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user/components/user-filter/user-filter.component.html b/admin/src/app/features/user/components/user-filter/user-filter.component.html
new file mode 100644
index 0000000..3da4913
--- /dev/null
+++ b/admin/src/app/features/user/components/user-filter/user-filter.component.html
@@ -0,0 +1,18 @@
+
+
+
diff --git a/admin/src/app/features/user/components/user-filter/user-filter.component.ts b/admin/src/app/features/user/components/user-filter/user-filter.component.ts
new file mode 100644
index 0000000..afb7d70
--- /dev/null
+++ b/admin/src/app/features/user/components/user-filter/user-filter.component.ts
@@ -0,0 +1,40 @@
+// dvbooking-cli/src/templates/angular/filter.component.ts.tpl
+
+// Generated by the CLI
+import { Component, EventEmitter, Output } from '@angular/core';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-user-filter',
+ templateUrl: './user-filter.component.html',
+ standalone: true,
+ imports: [ReactiveFormsModule]
+})
+export class UserFilterComponent {
+ @Output() filterChanged = new EventEmitter();
+ filterForm: FormGroup;
+
+ constructor(private fb: FormBuilder) {
+ this.filterForm = this.fb.group({
+ username: [''],
+ email: [''],
+ password: [''],
+ hashedRefreshToken: ['']
+ });
+
+ this.filterForm.valueChanges.pipe(
+ debounceTime(300),
+ distinctUntilChanged()
+ ).subscribe(values => {
+ const cleanFilter = Object.fromEntries(
+ Object.entries(values).filter(([_, v]) => v != null && v !== '')
+ );
+ this.filterChanged.emit(cleanFilter);
+ });
+ }
+
+ reset() {
+ this.filterForm.reset();
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user/components/user-form/user-form.component.html b/admin/src/app/features/user/components/user-form/user-form.component.html
new file mode 100644
index 0000000..ade414a
--- /dev/null
+++ b/admin/src/app/features/user/components/user-form/user-form.component.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+ {{ isEditMode ? 'Edit' : 'Create' }} User
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user/components/user-form/user-form.component.ts b/admin/src/app/features/user/components/user-form/user-form.component.ts
new file mode 100644
index 0000000..712c6c1
--- /dev/null
+++ b/admin/src/app/features/user/components/user-form/user-form.component.ts
@@ -0,0 +1,88 @@
+// dvbooking-cli/src/templates/angular/form.component.ts.tpl
+
+// Generated by the CLI
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute, Router, RouterModule } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { switchMap, tap } from 'rxjs/operators';
+import { User } from '../../models/user.model';
+import { UserService } from '../../services/user.service';
+
+@Component({
+ selector: 'app-user-form',
+ templateUrl: './user-form.component.html',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, RouterModule],
+})
+export class UserFormComponent implements OnInit {
+ form: FormGroup;
+ isEditMode = false;
+ id: number | null = null;
+
+ private numericFields = [];
+
+ constructor(
+ private fb: FormBuilder,
+ private route: ActivatedRoute,
+ private router: Router,
+ private userService: UserService
+ ) {
+ this.form = this.fb.group({
+ username: [null],
+ email: [null],
+ password: [null],
+ hashedRefreshToken: [null]
+ });
+ }
+
+ ngOnInit(): void {
+ this.route.params.pipe(
+ tap(params => {
+ if (params['id']) {
+ this.isEditMode = true;
+ this.id = +params['id'];
+ }
+ }),
+ switchMap(() => {
+ if (this.isEditMode && this.id) {
+ return this.userService.findOne(this.id);
+ }
+ return of(null);
+ })
+ ).subscribe(user => {
+ if (user) {
+ this.form.patchValue(user);
+ }
+ });
+ }
+
+ onSubmit(): void {
+ if (this.form.invalid) {
+ this.form.markAllAsTouched();
+ return;
+ }
+
+ const payload = { ...this.form.value };
+
+ for (const field of this.numericFields) {
+ if (payload[field] != null && payload[field] !== '') {
+ payload[field] = parseFloat(payload[field]);
+ }
+ }
+
+ let action$: Observable;
+
+ if (this.isEditMode && this.id) {
+ action$ = this.userService.update(this.id, payload);
+ } else {
+ action$ = this.userService.create(payload);
+ }
+
+ action$.subscribe({
+ next: () => this.router.navigate(['/user']),
+ error: (err) => console.error('Failed to save user', err)
+ });
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user/components/user-list/user-list.component.html b/admin/src/app/features/user/components/user-list/user-list.component.html
new file mode 100644
index 0000000..e7ba5c8
--- /dev/null
+++ b/admin/src/app/features/user/components/user-list/user-list.component.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | id |
+ username |
+ email |
+ Actions |
+
+
+
+
+ | {{ item.id }} |
+ {{ item.username }} |
+ {{ item.email }} |
+
+ View
+ Edit
+
+ |
+
+
+ | No user found. |
+
+
+
+
+
+
+ 1" class="flex justify-center mt-4">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/app/features/user/components/user-list/user-list.component.ts b/admin/src/app/features/user/components/user-list/user-list.component.ts
new file mode 100644
index 0000000..bd0f757
--- /dev/null
+++ b/admin/src/app/features/user/components/user-list/user-list.component.ts
@@ -0,0 +1,70 @@
+// dvbooking-cli/src/templates/angular/list.component.ts.tpl
+
+// Generated by the CLI
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
+import { switchMap, startWith } from 'rxjs/operators';
+import { User } from '../../models/user.model';
+import { UserService } from '../../services/user.service';
+import { UserFilterComponent } from '../user-filter/user-filter.component';
+import { PaginatedResponse } from '../../../../../types';
+
+
+@Component({
+ selector: 'app-user-list',
+ templateUrl: './user-list.component.html',
+ standalone: true,
+ imports: [CommonModule,RouterModule, UserFilterComponent],
+})
+export class UserListComponent implements OnInit {
+
+ private refresh$ = new BehaviorSubject(undefined);
+ private filter$ = new BehaviorSubject({});
+ private page$ = new BehaviorSubject(1);
+
+ paginatedResponse$!: Observable>;
+
+ constructor(private userService: UserService) { }
+
+ ngOnInit(): void {
+ this.paginatedResponse$ = combineLatest([
+ this.refresh$,
+ this.filter$.pipe(startWith({})),
+ this.page$.pipe(startWith(1))
+ ]).pipe(
+ switchMap(([_, filter, page]) => {
+ const query = { ...filter, page, limit: 10 };
+ return this.userService.find(query);
+ })
+ );
+ }
+
+ onFilterChanged(filter: any): void {
+ this.page$.next(1);
+ this.filter$.next(filter);
+ }
+
+ changePage(newPage: number): void {
+ if (newPage > 0) {
+ this.page$.next(newPage);
+ }
+ }
+
+ deleteItem(id: number): void {
+ if (confirm('Are you sure you want to delete this item?')) {
+ this.userService.remove(id).subscribe({
+ next: () => {
+ console.log(`Item with ID ${id} deleted successfully.`);
+ this.refresh$.next();
+ },
+ // --- THIS IS THE FIX ---
+ // Explicitly type 'err' to satisfy strict TypeScript rules.
+ error: (err: any) => {
+ console.error(`Error deleting item with ID ${id}:`, err);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user/components/user-table/user-data-provider.service.ts b/admin/src/app/features/user/components/user-table/user-data-provider.service.ts
new file mode 100644
index 0000000..52ea152
--- /dev/null
+++ b/admin/src/app/features/user/components/user-table/user-data-provider.service.ts
@@ -0,0 +1,26 @@
+// dvbooking-cli/src/templates/angular-generic/data-provider.service.ts.tpl
+
+// Generated by the CLI
+import { inject, Injectable } from '@angular/core';
+import { DataProvider, GetDataOptions, GetDataResponse } from '../../../../components/generic-table/data-provider.interface';
+import { User } from '../../models/user.model';
+import { map, Observable } from 'rxjs';
+import { UserService } from '../../services/user.service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class UserDataProvider implements DataProvider {
+ private userService = inject(UserService);
+
+ getData(options?: GetDataOptions): Observable> {
+ const {q,page,limit} = options?.params ?? {};
+ // The generic table's params are compatible with our NestJS Query DTO
+ return this.userService.search(q ?? '',page,limit, ).pipe(
+ map((res) => {
+ // Adapt the paginated response to the GetDataResponse format
+ return { data: res };
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/admin/src/app/features/user/components/user-table/user-table.component.html b/admin/src/app/features/user/components/user-table/user-table.component.html
new file mode 100644
index 0000000..0bf1d83
--- /dev/null
+++ b/admin/src/app/features/user/components/user-table/user-table.component.html
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/admin/src/app/features/user/components/user-table/user-table.component.ts b/admin/src/app/features/user/components/user-table/user-table.component.ts
new file mode 100644
index 0000000..4131e48
--- /dev/null
+++ b/admin/src/app/features/user/components/user-table/user-table.component.ts
@@ -0,0 +1,105 @@
+// dvbooking-cli/src/templates/angular-generic/table.component.ts.tpl
+
+// Generated by the CLI
+import { Component, inject, OnInit } from '@angular/core';
+import { Router, RouterModule } from '@angular/router';
+import { User } from '../../models/user.model';
+import { UserDataProvider } from './user-data-provider.service';
+import { ColumnDefinition } from '../../../../components/generic-table/column-definition.interface';
+import { GenericTable } from '../../../../components/generic-table/generic-table';
+import { GenericTableConfig } from '../../../../components/generic-table/generic-table.config';
+import {
+ ActionDefinition,
+ GenericActionColumn,
+} from '../../../../components/generic-action-column/generic-action-column';
+import { UserService } from '../../services/user.service';
+import { BehaviorSubject } from 'rxjs';
+
+@Component({
+ selector: 'app-user-table',
+ standalone: true,
+ imports: [GenericTable, RouterModule],
+ templateUrl: './user-table.component.html',
+})
+export class UserTableComponent implements OnInit {
+
+ private refresh$ = new BehaviorSubject(undefined);
+ private filter$ = new BehaviorSubject({});
+ private page$ = new BehaviorSubject(1);
+ private limit$ = new BehaviorSubject(10);
+
+ router = inject(Router);
+ tableConfig!: GenericTableConfig;
+
+ userDataProvider = inject(UserDataProvider);
+ userService = inject(UserService);
+
+ ngOnInit(): void {
+ const actionHandler = (action: ActionDefinition, item: User) => {
+ switch (action.action) {
+ case 'view':
+ this.router.navigate(['/user', item?.id]);
+ break;
+ case 'edit':
+ this.router.navigate(['/user', item?.id, 'edit']);
+ break;
+ case 'delete':
+ this.deleteItem(item.id);
+ break;
+ }
+ };
+
+ this.tableConfig = {
+ refresh$: this.refresh$,
+ filter$: this.filter$,
+ page$: this.page$,
+ limit$: this.limit$,
+ dataProvider: this.userDataProvider,
+ columns: [
+ {
+ attribute: 'username',
+ headerCell: true,
+ valueCell: true,
+ },
+ {
+ attribute: 'email',
+ headerCell: true,
+ valueCell: true,
+ },
+ {
+ attribute: 'actions',
+ headerCell: { value: 'Actions' },
+ valueCell: {
+ component: GenericActionColumn,
+ componentInputs: item => ({
+ item: item,
+ actions: [
+ { action: 'view', handler: actionHandler },
+ { action: 'edit', handler: actionHandler },
+ { action: 'delete', handler: actionHandler },
+ ] as ActionDefinition[],
+ }),
+ },
+ },
+ ] as ColumnDefinition[],
+ tableCssClass: 'user-table-container',
+ };
+ }
+
+ deleteItem(id: number): void {
+ if (confirm('Are you sure you want to delete this item?')) {
+ this.userService.remove(id).subscribe({
+ next: () => {
+ console.log(`Item with ID ${id} deleted successfully.`);
+ this.refresh$.next();
+ },
+ // --- THIS IS THE FIX ---
+ // Explicitly type 'err' to satisfy strict TypeScript rules.
+ error: (err: any) => {
+ console.error(`Error deleting item with ID ${id}:`, err);
+ }
+ });
+ }
+ }
+
+}
diff --git a/admin/src/app/features/user/models/user.model.ts b/admin/src/app/features/user/models/user.model.ts
new file mode 100644
index 0000000..b299b02
--- /dev/null
+++ b/admin/src/app/features/user/models/user.model.ts
@@ -0,0 +1,9 @@
+// dvbooking-cli/src/templates/angular/model.ts.tpl
+
+// Generated by the CLI
+export interface User {
+ id: number;
+ username: string;
+ email: string;
+ password: string;
+}
diff --git a/admin/src/app/features/user/services/user.service.ts b/admin/src/app/features/user/services/user.service.ts
new file mode 100644
index 0000000..6be4fc6
--- /dev/null
+++ b/admin/src/app/features/user/services/user.service.ts
@@ -0,0 +1,85 @@
+// dvbooking-cli/src/templates/angular/service.ts.tpl
+
+// Generated by the CLI
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { User } from '../models/user.model';
+import { ConfigurationService } from '../../../services/configuration.service';
+import { PaginatedResponse } from '../../../../types';
+
+
+export interface SearchResponse {
+ data: T[];
+ total: number;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserService {
+ private readonly apiUrl: string;
+
+ constructor(
+ private http: HttpClient,
+ private configService: ConfigurationService
+ ) {
+ this.apiUrl = `${this.configService.getApiUrl()}/user`;
+ }
+
+ /**
+ * Find records with pagination and filtering.
+ */
+ public find(filter: Record): Observable> {
+ // --- THIS IS THE FIX ---
+ // The incorrect line: .filter(([_, v]) for v != null)
+ // is now correctly written with an arrow function.
+ const cleanFilter = Object.fromEntries(
+ Object.entries(filter).filter(([_, v]) => v != null)
+ );
+ // --- END OF FIX ---
+
+ const params = new HttpParams({ fromObject: cleanFilter });
+ return this.http.get>(this.apiUrl, { params });
+ }
+
+ /**
+ * Search across multiple fields with a single term.
+ * @param term The search term (q).
+ */
+ public search(term: string, page: number = 1, limit: number = 10): Observable> {
+ const params = new HttpParams()
+ .set('q', term)
+ .set('page', page.toString())
+ .set('limit', limit.toString());
+ return this.http.get>(`${this.apiUrl}/search`, { params });
+ }
+
+ /**
+ * Find a single record by its ID.
+ */
+ public findOne(id: number): Observable {
+ return this.http.get(`${this.apiUrl}/${id}`);
+ }
+
+ /**
+ * Create a new record.
+ */
+ public create(data: Omit): Observable {
+ return this.http.post(this.apiUrl, data);
+ }
+
+ /**
+ * Update an existing record.
+ */
+ public update(id: number, data: Partial>): Observable {
+ return this.http.patch(`${this.apiUrl}/${id}`, data);
+ }
+
+ /**
+ * Remove a record by its ID.
+ */
+ public remove(id: number): Observable {
+ return this.http.delete(`${this.apiUrl}/${id}`);
+ }
+}
\ No newline at end of file
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
index 0e35f1f..2618fea 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -5,7 +5,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
-import { User } from './entity/user';
import { UserGroup } from './entity/user-group';
import { UserRole } from './entity/user-role';
import { LoggerModule } from './logger/logger.module';
@@ -13,8 +12,9 @@ import { EventType } from './entity/event-type.entity';
import { EventTypesModule } from './event-type/event-type.module';
import { Product } from './entity/product.entity';
import { ProductsModule } from './product/products.module';
-import { Event } from "./entity/event.entity";
-import { EventsModule } from "./event/events.module";
+import { Event } from './entity/event.entity';
+import { EventsModule } from './event/events.module';
+import { User } from './entity/user';
const moduleTypeOrm = TypeOrmModule.forRootAsync({
imports: [ConfigModule],
@@ -43,8 +43,8 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({
LoggerModule,
EventTypesModule,
ProductsModule,
- EventsModule
- ],
+ EventsModule,
+ ],
controllers: [AppController],
providers: [AppService],
})
diff --git a/server/src/user/dto/query-user.dto.ts b/server/src/user/dto/query-user.dto.ts
new file mode 100644
index 0000000..a872493
--- /dev/null
+++ b/server/src/user/dto/query-user.dto.ts
@@ -0,0 +1,14 @@
+import {
+ IsOptional,
+ IsString,
+ IsNumber,
+ IsIn,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class QueryUserDto {
+ @IsOptional() @Type(() => Number) @IsNumber() page?: number;
+ @IsOptional() @Type(() => Number) @IsNumber() limit?: number;
+ @IsOptional() @IsString() sortBy?: string;
+ @IsOptional() @IsIn(['ASC', 'DESC']) order?: 'ASC' | 'DESC';
+}
diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts
index 7413471..77e2723 100644
--- a/server/src/user/user.controller.ts
+++ b/server/src/user/user.controller.ts
@@ -8,6 +8,9 @@ import {
Delete,
UseGuards,
ValidationPipe,
+ Query,
+ DefaultValuePipe,
+ ParseIntPipe,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
@@ -18,7 +21,7 @@ import { Roles } from '../auth/roles.decorator';
import { Role } from '../auth/role.enum';
import { RolesGuard } from '../auth/roles.guard';
-@Controller('users')
+@Controller('user')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.Admin)
export class UserController {
@@ -36,6 +39,15 @@ export class UserController {
return this.userService.findAll();
}
+ @Get('search')
+ search(
+ @Query('q') term: string,
+ @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
+ @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
+ ) {
+ return this.userService.search(term, { page, limit });
+ }
+
@Get(':id')
findOne(@Param('id') id: string): Promise {
return this.userService.findOne(+id);
diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts
index 7f3528b..c9630a9 100644
--- a/server/src/user/user.service.ts
+++ b/server/src/user/user.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
-import { Repository } from 'typeorm';
+import { ILike, Repository } from 'typeorm';
import { User } from '../entity/user';
import * as bcrypt from 'bcrypt';
import { FindOptionsRelations } from 'typeorm/find-options/FindOptionsRelations';
@@ -8,6 +8,8 @@ import { DvbookingLoggerService } from '../logger/dvbooking-logger.service';
@Injectable()
export class UserService {
+ private readonly searchableFields: (keyof User)[] = ['username', 'email'];
+
constructor(
@InjectRepository(User)
private usersRepository: Repository,
@@ -55,6 +57,40 @@ export class UserService {
await this.usersRepository.delete(id);
}
+ async search(term: string, options: { page: number; limit: number }) {
+ if (this.searchableFields.length === 0) {
+ console.warn('Search is not configured for this entity.');
+ return {
+ data: [],
+ meta: {
+ totalItems: 0,
+ itemCount: 0,
+ itemsPerPage: options.limit,
+ totalPages: 0,
+ currentPage: options.page,
+ },
+ };
+ }
+ const whereConditions = this.searchableFields.map((field) => ({
+ [field]: ILike(`%${term}%`),
+ }));
+ const [data, totalItems] = await this.usersRepository.findAndCount({
+ where: whereConditions,
+ skip: (options.page - 1) * options.limit,
+ take: options.limit,
+ });
+ return {
+ data,
+ meta: {
+ totalItems,
+ itemCount: data.length,
+ itemsPerPage: options.limit,
+ totalPages: Math.ceil(totalItems / options.limit),
+ currentPage: options.page,
+ },
+ };
+ }
+
async setRefreshToken(
id: number,
refreshToken: string | null,