i18n-dashboard 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/nuxt.config.ts +4 -1
- package/package.json +2 -16
- package/{server → src/server}/api/auth/login.post.ts +3 -2
- package/src/server/api/auth/logout.post.ts +11 -0
- package/src/server/api/auth/refresh.post.ts +12 -0
- package/{server → src/server}/api/setup.post.ts +3 -2
- package/{server → src/server}/consts/commons.const.ts +1 -0
- package/{server → src/server}/db/index.ts +14 -0
- package/src/server/utils/auth.util.ts +185 -0
- package/{services → src/services}/base.service.ts +1 -1
- package/server/api/auth/logout.post.ts +0 -7
- package/server/utils/auth.util.ts +0 -89
- /package/{app.vue → src/app.vue} +0 -0
- /package/{assets → src/assets}/css/main.css +0 -0
- /package/{assets → src/assets}/locales/en.json +0 -0
- /package/{components → src/components}/GitRepoManager.vue +0 -0
- /package/{components → src/components}/LanguagePicker.vue +0 -0
- /package/{components → src/components}/LinkedKeyPicker.vue +0 -0
- /package/{components → src/components}/PathPicker.vue +0 -0
- /package/{components → src/components}/PluralEditor.vue +0 -0
- /package/{components → src/components}/ScanModal.vue +0 -0
- /package/{components → src/components}/TranslationHistoryModal.vue +0 -0
- /package/{components → src/components}/TranslationRow.vue +0 -0
- /package/{components → src/components}/dashboard/WidgetConfigModal.vue +0 -0
- /package/{components → src/components}/dashboard/WidgetGrid.vue +0 -0
- /package/{components → src/components}/dashboard/WidgetPicker.vue +0 -0
- /package/{components → src/components}/dashboard/widgets/ActivityWidget.vue +0 -0
- /package/{components → src/components}/dashboard/widgets/LanguagesCoverageWidget.vue +0 -0
- /package/{components → src/components}/dashboard/widgets/ProjectsWidget.vue +0 -0
- /package/{components → src/components}/dashboard/widgets/ReviewWidget.vue +0 -0
- /package/{components → src/components}/dashboard/widgets/StatWidget.vue +0 -0
- /package/{composables → src/composables}/useAuth.ts +0 -0
- /package/{composables → src/composables}/useConfig.ts +0 -0
- /package/{composables → src/composables}/useDashboard.ts +0 -0
- /package/{composables → src/composables}/useFormats.ts +0 -0
- /package/{composables → src/composables}/useKeys.ts +0 -0
- /package/{composables → src/composables}/useLanguages.ts +0 -0
- /package/{composables → src/composables}/useProfile.ts +0 -0
- /package/{composables → src/composables}/useProject.ts +0 -0
- /package/{composables → src/composables}/useReview.ts +0 -0
- /package/{composables → src/composables}/useSettings.ts +0 -0
- /package/{composables → src/composables}/useStats.ts +0 -0
- /package/{composables → src/composables}/useT.ts +0 -0
- /package/{composables → src/composables}/useUsers.ts +0 -0
- /package/{composables → src/composables}/useWidgetData.ts +0 -0
- /package/{consts → src/consts}/commons.const.js +0 -0
- /package/{consts → src/consts}/commons.const.ts +0 -0
- /package/{consts → src/consts}/dashboard.const.ts +0 -0
- /package/{consts → src/consts}/languages.const.ts +0 -0
- /package/{enums → src/enums}/commons.enum.ts +0 -0
- /package/{interfaces → src/interfaces}/commons.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/job.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/key.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/languages.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/project.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/scan.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/settings.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/stat.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/translation.interface.ts +0 -0
- /package/{interfaces → src/interfaces}/user.interface.ts +0 -0
- /package/{layouts → src/layouts}/auth.vue +0 -0
- /package/{layouts → src/layouts}/default.vue +0 -0
- /package/{middleware → src/middleware}/auth.global.ts +0 -0
- /package/{pages → src/pages}/index.vue +0 -0
- /package/{pages → src/pages}/login.vue +0 -0
- /package/{pages → src/pages}/onboarding.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/formats/datetime.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/formats/modifiers.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/formats/number.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/index.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/languages.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/review.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/settings.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/translations/[keyId].vue +0 -0
- /package/{pages → src/pages}/projects/[id]/translations/index.vue +0 -0
- /package/{pages → src/pages}/projects/[id]/users.vue +0 -0
- /package/{pages → src/pages}/projects/index.vue +0 -0
- /package/{pages → src/pages}/users/[id]/profile.vue +0 -0
- /package/{pages → src/pages}/users/index.vue +0 -0
- /package/{plugins → src/plugins}/loading.client.ts +0 -0
- /package/{plugins → src/plugins}/ui-i18n.ts +0 -0
- /package/{server → src/server}/api/auth/me.get.ts +0 -0
- /package/{server → src/server}/api/auth/me.put.ts +0 -0
- /package/{server → src/server}/api/auth/password.put.ts +0 -0
- /package/{server → src/server}/api/auth/status.get.ts +0 -0
- /package/{server → src/server}/api/config.get.ts +0 -0
- /package/{server → src/server}/api/dashboard/layout.get.ts +0 -0
- /package/{server → src/server}/api/dashboard/layout.post.ts +0 -0
- /package/{server → src/server}/api/db-config.get.ts +0 -0
- /package/{server → src/server}/api/db-config.post.ts +0 -0
- /package/{server → src/server}/api/export.get.ts +0 -0
- /package/{server → src/server}/api/formats/datetime/[id].delete.ts +0 -0
- /package/{server → src/server}/api/formats/datetime/[id].put.ts +0 -0
- /package/{server → src/server}/api/formats/datetime.get.ts +0 -0
- /package/{server → src/server}/api/formats/datetime.post.ts +0 -0
- /package/{server → src/server}/api/formats/modifiers/[id].delete.ts +0 -0
- /package/{server → src/server}/api/formats/modifiers/[id].put.ts +0 -0
- /package/{server → src/server}/api/formats/modifiers.get.ts +0 -0
- /package/{server → src/server}/api/formats/modifiers.post.ts +0 -0
- /package/{server → src/server}/api/formats/number/[id].delete.ts +0 -0
- /package/{server → src/server}/api/formats/number/[id].put.ts +0 -0
- /package/{server → src/server}/api/formats/number.get.ts +0 -0
- /package/{server → src/server}/api/formats/number.post.ts +0 -0
- /package/{server → src/server}/api/formats/snippet.get.ts +0 -0
- /package/{server → src/server}/api/fs/browse.get.ts +0 -0
- /package/{server → src/server}/api/history/[translationId].get.ts +0 -0
- /package/{server → src/server}/api/keys/[id].delete.ts +0 -0
- /package/{server → src/server}/api/keys/[id].get.ts +0 -0
- /package/{server → src/server}/api/keys/[id].patch.ts +0 -0
- /package/{server → src/server}/api/keys/index.get.ts +0 -0
- /package/{server → src/server}/api/keys/index.post.ts +0 -0
- /package/{server → src/server}/api/languages/[code].delete.ts +0 -0
- /package/{server → src/server}/api/languages/[id].put.ts +0 -0
- /package/{server → src/server}/api/languages/index.get.ts +0 -0
- /package/{server → src/server}/api/languages/index.post.ts +0 -0
- /package/{server → src/server}/api/onboarding.post.ts +0 -0
- /package/{server → src/server}/api/profile.get.ts +0 -0
- /package/{server → src/server}/api/project-snapshot.get.ts +0 -0
- /package/{server → src/server}/api/project-snapshot.post.ts +0 -0
- /package/{server → src/server}/api/projects/[id].delete.ts +0 -0
- /package/{server → src/server}/api/projects/[id].put.ts +0 -0
- /package/{server → src/server}/api/projects/check-name.get.ts +0 -0
- /package/{server → src/server}/api/projects/detect.post.ts +0 -0
- /package/{server → src/server}/api/projects/index.get.ts +0 -0
- /package/{server → src/server}/api/projects/index.post.ts +0 -0
- /package/{server → src/server}/api/scan.post.ts +0 -0
- /package/{server → src/server}/api/settings/index.get.ts +0 -0
- /package/{server → src/server}/api/settings/index.post.ts +0 -0
- /package/{server → src/server}/api/stats/global.get.ts +0 -0
- /package/{server → src/server}/api/stats.get.ts +0 -0
- /package/{server → src/server}/api/sync.post.ts +0 -0
- /package/{server → src/server}/api/translate.post.ts +0 -0
- /package/{server → src/server}/api/translations/batch-translate.post.ts +0 -0
- /package/{server → src/server}/api/translations/bulk-status.post.ts +0 -0
- /package/{server → src/server}/api/translations/index.post.ts +0 -0
- /package/{server → src/server}/api/translations/job/[id].get.ts +0 -0
- /package/{server → src/server}/api/translations/status.post.ts +0 -0
- /package/{server → src/server}/api/translations/translate-all.post.ts +0 -0
- /package/{server → src/server}/api/ui-locale.get.ts +0 -0
- /package/{server → src/server}/api/users/[id]/profile.get.ts +0 -0
- /package/{server → src/server}/api/users/[id]/roles.put.ts +0 -0
- /package/{server → src/server}/api/users/[id].delete.ts +0 -0
- /package/{server → src/server}/api/users/[id].put.ts +0 -0
- /package/{server → src/server}/api/users/index.get.ts +0 -0
- /package/{server → src/server}/api/users/index.post.ts +0 -0
- /package/{server → src/server}/consts/auto-translate.const.ts +0 -0
- /package/{server → src/server}/consts/db.const.ts +0 -0
- /package/{server → src/server}/consts/scanner.const.ts +0 -0
- /package/{server → src/server}/consts/translation-job.const.ts +0 -0
- /package/{server → src/server}/enums/auth.enum.ts +0 -0
- /package/{server → src/server}/enums/translation.enum.ts +0 -0
- /package/{server → src/server}/interfaces/profile.interface.ts +0 -0
- /package/{server → src/server}/interfaces/project-config.interface.ts +0 -0
- /package/{server → src/server}/interfaces/scanner.interface.ts +0 -0
- /package/{server → src/server}/interfaces/translation-job.interface.ts +0 -0
- /package/{server → src/server}/middleware/auth.ts +0 -0
- /package/{server → src/server}/plugins/db.ts +0 -0
- /package/{server → src/server}/routes/locale/[lang].get.ts +0 -0
- /package/{server → src/server}/types/auth.type.ts +0 -0
- /package/{server → src/server}/utils/auto-translate.util.ts +0 -0
- /package/{server → src/server}/utils/lang-api.util.ts +0 -0
- /package/{server → src/server}/utils/mailer.util.ts +0 -0
- /package/{server → src/server}/utils/project-config.util.ts +0 -0
- /package/{server → src/server}/utils/scanner.uti.ts +0 -0
- /package/{server → src/server}/utils/translation-job.util.ts +0 -0
- /package/{services → src/services}/auth.service.ts +0 -0
- /package/{services → src/services}/job.service.ts +0 -0
- /package/{services → src/services}/key.service.ts +0 -0
- /package/{services → src/services}/language.service.ts +0 -0
- /package/{services → src/services}/profile.service.ts +0 -0
- /package/{services → src/services}/project.service.ts +0 -0
- /package/{services → src/services}/scan.service.ts +0 -0
- /package/{services → src/services}/settings.service.ts +0 -0
- /package/{services → src/services}/stats.service.ts +0 -0
- /package/{services → src/services}/translation.service.ts +0 -0
- /package/{services → src/services}/user.service.ts +0 -0
- /package/{types → src/types}/commons.type.ts +0 -0
- /package/{types → src/types}/dashboard.type.ts +0 -0
- /package/{utils → src/utils}/config.util.ts +0 -0
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
- **GitHub Actions CI** — E2E tests run automatically on every push to `develop` and `main`; Cypress screenshots uploaded as artifacts on failure
|
|
71
71
|
- **Vitest unit test suite** — 344 unit tests covering all composables, services, and server utilities; runs in under 2 minutes with zero infrastructure required
|
|
72
72
|
- **Dual CI pipelines** — unit tests (`unit.yml`) and E2E tests (`e2e.yml`) run in parallel on every push; any regression blocks the pipeline
|
|
73
|
+
- **`src/` layout** — all source files live under `src/` (components, composables, pages, server, services, etc.) for a clean project root
|
|
73
74
|
---
|
|
74
75
|
|
|
75
76
|
## Requirements
|
package/nuxt.config.ts
CHANGED
|
@@ -3,6 +3,9 @@ export default defineNuxtConfig({
|
|
|
3
3
|
compatibilityDate: '2025-01-01',
|
|
4
4
|
devtools: { enabled: false },
|
|
5
5
|
|
|
6
|
+
srcDir: 'src',
|
|
7
|
+
serverDir: 'src/server',
|
|
8
|
+
|
|
6
9
|
modules: ['@nuxt/ui'],
|
|
7
10
|
|
|
8
11
|
css: ['~/assets/css/main.css'],
|
|
@@ -51,7 +54,7 @@ export default defineNuxtConfig({
|
|
|
51
54
|
serverAssets: [
|
|
52
55
|
{
|
|
53
56
|
baseName: 'locales',
|
|
54
|
-
dir: './assets/locales',
|
|
57
|
+
dir: './src/assets/locales',
|
|
55
58
|
},
|
|
56
59
|
],
|
|
57
60
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18n-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "A web dashboard to manage vue-i18n translation keys with database persistence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,22 +50,8 @@
|
|
|
50
50
|
"node": ">=18.0.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
|
-
"
|
|
53
|
+
"src/",
|
|
54
54
|
"bin/",
|
|
55
|
-
"components/",
|
|
56
|
-
"composables/",
|
|
57
|
-
"consts/",
|
|
58
|
-
"enums/",
|
|
59
|
-
"interfaces/",
|
|
60
|
-
"layouts/",
|
|
61
|
-
"middleware/",
|
|
62
|
-
"pages/",
|
|
63
|
-
"plugins/",
|
|
64
|
-
"server/",
|
|
65
|
-
"services/",
|
|
66
|
-
"types/",
|
|
67
|
-
"utils/",
|
|
68
|
-
"app.vue",
|
|
69
55
|
"nuxt.config.ts",
|
|
70
56
|
"tsconfig.json",
|
|
71
57
|
"i18n-dashboard.config.example.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import bcrypt from 'bcryptjs'
|
|
2
2
|
import { getDb } from '../../db/index'
|
|
3
|
-
import { getSession } from '../../utils/auth.util'
|
|
3
|
+
import { getSession, createRefreshToken } from '../../utils/auth.util'
|
|
4
4
|
|
|
5
5
|
export default defineEventHandler(async (event) => {
|
|
6
6
|
const { email, password } = await readBody(event)
|
|
@@ -19,9 +19,10 @@ export default defineEventHandler(async (event) => {
|
|
|
19
19
|
// Update last login
|
|
20
20
|
await db('users').where({ id: user.id }).update({ last_login_at: db.fn.now() })
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// Issue session (15 min) + refresh token (7 days, HttpOnly cookie)
|
|
23
23
|
const session = await getSession(event)
|
|
24
24
|
await session.update({ userId: user.id })
|
|
25
|
+
await createRefreshToken(event, user.id)
|
|
25
26
|
|
|
26
27
|
const { password_hash, ...safeUser } = user
|
|
27
28
|
return safeUser
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getSession, clearRefreshToken } from '../../utils/auth.util'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const session = await getSession(event)
|
|
5
|
+
const userId = (session.data as any).userId as number | undefined
|
|
6
|
+
|
|
7
|
+
await clearRefreshToken(event, userId)
|
|
8
|
+
await session.clear()
|
|
9
|
+
|
|
10
|
+
return { success: true }
|
|
11
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { verifyAndRotateRefreshToken, getUserProfile } from '../../utils/auth.util'
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const userId = await verifyAndRotateRefreshToken(event)
|
|
5
|
+
|
|
6
|
+
const user = await getUserProfile(userId)
|
|
7
|
+
if (!user || !user.is_active) {
|
|
8
|
+
throw createError({ statusCode: 401, message: 'Compte désactivé' })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return user
|
|
12
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import bcrypt from 'bcryptjs'
|
|
2
2
|
import { getDb } from '../db/index'
|
|
3
|
-
import { getSession } from '../utils/auth.util'
|
|
3
|
+
import { getSession, createRefreshToken } from '../utils/auth.util'
|
|
4
4
|
|
|
5
5
|
export default defineEventHandler(async (event) => {
|
|
6
6
|
const db = getDb()
|
|
@@ -29,9 +29,10 @@ export default defineEventHandler(async (event) => {
|
|
|
29
29
|
is_active: true,
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
// Auto-login
|
|
32
|
+
// Auto-login: session (15 min) + refresh token (7 days)
|
|
33
33
|
const session = await getSession(event)
|
|
34
34
|
await session.update({ userId: id })
|
|
35
|
+
await createRefreshToken(event, id)
|
|
35
36
|
|
|
36
37
|
console.log(`[i18n-dashboard] Super admin créé : ${email}`)
|
|
37
38
|
|
|
@@ -1017,4 +1017,18 @@ export async function initDb(): Promise<void> {
|
|
|
1017
1017
|
await addColumnIfMissing(db, 'projects', 'git_repos', (t) =>
|
|
1018
1018
|
t.text('git_repos').nullable(),
|
|
1019
1019
|
)
|
|
1020
|
+
|
|
1021
|
+
// ── refresh_tokens ────────────────────────────────────────────────────────
|
|
1022
|
+
const hasRefreshTokens = await db.schema.hasTable('refresh_tokens')
|
|
1023
|
+
if (!hasRefreshTokens) {
|
|
1024
|
+
await db.schema.createTable('refresh_tokens', (table) => {
|
|
1025
|
+
table.increments('id').primary()
|
|
1026
|
+
table.integer('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE')
|
|
1027
|
+
table.string('token_hash', 64).notNullable() // SHA-256 hex of the raw token
|
|
1028
|
+
table.timestamp('expires_at').notNullable()
|
|
1029
|
+
table.timestamp('created_at').defaultTo(db.fn.now())
|
|
1030
|
+
table.index(['user_id'])
|
|
1031
|
+
table.index(['token_hash'])
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
1020
1034
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { randomBytes, createHash } from 'node:crypto'
|
|
2
|
+
import type { H3Event } from 'h3'
|
|
3
|
+
import { useSession, getCookie, setCookie, deleteCookie } from 'h3'
|
|
4
|
+
import { useRuntimeConfig } from '#imports'
|
|
5
|
+
|
|
6
|
+
import { getDb } from '../db/index'
|
|
7
|
+
import { ROLES } from '../enums/auth.enum'
|
|
8
|
+
import type { Role } from '../types/auth.type'
|
|
9
|
+
|
|
10
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const REFRESH_COOKIE = 'i18n-refresh-token'
|
|
13
|
+
const REFRESH_TTL_SECONDS = 60 * 60 * 24 * 7 // 7 days
|
|
14
|
+
|
|
15
|
+
// ── Session (access token) ─────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export function sessionConfig() {
|
|
18
|
+
const config = useRuntimeConfig()
|
|
19
|
+
return {
|
|
20
|
+
password: config.sessionSecret as string,
|
|
21
|
+
name: 'i18n-dashboard-session',
|
|
22
|
+
maxAge: 60 * 15, // 15 minutes
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Get the current session data */
|
|
27
|
+
export async function getSession(event: H3Event) {
|
|
28
|
+
return useSession(event, sessionConfig())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Require a logged-in user. Returns user row or throws 401. */
|
|
32
|
+
export async function requireAuth(event: H3Event) {
|
|
33
|
+
const session = await getSession(event)
|
|
34
|
+
const userId = (session.data as any).userId
|
|
35
|
+
if (!userId) throw createError({ statusCode: 401, message: 'Non authentifié' })
|
|
36
|
+
|
|
37
|
+
const db = getDb()
|
|
38
|
+
const user = await db('users').where({ id: userId, is_active: true }).first()
|
|
39
|
+
if (!user) throw createError({ statusCode: 401, message: 'Session invalide' })
|
|
40
|
+
|
|
41
|
+
return user
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Refresh token helpers ──────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function hashToken(token: string): string {
|
|
47
|
+
return createHash('sha256').update(token).digest('hex')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Issue a new refresh token for a user.
|
|
52
|
+
* Stores the SHA-256 hash in DB and sets an HttpOnly cookie.
|
|
53
|
+
* Also removes expired tokens for that user (housekeeping).
|
|
54
|
+
*/
|
|
55
|
+
export async function createRefreshToken(event: H3Event, userId: number): Promise<void> {
|
|
56
|
+
const db = getDb()
|
|
57
|
+
const token = randomBytes(32).toString('hex')
|
|
58
|
+
const hash = hashToken(token)
|
|
59
|
+
const expiresAt = new Date(Date.now() + REFRESH_TTL_SECONDS * 1000)
|
|
60
|
+
|
|
61
|
+
// Housekeeping: remove expired tokens for this user
|
|
62
|
+
await db('refresh_tokens')
|
|
63
|
+
.where({ user_id: userId })
|
|
64
|
+
.where('expires_at', '<', new Date())
|
|
65
|
+
.delete()
|
|
66
|
+
|
|
67
|
+
await db('refresh_tokens').insert({
|
|
68
|
+
user_id: userId,
|
|
69
|
+
token_hash: hash,
|
|
70
|
+
expires_at: expiresAt,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
setCookie(event, REFRESH_COOKIE, token, {
|
|
74
|
+
httpOnly: true,
|
|
75
|
+
secure: process.env.NODE_ENV === 'production',
|
|
76
|
+
sameSite: 'lax',
|
|
77
|
+
maxAge: REFRESH_TTL_SECONDS,
|
|
78
|
+
path: '/',
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Verify the refresh token cookie, rotate it (delete old, issue new),
|
|
84
|
+
* and return the associated userId.
|
|
85
|
+
* Throws 401 if invalid or expired.
|
|
86
|
+
*/
|
|
87
|
+
export async function verifyAndRotateRefreshToken(event: H3Event): Promise<number> {
|
|
88
|
+
const token = getCookie(event, REFRESH_COOKIE)
|
|
89
|
+
if (!token) throw createError({ statusCode: 401, message: 'Refresh token manquant' })
|
|
90
|
+
|
|
91
|
+
const hash = hashToken(token)
|
|
92
|
+
const db = getDb()
|
|
93
|
+
const row = await db('refresh_tokens')
|
|
94
|
+
.where({ token_hash: hash })
|
|
95
|
+
.where('expires_at', '>', new Date())
|
|
96
|
+
.first()
|
|
97
|
+
|
|
98
|
+
if (!row) throw createError({ statusCode: 401, message: 'Refresh token invalide ou expiré' })
|
|
99
|
+
|
|
100
|
+
// Rotation: delete consumed token immediately
|
|
101
|
+
await db('refresh_tokens').where({ id: row.id }).delete()
|
|
102
|
+
|
|
103
|
+
const userId = row.user_id
|
|
104
|
+
|
|
105
|
+
// Issue new session + new refresh token
|
|
106
|
+
const session = await getSession(event)
|
|
107
|
+
await session.update({ userId })
|
|
108
|
+
await createRefreshToken(event, userId)
|
|
109
|
+
|
|
110
|
+
return userId
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Delete all refresh tokens for a user and clear the cookie.
|
|
115
|
+
*/
|
|
116
|
+
export async function clearRefreshToken(event: H3Event, userId?: number): Promise<void> {
|
|
117
|
+
const db = getDb()
|
|
118
|
+
if (userId) {
|
|
119
|
+
await db('refresh_tokens').where({ user_id: userId }).delete()
|
|
120
|
+
} else {
|
|
121
|
+
// Best-effort: delete by hash from cookie
|
|
122
|
+
const token = getCookie(event, REFRESH_COOKIE)
|
|
123
|
+
if (token) {
|
|
124
|
+
await db('refresh_tokens').where({ token_hash: hashToken(token) }).delete()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
deleteCookie(event, REFRESH_COOKIE, { path: '/' })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Role helpers ───────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/** Get effective role for a user on a specific project. Returns null if no access. */
|
|
133
|
+
export async function getUserRole(userId: number, projectId: number): Promise<Role | null> {
|
|
134
|
+
const db = getDb()
|
|
135
|
+
|
|
136
|
+
// Specific project role takes priority
|
|
137
|
+
const specific = await db('user_project_roles')
|
|
138
|
+
.where({ user_id: userId, project_id: projectId })
|
|
139
|
+
.first()
|
|
140
|
+
if (specific) return specific.role as Role
|
|
141
|
+
|
|
142
|
+
// Global role (project_id IS NULL) — access to all projects
|
|
143
|
+
const global_ = await db('user_project_roles')
|
|
144
|
+
.where({ user_id: userId })
|
|
145
|
+
.whereNull('project_id')
|
|
146
|
+
.first()
|
|
147
|
+
if (global_) return global_.role as Role
|
|
148
|
+
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Check if user can edit translations (translator+) */
|
|
153
|
+
export function canEdit(role: Role | null, isSuperAdmin: boolean) {
|
|
154
|
+
return isSuperAdmin || role !== null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Check if user can approve translations (moderator+) */
|
|
158
|
+
export function canApprove(role: Role | null, isSuperAdmin: boolean) {
|
|
159
|
+
return isSuperAdmin || role === ROLES.MODERATOR || role === ROLES.ADMIN
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Check if user can manage project settings, scan, sync (admin+) */
|
|
163
|
+
export function canManageProject(role: Role | null, isSuperAdmin: boolean) {
|
|
164
|
+
return isSuperAdmin || role === ROLES.ADMIN
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Check if user can manage users (admin+ of that project, or super_admin) */
|
|
168
|
+
export function canManageUsers(role: Role | null, isSuperAdmin: boolean) {
|
|
169
|
+
return isSuperAdmin || role === ROLES.ADMIN
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Full user profile with roles */
|
|
173
|
+
export async function getUserProfile(userId: number) {
|
|
174
|
+
const db = getDb()
|
|
175
|
+
const user = await db('users').where({ id: userId }).first()
|
|
176
|
+
if (!user) return null
|
|
177
|
+
|
|
178
|
+
const roles = await db('user_project_roles as r')
|
|
179
|
+
.leftJoin('projects as p', 'r.project_id', 'p.id')
|
|
180
|
+
.where('r.user_id', userId)
|
|
181
|
+
.select('r.role', 'r.project_id', 'p.name as project_name')
|
|
182
|
+
|
|
183
|
+
const { password_hash, ...safeUser } = user
|
|
184
|
+
return { ...safeUser, roles }
|
|
185
|
+
}
|
|
@@ -39,7 +39,7 @@ export abstract class BaseService {
|
|
|
39
39
|
|
|
40
40
|
private async _tryRefresh(): Promise<boolean> {
|
|
41
41
|
if (_refreshing) return _refreshing
|
|
42
|
-
_refreshing = $fetch('/api/auth/
|
|
42
|
+
_refreshing = $fetch('/api/auth/refresh', { method: 'POST' })
|
|
43
43
|
.then(() => true)
|
|
44
44
|
.catch(() => false)
|
|
45
45
|
.finally(() => { _refreshing = null })
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { H3Event } from 'h3'
|
|
2
|
-
import { useSession } from 'h3'
|
|
3
|
-
import { useRuntimeConfig } from '#imports'
|
|
4
|
-
|
|
5
|
-
import { getDb } from '../db/index'
|
|
6
|
-
import { ROLES } from '../enums/auth.enum'
|
|
7
|
-
import type { Role } from '../types/auth.type'
|
|
8
|
-
|
|
9
|
-
export function sessionConfig() {
|
|
10
|
-
const config = useRuntimeConfig()
|
|
11
|
-
return {
|
|
12
|
-
password: config.sessionSecret as string,
|
|
13
|
-
name: 'i18n-dashboard-session',
|
|
14
|
-
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Get the current session data */
|
|
19
|
-
export async function getSession(event: H3Event) {
|
|
20
|
-
return useSession(event, sessionConfig())
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Require a logged-in user. Returns user row or throws 401. */
|
|
24
|
-
export async function requireAuth(event: H3Event) {
|
|
25
|
-
const session = await getSession(event)
|
|
26
|
-
const userId = (session.data as any).userId
|
|
27
|
-
if (!userId) throw createError({ statusCode: 401, message: 'Non authentifié' })
|
|
28
|
-
|
|
29
|
-
const db = getDb()
|
|
30
|
-
const user = await db('users').where({ id: userId, is_active: true }).first()
|
|
31
|
-
if (!user) throw createError({ statusCode: 401, message: 'Session invalide' })
|
|
32
|
-
|
|
33
|
-
return user
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Get effective role for a user on a specific project. Returns null if no access. */
|
|
37
|
-
export async function getUserRole(userId: number, projectId: number): Promise<Role | null> {
|
|
38
|
-
const db = getDb()
|
|
39
|
-
|
|
40
|
-
// Specific project role takes priority
|
|
41
|
-
const specific = await db('user_project_roles')
|
|
42
|
-
.where({ user_id: userId, project_id: projectId })
|
|
43
|
-
.first()
|
|
44
|
-
if (specific) return specific.role as Role
|
|
45
|
-
|
|
46
|
-
// Global role (project_id IS NULL) — access to all projects
|
|
47
|
-
const global_ = await db('user_project_roles')
|
|
48
|
-
.where({ user_id: userId })
|
|
49
|
-
.whereNull('project_id')
|
|
50
|
-
.first()
|
|
51
|
-
if (global_) return global_.role as Role
|
|
52
|
-
|
|
53
|
-
return null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Check if user can edit translations (translator+) */
|
|
57
|
-
export function canEdit(role: Role | null, isSuperAdmin: boolean) {
|
|
58
|
-
return isSuperAdmin || role !== null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Check if user can approve translations (moderator+) */
|
|
62
|
-
export function canApprove(role: Role | null, isSuperAdmin: boolean) {
|
|
63
|
-
return isSuperAdmin || role === ROLES.MODERATOR || role === ROLES.ADMIN
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Check if user can manage project settings, scan, sync (admin+) */
|
|
67
|
-
export function canManageProject(role: Role | null, isSuperAdmin: boolean) {
|
|
68
|
-
return isSuperAdmin || role === ROLES.ADMIN
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Check if user can manage users (admin+ of that project, or super_admin) */
|
|
72
|
-
export function canManageUsers(role: Role | null, isSuperAdmin: boolean) {
|
|
73
|
-
return isSuperAdmin || role === ROLES.ADMIN
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Full user profile with roles */
|
|
77
|
-
export async function getUserProfile(userId: number) {
|
|
78
|
-
const db = getDb()
|
|
79
|
-
const user = await db('users').where({ id: userId }).first()
|
|
80
|
-
if (!user) return null
|
|
81
|
-
|
|
82
|
-
const roles = await db('user_project_roles as r')
|
|
83
|
-
.leftJoin('projects as p', 'r.project_id', 'p.id')
|
|
84
|
-
.where('r.user_id', userId)
|
|
85
|
-
.select('r.role', 'r.project_id', 'p.name as project_name')
|
|
86
|
-
|
|
87
|
-
const { password_hash, ...safeUser } = user
|
|
88
|
-
return { ...safeUser, roles }
|
|
89
|
-
}
|
/package/{app.vue → src/app.vue}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|