Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions adonisrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export default defineConfig({
providers: [
() => import('@adonisjs/core/providers/app_provider'),
() => import('@adonisjs/core/providers/hash_provider'),
() => import('@adonisjs/session/session_provider'),
() => import('@adonisjs/auth/auth_provider'),
{
file: () => import('@adonisjs/core/providers/repl_provider'),
environment: ['repl', 'test'],
Expand Down
36 changes: 36 additions & 0 deletions app/auth/mikro_orm_user_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { symbols } from '@adonisjs/auth'
import type { SessionGuardUser, SessionUserProviderContract } from '@adonisjs/auth/types/session'
import type { EntityManager } from '@mikro-orm/sqlite'
import { User } from '#entities/user'

/**
* Bridges MikroORM with the AdonisJS session guard.
* Implements SessionUserProviderContract so the auth system
* can look up users via MikroORM's EntityManager.
*/
export class MikroOrmUserProvider implements SessionUserProviderContract<User> {
declare [symbols.PROVIDER_REAL_USER]: User

constructor(private em: EntityManager) {}

async createUserForGuard(user: User): Promise<SessionGuardUser<User>> {
return {
getId() {
return user.id
},
getOriginal() {
return user
},
}
}

async findById(identifier: number): Promise<SessionGuardUser<User> | null> {
const user = await this.em.findOne(User, identifier)

if (!user) {
return null
}

return this.createUserForGuard(user)
}
}
29 changes: 14 additions & 15 deletions app/controllers/articles_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { EntityManager } from '@mikro-orm/sqlite'
import { CommentSchema } from '#entities/comment'
import { AuthError } from '#repositories/user_repository'
import { ArticleRepository } from '#repositories/article_repository'
import { getUserFromCtx } from '#utils/auth'
import { type IArticle } from '#entities/article'

function verifyArticlePermissions(user: { id: number }, article: IArticle) {
Expand Down Expand Up @@ -38,9 +37,9 @@ export default class ArticlesController {
})
}

async store(ctx: HttpContext) {
const author = getUserFromCtx(ctx)
const { title, description, text } = ctx.request.body()
async store({ auth, request }: HttpContext) {
const author = auth.getUserOrFail()
const { title, description, text } = request.body()

const article = this.articleRepo.create({
title, description, text,
Expand All @@ -52,11 +51,11 @@ export default class ArticlesController {
return article
}

async update(ctx: HttpContext) {
const user = getUserFromCtx(ctx)
const article = await this.articleRepo.findOneOrFail(+ctx.params.id)
async update({ auth, params, request }: HttpContext) {
const user = auth.getUserOrFail()
const article = await this.articleRepo.findOneOrFail(+params.id)
verifyArticlePermissions(user, article)
const body = ctx.request.body()
const body = request.body()
const data: Record<string, unknown> = {}

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

async destroy(ctx: HttpContext) {
const user = getUserFromCtx(ctx)
const article = await this.articleRepo.findOneOrFail(+ctx.params.id)
async destroy({ auth, params }: HttpContext) {
const user = auth.getUserOrFail()
const article = await this.articleRepo.findOneOrFail(+params.id)
verifyArticlePermissions(user, article)
await this.em.remove(article).flush()

return { success: true }
}

async addComment(ctx: HttpContext) {
const author = getUserFromCtx(ctx)
const article = await this.articleRepo.findOneOrFail({ slug: ctx.params.slug })
const { text } = ctx.request.body()
async addComment({ auth, params, request }: HttpContext) {
const author = auth.getUserOrFail()
const article = await this.articleRepo.findOneOrFail({ slug: params.slug })
const { text } = request.body()
const comment = this.em.create(CommentSchema, { author, article, text })
await this.em.flush()

Expand Down
21 changes: 9 additions & 12 deletions app/controllers/users_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import { wrap } from '@mikro-orm/core'
import { EntityManager } from '@mikro-orm/sqlite'
import { signJwt } from '#services/jwt'
import { AuthError, UserRepository } from '#repositories/user_repository'
import { getUserFromCtx } from '#utils/auth'

@inject()
export default class UsersController {
Expand All @@ -13,7 +11,7 @@ export default class UsersController {
protected userRepo: UserRepository,
) {}

async signUp({ request, response }: HttpContext) {
async signUp({ request, response, auth }: HttpContext) {
const { fullName, email, password, bio } = request.body()

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

const user = this.userRepo.create({ fullName, email, password, bio })
await this.em.flush()

user.token = signJwt({ id: user.id })
await auth.use('web').login(user)

return user
}

async signIn({ request, response }: HttpContext) {
async signIn({ request, response, auth }: HttpContext) {
const { email, password } = request.body()

try {
const user = await this.userRepo.login(email, password)
user.token = signJwt({ id: user.id })
await auth.use('web').login(user)
return user
} catch (error) {
if (error instanceof AuthError) {
Expand All @@ -46,13 +43,13 @@ export default class UsersController {
}
}

async profile(ctx: HttpContext) {
return getUserFromCtx(ctx)
async profile({ auth }: HttpContext) {
return auth.getUserOrFail()
}

async updateProfile(ctx: HttpContext) {
const user = getUserFromCtx(ctx)
const body = ctx.request.body()
async updateProfile({ auth, request }: HttpContext) {
const user = auth.getUserOrFail()
const body = request.body()
const data: Record<string, unknown> = {}

for (const key of ['fullName', 'bio', 'social'] as const) {
Expand Down
1 change: 0 additions & 1 deletion app/entities/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export const UserSchema = defineEntity({
password: p.string().hidden().lazy(),
bio: p.text().default(''),
articles: () => p.oneToMany(ArticleSchema).mappedBy('author'),
token: p.string().persist(false).nullable(),
social: () => p.embedded(SocialSchema).object().nullable(),
},
})
Expand Down
8 changes: 7 additions & 1 deletion app/exceptions/handler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import app from '@adonisjs/core/services/app'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
import { errors as authErrors } from '@adonisjs/auth'
import { NotFoundError } from '@mikro-orm/core'
import { AuthError } from '#repositories/user_repository'

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

async handle(error: unknown, ctx: HttpContext) {
if (error instanceof authErrors.E_UNAUTHORIZED_ACCESS) {
ctx.response.status(401).send({ error: 'Unauthorized' })
return
}

if (error instanceof AuthError) {
ctx.response.status(401).send({ error: error.message })
return
Expand All @@ -21,7 +27,7 @@ export default class HttpExceptionHandler extends ExceptionHandler {
}

async report(error: unknown, ctx: HttpContext) {
if (error instanceof AuthError) {
if (error instanceof authErrors.E_UNAUTHORIZED_ACCESS || error instanceof AuthError) {
return
}

Expand Down
27 changes: 5 additions & 22 deletions app/middleware/auth_middleware.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { UserRepository } from '#repositories/user_repository'
import { verifyJwt } from '#services/jwt'
import type { Authenticators } from '@adonisjs/auth/types'

/**
* Attempts to authenticate the user from the Authorization header.
* Does not reject unauthenticated requests — just sets `ctx.user` if valid.
* Protects routes from unauthenticated users. Throws E_UNAUTHORIZED_ACCESS
* when the user is not logged in.
*/
@inject()
export default class AuthMiddleware {
constructor(protected userRepo: UserRepository) {}

async handle(ctx: HttpContext, next: NextFn) {
const header = ctx.request.header('authorization')

if (header?.startsWith('Bearer ')) {
const token = header.slice(7)

try {
const payload = verifyJwt(token)
ctx.user = await this.userRepo.findOneOrFail(payload.id)
} catch {
// ignore invalid tokens, we validate ctx.user where needed
}
}

async handle(ctx: HttpContext, next: NextFn, options: { guards?: (keyof Authenticators)[] } = {}) {
await ctx.auth.authenticateUsing(options.guards || [ctx.auth.defaultGuard])
return next()
}
}
42 changes: 0 additions & 42 deletions app/services/jwt.ts

This file was deleted.

7 changes: 0 additions & 7 deletions app/types.ts

This file was deleted.

11 changes: 0 additions & 11 deletions app/utils/auth.ts

This file was deleted.

25 changes: 25 additions & 0 deletions config/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig } from '@adonisjs/auth'
import { sessionGuard } from '@adonisjs/auth/session'
import { configProvider } from '@adonisjs/core'
import { EntityManager } from '@mikro-orm/sqlite'
import type { InferAuthenticators } from '@adonisjs/auth/types'

const authConfig = defineConfig({
default: 'web',
guards: {
web: sessionGuard({
useRememberMeTokens: false,
provider: configProvider.create(async (app) => {
const { MikroOrmUserProvider } = await import('#auth/mikro_orm_user_provider')
const em = await app.container.make(EntityManager)
return new MikroOrmUserProvider(em)
}),
}),
},
})

export default authConfig

declare module '@adonisjs/auth/types' {
interface Authenticators extends InferAuthenticators<typeof authConfig> {}
}
13 changes: 13 additions & 0 deletions config/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import app from '@adonisjs/core/services/app'
import { defineConfig, stores } from '@adonisjs/session'

const sessionConfig = defineConfig({
age: '2h',
store: app.inTest ? 'memory' : 'cookie',
cookie: {},
stores: {
cookie: stores.cookie(),
},
})

export default sessionConfig
Loading
Loading