Skip to content

Commit 0606e34

Browse files
committed
✨ Add MinIO Service and Update Dependencies
- Introduce MinIO service in Docker Compose for object storage, including environment variables for configuration. - Update package.json and package-lock.json to include new dependencies: axios and minio. - Refactor articles and user models to utilize enums for article status and user roles, enhancing code clarity and maintainability. - Modify article and register validators to incorporate new fields and validation rules. - Update article creation and registration forms to support new fields and improve user experience.
1 parent a1a303c commit 0606e34

25 files changed

Lines changed: 715 additions & 116 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { HttpContext } from '@adonisjs/core/http'
2+
import MinioService from '#services/minio_service'
3+
4+
export default class UploadController {
5+
async presign({ request }: HttpContext) {
6+
const { fileName, mimeType } = request.only(['fileName', 'mimeType'])
7+
const { url, key } = await MinioService.getPresignedUrl(fileName, mimeType)
8+
return { url, key }
9+
}
10+
11+
async presignView({ request }: HttpContext) {
12+
const key = request.input('key') || request.qs().key
13+
if (!key) {
14+
return { error: 'Missing key' }
15+
}
16+
const url = await MinioService.getPresignedViewUrl(key)
17+
return { url }
18+
}
19+
}

app/controllers/articles_controller.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { articleValidator } from '#validators/article_validator'
33
import Article from '#models/article'
44
import { DateTime } from 'luxon'
55
import ArticleStatsService from '#services/article_stats_service'
6+
import { ArticleStatus } from '#enums/article_status'
67

