Compare commits
2 Commits
f673db572e
...
13a282f71f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a282f71f | ||
|
|
08d21631e2 |
@ -46,7 +46,13 @@
|
|||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { RecipePage } from './pages/recipe-page/recipe-page';
|
|||||||
import { RecipesPage } from './pages/recipes-page/recipes-page';
|
import { RecipesPage } from './pages/recipes-page/recipes-page';
|
||||||
import { RecipesSearchPage } from './pages/recipes-search-page/recipes-search-page';
|
import { RecipesSearchPage } from './pages/recipes-search-page/recipes-search-page';
|
||||||
import { RecipeUploadPage } from './pages/recipe-upload-page/recipe-upload-page';
|
import { RecipeUploadPage } from './pages/recipe-upload-page/recipe-upload-page';
|
||||||
|
import { authGuard } from './shared/guards/auth-guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -16,6 +17,7 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'recipe-upload',
|
path: 'recipe-upload',
|
||||||
component: RecipeUploadPage,
|
component: RecipeUploadPage,
|
||||||
|
canActivate: [authGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'recipes/:username/:slug',
|
path: 'recipes/:username/:slug',
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
export const Endpoints = {
|
export const Endpoints = {
|
||||||
|
authLogin: 'auth/login',
|
||||||
|
authLogout: 'auth/logout',
|
||||||
|
authRefresh: 'auth/refresh',
|
||||||
recipes: 'recipes',
|
recipes: 'recipes',
|
||||||
};
|
};
|
||||||
|
|||||||
5
src/app/shared/components/nav/NavLinkConfig.ts
Normal file
5
src/app/shared/components/nav/NavLinkConfig.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface NavLinkConfig {
|
||||||
|
relativeUrl: string;
|
||||||
|
title: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<h2>Nav</h2>
|
<h2>Nav</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a [routerLink]="'/'">Browse Recipes</a></li>
|
@for (link of links(); track link.relativeUrl) {
|
||||||
<li><a [routerLink]="'/recipes-search'">Search Recipes</a></li>
|
@if (!link.disabled) {
|
||||||
<li><a [routerLink]="'/recipe-upload'">Upload Recipe</a></li>
|
<li><a [routerLink]="link.relativeUrl">{{ link.title }}</a></li>
|
||||||
|
}
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, computed, inject } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { AuthService } from '../../services/AuthService';
|
||||||
|
import { NavLinkConfig } from './NavLinkConfig';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nav',
|
selector: 'app-nav',
|
||||||
@ -7,4 +9,26 @@ import { RouterLink } from '@angular/router';
|
|||||||
templateUrl: './nav.html',
|
templateUrl: './nav.html',
|
||||||
styleUrl: './nav.css',
|
styleUrl: './nav.css',
|
||||||
})
|
})
|
||||||
export class Nav {}
|
export class Nav {
|
||||||
|
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
|
|
||||||
|
protected readonly links = computed<NavLinkConfig[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
relativeUrl: '/',
|
||||||
|
title: 'Browse Recipes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativeUrl: '/recipes-search',
|
||||||
|
title: 'Search Recipes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativeUrl: '/recipe-upload',
|
||||||
|
title: 'Upload Recipe',
|
||||||
|
disabled: this.authService.accessToken() === null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
17
src/app/shared/guards/auth-guard.spec.ts
Normal file
17
src/app/shared/guards/auth-guard.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
|
||||||
|
import { authGuard } from './auth-guard';
|
||||||
|
|
||||||
|
describe('authGuardGuard', () => {
|
||||||
|
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||||
|
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(executeGuard).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
8
src/app/shared/guards/auth-guard.ts
Normal file
8
src/app/shared/guards/auth-guard.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { AuthService } from '../services/AuthService';
|
||||||
|
|
||||||
|
export const authGuard: CanActivateFn = (route, state) => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
return authService.accessToken() !== null;
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import { LoginView } from '../models/LoginView.model';
|
|||||||
import { firstValueFrom, Observable, tap } from 'rxjs';
|
import { firstValueFrom, Observable, tap } from 'rxjs';
|
||||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { EndpointService } from './EndpointService';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -18,11 +19,12 @@ export class AuthService {
|
|||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly queryClient = inject(QueryClient);
|
private readonly queryClient = inject(QueryClient);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
private readonly endpointService = inject(EndpointService);
|
||||||
|
|
||||||
public async login(username: string, password: string): Promise<LoginView> {
|
public async login(username: string, password: string): Promise<LoginView> {
|
||||||
const loginView = await firstValueFrom(
|
const loginView = await firstValueFrom(
|
||||||
this.http.post<LoginView>(
|
this.http.post<LoginView>(
|
||||||
'http://localhost:8080/auth/login',
|
this.endpointService.getUrl('authLogin'),
|
||||||
{ username, password },
|
{ username, password },
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@ -36,7 +38,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async logout(): Promise<void> {
|
public async logout(): Promise<void> {
|
||||||
await firstValueFrom(this.http.post('http://localhost:8080/auth/logout', null));
|
await firstValueFrom(this.http.post(this.endpointService.getUrl('authLogout'), null));
|
||||||
this._username.set(null);
|
this._username.set(null);
|
||||||
this._accessToken.set(null);
|
this._accessToken.set(null);
|
||||||
await this.router.navigate(['/']);
|
await this.router.navigate(['/']);
|
||||||
@ -47,7 +49,7 @@ export class AuthService {
|
|||||||
this._accessToken.set(null);
|
this._accessToken.set(null);
|
||||||
this._username.set(null);
|
this._username.set(null);
|
||||||
return this.http
|
return this.http
|
||||||
.post<LoginView>('http://localhost:8080/auth/refresh', null, {
|
.post<LoginView>(this.endpointService.getUrl('authRefresh'), null, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Endpoints } from '../../endpoints';
|
import { Endpoints } from '../../endpoints';
|
||||||
import { QueryParams } from '../models/Query.model';
|
import { QueryParams } from '../models/Query.model';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class EndpointService {
|
export class EndpointService {
|
||||||
public getUrl(endpoint: keyof typeof Endpoints, pathParams?: string[], queryParams?: QueryParams): string {
|
public getUrl(endpoint: keyof typeof Endpoints, pathParts?: string[], queryParams?: QueryParams): string {
|
||||||
const urlSearchParams = new URLSearchParams();
|
const urlSearchParams = new URLSearchParams();
|
||||||
if (queryParams?.page !== undefined) {
|
if (queryParams?.page !== undefined) {
|
||||||
urlSearchParams.set('page', queryParams.page.toString());
|
urlSearchParams.set('page', queryParams.page.toString());
|
||||||
@ -29,7 +30,7 @@ export class EndpointService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let pathString = pathParams?.join('/') || '';
|
let pathString = pathParts?.join('/') || '';
|
||||||
if (pathString?.length) {
|
if (pathString?.length) {
|
||||||
pathString = '/' + pathString;
|
pathString = '/' + pathString;
|
||||||
}
|
}
|
||||||
@ -39,6 +40,6 @@ export class EndpointService {
|
|||||||
queryString = '?' + queryString;
|
queryString = '?' + queryString;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `http://localhost:8080/${Endpoints[endpoint]}${pathString}${queryString}`;
|
return environment.apiBaseUrl + '/' + Endpoints[endpoint] + pathString + queryString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,24 +19,29 @@ export class RecipeService {
|
|||||||
|
|
||||||
public getRecipes(): Promise<Recipe[]> {
|
public getRecipes(): Promise<Recipe[]> {
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.http.get<RecipeInfoViews>('http://localhost:8080/recipes').pipe(map((res) => res.content)),
|
this.http.get<RecipeInfoViews>(this.endpointService.getUrl('recipes'))
|
||||||
|
.pipe(map((res) => res.content))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
public getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
||||||
return firstValueFrom(this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`));
|
return firstValueFrom(this.http.get<RecipeView>(
|
||||||
|
this.endpointService.getUrl('recipes', [username, slug])
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRecipeUrl(recipeView: RecipeView): string {
|
private getRecipeBaseUrl(recipeView: RecipeView): string {
|
||||||
return `http://localhost:8080/recipes/${recipeView.recipe.owner.username}/${recipeView.recipe.slug}`;
|
return this.endpointService.getUrl(
|
||||||
|
'recipes', [recipeView.recipe.owner.username, recipeView.recipe.slug]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toggleStar(recipeView: RecipeView): Promise<void> {
|
public async toggleStar(recipeView: RecipeView): Promise<void> {
|
||||||
if (this.authService.accessToken()) {
|
if (this.authService.accessToken()) {
|
||||||
if (recipeView.isStarred) {
|
if (recipeView.isStarred) {
|
||||||
await lastValueFrom(this.http.delete(this.getRecipeUrl(recipeView) + '/star'));
|
await lastValueFrom(this.http.delete(this.getRecipeBaseUrl(recipeView) + '/star'));
|
||||||
} else {
|
} else {
|
||||||
await lastValueFrom(this.http.post(this.getRecipeUrl(recipeView) + '/star', null));
|
await lastValueFrom(this.http.post(this.getRecipeBaseUrl(recipeView) + '/star', null));
|
||||||
}
|
}
|
||||||
await this.queryClient.invalidateQueries({
|
await this.queryClient.invalidateQueries({
|
||||||
queryKey: ['recipe', recipeView.recipe.owner.username, recipeView.recipe.slug],
|
queryKey: ['recipe', recipeView.recipe.owner.username, recipeView.recipe.slug],
|
||||||
@ -56,9 +61,10 @@ export class RecipeService {
|
|||||||
|
|
||||||
public async addComment(username: string, slug: string, commentText: string): Promise<RecipeComment> {
|
public async addComment(username: string, slug: string, commentText: string): Promise<RecipeComment> {
|
||||||
const comment = await firstValueFrom(
|
const comment = await firstValueFrom(
|
||||||
this.http.post<RecipeComment>(`http://localhost:8080/recipes/${username}/${slug}/comments`, {
|
this.http.post<RecipeComment>(
|
||||||
text: commentText,
|
this.endpointService.getUrl('recipes', [username, slug, 'comments']),
|
||||||
}),
|
{ text: commentText }
|
||||||
|
)
|
||||||
);
|
);
|
||||||
await this.queryClient.invalidateQueries({
|
await this.queryClient.invalidateQueries({
|
||||||
queryKey: ['recipeComments', username, slug],
|
queryKey: ['recipeComments', username, slug],
|
||||||
|
|||||||
3
src/environments/environment.development.ts
Normal file
3
src/environments/environment.development.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
apiBaseUrl: 'http://localhost:8080'
|
||||||
|
};
|
||||||
3
src/environments/environment.ts
Normal file
3
src/environments/environment.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
apiBaseUrl: 'http://localhost:8080'
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user