i18n-dashboard 0.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18n-dashboard",
3
- "version": "0.12.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": {
@@ -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
@@ -1,7 +1,11 @@
1
- import { getSession } from '../../utils/auth.util'
1
+ import { getSession, clearRefreshToken } from '../../utils/auth.util'
2
2
 
3
3
  export default defineEventHandler(async (event) => {
4
4
  const session = await getSession(event)
5
+ const userId = (session.data as any).userId as number | undefined
6
+
7
+ await clearRefreshToken(event, userId)
5
8
  await session.clear()
9
+
6
10
  return { success: true }
7
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
  }
@@ -1,17 +1,25 @@
1
+ import { randomBytes, createHash } from 'node:crypto'
1
2
  import type { H3Event } from 'h3'
2
- import { useSession } from 'h3'
3
+ import { useSession, getCookie, setCookie, deleteCookie } from 'h3'
3
4
  import { useRuntimeConfig } from '#imports'
4
5
 
5
6
  import { getDb } from '../db/index'
6
7
  import { ROLES } from '../enums/auth.enum'
7
8
  import type { Role } from '../types/auth.type'
8
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
+
9
17
  export function sessionConfig() {
10
18
  const config = useRuntimeConfig()
11
19
  return {
12
20
  password: config.sessionSecret as string,
13
21
  name: 'i18n-dashboard-session',
14
- maxAge: 60 * 60 * 24 * 7, // 7 days
22
+ maxAge: 60 * 15, // 15 minutes
15
23
  }
16
24
  }
17
25
 
@@ -33,6 +41,94 @@ export async function requireAuth(event: H3Event) {
33
41
  return user
34
42
  }
35
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
+
36
132
  /** Get effective role for a user on a specific project. Returns null if no access. */
37
133
  export async function getUserRole(userId: number, projectId: number): Promise<Role | null> {
38
134
  const db = getDb()
@@ -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 })