78
export default class ArticlesController {
89
async index({ inertia, request }: HttpContext) {
910
const page = request.input('page', 1)
1011
const articles = await Article.query()
1112
.preload('author')
12-
.where('is_published', true)
13+
.where('status', ArticleStatus.PUBLISHED)
1314
.orderBy('published_at', 'desc')
1415
.paginate(page, 10)
1516

@@ -29,11 +30,14 @@ export default class ArticlesController {
2930
article.title = data.title
3031
article.content = data.content
3132
article.excerpt = data.excerpt
32-
article.isPublished = data.isPublished || false
33+
article.status = data.status as ArticleStatus
3334
article.authorId = auth.user!.id
34-
if (data.isPublished) {
35+
if (data.status === ArticleStatus.PUBLISHED) {
3536
article.publishedAt = DateTime.now()
3637
}
38+
article.coverImage = data.coverImage || null
39+
article.canonicalUrl = data.canonicalUrl || null
40+
article.tags = data.tags || []
3741

3842
await article.generateSlug()
3943
await article.save()
@@ -52,6 +56,7 @@ export default class ArticlesController {
5256
return inertia.render('dashboard/index', {
5357
publishedArticles: stats.published,
5458
draftArticles: stats.drafts,
59+
waitingArticles: stats.waiting,
5560
discussions: 0,
5661
questions: 0,
5762
})

app/controllers/auth/register_controller.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { HttpContext } from '@adonisjs/core/http'
22
import User from '#models/user'
33
import { registerValidator } from '#validators/register_validator'
4+
import { Role } from '#enums/role'
45

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

13-
const user = await User.create(data)
14+
const user = await User.create({
15+
...data,
16+
role: (data.role as Role) || Role.MEMBER,
17+
})
1418
await auth.use('web').login(user)
1519

1620
return response.redirect().toRoute('dashboard')

app/models/article.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm'
33
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
44
import string from '@adonisjs/core/helpers/string'
55
import User from '#models/user'
6+
import { ArticleStatus } from '#enums/article_status'
67

78
export default class Article extends BaseModel {
89
@column({ isPrimary: true })
@@ -21,7 +22,7 @@ export default class Article extends BaseModel {
2122
declare excerpt: string
2223

2324
@column()
24-
declare isPublished: boolean
25+
declare status: ArticleStatus
2526

2627
@column()
2728
declare authorId: number
@@ -38,6 +39,15 @@ export default class Article extends BaseModel {
3839
@column.dateTime()
3940
declare publishedAt: DateTime | null
4041

42+
@column()
43+
declare coverImage: string | null
44+
45+
@column()
46+
declare canonicalUrl: string | null
47+
48+
@column()
49+
declare tags: string[] | null
50+
4151
// Generate slug before saving
4252
public async generateSlug() {
4353
this.slug = string.slug(this.title)

app/models/user.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
66
import type { HasMany } from '@adonisjs/lucid/types/relations'
77
import Article from '#models/article'
88
import { DbRememberMeTokensProvider } from '@adonisjs/auth/session'
9+
import { Role } from '#enums/role'
910

1011
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
1112
uids: ['email'],
@@ -38,10 +39,7 @@ export default class User extends compose(BaseModel, AuthFinder) {
3839
declare twitterId: string | null
3940

4041
@column()
41-
declare isAdmin: boolean
42-
43-
@column()
44-
declare isSponsor: boolean
42+
declare role: Role
4543

4644
@column.dateTime()
4745
declare emailVerifiedAt: DateTime | null
@@ -70,8 +68,7 @@ export default class User extends compose(BaseModel, AuthFinder) {
7068
name: this.name,
7169
email: this.email,
7270
avatar: this.avatar,
73-
isAdmin: this.isAdmin,
74-
isSponsor: this.isSponsor,
71+
role: this.role,
7572
}
7673
}
7774
}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1+
import { ArticleStatus } from '#enums/article_status'
12
import Article from '#models/article'
23

34
export default class ArticleStatsService {
45
static async getStats() {
5-
const publishedResult = await Article.query().where('is_published', true).count('* as total')
6-
const draftResult = await Article.query().where('is_published', false).count('* as total')
6+
const publishedResult = await Article.query()
7+
.where('status', ArticleStatus.PUBLISHED)
8+
.count('* as total')
9+
const draftResult = await Article.query()
10+
.where('status', ArticleStatus.DRAFT)
11+
.count('* as total')
12+
const waitingResult = await Article.query()
13+
.where('status', ArticleStatus.WAITING_APPROVAL)
14+
.count('* as total')
715
return {
816
published: Number(publishedResult[0].$extras.total),
917
drafts: Number(draftResult[0].$extras.total),
18+
waiting: Number(waitingResult[0].$extras.total),
1019
}
1120
}
1221
}

app/services/minio_service.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Client } from 'minio'
2+
import env from '#start/env'
3+
import { v4 as uuidv4 } from 'uuid'
4+
5+
const minioClient = new Client({
6+
endPoint: env.get('MINIO_ENDPOINT') || 'localhost',
7+
port: Number(env.get('MINIO_PORT') || '9000'),
8+
useSSL: false,
9+
accessKey: env.get('MINIO_ROOT_USER') || 'minio',
10+
secretKey: env.get('MINIO_ROOT_PASSWORD') || 'minio123',
11+
})
12+
13+
const BUCKET = env.get('MINIO_DEFAULT_BUCKETS') || 'public'
14+
15+
export default class MinioService {
16+
static async upload(file: Buffer, fileName: string, mimeType: string): Promise<string> {
17+
// S'assurer que le bucket existe
18+
const exists = await minioClient.bucketExists(BUCKET)
19+
if (!exists) {
20+
await minioClient.makeBucket(BUCKET)
21+
}
22+
await minioClient.putObject(BUCKET, fileName, file, undefined, { 'Content-Type': mimeType })
23+
// URL publique
24+
return `${env.get('MINIO_PUBLIC_URL') || 'http://localhost:9000'}/${BUCKET}/${fileName}`
25+
}
26+
27+
static async getPresignedUrl(fileName: string, mimeType: string) {
28+
const ext = fileName.split('.').pop()
29+
const key = `${uuidv4()}.${ext}`
30+
const url = await minioClient.presignedPutObject(BUCKET, key, 60 * 5)
31+
return { url, key }
32+
}
33+
34+
static async getPresignedViewUrl(key: string, expirySeconds = 300): Promise<string> {
35+
return minioClient.presignedGetObject(BUCKET, key, expirySeconds)
36+
}
37+
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import { ARTICLE_STATUS_LIST } from '#enums/article_status'
12
import vine from '@vinejs/vine'
23

34
export const articleValidator = vine.compile(
45
vine.object({
56
title: vine.string().minLength(10).maxLength(255),
67
content: vine.string().minLength(100),
78
excerpt: vine.string().minLength(50).maxLength(160),
8-
tags: vine.array(vine.string()),
9-
isPublished: vine.boolean().optional(),
9+
coverImage: vine.string().optional().nullable(),
10+
canonicalUrl: vine.string().optional().nullable(),
11+
tags: vine.array(vine.string()).optional(),
12+
status: vine.enum(ARTICLE_STATUS_LIST),
1013
})
1114
)

app/validators/register_validator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import vine from '@vinejs/vine'
2+
import { ROLES_LIST } from '#enums/role'
23

34
/**
45
* Validates the register action
@@ -9,5 +10,6 @@ export const registerValidator = vine.compile(
910
name: vine.string(),
1011
email: vine.string().email(),
1112
password: vine.string().minLength(6),
13+
role: vine.enum(ROLES_LIST).optional(),
1214
})
1315
)

compose.yaml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,25 @@ services:
6767
- mysql
6868
command: sh -c "node ace migration:run"
6969

70+
minio:
71+
image: minio/minio:latest
72+
container_name: jscm-minio
73+
ports:
74+
- "9000:9000"
75+
- "9001:9001"
76+
env_file:
77+
- .env
78+
environment:
79+
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
80+
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
81+
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL}
82+
MINIO_DEFAULT_BUCKETS: ${MINIO_DEFAULT_BUCKETS}
83+
MINIO_ENDPOINT: ${MINIO_ENDPOINT}
84+
MINIO_PORT: ${MINIO_PORT}
85+
command: server /data --console-address ":9001"
86+
volumes:
87+
- minio_data:/data
88+
7089
volumes:
71-
mysql_data:
90+
mysql_data:
91+
minio_data:

0 commit comments

Comments
 (0)