Skip to content

Commit 6f3e284

Browse files
committed
✨ Implement Article Banning and Role-Based Access Control
- Enhance ArticlesController to support banning and unbanning articles, with appropriate role checks for admin users. - Update ArticleStatsService to include statistics for banned articles. - Modify Article model to add a banReason field for tracking reasons for article bans. - Refactor dashboard and articles pages to display banned articles and adjust filtering based on user roles. - Update routes to include new endpoints for banning and unbanning articles, ensuring proper middleware enforcement.
1 parent 313df1f commit 6f3e284

11 files changed

Lines changed: 575 additions & 171 deletions

File tree

app/controllers/articles_controller.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ export default class ArticlesController {
5151
}
5252

5353
async dashboard({ inertia, auth }: HttpContext) {
54-
// Chaque utilisateur ne voit que ses propres statistiques
55-
const stats = await ArticleStatsService.getStats(auth.user!.id)
54+
const isAdmin = auth.user!.role === Role.ADMIN
55+
const stats = await ArticleStatsService.getStats(auth.user!.id, isAdmin)
5656
return inertia.render('dashboard/index', {
5757
publishedArticles: stats.published,
5858
draftArticles: stats.drafts,
59-
waitingArticles: stats.waiting,
59+
bannedArticles: stats.banned,
6060
discussions: 0,
6161
questions: 0,
6262
})
@@ -65,21 +65,49 @@ export default class ArticlesController {
6565
async articles({ inertia, request, auth }: HttpContext) {
6666
const page = request.input('page', 1)
6767
const status = request.input('status', null)
68+
const isAdmin = auth.user!.role === Role.ADMIN
6869

69-
const query = Article.query()
70-
.preload('author')
71-
.where('author_id', auth.user!.id)
72-
.orderBy('created_at', 'desc')
70+
let query = Article.query().preload('author').orderBy('created_at', 'desc')
7371

74-
if (status && Object.values(ArticleStatus).includes(status as ArticleStatus)) {
75-
query.where('status', status as ArticleStatus)
72+
// Logique différente selon le rôle
73+
if (isAdmin) {
74+
// Admin : voir tous les articles publiés/banned + ses propres brouillons
75+
if (status && Object.values(ArticleStatus).includes(status as ArticleStatus)) {
76+
if (status === ArticleStatus.PUBLISHED) {
77+
// Voir tous les articles publiés (de tous les auteurs)
78+
query.where('status', ArticleStatus.PUBLISHED)
79+
} else if (status === ArticleStatus.BANNED) {
80+
// Voir tous les articles banned (de tous les auteurs)
81+
query.where('status', ArticleStatus.BANNED)
82+
} else {
83+
// Voir uniquement ses propres brouillons
84+
query.where('status', status as ArticleStatus).where('author_id', auth.user!.id)
85+
}
86+
} else {
87+
// Par défaut : tous les publiés + ses brouillons
88+
query.where((builder) => {
89+
builder
90+
.where('status', ArticleStatus.PUBLISHED)
91+
.orWhere((subBuilder) => {
92+
subBuilder.where('author_id', auth.user!.id).where('status', ArticleStatus.DRAFT)
93+
})
94+
})
95+
}
96+
} else {
97+
// Membre : voir uniquement ses propres articles
98+
query.where('author_id', auth.user!.id)
99+
100+
if (status && Object.values(ArticleStatus).includes(status as ArticleStatus)) {
101+
query.where('status', status as ArticleStatus)
102+
}
76103
}
77104

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

80107
return inertia.render('dashboard/articles', {
81108
articles: articles.toJSON(),
82109
currentStatus: status,
110+
isAdmin,
83111
})
84112
}
85113

@@ -184,4 +212,41 @@ export default class ArticlesController {
184212
session.flash('success', 'Article dépublié avec succès')
185213
return response.redirect().back()
186214
}
215+
216+
/**
217+
* Bannir un article (admin seulement)
218+
*/
219+
async ban({ params, request, response, session }: HttpContext) {
220+
const article = await Article.query().where('slug', params.slug).firstOrFail()
221+
const banReason = request.input('ban_reason')
222+
223+
article.status = ArticleStatus.BANNED
224+
article.banReason = banReason
225+
article.publishedAt = null
226+
await article.save()
227+
228+
session.flash('success', 'Article banni avec succès')
229+
return response.redirect().back()
230+
}
231+
232+
/**
233+
* Débannir un article (admin seulement)
234+
*/
235+
async unban({ params, response, session }: HttpContext) {
236+
const article = await Article.query().where('slug', params.slug).firstOrFail()
237+
238+
// Vérifier que l'article est banni
239+
if (article.status !== ArticleStatus.BANNED) {
240+
session.flash('error', 'Cet article n\'est pas banni')
241+
return response.redirect().back()
242+
}
243+
244+
// Débannir l'article (le repasser en brouillon)
245+
article.status = ArticleStatus.DRAFT
246+
article.banReason = null
247+
await article.save()
248+
249+
session.flash('success', 'Article débanni avec succès')
250+
return response.redirect().back()
251+
}
187252
}

app/models/article.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ export default class Article extends BaseModel {
5151
})
5252
declare tags: string[]
5353

54+
@column()
55+
declare banReason: string | null
56+
5457
// Generate slug before saving
5558
public static generateSlug(title: string) {
5659
return string.slug(title)

app/services/article_stats_service.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,31 @@ import { ArticleStatus } from '#enums/article_status'
22
import Article from '#models/article'
33

44
export default class ArticleStatsService {
5-
static async getStats(userId?: number) {
5+
static async getStats(userId?: number, isAdmin: boolean = false) {
66
const publishedQuery = Article.query().where('status', ArticleStatus.PUBLISHED)
77
const draftQuery = Article.query().where('status', ArticleStatus.DRAFT)
8-
const waitingQuery = Article.query().where('status', ArticleStatus.WAITING_APPROVAL)
8+
const bannedQuery = Article.query().where('status', ArticleStatus.BANNED)
99

10-
// Si userId est fourni, filtrer par l'auteur
11-
if (userId) {
10+
// Pour les admins, published = tous les articles publiés (pas de filtre userId)
11+
// Pour les membres, filtrer tous les comptes par userId
12+
if (userId && !isAdmin) {
1213
publishedQuery.where('author_id', userId)
1314
draftQuery.where('author_id', userId)
14-
waitingQuery.where('author_id', userId)
15+
bannedQuery.where('author_id', userId)
16+
} else if (userId && isAdmin) {
17+
// Admin: published = tous, mais draft/banned = uniquement les siens
18+
draftQuery.where('author_id', userId)
19+
bannedQuery.where('author_id', userId)
1520
}
1621

1722
const publishedResult = await publishedQuery.count('* as total')
1823
const draftResult = await draftQuery.count('* as total')
19-
const waitingResult = await waitingQuery.count('* as total')
24+
const bannedResult = await bannedQuery.count('* as total')
2025

2126
return {
2227
published: Number(publishedResult[0].$extras.total),
2328
drafts: Number(draftResult[0].$extras.total),
24-
waiting: Number(waitingResult[0].$extras.total),
29+
banned: Number(bannedResult[0].$extras.total),
2530
}
2631
}
2732
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { BaseSchema } from '@adonisjs/lucid/schema'
2+
3+
export default class extends BaseSchema {
4+
protected tableName = 'articles'
5+
6+
async up() {
7+
this.schema.alterTable(this.tableName, (table) => {
8+
table.text('ban_reason').nullable()
9+
})
10+
}
11+
12+
async down() {
13+
this.schema.alterTable(this.tableName, (table) => {
14+
table.dropColumn('ban_reason')
15+
})
16+
}
17+
}

enums/article_status.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export enum ArticleStatus {
22
DRAFT = 'draft',
3-
WAITING_APPROVAL = 'waiting_approval',
3+
BANNED = 'banned',
44
PUBLISHED = 'published',
55
}
66

inertia/components/articles/article-card.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Link } from '@inertiajs/react'
22
import { useState } from 'react'
3+
import { useImageUrl } from '../../utils/image'
34

45
interface ArticleCardProps {
56
article: {
@@ -16,19 +17,19 @@ interface ArticleCardProps {
1617

1718
export default function ArticleCard({ article }: ArticleCardProps) {
1819
const [imgError, setImgError] = useState(false)
19-
const showFallback = imgError || !article.coverImage
20-
21-
console.log('article.coverImage', article.coverImage)
20+
const displayImageUrl = useImageUrl(article.coverImage)
21+
const showFallback = imgError || !displayImageUrl
2222

2323
return (
24-
<div className="bg-white rounded-xl shadow flex flex-col h-full cursor-pointer overflow-hidden border-2 border-gray-200 transition-all duration-300 hover:border-indigo-600">
25-
<div
26-
className={`h-48 flex items-center justify-center overflow-hidden ${showFallback ? 'bg-gray-100' : ''}`}
27-
style={{ marginBottom: '0' }}
28-
>
24+
<Link href={`/articles/${article.slug}`} className="block">
25+
<div className="bg-white rounded-xl shadow flex flex-col h-full cursor-pointer overflow-hidden border-2 border-gray-200 transition-all duration-300 hover:border-indigo-600">
26+
<div
27+
className={`h-48 flex items-center justify-center overflow-hidden ${showFallback ? 'bg-gray-100' : ''}`}
28+
style={{ marginBottom: '0' }}
29+
>
2930
{!showFallback ? (
3031
<img
31-
src={article.coverImage!}
32+
src={displayImageUrl!}
3233
alt={article.title}
3334
className="object-cover w-full h-full"
3435
onError={() => setImgError(true)}
@@ -57,12 +58,9 @@ export default function ArticleCard({ article }: ArticleCardProps) {
5758
</span>
5859
))}
5960
</div>
60-
<Link
61-
href={`/articles/${article.slug}`}
62-
className="font-bold text-lg mb-2 hover:text-indigo-600 transition"
63-
>
61+
<h3 className="font-bold text-lg mb-2 hover:text-indigo-600 transition">
6462
{article.title}
65-
</Link>
63+
</h3>
6664
<p className="text-gray-600 text-sm flex-1">{article.excerpt}</p>
6765
<div className="flex items-center mt-4">
6866
{article.author.avatar ? (
@@ -80,5 +78,6 @@ export default function ArticleCard({ article }: ArticleCardProps) {
8078
</div>
8179
</div>
8280
</div>
81+
</Link>
8382
)
8483
}

0 commit comments

Comments
 (0)