diff --git a/angular.json b/angular.json index 612a72e..6bf206b 100644 --- a/angular.json +++ b/angular.json @@ -61,6 +61,15 @@ "with": "src/environments/environment.development.ts" } ] + }, + "staging": { + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.staging.ts" + } + ] } }, "defaultConfiguration": "production" @@ -73,6 +82,9 @@ }, "development": { "buildTarget": "humainFlow-gui-a21:build:development" + }, + "staging": { + "buildTarget": "humainFlow-gui-a21:build:staging" } }, "defaultConfiguration": "development" diff --git a/src/app/app.config.ts b/src/app/app.config.ts index cb1270e..7498b00 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,11 +1,14 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideRouter } from '@angular/router'; +import { authTokenInterceptor } from './interceptors/auth-token.interceptor'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), + provideHttpClient(withInterceptors([authTokenInterceptor])), provideRouter(routes) ] }; diff --git a/src/app/interceptors/auth-token.interceptor.ts b/src/app/interceptors/auth-token.interceptor.ts new file mode 100644 index 0000000..79ddf82 --- /dev/null +++ b/src/app/interceptors/auth-token.interceptor.ts @@ -0,0 +1,42 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { Authorization } from '@services/authorization/authorization'; + +export const authTokenInterceptor: HttpInterceptorFn = (req, next) => { + const token = getToken(); + if (!token) return next(req); + + const requestPath = req.url.split('?')[0]; + const isAuthEndpoint = + requestPath.endsWith('/auth/login') || + requestPath.endsWith('/auth/register'); + if (isAuthEndpoint) { + return next(req); + } + + if (req.headers.has('Authorization')) { + return next(req); + } + + return next( + req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }) + ); +}; + +function getToken(): string | null { + const directToken = localStorage.getItem(Authorization.TOKEN_STORAGE_KEY); + if (directToken) return directToken; + + const rawUser = localStorage.getItem(Authorization.USER_STORAGE_KEY); + if (!rawUser) return null; + + try { + const parsed = JSON.parse(rawUser) as { token?: unknown }; + return typeof parsed?.token === 'string' && parsed.token.length > 0 ? parsed.token : null; + } catch { + return null; + } +} diff --git a/src/app/models/user.ts b/src/app/models/user.ts index 030cee6..667f10c 100644 --- a/src/app/models/user.ts +++ b/src/app/models/user.ts @@ -1,9 +1,10 @@ export type User = { username: string; email: string; + token?: string; }; export type UserRegistration = User & { password: string; fullname: string; -} \ No newline at end of file +} diff --git a/src/app/services/authorization/authorization-call.ts b/src/app/services/authorization/authorization-call.ts index 855380e..597ac74 100644 --- a/src/app/services/authorization/authorization-call.ts +++ b/src/app/services/authorization/authorization-call.ts @@ -1,15 +1,48 @@ import { AuthorizationCallServiceBase } from "./authorization-call.base"; import { User, UserRegistration } from "@models/user"; -import { Observable } from "rxjs"; +import { map, Observable } from "rxjs"; +import { HttpClient } from "@angular/common/http"; +import { inject } from "@angular/core"; +import { environment } from "@environment"; export class AuthorizationCallService extends AuthorizationCallServiceBase { - override login(_username: string, _password: string): Observable { - throw new Error("Method not implemented."); - } - override register(_userRegistration: UserRegistration): Observable { - throw new Error("Method not implemented."); - } - - + private readonly http = inject(HttpClient); + override login(username: string, password: string): Observable { + return this.http + .post(`${environment.apiUrl}/auth/login`, { username, password }) + .pipe( + map((raw) => { + const payload = (raw ?? {}) as Record; + const userSource = + (payload["user"] as Record | undefined) + ?? (payload["profile"] as Record | undefined) + ?? payload; + const token = this.extractToken(payload, userSource); + + return { + username: String(userSource["username"] ?? username), + email: String(userSource["email"] ?? ''), + token: typeof token === "string" && token.length > 0 ? token : undefined + } satisfies User; + }) + ); + } + override register(userRegistration: UserRegistration): Observable { + return this.http.post(`${environment.apiUrl}/auth/register`, userRegistration); + } + + private extractToken(...sources: Array | undefined>): unknown { + const tokenKeys = ['token', 'accessToken', 'access_token', 'jwt', 'id_token']; + for (const source of sources) { + if (!source) continue; + for (const key of tokenKeys) { + const value = source[key]; + if (typeof value === 'string' && value.length > 0) { + return value; + } + } + } + return undefined; + } } diff --git a/src/app/services/authorization/authorization.ts b/src/app/services/authorization/authorization.ts index 1db71f5..4143dc7 100644 --- a/src/app/services/authorization/authorization.ts +++ b/src/app/services/authorization/authorization.ts @@ -8,6 +8,8 @@ import { take, tap } from 'rxjs'; providedIn: 'root', }) export class Authorization { + static readonly USER_STORAGE_KEY = 'loggedInUser'; + static readonly TOKEN_STORAGE_KEY = 'authToken'; private user = signal(null); @@ -20,7 +22,12 @@ export class Authorization { take(1), tap(res => { this.user.set(res); - localStorage.setItem('loggedInUser', JSON.stringify(res)); + localStorage.setItem(Authorization.USER_STORAGE_KEY, JSON.stringify(res)); + if (res.token) { + localStorage.setItem(Authorization.TOKEN_STORAGE_KEY, res.token); + } else { + localStorage.removeItem(Authorization.TOKEN_STORAGE_KEY); + } }) ); } @@ -32,12 +39,17 @@ export class Authorization { } logout() { - localStorage.removeItem('loggedInUser'); + localStorage.removeItem(Authorization.USER_STORAGE_KEY); + localStorage.removeItem(Authorization.TOKEN_STORAGE_KEY); this.user.set(null); } + token(): string | null { + return localStorage.getItem(Authorization.TOKEN_STORAGE_KEY); + } + isLoggedIn(): boolean { - const storedUser = localStorage.getItem('loggedInUser'); + const storedUser = localStorage.getItem(Authorization.USER_STORAGE_KEY); this.user.set(storedUser != null ? JSON.parse(storedUser) : null); return storedUser != null; } diff --git a/src/app/services/flows/flow-mapper.ts b/src/app/services/flows/flow-mapper.ts index 281cbaa..bd6aced 100644 --- a/src/app/services/flows/flow-mapper.ts +++ b/src/app/services/flows/flow-mapper.ts @@ -34,8 +34,15 @@ export function flowFromApi(raw: unknown): Flow { }; } -export function toFlowCreateRequest(name: string) { - return { name }; +export function toFlowCreateRequest(name: string, description?: string, flow?: FlowData) { + return { + name, + description: description ?? '', + flow: flow ?? { + blocks: [], + connections: [] + } + }; } export function toFlowUpdateRequest(flow: Flow) { diff --git a/src/app/services/retreiver/field-retreiver-call.base.ts b/src/app/services/retriever/field-retriever-call.base.ts similarity index 84% rename from src/app/services/retreiver/field-retreiver-call.base.ts rename to src/app/services/retriever/field-retriever-call.base.ts index 30d52df..e72183d 100644 --- a/src/app/services/retreiver/field-retreiver-call.base.ts +++ b/src/app/services/retriever/field-retriever-call.base.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -export abstract class FieldRetreiverCallServiceBase { +export abstract class FieldRetrieverCallServiceBase { abstract retrieveValues( blockType: string, key: string, diff --git a/src/app/services/retreiver/field-retreiver-call.fake.ts b/src/app/services/retriever/field-retriever-call.fake.ts similarity index 88% rename from src/app/services/retreiver/field-retreiver-call.fake.ts rename to src/app/services/retriever/field-retriever-call.fake.ts index d3a7e45..20de5bc 100644 --- a/src/app/services/retreiver/field-retreiver-call.fake.ts +++ b/src/app/services/retriever/field-retriever-call.fake.ts @@ -1,7 +1,7 @@ import { Observable, of } from "rxjs"; -import { FieldRetreiverCallServiceBase } from "./field-retreiver-call.base"; +import { FieldRetrieverCallServiceBase } from "./field-retriever-call.base"; -export class FieldRetreiverCallServiceFake extends FieldRetreiverCallServiceBase { +export class FieldRetrieverCallServiceFake extends FieldRetrieverCallServiceBase { private readonly providersByBlockType: Record = { LLM: ["OpenAI", "Anthropic", "OllamaTestProvider"], }; diff --git a/src/app/services/retreiver/field-retreiver-call.ts b/src/app/services/retriever/field-retriever-call.ts similarity index 60% rename from src/app/services/retreiver/field-retreiver-call.ts rename to src/app/services/retriever/field-retriever-call.ts index cd14fd0..9700c0a 100644 --- a/src/app/services/retreiver/field-retreiver-call.ts +++ b/src/app/services/retriever/field-retriever-call.ts @@ -2,21 +2,34 @@ import { HttpClient, HttpParams } from "@angular/common/http"; import { inject } from "@angular/core"; import { environment } from "@environment"; import { Observable } from "rxjs"; -import { FieldRetreiverCallServiceBase } from "./field-retreiver-call.base"; +import { FieldRetrieverCallServiceBase } from "./field-retriever-call.base"; -export class FieldRetreiverCallService extends FieldRetreiverCallServiceBase { +export class FieldRetrieverCallService extends FieldRetrieverCallServiceBase { private readonly http = inject(HttpClient); + private buildParams(context?: Record) { + let params = new HttpParams(); + const entries = Object.entries(context ?? {}); + + if (!entries.length) { + // New API marks `params` as required even when empty. + return params.set('params', '{}'); + } + + for (const [ctxKey, ctxValue] of entries) { + params = params.set(`params[${ctxKey}]`, ctxValue); + } + + return params; + } + override retrieveValues( blockType: string, key: string, context?: Record ): Observable { const url = `${environment.apiUrl}/retriever/${encodeURIComponent(blockType)}/${encodeURIComponent(key)}`; - let params = new HttpParams(); - for (const [ctxKey, ctxValue] of Object.entries(context ?? {})) { - params = params.set(ctxKey, ctxValue); - } + const params = this.buildParams(context); return this.http.get(url, { params }); } @@ -26,10 +39,7 @@ export class FieldRetreiverCallService extends FieldRetreiverCallServiceBase { context?: Record ): Observable { const url = `${environment.apiUrl}/retriever/${encodeURIComponent(blockType)}/${encodeURIComponent(key)}/required`; - let params = new HttpParams(); - for (const [ctxKey, ctxValue] of Object.entries(context ?? {})) { - params = params.set(ctxKey, ctxValue); - } + const params = this.buildParams(context); return this.http.get(url, { params }); } } diff --git a/src/app/services/retreiver/field-retreiver.spec.ts b/src/app/services/retriever/field-retriever.spec.ts similarity index 54% rename from src/app/services/retreiver/field-retreiver.spec.ts rename to src/app/services/retriever/field-retriever.spec.ts index 8e3a2d3..708fa63 100644 --- a/src/app/services/retreiver/field-retreiver.spec.ts +++ b/src/app/services/retriever/field-retriever.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { FieldRetreiver } from './field-retreiver'; +import { FieldRetriever } from './field-retriever'; -describe('FieldRetreiver', () => { - let service: FieldRetreiver; +describe('FieldRetriever', () => { + let service: FieldRetriever; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(FieldRetreiver); + service = TestBed.inject(FieldRetriever); }); it('should be created', () => { diff --git a/src/app/services/retreiver/field-retreiver.ts b/src/app/services/retriever/field-retriever.ts similarity index 68% rename from src/app/services/retreiver/field-retreiver.ts rename to src/app/services/retriever/field-retriever.ts index 7094385..44623f8 100644 --- a/src/app/services/retreiver/field-retreiver.ts +++ b/src/app/services/retriever/field-retriever.ts @@ -1,20 +1,20 @@ import { Injectable } from '@angular/core'; import { environment } from '@environment'; import { catchError, throwError } from 'rxjs'; -import { FieldRetreiverCallServiceBase } from './field-retreiver-call.base'; +import { FieldRetrieverCallServiceBase } from './field-retriever-call.base'; @Injectable({ providedIn: 'root', }) -export class FieldRetreiver { - fieldRetreiverCallService: FieldRetreiverCallServiceBase = new environment.fieldRetreiverCallService(); +export class FieldRetriever { + fieldRetrieverCallService: FieldRetrieverCallServiceBase = new environment.fieldRetrieverCallService(); retrieveValues( blockType: string, key: string, context?: Record ) { - return this.fieldRetreiverCallService.retrieveValues(blockType, key, context).pipe( + return this.fieldRetrieverCallService.retrieveValues(blockType, key, context).pipe( catchError((err) => { console.error('Field retrieval failed', err); return throwError(() => err); @@ -27,7 +27,7 @@ export class FieldRetreiver { key: string, context?: Record ) { - return this.fieldRetreiverCallService.isFieldRequired(blockType, key, context).pipe( + return this.fieldRetrieverCallService.isFieldRequired(blockType, key, context).pipe( catchError((err) => { console.error('Field required check failed', err); return throwError(() => err); diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index 769ccf0..87a1c9f 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -5,7 +5,7 @@ import { ClassicPreset } from 'rete'; import { ReteModule } from 'rete-angular-plugin/21'; import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; import { EditorStateHolder } from '@stores/flow-editor'; -import { FieldRetreiver as FieldRetriever } from '@services/retreiver/field-retreiver'; +import { FieldRetriever } from '@services/retriever/field-retriever'; import { BlocksService } from '@services/blocks/blocks'; import { firstValueFrom } from 'rxjs'; import { ConditionalRequiredField, extractSchemaRequirements, SchemaRequirements } from '../schema-requirements'; diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 9f7156e..d4a7ee0 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,7 +1,7 @@ import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake"; import { BlocksCallServiceFake } from "@services/blocks/blocks-call.fake"; import { FlowsCallServiceFake } from "@services/flows/flows-call.fake"; -import { FieldRetreiverCallServiceFake } from "@services/retreiver/field-retreiver-call.fake"; +import { FieldRetrieverCallServiceFake } from "@services/retriever/field-retriever-call.fake"; import { TaskExecutionsCallServiceFake } from "@services/task-executions/task-executions-call.fake"; export const environment = { @@ -10,6 +10,6 @@ export const environment = { authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here flowsCallService: FlowsCallServiceFake, blocksCallService: BlocksCallServiceFake, - fieldRetreiverCallService: FieldRetreiverCallServiceFake, + fieldRetrieverCallService: FieldRetrieverCallServiceFake, taskExecutionsCallService: TaskExecutionsCallServiceFake }; diff --git a/src/environments/environment.staging.ts b/src/environments/environment.staging.ts new file mode 100644 index 0000000..a242617 --- /dev/null +++ b/src/environments/environment.staging.ts @@ -0,0 +1,15 @@ +import { AuthorizationCallService } from "@services/authorization/authorization-call"; +import { BlocksCallService } from "@services/blocks/blocks-call"; +import { FlowsCallService } from "@services/flows/flows-call"; +import { FieldRetrieverCallService } from "@services/retriever/field-retriever-call"; +import { TaskExecutionsCallService } from "@services/task-executions/task-executions-call"; + +export const environment = { + production: false, + apiUrl: 'http://localhost:8080', + authorizationCallService: AuthorizationCallService, + flowsCallService: FlowsCallService, + blocksCallService: BlocksCallService, + fieldRetrieverCallService: FieldRetrieverCallService, + taskExecutionsCallService: TaskExecutionsCallService +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 83cd28b..a8e10c3 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,7 +1,7 @@ import { AuthorizationCallService } from "@services/authorization/authorization-call"; import { BlocksCallService } from "@services/blocks/blocks-call"; import { FlowsCallService } from "@services/flows/flows-call"; -import { FieldRetreiverCallService } from "@services/retreiver/field-retreiver-call"; +import { FieldRetrieverCallService } from "@services/retriever/field-retriever-call"; import { TaskExecutionsCallService } from "@services/task-executions/task-executions-call"; export const environment = { @@ -10,6 +10,6 @@ export const environment = { authorizationCallService: AuthorizationCallService, flowsCallService: FlowsCallService, blocksCallService: BlocksCallService, - fieldRetreiverCallService: FieldRetreiverCallService, + fieldRetrieverCallService: FieldRetrieverCallService, taskExecutionsCallService: TaskExecutionsCallService };