-
Notifications
You must be signed in to change notification settings - Fork 0
7. Authentication
Andrei Antal edited this page Oct 24, 2024
·
2 revisions
- We'll start by creating an auth service:
ng generate service services/auth- Next we'll define the
Roleenum in theauth.servicefile:
export enum Role {
Admin = 'Admin
User = 'User',
}- We'll define a few mocks we'll use in the auth process (normally these should be fetched from a server):
const MOCK_USER = {
user: 'user',
password: 'user',
};
const MOCK_ADMIN = {
user: 'admin',
password: 'admin',
};
const MOCK_TOKEN = '1234567890';- We'll define the internal variables that will hold the state for the auth service:
// internal state
#isAuthenticated = signal(false);
#token = signal<string | undefined>(undefined);
#role = signal<Role | undefined>(undefined);
// public signals
isAuthenticated = this.#isAuthenticated.asReadonly();
token = this.#token.asReadonly();
role = this.#role.asReadonly();- We'll define the
loginandlogoutmethods that will be used to authenticate the user:
login(user?: string, pass?: string): Observable<{ token: string }> {
// check if user and password are provided and match the mocks
// save auth state and role
// return token as observable and store it as well
}
logout() {
// clear auth state and token
}- In the
App componentwe'll add alogoutbutton that will call thelogoutmethod from theAuthService:
export class AppComponent {
authService = inject(AuthService);
#router = inject(Router);
...
logout() {
this.authService.logout();
this.#router.navigate(['/']); // navigate back to home page
}
}- In the Home component we'll add a login form that will be displayed only if the user is not authenticated. If the user is authenticated, we show the link as it's displayed now:
@if (authService.isAuthenticated()) {
...
} @else {
<div class="card" style="width: 30vw" [formGroup]="loginForm">
<form class="card-body">
<h3 class="card-title">Movies app login form</h3>
<div class="mb-3">
<label class="form-label"> User </label>
<input type="text" class="form-control" formControlName="user" />
</div>
<div class="mb-3">
<label class="form-label"> Password </label>
<input type="password" class="form-control" formControlName="password" />
</div>
<button href="#" class="btn btn-primary" (click)="submit()">Log in</button>
</form>
</div>
}- Next we need to define the
loginFormin theHomeComponentclass:
loginForm = this.#fb.group({
user: '',
password: '',
});- We'll define the
submitmethod that will be called when the user clicks theLog inbutton. This method will call theloginmethod from theAuthService:
submit() {
const { user, password } = this.loginForm.value;
this.authService.login(user, password).subscribe({
next: () => {
this.error.set(undefined);
this.#router.navigate(['/movies']);
},
error: (err) => {
this.error.set(err);
},
});
}- We'll also show the error message if user/password combination is not correct:
@if(error()) {
<div class="alert alert-danger" style="width: 30vw">
{{ error() }}
</div>
}- Try the user/password combinations form the mocks to see if the login works as expected.
For this challenge we will create a structural directive that will be used to show/hide elements based on the user role. The directive will be applied to an element and will take a role as an input. If the user has the role, the element will be displayed, otherwise it will be removed from the DOM.
- Let's create a
hasRoledirective using the CLI command:
ng generate directive movies/directive/has-role --flat=false- Next we'll define, as for every structural directive, a
TemplateRefand aViewContainerRefas properties of the directive class:
export class HasRoleDirective {
#template = inject(TemplateRef);
#view = inject(ViewContainerRef);
}- We'll also define an
inputsignal property for the role. This will be the role that the user needs to have in order to display the element:
ngmHasRole = input<keyof typeof Role | undefined>(undefined); // Role is imported form auth.service- Finally, when we initialize the directive we'll make use of the
AuthServiceto check if the user has the role and based on that we'll either display the element or remove it from the DOM:
ngOnInit() {
// check if user is authenticated and provided role through the Input is the user role
// if true, display the element, otherwise remove it from the DOM
}- Apply the directive on the
MovieItemComponenttemplates to show the edit and delete buttons only if the user has theadminrole and the rating component only if the user has theuserrole. And also on theMovieListComponenttemplate to show theNew Moviebutton only if the user has theadminrole.
*ngmHasRole="'Admin'"
*ngmHasRole="'User'"- In the first part of the challenge challenge we'll create a route guard that will check if the user is authenticated and has the role to access the route. First we'll create a
canActivatefunction that will take the route and the state as parameters and will return abooleanor aUrlTree:
ng generate guard movies/guards/is-authenticatedexport const isAuthenticatedGuard: CanActivateFn = () => {
const router = inject(Router);
const authService = inject(AuthService);
if (!authService.isAuthenticated) { // check if user is (not) authenticated
return router.parseUrl('/'); // route back to home page
}
return true; // allow navigation
};Next we'll add the guard to the routes that require authentication. In app.config we need to make sure that we restrict only access to movies only to authenticated users:
{
path: 'movies',
canActivate: [isAuthenticatedGuard],
...
}- In the second part of the challenge we'll create a route guard that will check if the user is authenticated and has the role to access the route. First we'll create a
canActivatefunction that will take the route and the state as parameters and will return abooleanor aUrlTree:
ng generate guard movies/guards/has-role- Add a
dataproperty to the routes that require roles in themovies.routesfile:
{
path: 'new',
...
data: {
roles: [Role.Admin],
}
},
{
path: ':id',
...
data: {
roles: [Role.Admin],
},
},- Add conditions to the
has-roleguard:
export const hasRoleGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
const authService = inject(AuthService);
const roles: Role[] = route.data['roles']; // get roles from route data
if (roles?.includes(authService.role!)) { // check if user has the required role
return true; // allow navigation
}
return router.parseUrl('/'); // route back to home page
};- Add the guard to the routes that require authentication. In
movies.routeswe need to make sure that we restrict access to thenewand:idroutes that require roles only to users that have the required role:
canActivate: [canActivateGuard]- First, revert the changes made in the
MovieDetailComponentto use themovies$so we can edit the movies:
@for (movie of movies$(); track movie.id) {- Next, in the
MovieDetailComponentadd aconfirmCancelthat will show a confirm dialog if form changes are not saved.
confirmCancel(): Observable<boolean> {
if (!this.#changesSaved && this.movieForm.dirty) {
return of(
window.confirm('You have unsaved changes. Do you really want to leave?')
);
}
return of(true);
}- Use the
canDeactivateguard in themovies.routesfile to check if the user wants to leave the page without saving the changes. You can use theconfirmCancelmethod on the provided component:
canDeactivate: [
(component: MovieDetailComponent) => component.confirmCancel(),
],- Create an
authInterceptorthat will add theAuthorizationheader to the request:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.token; // Get token form auth service
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`), // Add token to request headers
});
return next(authReq); // return forward updated request body
}- in
app.configadd thewithInterceptorsfunction to theprovideHttpClientprovider that will take an array of interceptors and will return aHttpClientconfiguration object that will be used to update theHttpClientconfiguration:
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor])), // update http client configuration
...
],
};