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.
Files changed (180) hide show
  1. package/README.md +1 -0
  2. package/nuxt.config.ts +4 -1
  3. package/package.json +2 -16
  4. package/{server → src/server}/api/auth/login.post.ts +3 -2
  5. package/src/server/api/auth/logout.post.ts +11 -0
  6. package/src/server/api/auth/refresh.post.ts +12 -0
  7. package/{server → src/server}/api/setup.post.ts +3 -2
  8. package/{server → src/server}/consts/commons.const.ts +1 -0
  9. package/{server → src/server}/db/index.ts +14 -0
  10. package/src/server/utils/auth.util.ts +185 -0
  11. package/{services → src/services}/base.service.ts +1 -1
  12. package/server/api/auth/logout.post.ts +0 -7
  13. package/server/utils/auth.util.ts +0 -89
  14. /package/{app.vue → src/app.vue} +0 -0
  15. /package/{assets → src/assets}/css/main.css +0 -0
  16. /package/{assets → src/assets}/locales/en.json +0 -0
  17. /package/{components → src/components}/GitRepoManager.vue +0 -0
  18. /package/{components → src/components}/LanguagePicker.vue +0 -0
  19. /package/{components → src/components}/LinkedKeyPicker.vue +0 -0
  20. /package/{components → src/components}/PathPicker.vue +0 -0
  21. /package/{components → src/components}/PluralEditor.vue +0 -0
  22. /package/{components → src/components}/ScanModal.vue +0 -0
  23. /package/{components → src/components}/TranslationHistoryModal.vue +0 -0
  24. /package/{components → src/components}/TranslationRow.vue +0 -0
  25. /package/{components → src/components}/dashboard/WidgetConfigModal.vue +0 -0
  26. /package/{components → src/components}/dashboard/WidgetGrid.vue +0 -0
  27. /package/{components → src/components}/dashboard/WidgetPicker.vue +0 -0
  28. /package/{components → src/components}/dashboard/widgets/ActivityWidget.vue +0 -0
  29. /package/{components → src/components}/dashboard/widgets/LanguagesCoverageWidget.vue +0 -0
  30. /package/{components → src/components}/dashboard/widgets/ProjectsWidget.vue +0 -0
  31. /package/{components → src/components}/dashboard/widgets/ReviewWidget.vue +0 -0
  32. /package/{components → src/components}/dashboard/widgets/StatWidget.vue +0 -0
  33. /package/{composables → src/composables}/useAuth.ts +0 -0
  34. /package/{composables → src/composables}/useConfig.ts +0 -0
  35. /package/{composables → src/composables}/useDashboard.ts +0 -0
  36. /package/{composables → src/composables}/useFormats.ts +0 -0
  37. /package/{composables → src/composables}/useKeys.ts +0 -0
  38. /package/{composables → src/composables}/useLanguages.ts +0 -0
  39. /package/{composables → src/composables}/useProfile.ts +0 -0
  40. /package/{composables → src/composables}/useProject.ts +0 -0
  41. /package/{composables → src/composables}/useReview.ts +0 -0
  42. /package/{composables → src/composables}/useSettings.ts +0 -0
  43. /package/{composables → src/composables}/useStats.ts +0 -0
  44. /package/{composables → src/composables}/useT.ts +0 -0
  45. /package/{composables → src/composables}/useUsers.ts +0 -0
  46. /package/{composables → src/composables}/useWidgetData.ts +0 -0
  47. /package/{consts → src/consts}/commons.const.js +0 -0
  48. /package/{consts → src/consts}/commons.const.ts +0 -0
  49. /package/{consts → src/consts}/dashboard.const.ts +0 -0
  50. /package/{consts → src/consts}/languages.const.ts +0 -0
  51. /package/{enums → src/enums}/commons.enum.ts +0 -0
  52. /package/{interfaces → src/interfaces}/commons.interface.ts +0 -0
  53. /package/{interfaces → src/interfaces}/job.interface.ts +0 -0
  54. /package/{interfaces → src/interfaces}/key.interface.ts +0 -0
  55. /package/{interfaces → src/interfaces}/languages.interface.ts +0 -0
  56. /package/{interfaces → src/interfaces}/project.interface.ts +0 -0
  57. /package/{interfaces → src/interfaces}/scan.interface.ts +0 -0
  58. /package/{interfaces → src/interfaces}/settings.interface.ts +0 -0
  59. /package/{interfaces → src/interfaces}/stat.interface.ts +0 -0
  60. /package/{interfaces → src/interfaces}/translation.interface.ts +0 -0
  61. /package/{interfaces → src/interfaces}/user.interface.ts +0 -0
  62. /package/{layouts → src/layouts}/auth.vue +0 -0
  63. /package/{layouts → src/layouts}/default.vue +0 -0
  64. /package/{middleware → src/middleware}/auth.global.ts +0 -0
  65. /package/{pages → src/pages}/index.vue +0 -0
  66. /package/{pages → src/pages}/login.vue +0 -0
  67. /package/{pages → src/pages}/onboarding.vue +0 -0
  68. /package/{pages → src/pages}/projects/[id]/formats/datetime.vue +0 -0
  69. /package/{pages → src/pages}/projects/[id]/formats/modifiers.vue +0 -0
  70. /package/{pages → src/pages}/projects/[id]/formats/number.vue +0 -0
  71. /package/{pages → src/pages}/projects/[id]/index.vue +0 -0
  72. /package/{pages → src/pages}/projects/[id]/languages.vue +0 -0
  73. /package/{pages → src/pages}/projects/[id]/review.vue +0 -0
  74. /package/{pages → src/pages}/projects/[id]/settings.vue +0 -0
  75. /package/{pages → src/pages}/projects/[id]/translations/[keyId].vue +0 -0
  76. /package/{pages → src/pages}/projects/[id]/translations/index.vue +0 -0
  77. /package/{pages → src/pages}/projects/[id]/users.vue +0 -0
  78. /package/{pages → src/pages}/projects/index.vue +0 -0
  79. /package/{pages → src/pages}/users/[id]/profile.vue +0 -0
  80. /package/{pages → src/pages}/users/index.vue +0 -0
  81. /package/{plugins → src/plugins}/loading.client.ts +0 -0
  82. /package/{plugins → src/plugins}/ui-i18n.ts +0 -0
  83. /package/{server → src/server}/api/auth/me.get.ts +0 -0
  84. /package/{server → src/server}/api/auth/me.put.ts +0 -0
  85. /package/{server → src/server}/api/auth/password.put.ts +0 -0
  86. /package/{server → src/server}/api/auth/status.get.ts +0 -0
  87. /package/{server → src/server}/api/config.get.ts +0 -0
  88. /package/{server → src/server}/api/dashboard/layout.get.ts +0 -0
  89. /package/{server → src/server}/api/dashboard/layout.post.ts +0 -0
  90. /package/{server → src/server}/api/db-config.get.ts +0 -0
  91. /package/{server → src/server}/api/db-config.post.ts +0 -0
  92. /package/{server → src/server}/api/export.get.ts +0 -0
  93. /package/{server → src/server}/api/formats/datetime/[id].delete.ts +0 -0
  94. /package/{server → src/server}/api/formats/datetime/[id].put.ts +0 -0
  95. /package/{server → src/server}/api/formats/datetime.get.ts +0 -0
  96. /package/{server → src/server}/api/formats/datetime.post.ts +0 -0
  97. /package/{server → src/server}/api/formats/modifiers/[id].delete.ts +0 -0
  98. /package/{server → src/server}/api/formats/modifiers/[id].put.ts +0 -0
  99. /package/{server → src/server}/api/formats/modifiers.get.ts +0 -0
  100. /package/{server → src/server}/api/formats/modifiers.post.ts +0 -0
  101. /package/{server → src/server}/api/formats/number/[id].delete.ts +0 -0
  102. /package/{server → src/server}/api/formats/number/[id].put.ts +0 -0
  103. /package/{server → src/server}/api/formats/number.get.ts +0 -0
  104. /package/{server → src/server}/api/formats/number.post.ts +0 -0
  105. /package/{server → src/server}/api/formats/snippet.get.ts +0 -0
  106. /package/{server → src/server}/api/fs/browse.get.ts +0 -0
  107. /package/{server → src/server}/api/history/[translationId].get.ts +0 -0
  108. /package/{server → src/server}/api/keys/[id].delete.ts +0 -0
  109. /package/{server → src/server}/api/keys/[id].get.ts +0 -0
  110. /package/{server → src/server}/api/keys/[id].patch.ts +0 -0
  111. /package/{server → src/server}/api/keys/index.get.ts +0 -0
  112. /package/{server → src/server}/api/keys/index.post.ts +0 -0
  113. /package/{server → src/server}/api/languages/[code].delete.ts +0 -0
  114. /package/{server → src/server}/api/languages/[id].put.ts +0 -0
  115. /package/{server → src/server}/api/languages/index.get.ts +0 -0
  116. /package/{server → src/server}/api/languages/index.post.ts +0 -0
  117. /package/{server → src/server}/api/onboarding.post.ts +0 -0
  118. /package/{server → src/server}/api/profile.get.ts +0 -0
  119. /package/{server → src/server}/api/project-snapshot.get.ts +0 -0
  120. /package/{server → src/server}/api/project-snapshot.post.ts +0 -0
  121. /package/{server → src/server}/api/projects/[id].delete.ts +0 -0
  122. /package/{server → src/server}/api/projects/[id].put.ts +0 -0
  123. /package/{server → src/server}/api/projects/check-name.get.ts +0 -0
  124. /package/{server → src/server}/api/projects/detect.post.ts +0 -0
  125. /package/{server → src/server}/api/projects/index.get.ts +0 -0
  126. /package/{server → src/server}/api/projects/index.post.ts +0 -0
  127. /package/{server → src/server}/api/scan.post.ts +0 -0
  128. /package/{server → src/server}/api/settings/index.get.ts +0 -0
  129. /package/{server → src/server}/api/settings/index.post.ts +0 -0
  130. /package/{server → src/server}/api/stats/global.get.ts +0 -0
  131. /package/{server → src/server}/api/stats.get.ts +0 -0
  132. /package/{server → src/server}/api/sync.post.ts +0 -0
  133. /package/{server → src/server}/api/translate.post.ts +0 -0
  134. /package/{server → src/server}/api/translations/batch-translate.post.ts +0 -0
  135. /package/{server → src/server}/api/translations/bulk-status.post.ts +0 -0
  136. /package/{server → src/server}/api/translations/index.post.ts +0 -0
  137. /package/{server → src/server}/api/translations/job/[id].get.ts +0 -0
  138. /package/{server → src/server}/api/translations/status.post.ts +0 -0
  139. /package/{server → src/server}/api/translations/translate-all.post.ts +0 -0
  140. /package/{server → src/server}/api/ui-locale.get.ts +0 -0
  141. /package/{server → src/server}/api/users/[id]/profile.get.ts +0 -0
  142. /package/{server → src/server}/api/users/[id]/roles.put.ts +0 -0
  143. /package/{server → src/server}/api/users/[id].delete.ts +0 -0
  144. /package/{server → src/server}/api/users/[id].put.ts +0 -0
  145. /package/{server → src/server}/api/users/index.get.ts +0 -0
  146. /package/{server → src/server}/api/users/index.post.ts +0 -0
  147. /package/{server → src/server}/consts/auto-translate.const.ts +0 -0
  148. /package/{server → src/server}/consts/db.const.ts +0 -0
  149. /package/{server → src/server}/consts/scanner.const.ts +0 -0
  150. /package/{server → src/server}/consts/translation-job.const.ts +0 -0
  151. /package/{server → src/server}/enums/auth.enum.ts +0 -0
  152. /package/{server → src/server}/enums/translation.enum.ts +0 -0
  153. /package/{server → src/server}/interfaces/profile.interface.ts +0 -0
  154. /package/{server → src/server}/interfaces/project-config.interface.ts +0 -0
  155. /package/{server → src/server}/interfaces/scanner.interface.ts +0 -0
  156. /package/{server → src/server}/interfaces/translation-job.interface.ts +0 -0
  157. /package/{server → src/server}/middleware/auth.ts +0 -0
  158. /package/{server → src/server}/plugins/db.ts +0 -0
  159. /package/{server → src/server}/routes/locale/[lang].get.ts +0 -0
  160. /package/{server → src/server}/types/auth.type.ts +0 -0
  161. /package/{server → src/server}/utils/auto-translate.util.ts +0 -0
  162. /package/{server → src/server}/utils/lang-api.util.ts +0 -0
  163. /package/{server → src/server}/utils/mailer.util.ts +0 -0
  164. /package/{server → src/server}/utils/project-config.util.ts +0 -0
  165. /package/{server → src/server}/utils/scanner.uti.ts +0 -0
  166. /package/{server → src/server}/utils/translation-job.util.ts +0 -0
  167. /package/{services → src/services}/auth.service.ts +0 -0
  168. /package/{services → src/services}/job.service.ts +0 -0
  169. /package/{services → src/services}/key.service.ts +0 -0
  170. /package/{services → src/services}/language.service.ts +0 -0
  171. /package/{services → src/services}/profile.service.ts +0 -0
  172. /package/{services → src/services}/project.service.ts +0 -0
  173. /package/{services → src/services}/scan.service.ts +0 -0
  174. /package/{services → src/services}/settings.service.ts +0 -0
  175. /package/{services → src/services}/stats.service.ts +0 -0
  176. /package/{services → src/services}/translation.service.ts +0 -0
  177. /package/{services → src/services}/user.service.ts +0 -0
  178. /package/{types → src/types}/commons.type.ts +0 -0
  179. /package/{types → src/types}/dashboard.type.ts +0 -0
  180. /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.11.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
- "assets/",
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
- // Set session
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
 
@@ -2,6 +2,7 @@ export const PUBLIC_ROUTES = [
2
2
  '/api/auth/login',
3
3
  '/api/auth/logout',
4
4
  '/api/auth/me',
5
+ '/api/auth/refresh',
5
6
  '/api/auth/status',
6
7
  '/api/setup',
7
8
  '/api/ui-locale',
@@ -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/me')
42
+ _refreshing = $fetch('/api/auth/refresh', { method: 'POST' })
43
43
  .then(() => true)
44
44
  .catch(() => false)
45
45
  .finally(() => { _refreshing = null })
@@ -1,7 +0,0 @@
1
- import { getSession } from '../../utils/auth.util'
2
-
3
- export default defineEventHandler(async (event) => {
4
- const session = await getSession(event)
5
- await session.clear()
6
- return { success: true }
7
- })
@@ -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
- }
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