Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4d925cc
🗑️ Update .gitignore to exclude memory-bank directory
BrightkyEfoo May 11, 2025
e9d0afa
✨ Update Environment Configuration and Logger Setup
BrightkyEfoo Apr 27, 2025
d6889a5
✨ Update Deployment Workflow to Include Environment Variable Setup
BrightkyEfoo Apr 27, 2025
9e74719
✨ Update Docker and Deployment Configuration
BrightkyEfoo Apr 27, 2025
61b1ab8
✨ Update Docker Configuration and Remove Unused Files
BrightkyEfoo Apr 29, 2025
0177d08
🗑️ Remove Environment File
BrightkyEfoo Apr 29, 2025
4bc2ff6
🗑️ Remove Deployment Workflow for Development Environment
BrightkyEfoo Apr 29, 2025
6debc9d
🗑️ Remove Deployment Workflow for Development Environment
BrightkyEfoo Apr 29, 2025
1bc706f
✨ Update Deployment Workflow to Include Branch Triggers
BrightkyEfoo Apr 29, 2025
c499944
✨ Update Deployment Workflow to Include Environment Specification
BrightkyEfoo Apr 29, 2025
cb176ad
✨ Update Deployment Workflow to Change Port Configuration
BrightkyEfoo Apr 29, 2025
95ba6d3
✨ Update Deployment Workflow to Change MySQL Port Configuration
BrightkyEfoo Apr 29, 2025
3b8bb4d
✨ Update Docker Compose Configuration for Environment Variables
BrightkyEfoo Apr 29, 2025
e6c670a
✨ Update Docker Compose and Deployment Workflow for Environment Varia…
BrightkyEfoo Apr 29, 2025
49a8ae1
✨ Update MySQL Port Configuration in Docker Compose
BrightkyEfoo Apr 29, 2025
489c53e
✨ Refactor Docker Configuration and Update Memory Bank
BrightkyEfoo Apr 29, 2025
203cc68
✨ Update Docker Configuration and .gitignore
BrightkyEfoo Apr 29, 2025
a1a303c
✨ Enhance Application Functionality and Update Configuration
BrightkyEfoo May 12, 2025
0606e34
✨ Add MinIO Service and Update Dependencies
BrightkyEfoo May 18, 2025
5316334
✨ Update Dependencies and Enhance Article Management
BrightkyEfoo May 27, 2025
313df1f
✨ Enhance Article and Discussion Management
BrightkyEfoo Oct 30, 2025
6f3e284
✨ Implement Article Banning and Role-Based Access Control
BrightkyEfoo Oct 30, 2025
0c03265
Merge branch 'develop' into feat/disable-cors
BrightkyEfoo Oct 30, 2025
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ yarn-error.log

# Platform specific
.DS_Store
memory-bank/
memory-bank/
.claude/
19 changes: 19 additions & 0 deletions app/controllers/api/upload_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HttpContext } from '@adonisjs/core/http'
import MinioService from '#services/minio_service'

export default class UploadController {
async presign({ request }: HttpContext) {
const { fileName, mimeType } = request.only(['fileName', 'mimeType'])
const { url, key } = await MinioService.getPresignedUrl(fileName, mimeType)
return { url, key }
}

async presignView({ request }: HttpContext) {
const key = request.input('key') || request.qs().key
if (!key) {
return { error: 'Missing key' }
}
const url = await MinioService.getPresignedViewUrl(key)
return { url }
}
}
232 changes: 218 additions & 14 deletions app/controllers/articles_controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { HttpContext } from '@adonisjs/core/http'
import { articleValidator } from '#validators/article_validator'
import { ArticleStatus } from '#enums/article_status'
import { Role } from '#enums/role'
import Article from '#models/article'
import ArticleStatsService from '#services/article_stats_service'
import { articleValidator } from '#validators/article_validator'
import { HttpContext } from '@adonisjs/core/http'
import { DateTime } from 'luxon'

