import { Injectable, signal, computed } from '@angular/core';
export interface CurrentUser {
id: string;
email: string;
roles: string[];
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly user = signal<CurrentUser | null>(null);
readonly isLoggedIn = computed(() => this.user() !== null);
setUser(user: CurrentUser | null): void {
this.user.set(user);
}
hasRole(role: string): boolean {
const current = this.user();
return current !== null && current.roles.includes(role);
}
}
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export function requireRole(role: string): CanActivateFn {
return (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (!auth.isLoggedIn()) {
return router.parseUrl(`/login?returnUrl=${encodeURIComponent(state.url)}`);
}
if (auth.hasRole(role)) {
return true;
}
// Authenticated but unauthorized: send to a friendly denial page.
return router.parseUrl('/forbidden');
};
}
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.parseUrl(`/login?returnUrl=${encodeURIComponent(state.url)}`);
};
import { Routes } from '@angular/router';
import { authGuard, requireRole } from './admin.guard';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./home/home.component').then((m) => m.HomeComponent),
},
{
path: 'dashboard',
canActivate: [authGuard],
loadComponent: () =>
import('./dashboard/dashboard.component').then((m) => m.DashboardComponent),
},
{
path: 'admin',
canActivate: [requireRole('admin')],
loadChildren: () =>
import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
{
path: 'login',
loadComponent: () =>
import('./login/login.component').then((m) => m.LoginComponent),
},
{
path: 'forbidden',
loadComponent: () =>
import('./errors/forbidden.component').then((m) => m.ForbiddenComponent),
},
{ path: '**', redirectTo: '' },
];
Lazy-loaded feature modules keep the initial bundle small, but they also create a natural authorization boundary: the admin chunk should never even download for a user who lacks the right role, and its routes should never render if the role check fails. This snippet wires a functional CanActivate guard onto a loadChildren route so both concerns are handled in one place.
In auth.service.ts, the current user is exposed through a signal, and hasRole derives authorization state synchronously from that signal. Keeping roles in a signal means the guard can read them without subscribing, and any component can react to login changes reactively. The service is providedIn: 'root' so the same instance is shared by the guard, the components, and the app shell.
admin.guard.ts shows the modern functional guard style: authGuard is a plain CanActivateFn, so it runs inside an injection context and uses inject() to pull in AuthService and Router rather than being a class with a constructor. The requireRole factory closes over a role string and returns a fresh CanActivateFn, which makes the same logic reusable for admin, editor, or any future role. When the check fails, the guard returns a UrlTree from router.parseUrl instead of false; returning a UrlTree tells the router to cancel the current navigation and redirect atomically, avoiding a flash of a blank or partially-activated route. The returnUrl query param preserves where the user was heading so login can bounce them back.
app.routes.ts attaches requireRole('admin') to the canActivate array of the lazily loadChildren-ed admin route. Because guards run before the router fetches the lazy chunk, an unauthorized user never triggers the network request for the admin code — a real security and performance win over guarding only inside the component.
A subtle pitfall worth noting: guards protect navigation, not the API. The server must still enforce roles, since a determined user can call endpoints directly. This client-side guard is about UX and defense in depth, not the final authority. The functional style also composes cleanly — multiple CanActivateFns in the array run in order, short-circuiting on the first that denies.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
Django model signals vs overriding save
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
Share this code
Here's the card — post it anywhere.