Skip to content

Commit 8d74a6e

Browse files
B4nanclaude
andauthored
feat: use @adonisjs/auth with custom MikroORM user provider (#1)
Replace custom JWT authentication with AdonisJS's built-in auth system using a SessionUserProviderContract backed by MikroORM. This provides proper framework integration with session guards, route-level auth middleware, and loginAs test helpers. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d1a9800 commit 8d74a6e

File tree

19 files changed

+380
-232
lines changed

19 files changed

+380
-232
lines changed

adonisrc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export default defineConfig({
66
providers: [
77
() => import('@adonisjs/core/providers/app_provider'),
88
() => import('@adonisjs/core/providers/hash_provider'),
9+
() => import('@adonisjs/session/session_provider'),
10+
() => import('@adonisjs/auth/auth_provider'),
911
{
1012
file: () => import('@adonisjs/core/providers/repl_provider'),
1113
environment: ['repl', 'test'],
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { symbols } from '@adonisjs/auth'
2+
import type { SessionGuardUser, SessionUserProviderContract } from '@adonisjs/auth/types/session'
3+
import type { EntityManager } from '@mikro-orm/sqlite'
4+
import { User } from '#entities/user'
5+
6+
/**
7+
* Bridges MikroORM with the AdonisJS session guard.
8+
* Implements SessionUserProviderContract so the auth system
9+
* can look up users via MikroORM's EntityManager.
10+
*/
11+
export class MikroOrmUserProvider implements SessionUserProviderContract<User> {
12+
declare [symbols.PROVIDER_REAL_USER]: User
13+
14+
constructor(private em: EntityManager) {}
15+
16+
async createUserForGuard(user: User): Promise<SessionGuardUser<User>> {
17+
return {
18+
getId() {
19+
return user.id
20+
},
21+
getOriginal() {
22+
return user
23+
},
24+
}
25+
}
26+
27+
async findById(identifier: number): Promise<SessionGuardUser<User> | null> {
28+
const user = await this.em.findOne(User, identifier)
29+
30+
if (!user) {
31+
return null
32+
}
33+
34+
return this.createUserForGuard(user)
35+
}
36+
}

app/controllers/articles_controller.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { EntityManager } from '@mikro-orm/sqlite'
55
import { CommentSchema } from '#entities/comment'
66
import { AuthError } from '#repositories/user_repository'
77
import { ArticleRepository } from '#repositories/article_repository'
8-
import { getUserFromCtx } from '#utils/auth'
98
import { type IArticle } from '#entities/article'
109

1110
function verifyArticlePermissions(user: { id: number }, article: IArticle) {
@@ -38,9 +37,9 @@ export default class ArticlesController {
3837
})
3938
}
4039

41-
async store(ctx: HttpContext) {
42-
const author = getUserFromCtx(ctx)
43-
const { title, description, text } = ctx.request.body()
40+
async store({ auth, request }: HttpContext) {
41+
const author = auth.getUserOrFail()
42+
const { title, description, text } = request.body()
4443

4544
const article = this.articleRepo.create({
4645
title, description, text,
@@ -52,11 +51,11 @@ export default class ArticlesController {
5251
return article
5352
}
5453

55-
async update(ctx: HttpContext) {
56-
const user = getUserFromCtx(ctx)
57-
const article = await this.articleRepo.findOneOrFail(+ctx.params.id)
54+
async update({ auth, params, request }: HttpContext) {
55+
const user = auth.getUserOrFail()
56+
const article = await this.articleRepo.findOneOrFail(+params.id)
5857
verifyArticlePermissions(user, article)
59-
const body = ctx.request.body()
58+
const body = request.body()
6059
const data: Record<string, unknown> = {}
6160

6261
for (const key of ['title', 'description', 'text'] as const) {
@@ -71,19 +70,19 @@ export default class ArticlesController {
7170
return article
7271
}
7372

74-
async destroy(ctx: HttpContext) {
75-
const user = getUserFromCtx(ctx)
76-
const article = await this.articleRepo.findOneOrFail(+ctx.params.id)
73+
async destroy({ auth, params }: HttpContext) {
74+
const user = auth.getUserOrFail()
75+
const article = await this.articleRepo.findOneOrFail(+params.id)
7776
verifyArticlePermissions(user, article)
7877
await this.em.remove(article).flush()
7978

8079
return { success: true }
8180
}
8281

83-
async addComment(ctx: HttpContext) {
84-
const author = getUserFromCtx(ctx)
85-
const article = await this.articleRepo.findOneOrFail({ slug: ctx.params.slug })
86-
const { text } = ctx.request.body()
82+
async addComment({ auth, params, request }: HttpContext) {
83+
const author = auth.getUserOrFail()
84+
const article = await this.articleRepo.findOneOrFail({ slug: params.slug })
85+
const { text } = request.body()
8786
const comment = this.em.create(CommentSchema, { author, article, text })
8887
await this.em.flush()
8988

app/controllers/users_controller.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { inject } from '@adonisjs/core'
22
import type { HttpContext } from '@adonisjs/core/http'
33
import { wrap } from '@mikro-orm/core'
44
import { EntityManager } from '@mikro-orm/sqlite'
5-
import { signJwt } from '#services/jwt'
65
import { AuthError, UserRepository } from '#repositories/user_repository'
7-
import { getUserFromCtx } from '#utils/auth'
86

97
@inject()
108
export default class UsersController {
@@ -13,7 +11,7 @@ export default class UsersController {
1311
protected userRepo: UserRepository,
1412
) {}
1513

16-
async signUp({ request, response }: HttpContext) {
14+
async signUp({ request, response, auth }: HttpContext) {
1715
const { fullName, email, password, bio } = request.body()
1816

1917
if (await this.userRepo.exists(email)) {
@@ -24,18 +22,17 @@ export default class UsersController {
2422

2523
const user = this.userRepo.create({ fullName, email, password, bio })
2624
await this.em.flush()
27-
28-
user.token = signJwt({ id: user.id })
25+
await auth.use('web').login(user)
2926

3027
return user
3128
}
3229

33-
async signIn({ request, response }: HttpContext) {
30+
async signIn({ request, response, auth }: HttpContext) {
3431
const { email, password } = request.body()
3532

3633
try {
3734
const user = await this.userRepo.login(email, password)
38-
user.token = signJwt({ id: user.id })
35+
await auth.use('web').login(user)
3936
return user
4037
} catch (error) {
4138
if (error instanceof AuthError) {
@@ -46,13 +43,13 @@ export default class UsersController {
4643
}
4744
}
4845

49-
async profile(ctx: HttpContext) {
50-
return getUserFromCtx(ctx)
46+
async profile({ auth }: HttpContext) {
47+
return auth.getUserOrFail()
5148
}
5249

53-
async updateProfile(ctx: HttpContext) {
54-
const user = getUserFromCtx(ctx)
55-
const body = ctx.request.body()
50+
async updateProfile({ auth, request }: HttpContext) {
51+
const user = auth.getUserOrFail()
52+
const body = request.body()
5653
const data: Record<string, unknown> = {}
5754

5855
for (const key of ['fullName', 'bio', 'social'] as const) {

app/entities/user.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export const UserSchema = defineEntity({
3939
password: p.string().hidden().lazy(),
4040
bio: p.text().default(''),
4141
articles: () => p.oneToMany(ArticleSchema).mappedBy('author'),
42-
token: p.string().persist(false).nullable(),
4342
social: () => p.embedded(SocialSchema).object().nullable(),
4443
},
4544
})

app/exceptions/handler.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import app from '@adonisjs/core/services/app'
22
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
3+
import { errors as authErrors } from '@adonisjs/auth'
34
import { NotFoundError } from '@mikro-orm/core'
45
import { AuthError } from '#repositories/user_repository'
56

67
export default class HttpExceptionHandler extends ExceptionHandler {
78
protected debug = !app.inProduction
89

910
async handle(error: unknown, ctx: HttpContext) {
11+
if (error instanceof authErrors.E_UNAUTHORIZED_ACCESS) {
12+
ctx.response.status(401).send({ error: 'Unauthorized' })
13+
return
14+
}
15+
1016
if (error instanceof AuthError) {
1117
ctx.response.status(401).send({ error: error.message })
1218
return
@@ -21,7 +27,7 @@ export default class HttpExceptionHandler extends ExceptionHandler {
2127
}
2228

2329
async report(error: unknown, ctx: HttpContext) {
24-
if (error instanceof AuthError) {
30+
if (error instanceof authErrors.E_UNAUTHORIZED_ACCESS || error instanceof AuthError) {
2531
return
2632
}
2733

app/middleware/auth_middleware.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
1-
import { inject } from '@adonisjs/core'
21
import type { HttpContext } from '@adonisjs/core/http'
32
import type { NextFn } from '@adonisjs/core/types/http'
4-
import { UserRepository } from '#repositories/user_repository'
5-
import { verifyJwt } from '#services/jwt'
3+
import type { Authenticators } from '@adonisjs/auth/types'
64

75
/**
8-
* Attempts to authenticate the user from the Authorization header.
9-
* Does not reject unauthenticated requests — just sets `ctx.user` if valid.
6+
* Protects routes from unauthenticated users. Throws E_UNAUTHORIZED_ACCESS
7+
* when the user is not logged in.
108
*/
11-
@inject()
129
export default class AuthMiddleware {
13-
constructor(protected userRepo: UserRepository) {}
14-
15-
async handle(ctx: HttpContext, next: NextFn) {
16-
const header = ctx.request.header('authorization')
17-
18-
if (header?.startsWith('Bearer ')) {
19-
const token = header.slice(7)
20-
21-
try {
22-
const payload = verifyJwt(token)
23-
ctx.user = await this.userRepo.findOneOrFail(payload.id)
24-
} catch {
25-
// ignore invalid tokens, we validate ctx.user where needed
26-
}
27-
}
28-
10+
async handle(ctx: HttpContext, next: NextFn, options: { guards?: (keyof Authenticators)[] } = {}) {
11+
await ctx.auth.authenticateUsing(options.guards || [ctx.auth.defaultGuard])
2912
return next()
3013
}
3114
}

app/services/jwt.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

app/types.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

app/utils/auth.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)