export default class ArticlesController {
async index({ inertia, request }: HttpContext) {
const page = request.input('page', 1)
const articles = await Article.query()
.preload('author')
.where('is_published', true)
.where('status', ArticleStatus.PUBLISHED)
.orderBy('published_at', 'desc')
.paginate(page, 10)

Expand All @@ -23,19 +26,20 @@ export default class ArticlesController {

async store({ request, auth, response }: HttpContext) {
const data = await articleValidator.validate(request.all())
const article = new Article()

article.title = data.title
article.content = data.content
article.excerpt = data.excerpt
article.isPublished = data.isPublished || false
article.authorId = auth.user!.id
if (data.isPublished) {
article.publishedAt = DateTime.now()

const payload: Partial<Article> = { ...data, authorId: auth.user!.id }

if (data.status === ArticleStatus.PUBLISHED) {
payload.publishedAt = DateTime.now()
}

await article.generateSlug()
await article.save()
payload.slug = Article.generateSlug(data.title)

console.log('payload', payload)

const article = await Article.create(payload)

console.log('article', article)

return response.redirect().toRoute('articles.show', { slug: article.slug })
}
Expand All @@ -45,4 +49,204 @@ export default class ArticlesController {

return inertia.render('articles/[slug]', { article })
}

async dashboard({ inertia, auth }: HttpContext) {
const isAdmin = auth.user!.role === Role.ADMIN
const stats = await ArticleStatsService.getStats(auth.user!.id, isAdmin)
return inertia.render('dashboard/index', {
publishedArticles: stats.published,
draftArticles: stats.drafts,
bannedArticles: stats.banned,
discussions: 0,
questions: 0,
})
}

async articles({ inertia, request, auth }: HttpContext) {
const page = request.input('page', 1)
const status = request.input('status', null)
const isAdmin = auth.user!.role === Role.ADMIN

let query = Article.query().preload('author').orderBy('created_at', 'desc')

// Logique différente selon le rôle
if (isAdmin) {
// Admin : voir tous les articles publiés/banned + ses propres brouillons
if (status && Object.values(ArticleStatus).includes(status as ArticleStatus)) {
if (status === ArticleStatus.PUBLISHED) {
// Voir tous les articles publiés (de tous les auteurs)
query.where('status', ArticleStatus.PUBLISHED)
} else if (status === ArticleStatus.BANNED) {
// Voir tous les articles banned (de tous les auteurs)
query.where('status', ArticleStatus.BANNED)
} else {
// Voir uniquement ses propres brouillons
query.where('status', status as ArticleStatus).where('author_id', auth.user!.id)
}
} else {
// Par défaut : tous les publiés + ses brouillons
query.where((builder) => {
builder
.where('status', ArticleStatus.PUBLISHED)
.orWhere((subBuilder) => {
subBuilder.where('author_id', auth.user!.id).where('status', ArticleStatus.DRAFT)
})
})
}
} else {
// Membre : voir uniquement ses propres articles
query.where('author_id', auth.user!.id)

if (status && Object.values(ArticleStatus).includes(status as ArticleStatus)) {
query.where('status', status as ArticleStatus)
}
}

const articles = await query.paginate(page, 10)

return inertia.render('dashboard/articles', {
articles: articles.toJSON(),
currentStatus: status,
isAdmin,
})
}

async edit({ inertia, params, auth, response }: HttpContext) {
const article = await Article.query()
.where('slug', params.slug)
.where('author_id', auth.user!.id)
.firstOrFail()

return inertia.render('dashboard/articles/edit', { article })
}

async update({ request, params, auth, response }: HttpContext) {
const article = await Article.query()
.where('slug', params.slug)
.where('author_id', auth.user!.id)
.firstOrFail()

const data = await articleValidator.validate(request.all())

const payload: Partial<Article> = { ...data }

// Si on passe de draft/waiting à published, mettre à jour publishedAt
if (
data.status === ArticleStatus.PUBLISHED &&
article.status !== ArticleStatus.PUBLISHED
) {
payload.publishedAt = DateTime.now()
}

// Si le titre change, générer un nouveau slug
if (data.title !== article.title) {
payload.slug = Article.generateSlug(data.title)
}

await article.merge(payload).save()

return response.redirect().toRoute('articles.show', { slug: article.slug })
}

// ADMIN METHODS

/**
* Liste des articles pour l'admin
* - Tous les articles publiés (de tous les auteurs)
* - Ses propres brouillons seulement
*/
async adminArticles({ inertia, request, auth }: HttpContext) {
const page = request.input('page', 1)
const status = request.input('status', null)

let query = Article.query().preload('author').orderBy('created_at', 'desc')

if (status && Object.values(ArticleStatus).includes(status as ArticleStatus)) {
if (status === ArticleStatus.PUBLISHED) {
// Admin voit tous les articles publiés
query.where('status', ArticleStatus.PUBLISHED)
} else {
// Admin ne voit que ses propres brouillons/en attente
query.where('status', status as ArticleStatus).where('author_id', auth.user!.id)
}
} else {
// Par défaut : tous les publiés + ses brouillons
query.where((builder) => {
builder
.where('status', ArticleStatus.PUBLISHED)
.orWhere((subBuilder) => {
subBuilder.where('author_id', auth.user!.id).whereIn('status', [
ArticleStatus.DRAFT,
ArticleStatus.WAITING_APPROVAL,
])
})
})
}

const articles = await query.paginate(page, 10)

return inertia.render('admin/articles', {
articles: articles.toJSON(),
currentStatus: status,
})
}

/**
* Dépublier un article (admin seulement)
* Passe le statut de PUBLISHED à DRAFT
*/
async unpublish({ params, response, session, auth }: HttpContext) {
const article = await Article.query().where('slug', params.slug).firstOrFail()

// Vérifier que l'article est publié
if (article.status !== ArticleStatus.PUBLISHED) {
session.flash('error', 'Cet article n\'est pas publié')
return response.redirect().back()
}

// Dépublier l'article
article.status = ArticleStatus.DRAFT
article.publishedAt = null
await article.save()

session.flash('success', 'Article dépublié avec succès')
return response.redirect().back()
}

/**
* Bannir un article (admin seulement)
*/
async ban({ params, request, response, session }: HttpContext) {
const article = await Article.query().where('slug', params.slug).firstOrFail()
const banReason = request.input('ban_reason')

article.status = ArticleStatus.BANNED
article.banReason = banReason
article.publishedAt = null
await article.save()

session.flash('success', 'Article banni avec succès')
return response.redirect().back()
}

/**
* Débannir un article (admin seulement)
*/
async unban({ params, response, session }: HttpContext) {
const article = await Article.query().where('slug', params.slug).firstOrFail()

// Vérifier que l'article est banni
if (article.status !== ArticleStatus.BANNED) {
session.flash('error', 'Cet article n\'est pas banni')
return response.redirect().back()
}

// Débannir l'article (le repasser en brouillon)
article.status = ArticleStatus.DRAFT
article.banReason = null
await article.save()

session.flash('success', 'Article débanni avec succès')
return response.redirect().back()
}
}
6 changes: 5 additions & 1 deletion app/controllers/auth/register_controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import { registerValidator } from '#validators/register_validator'
import { Role } from '#enums/role'

export default class RegisterController {
async show({ inertia }: HttpContext) {
Expand All @@ -10,7 +11,10 @@ export default class RegisterController {
async store({ request, auth, response }: HttpContext) {
const data = await registerValidator.validate(request.all())

const user = await User.create(data)
const user = await User.create({
...data,
role: (data.role as Role) || Role.MEMBER,
})
await auth.use('web').login(user)

return response.redirect().toRoute('dashboard')
Expand Down
Loading
Loading