Auth User Injection
Suppose authentication context is needed within a controller itself. Say there is some action that expects a userId
and that id needs to come from the authenticated user.
Well, there's a few ways to do this. One could use injectService
decorator on authentication service and get access to the authenticated user that way.
For example:
@controller()
class SomeController extends BaseController {
@injectService(SERVICE.AUTHENTICATION)
private readonly authenticationService: IAuthenticationService;
@httpPatch()
public async updateBook(@body() payload: SomeDto) {
return this.mediator.send('UpdateBook', {
...payload,
userId: await this.authenticationService.getUserIdSafe(),
});
}
}
That has a disadvantage though. If another handler was added to this controller, for example getBooks
and that handler does not make use of the authentication service,
then the service would still be injected, because it's scoped to the controller itself.
One solution would be to segregate controllers and have handlers within that consumes similar services. It is allowed for multiple controllers to have the same path and thus this solution works.
However, auth
decorator can also be used. It takes an additional argument that allows user injection scoped to handlers.
Auth Decorator
src/api/auth-controller.ts
import {
controller,
httpGet,
httpPost,
body,
auth,
MediatorResultSuccess,
} from '@lindeneg/funkallero';
import BaseController from './base-controller';
import { loginUserSchema, type ILoginUserDto } from '@/contracts/login-user';
import type User from '@/domain/user';
@controller('auth')
class AuthController extends BaseController {
private readonly userId: string;
private readonly user: User;
@httpPost('/login')
public async loginUser(@body(loginUserSchema) dto: ILoginUserDto) {
return this.mediator.send('LoginUserCommand', dto);
}
@httpGet('/guard')
// 'id' property from authenticated user injected into property 'userId'
@auth('authenticated', { srcProperty: 'id', destProperty: 'userId' })
public async mustBeAuthenticated() {
this.logger.info({
msg: 'user injection on /guard',
// should be id of authenticated user
userId: this.userId,
// should be undefined
user: this.user,
});
// this will only be executed if the auth policy is satisfied
return new MediatorResultSuccess('you are authenticated');
}
@httpGet('/miles')
// everything from authenticated user injected into property 'user'
@auth('name-is-miles-davis', 'user')
public async mustBeMilesDavis() {
this.logger.info({
msg: 'user injection on /miles',
// should be undefined
userId: this.userId,
// should be user entity
user: this.user,
});
// this will only be executed if the auth policy is satisfied
return new MediatorResultSuccess('you are miles davis');
}
}
Test It
Build the project again and start the server.
Create Miles.
curl http://localhost:3000/api/user \
-d '{"name":"Miles Davis", "email":"miles@davis.org", "password": "some-password"}' \
-H "Content-Type: application/json" -X POST
Use the token to send a request to guard endpoint.
Watch the terminal where the server is running.
On this endpoint, we'd expect userId
to be defined but user
to be undefined.
curl http://localhost:3000/api/auth/guard \
-H "Authorization: Bearer MILES_TOKEN" -X GET
stdout:
user injection on /guard { userId: 'a4398777-0dad-49f8-a246-1a48c6b8d546', user: undefined }
Lets try the other one, where we'd expect userId
to be undefined but user
to be defined.
curl http://localhost:3000/api/auth/miles \
-H "Authorization: Bearer MILES_TOKEN" -X GET
stdout:
user injection on /miles {
userId: undefined,
user: {
name: 'Miles Davis',
email: 'miles@davis.org',
password: HASHED_PASSWORD,
id: GENERATED_ID,
createdAt: DATETIME,
updatedAt: DATETIME
}
}