-
@@ -21,13 +52,15 @@
import ContextMenu from './ContextMenu.vue'
import AddWorkspaceModal from './modals/AddWorkspaceModal.vue'
import DuplicateWorkspaceModal from './modals/DuplicateWorkspaceModal.vue'
+import AuthModal from './modals/AuthModal.vue'
import dayjs from 'dayjs'
export default {
components: {
ContextMenu,
AddWorkspaceModal,
- DuplicateWorkspaceModal
+ DuplicateWorkspaceModal,
+ AuthModal
},
data() {
return {
@@ -36,7 +69,8 @@ export default {
contextMenuWorkspace: null,
showAddWorkspaceModal: false,
showDuplicateWorkspaceModal: false,
- workspaceToDuplicate: null
+ workspaceToDuplicate: null,
+ showTestAuthModal: false
}
},
computed: {
@@ -119,6 +153,21 @@ export default {
},
dateFormat(date) {
return dayjs(date).format('DD-MMM-YY hh:mm A')
+ },
+ async handleTestAuthSuccess(authData) {
+ try {
+ await this.$store.dispatch('setCurrentUser', authData.user)
+ await this.$store.dispatch('setCurrentSession', authData.session)
+
+ this.$toast.success(
+ authData.isNewUser
+ ? `隆Bienvenido ${authData.user.username}! Tu cuenta ha sido creada.`
+ : `隆Bienvenido de vuelta, ${authData.user.username}!`
+ )
+ } catch (error) {
+ console.error('Error setting user session:', error)
+ this.$toast.error('Autenticaci贸n exitosa pero hubo un error configurando tu sesi贸n.')
+ }
}
}
}
diff --git a/packages/ui/src/components/modals/AuthModal.vue b/packages/ui/src/components/modals/AuthModal.vue
new file mode 100644
index 00000000..b8cce104
--- /dev/null
+++ b/packages/ui/src/components/modals/AuthModal.vue
@@ -0,0 +1,394 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Your data is stored locally and encrypted on your device.
+
+
+
+ {{ isRegistering ? 'Already have an account? Click "Sign In" above.' : 'Need an account? Click "Register" above.' }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/ui/src/components/modals/UserProfileModal.vue b/packages/ui/src/components/modals/UserProfileModal.vue
new file mode 100644
index 00000000..c1c177c2
--- /dev/null
+++ b/packages/ui/src/components/modals/UserProfileModal.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/ui/src/composables/useAuth.ts b/packages/ui/src/composables/useAuth.ts
new file mode 100644
index 00000000..0f2ab581
--- /dev/null
+++ b/packages/ui/src/composables/useAuth.ts
@@ -0,0 +1,264 @@
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+
+/**
+ * Composable para manejar autenticaci贸n en componentes Vue
+ */
+export function useAuth() {
+ const store = useStore()
+
+ // Estados computados
+ const isAuthenticated = computed(() => store.state.isAuthenticated)
+ const currentUser = computed(() => store.state.currentUser)
+ const currentSession = computed(() => store.state.currentSession)
+ const authInitialized = computed(() => store.state.authInitialized)
+
+ // Informaci贸n del usuario
+ const userDisplayName = computed(() => {
+ if (!currentUser.value) return ''
+ return currentUser.value.profile?.displayName || currentUser.value.username
+ })
+
+ const userEmail = computed(() => currentUser.value?.email || '')
+ const userId = computed(() => currentUser.value?._id || null)
+
+ // M茅todos de autenticaci贸n
+ const signOut = async () => {
+ await store.dispatch('signOutUser')
+ }
+
+ const updateSessionActivity = async () => {
+ await store.dispatch('updateSessionActivity')
+ }
+
+ const initializeAuth = async () => {
+ await store.dispatch('initializeAuth')
+ }
+
+ // Guards para proteger funcionalidades
+ const requireAuth = (callback?: () => void) => {
+ if (!isAuthenticated.value) {
+ // TODO: Mostrar modal de login o mensaje
+ console.warn('Authentication required')
+ return false
+ }
+ if (callback) callback()
+ return true
+ }
+
+ const requireGuest = (callback?: () => void) => {
+ if (isAuthenticated.value) {
+ console.warn('Guest access required (user is authenticated)')
+ return false
+ }
+ if (callback) callback()
+ return true
+ }
+
+ // Verificar permisos (placeholder para futuras funcionalidades)
+ const hasPermission = (permission: string) => {
+ if (!isAuthenticated.value) return false
+ // TODO: Implementar sistema de permisos m谩s robusto
+ return true
+ }
+
+ const canAccessWorkspace = (workspaceId: string) => {
+ if (!isAuthenticated.value) return true // Permitir acceso sin auth por ahora
+ // TODO: Implementar permisos por workspace cuando sea necesario
+ return true
+ }
+
+ const canModifyWorkspace = (workspaceId: string) => {
+ if (!isAuthenticated.value) return true // Permitir modificaci贸n sin auth por ahora
+ // TODO: Implementar permisos de modificaci贸n por workspace
+ return true
+ }
+
+ return {
+ // Estados
+ isAuthenticated,
+ currentUser,
+ currentSession,
+ authInitialized,
+ userDisplayName,
+ userEmail,
+ userId,
+
+ // M茅todos
+ signOut,
+ updateSessionActivity,
+ initializeAuth,
+
+ // Guards
+ requireAuth,
+ requireGuest,
+ hasPermission,
+ canAccessWorkspace,
+ canModifyWorkspace
+ }
+}
+
+/**
+ * Guard para proteger funcionalidades que requieren autenticaci贸n
+ */
+export function withAuthGuard
any>(
+ fn: T,
+ options: {
+ showLoginModal?: boolean
+ onAuthRequired?: () => void
+ fallback?: () => any
+ } = {}
+): T {
+ return ((...args: Parameters) => {
+ const { isAuthenticated } = useAuth()
+
+ if (!isAuthenticated.value) {
+ if (options.onAuthRequired) {
+ options.onAuthRequired()
+ } else if (options.showLoginModal) {
+ // TODO: Trigger login modal
+ console.warn('Authentication required - should show login modal')
+ }
+
+ return options.fallback ? options.fallback() : undefined
+ }
+
+ return fn(...args)
+ }) as T
+}
+
+/**
+ * Decorator para m茅todos de clase que requieren autenticaci贸n
+ */
+export function RequireAuth(
+ options: {
+ showLoginModal?: boolean
+ onAuthRequired?: () => void
+ } = {}
+) {
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
+ const originalMethod = descriptor.value
+
+ descriptor.value = function (...args: any[]) {
+ const { isAuthenticated } = useAuth()
+
+ if (!isAuthenticated.value) {
+ if (options.onAuthRequired) {
+ options.onAuthRequired()
+ } else if (options.showLoginModal) {
+ // TODO: Trigger login modal
+ console.warn('Authentication required - should show login modal')
+ }
+ return
+ }
+
+ return originalMethod.apply(this, args)
+ }
+
+ return descriptor
+ }
+}
+
+/**
+ * Middleware para interceptar acciones que requieren autenticaci贸n
+ */
+export class AuthMiddleware {
+ private static instance: AuthMiddleware
+ private authCallbacks: Map void> = new Map()
+
+ static getInstance(): AuthMiddleware {
+ if (!AuthMiddleware.instance) {
+ AuthMiddleware.instance = new AuthMiddleware()
+ }
+ return AuthMiddleware.instance
+ }
+
+ /**
+ * Registra una callback para cuando se requiere autenticaci贸n
+ */
+ onAuthRequired(key: string, callback: () => void) {
+ this.authCallbacks.set(key, callback)
+ }
+
+ /**
+ * Remueve una callback registrada
+ */
+ removeAuthCallback(key: string) {
+ this.authCallbacks.delete(key)
+ }
+
+ /**
+ * Ejecuta callbacks cuando se requiere autenticaci贸n
+ */
+ triggerAuthRequired() {
+ this.authCallbacks.forEach(callback => callback())
+ }
+
+ /**
+ * Intercepta una funci贸n y verifica autenticaci贸n
+ */
+ intercept any>(
+ fn: T,
+ options: {
+ requireAuth?: boolean
+ requireGuest?: boolean
+ onAuthRequired?: () => void
+ } = {}
+ ): T {
+ return ((...args: Parameters) => {
+ const { isAuthenticated } = useAuth()
+
+ if (options.requireAuth && !isAuthenticated.value) {
+ if (options.onAuthRequired) {
+ options.onAuthRequired()
+ } else {
+ this.triggerAuthRequired()
+ }
+ return
+ }
+
+ if (options.requireGuest && isAuthenticated.value) {
+ console.warn('Guest access required (user is authenticated)')
+ return
+ }
+
+ return fn(...args)
+ }) as T
+ }
+}
+
+// Instancia global del middleware
+export const authMiddleware = AuthMiddleware.getInstance()
+
+/**
+ * Utility para verificar si una acci贸n debe mostrar el modal de login
+ */
+export function shouldShowAuthModal(action: string): boolean {
+ const sensitiveActions = [
+ 'create-workspace',
+ 'delete-workspace',
+ 'export-data',
+ 'import-data',
+ 'modify-settings',
+ 'create-backup'
+ ]
+
+ return sensitiveActions.includes(action)
+}
+
+/**
+ * Mensajes de ayuda para diferentes acciones que requieren autenticaci贸n
+ */
+export const authMessages = {
+ 'create-workspace': 'Sign in to create and sync your workspaces across devices',
+ 'delete-workspace': 'Authentication required to delete workspaces',
+ 'export-data': 'Sign in to access advanced export features',
+ 'import-data': 'Authentication required for secure data import',
+ 'modify-settings': 'Sign in to save your personalized settings',
+ 'create-backup': 'Authentication required to create secure backups',
+ 'default': 'Sign in to access this feature'
+}
+
+export function getAuthMessage(action: string): string {
+ return authMessages[action as keyof typeof authMessages] || authMessages.default
+}
\ No newline at end of file
diff --git a/packages/ui/src/db.ts b/packages/ui/src/db.ts
index 3d847fb7..16f1e5e1 100644
--- a/packages/ui/src/db.ts
+++ b/packages/ui/src/db.ts
@@ -7,6 +7,8 @@ import {
Plugin,
RequestFinalResponse,
Workspace,
+ User,
+ UserSession,
} from './global'
export class RestfoxDatabase extends Dexie {
@@ -14,34 +16,42 @@ export class RestfoxDatabase extends Dexie {
collections!: Dexie.Table
plugins!: Dexie.Table
responses!: Dexie.Table
+ users!: Dexie.Table
+ sessions!: Dexie.Table
constructor() {
super('Restfox')
- // Define the database schema
- this.version(5).stores({
- workspaces: '_id',
- collections: '_id, workspaceId',
+ // Define the database schema with version 71 (higher than existing 70)
+ this.version(71).stores({
+ workspaces: '_id, userId',
+ collections: '_id, workspaceId, userId',
plugins: '_id, workspaceId, collectionId',
responses: '_id, collectionId',
+ users: '_id, username, email',
+ sessions: '_id, userId, token'
})
}
}
const db = new RestfoxDatabase()
-db.version(5).stores({
- workspaces: '_id',
- collections: '_id, workspaceId',
- plugins: '_id, workspaceId, collectionId',
- responses: '_id, collectionId'
-})
-
export async function exportDB() {
const blob = await db.export()
return blob
}
+// TEMPORARY: Clear database for version upgrade
+export async function clearDatabaseForUpgrade() {
+ try {
+ await db.delete()
+ console.log('Database cleared for version upgrade')
+ window.location.reload()
+ } catch (error) {
+ console.error('Error clearing database:', error)
+ }
+}
+
export async function importDB(file: File) {
await db.delete()
await db.open()
@@ -521,3 +531,159 @@ export async function createPlugins(plugins: Plugin[], workspaceId: string | nul
await db.plugins.bulkPut(plugins)
}
+
+// Users
+
+export async function createUser(user: User): Promise {
+ await db.users.add(user)
+ return user
+}
+
+export async function getUserByUsername(username: string): Promise {
+ return db.users.where('username').equals(username).first()
+}
+
+export async function getUserByEmail(email: string): Promise {
+ return db.users.where('email').equals(email).first()
+}
+
+export async function getUserById(userId: string): Promise {
+ return db.users.get(userId)
+}
+
+export async function updateUser(userId: string, updatedFields: Partial) {
+ await db.users.update(userId, updatedFields)
+}
+
+export async function deleteUser(userId: string) {
+ await db.users.delete(userId)
+ // Tambi茅n eliminar todas las sesiones del usuario
+ await db.sessions.where('userId').equals(userId).delete()
+}
+
+// Sessions
+
+export async function createSession(session: UserSession) {
+ await db.sessions.add(session)
+}
+
+export async function getSessionByToken(token: string): Promise {
+ return db.sessions.where('token').equals(token).first()
+}
+
+export async function getActiveSessionsByUserId(userId: string): Promise {
+ const now = Date.now()
+ return db.sessions
+ .where('userId').equals(userId)
+ .and(session => session.isActive && session.expiresAt > now)
+ .toArray()
+}
+
+export async function updateSession(sessionId: string, updatedFields: Partial) {
+ await db.sessions.update(sessionId, updatedFields)
+}
+
+export async function deactivateSession(sessionId: string) {
+ await db.sessions.update(sessionId, { isActive: false })
+}
+
+export async function deleteSession(sessionId: string) {
+ await db.sessions.delete(sessionId)
+}
+
+export async function deleteExpiredSessions() {
+ const now = Date.now()
+ await db.sessions.where('expiresAt').below(now).delete()
+}
+
+export async function deleteAllSessionsForUser(userId: string) {
+ await db.sessions.where('userId').equals(userId).delete()
+}
+
+// ===== CONTEXTO DE USUARIO =====
+let currentUserId: string | null = null
+
+export function getCurrentUserId(): string | null {
+ return currentUserId
+}
+
+export function setCurrentUserId(userId: string | null) {
+ currentUserId = userId
+}
+
+// ===== FUNCIONES DE DATOS FILTRADAS POR USUARIO =====
+
+// Workspaces filtrados por usuario
+export async function getAllWorkspacesForCurrentUser(): Promise {
+ const userId = getCurrentUserId()
+ if (!userId) {
+ // Usuario guest - devolver todos los workspaces sin userId
+ const allWorkspaces = await db.workspaces.toArray()
+ return allWorkspaces.filter(w => !w.userId || w.userId === null || w.userId === '')
+ }
+ return db.workspaces.where('userId').equals(userId).toArray()
+}
+
+// Collections filtradas por usuario
+export async function getCollectionForWorkspaceForCurrentUser(workspaceId: string): Promise {
+ const userId = getCurrentUserId()
+ const collections = await db.collections.where('workspaceId').equals(workspaceId).toArray()
+
+ if (!userId) {
+ // Usuario guest - devolver collections sin userId
+ return collections.filter(c => !c.userId || c.userId === null || c.userId === '')
+ }
+
+ return collections.filter(c => c.userId === userId)
+}
+
+// Wrapper para getCollectionForWorkspace que filtra por usuario
+export async function getCollectionForWorkspaceFiltered(workspaceId: string, type = null): Promise<{ error: string | null, collection: CollectionItem[], workspace: FileWorkspace | null, idMap: Map | null }> {
+ const result = await getCollectionForWorkspace(workspaceId, type)
+
+ // Si no hay error y tenemos collections, filtrarlas por usuario
+ if (!result.error && result.collection) {
+ const userId = getCurrentUserId()
+
+ if (!userId) {
+ // Usuario guest - filtrar collections sin userId
+ result.collection = result.collection.filter(c => !c.userId || c.userId === null || c.userId === '')
+ } else {
+ // Usuario logueado - filtrar por su userId
+ result.collection = result.collection.filter(c => c.userId === userId)
+ }
+ }
+
+ return result
+}
+
+// ===== MIGRACI脫N DE DATOS =====
+
+/**
+ * Migra datos existentes (guest) para asignarlos a un usuario espec铆fico
+ * Esto es 煤til para convertir datos de usuario guest a usuario logueado
+ */
+export async function migrateDataToUserContext(userId: string) {
+ if (!userId || userId === 'undefined') {
+ console.error('馃毃 Refusing to migrate data - invalid userId:', userId)
+ return
+ }
+
+ try {
+ // Migrar workspaces sin userId al usuario actual
+ const allWorkspaces = await db.workspaces.toArray()
+ const guestWorkspaces = allWorkspaces.filter(w => !w.userId || w.userId === null || w.userId === '')
+ for (const workspace of guestWorkspaces) {
+ await db.workspaces.update(workspace._id, { userId })
+ }
+
+ // Migrar collections sin userId al usuario actual
+ const allCollections = await db.collections.toArray()
+ const guestCollections = allCollections.filter(c => !c.userId || c.userId === null || c.userId === '')
+ for (const collection of guestCollections) {
+ await db.collections.update(collection._id, { userId })
+ }
+ } catch (error) {
+ console.error('Error migrating data to user context:', error)
+ }
+}
diff --git a/packages/ui/src/global.d.ts b/packages/ui/src/global.d.ts
index d908502e..28cb5cfe 100644
--- a/packages/ui/src/global.d.ts
+++ b/packages/ui/src/global.d.ts
@@ -24,6 +24,7 @@ export interface CollectionItem {
children?: CollectionItem[]
parentId: string | null
workspaceId: string
+ userId?: string | null
method?: string
url?: string
body?: RequestBody
@@ -171,6 +172,11 @@ export interface State {
tabEnvironmentResolved: any
idMap: Map | null
skipPersistingActiveTab: boolean
+ // Authentication state
+ currentUser: User | null
+ currentSession: UserSession | null
+ isAuthenticated: boolean
+ authInitialized: boolean
}
export interface Plugin {
@@ -204,6 +210,7 @@ export interface WorkspaceCache {
export interface Workspace {
_id: string
name: string
+ userId?: string | null
environment?: any
environments?: any[]
currentEnvironment?: string
@@ -275,4 +282,29 @@ export interface EditorConfig {
indentSize: number
}
+export interface User {
+ _id: string
+ username: string
+ email?: string
+ passwordHash: string
+ salt: string
+ createdAt: number
+ updatedAt: number
+ isActive: boolean
+ profile?: {
+ displayName?: string
+ avatar?: string
+ }
+}
+
+export interface UserSession {
+ _id: string
+ userId: string
+ token: string
+ createdAt: number
+ expiresAt: number
+ isActive: boolean
+ lastActivity: number
+}
+
export type SetEnvironmentVariableFunction = (name: string, value: string, scope?: 'workspace' | 'folder', pluginCollectionId?: string | null) => void
diff --git a/packages/ui/src/store.ts b/packages/ui/src/store.ts
index 030f1d3b..4861d31d 100644
--- a/packages/ui/src/store.ts
+++ b/packages/ui/src/store.ts
@@ -61,6 +61,8 @@ import {
Workspace,
RequestFinalResponse,
RequestAuthentication,
+ User,
+ UserSession,
} from './global'
import * as queryParamsSync from '@/utils/query-params-sync'
@@ -363,6 +365,11 @@ export const store = createStore({
idMap: null,
skipPersistingActiveTab: false,
consoleLogs: [],
+ // Authentication state
+ currentUser: null,
+ currentSession: null,
+ isAuthenticated: false,
+ authInitialized: false,
}
},
getters: {
@@ -674,6 +681,25 @@ export const store = createStore({
clearConsoleLogs(state) {
state.consoleLogs = []
},
+ // Authentication mutations
+ setCurrentUser(state, user) {
+ state.currentUser = user
+ state.isAuthenticated = !!user
+ },
+ setCurrentSession(state, session) {
+ state.currentSession = session
+ },
+ updateCurrentUser(state, updatedUser) {
+ state.currentUser = updatedUser
+ },
+ clearAuthentication(state) {
+ state.currentUser = null
+ state.currentSession = null
+ state.isAuthenticated = false
+ },
+ setAuthInitialized(state, initialized) {
+ state.authInitialized = initialized
+ },
},
actions: {
addTab(context, tab) {
@@ -876,6 +902,10 @@ export const store = createStore({
throw new Error('activeWorkspace is null')
}
+ // Get current user
+ const { getCurrentUserId } = await import('./db')
+ const currentUserId = getCurrentUserId()
+
let newCollectionItem: CollectionItem | null = null
if(payload.type === 'request') {
@@ -889,6 +919,7 @@ export const store = createStore({
},
parentId: payload.parentId,
workspaceId: context.state.activeWorkspace._id,
+ userId: currentUserId,
url: payload.url || ''
}
@@ -903,7 +934,8 @@ export const store = createStore({
_type: 'socket',
name: payload.name,
parentId: payload.parentId,
- workspaceId: context.state.activeWorkspace._id
+ workspaceId: context.state.activeWorkspace._id,
+ userId: currentUserId
}
}
@@ -914,7 +946,8 @@ export const store = createStore({
name: payload.name,
children: [],
parentId: payload.parentId,
- workspaceId: context.state.activeWorkspace._id
+ workspaceId: context.state.activeWorkspace._id,
+ userId: currentUserId
}
}
@@ -1165,10 +1198,15 @@ export const store = createStore({
},
async createWorkspace(context, payload) {
const newWorkspaceId = nanoid()
+
+ // Obtener el usuario actual
+ const { getCurrentUserId } = await import('./db')
+ const currentUserId = getCurrentUserId()
const newWorkspace = {
_id: newWorkspaceId,
name: payload.name,
+ userId: currentUserId,
_type: payload._type,
location: payload.location,
createdAt: new Date().getTime(),
@@ -1227,7 +1265,8 @@ export const store = createStore({
context.state.workspaces = context.state.workspaces.filter(item => item._id !== workspaceId)
},
async loadWorkspaces(context, noActiveWorkspaceCallback = null) {
- const workspaces = await getAllWorkspaces()
+ const { getAllWorkspacesForCurrentUser } = await import('./db')
+ const workspaces = await getAllWorkspacesForCurrentUser()
if(workspaces.length > 0) {
context.commit('setWorkspaces', workspaces)
@@ -1283,7 +1322,8 @@ export const store = createStore({
throw new Error('newWorkspaceId is null')
}
- const { collection: workspaceCollectionItems, workspace } = await getCollectionForWorkspace(sourceWorkspace._id)
+ const { getCollectionForWorkspaceFiltered } = await import('./db')
+ const { collection: workspaceCollectionItems, workspace } = await getCollectionForWorkspaceFiltered(sourceWorkspace._id)
workspaceCollectionItems.forEach(collectionItem => {
collectionItem.workspaceId = newWorkspaceId as string
@@ -1296,7 +1336,7 @@ export const store = createStore({
if (type === 'file') {
// this will call ensureRestfoxCollection to create restfox workspace if it doesn't exist
// this method will return 0 records
- await getCollectionForWorkspace(newWorkspaceId)
+ await getCollectionForWorkspaceFiltered(newWorkspaceId)
}
const result = await createCollections(newWorkspaceId, flattenTree(collectionTree))
@@ -1453,7 +1493,8 @@ export const store = createStore({
context.state.plugins.workspace.push(...plugins)
}
- const { collection } = await getCollectionForWorkspace(context.state.activeWorkspace._id)
+ const { getCollectionForWorkspaceFiltered } = await import('./db')
+ const { collection } = await getCollectionForWorkspaceFiltered(context.state.activeWorkspace._id)
context.commit('setCollection', collection)
@@ -1476,7 +1517,8 @@ export const store = createStore({
throw new Error('activeWorkspace is null')
}
- const { collection, workspace, idMap } = await getCollectionForWorkspace(context.state.activeWorkspace._id)
+ const { getCollectionForWorkspaceFiltered } = await import('./db')
+ const { collection, workspace, idMap } = await getCollectionForWorkspaceFiltered(context.state.activeWorkspace._id)
context.commit('setCollection', collection)
context.state.idMap = idMap
@@ -1644,6 +1686,112 @@ export const store = createStore({
loadWorkspaceTabs(context)
},
+ // Authentication actions
+ async initializeAuth(context) {
+ try {
+ // Buscar sesi贸n activa en localStorage
+ const sessionToken = localStorage.getItem('restfork_session_token')
+ if (!sessionToken) {
+ context.commit('setAuthInitialized', true)
+ return
+ }
+
+ // Importar funciones de DB
+ const { getSessionByToken, getUserById, deleteExpiredSessions } = await import('./db')
+ const { isSessionValid, updateSessionActivity } = await import('./utils/auth-utils')
+
+ // Limpiar sesiones expiradas
+ await deleteExpiredSessions()
+
+ // Buscar sesi贸n por token
+ const session = await getSessionByToken(sessionToken)
+ if (!session || !isSessionValid(session)) {
+ localStorage.removeItem('restfork_session_token')
+ context.commit('setAuthInitialized', true)
+ return
+ }
+
+ // Buscar usuario
+ const user = await getUserById(session.userId)
+ if (!user || !user.isActive) {
+ localStorage.removeItem('restfork_session_token')
+ context.commit('setAuthInitialized', true)
+ return
+ }
+
+ // Actualizar actividad de sesi贸n
+ await updateSessionActivity(session._id)
+
+ // Establecer usuario y sesi贸n actuales
+ context.commit('setCurrentUser', user)
+ context.commit('setCurrentSession', session)
+
+ // IMPORTANTE: Configurar el contexto de usuario en la base de datos
+ const { setCurrentUserId } = await import('./db')
+ setCurrentUserId(user._id)
+
+ context.commit('setAuthInitialized', true)
+ } catch (error) {
+ console.error('Error initializing auth:', error)
+ localStorage.removeItem('restfork_session_token')
+ context.commit('setAuthInitialized', true)
+ }
+ },
+
+ async signOutUser(context) {
+ try {
+ if (context.state.currentSession) {
+ const { deactivateSession } = await import('./db')
+ await deactivateSession(context.state.currentSession._id)
+ }
+ } catch (error) {
+ console.error('Error deactivating session:', error)
+ } finally {
+ // Limpiar estado y localStorage
+ localStorage.removeItem('restfork_session_token')
+ context.commit('clearAuthentication')
+
+ // Limpiar contexto de usuario en la base de datos
+ const { setCurrentUserId } = await import('./db')
+ setCurrentUserId(null)
+
+ // Limpiar workspace activo para volver a la pantalla de workspaces
+ context.commit('setActiveWorkspace', null)
+ localStorage.removeItem(constants.LOCAL_STORAGE_KEY.ACTIVE_WORKSPACE_ID)
+
+ // Recargar datos del contexto guest
+ await context.dispatch('loadWorkspaces')
+ }
+ },
+
+ async setCurrentUser(context, user) {
+ context.commit('setCurrentUser', user)
+ // Actualizar el contexto de usuario en la base de datos
+ const { setCurrentUserId } = await import('./db')
+ setCurrentUserId(user ? user._id : null)
+
+ // Recargar los datos para el nuevo contexto de usuario
+ if (user) {
+ await context.dispatch('loadWorkspaces')
+ }
+ },
+
+ async setCurrentSession(context, session) {
+ context.commit('setCurrentSession', session)
+ // Guardar token en localStorage para persistencia
+ localStorage.setItem('restfork_session_token', session.token)
+ },
+
+ async updateCurrentUser(context, updatedUser) {
+ context.commit('updateCurrentUser', updatedUser)
+ },
+
+ async updateSessionActivity(context) {
+ if (context.state.currentSession) {
+ const { updateSessionActivity } = await import('./utils/auth-utils')
+ await updateSessionActivity(context.state.currentSession._id)
+ }
+ }
}
})
diff --git a/packages/ui/src/utils/auth-utils.ts b/packages/ui/src/utils/auth-utils.ts
new file mode 100644
index 00000000..14f03903
--- /dev/null
+++ b/packages/ui/src/utils/auth-utils.ts
@@ -0,0 +1,186 @@
+import { nanoid } from 'nanoid'
+import type { User, UserSession } from '../global'
+
+/**
+ * Genera un salt aleatorio para hash de contrase帽a
+ */
+export function generateSalt(): string {
+ return nanoid(32)
+}
+
+/**
+ * Genera un token de sesi贸n 煤nico
+ */
+export function generateSessionToken(): string {
+ return nanoid(64)
+}
+
+/**
+ * Hash simple de contrase帽a usando Web Crypto API
+ * Nota: Para producci贸n se recomendar铆a usar bcrypt o similar
+ */
+export async function hashPassword(password: string, salt: string): Promise {
+ const encoder = new TextEncoder()
+ const data = encoder.encode(password + salt)
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data)
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
+}
+
+/**
+ * Genera salt y hash para una contrase帽a nueva
+ */
+export async function hashPasswordWithSalt(password: string): Promise<{ hash: string; salt: string }> {
+ const salt = generateSalt()
+ const hash = await hashPassword(password, salt)
+ return { hash, salt }
+}
+
+/**
+ * Verifica si una contrase帽a coincide con el hash almacenado
+ */
+export async function verifyPassword(password: string, storedHash: string, salt: string): Promise {
+ const passwordHash = await hashPassword(password, salt)
+ return passwordHash === storedHash
+}
+
+/**
+ * Crea un nuevo usuario con contrase帽a hasheada
+ */
+export async function createUserData(
+ username: string,
+ password: string,
+ email?: string,
+ profile?: { displayName?: string; avatar?: string }
+): Promise {
+ const salt = generateSalt()
+ const passwordHash = await hashPassword(password, salt)
+ const now = Date.now()
+
+ return {
+ _id: nanoid(),
+ username,
+ email,
+ passwordHash,
+ salt,
+ createdAt: now,
+ updatedAt: now,
+ isActive: true,
+ profile
+ }
+}
+
+/**
+ * Crea una nueva sesi贸n de usuario
+ */
+export function createSessionData(userId: string, expirationHours: number = 24 * 7): UserSession {
+ const now = Date.now()
+ const expiresAt = now + (expirationHours * 60 * 60 * 1000) // Convertir horas a ms
+
+ return {
+ _id: nanoid(),
+ userId,
+ token: generateSessionToken(),
+ createdAt: now,
+ expiresAt,
+ isActive: true,
+ lastActivity: now
+ }
+}
+
+/**
+ * Crea datos b谩sicos de sesi贸n para login (simplificado)
+ */
+export function createLoginSessionData(username: string): { token: string; expiresAt: number } {
+ const now = Date.now()
+ const expiresAt = now + (7 * 24 * 60 * 60 * 1000) // 7 d铆as en ms
+
+ return {
+ token: generateSessionToken(),
+ expiresAt
+ }
+}
+
+/**
+ * Valida que una sesi贸n est茅 activa y no haya expirado
+ */
+export function isSessionValid(session: UserSession): boolean {
+ const now = Date.now()
+ return session.isActive && session.expiresAt > now
+}
+
+/**
+ * Valida formato de email
+ */
+export function isValidEmail(email: string): boolean {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
+}
+
+/**
+ * Valida fortaleza de contrase帽a
+ */
+export function isValidPassword(password: string): { valid: boolean; message?: string } {
+ if (password.length < 6) {
+ return { valid: false, message: 'Password must be at least 6 characters long' }
+ }
+
+ if (password.length > 128) {
+ return { valid: false, message: 'Password must be less than 128 characters' }
+ }
+
+ // Opcional: agregar m谩s validaciones (may煤sculas, n煤meros, s铆mbolos)
+ return { valid: true }
+}
+
+/**
+ * Valida nombre de usuario
+ */
+export function isValidUsername(username: string): { valid: boolean; message?: string } {
+ if (username.length < 3) {
+ return { valid: false, message: 'Username must be at least 3 characters long' }
+ }
+
+ if (username.length > 30) {
+ return { valid: false, message: 'Username must be less than 30 characters' }
+ }
+
+ if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
+ return { valid: false, message: 'Username can only contain letters, numbers, underscores and hyphens' }
+ }
+
+ return { valid: true }
+}
+
+/**
+ * Funci贸n de test para verificar que el hashing funciona correctamente
+ */
+export async function testPasswordHashing() {
+ const testPassword = "123456"
+ const { hash, salt } = await hashPasswordWithSalt(testPassword)
+
+ console.log('Password hash test:', {
+ password: testPassword,
+ hash,
+ salt
+ })
+
+ // Test verification
+ const isValid = await verifyPassword(testPassword, hash, salt)
+ const isInvalid = await verifyPassword("wrongpassword", hash, salt)
+
+ console.log('Verification test:', {
+ correctPassword: isValid, // Should be true
+ wrongPassword: isInvalid // Should be false
+ })
+
+ return { isValid, isInvalid }
+}
+
+/**
+ * Actualiza la 煤ltima actividad de una sesi贸n
+ */
+export async function updateSessionActivity(sessionId: string) {
+ const { updateSession } = await import('../db')
+ await updateSession(sessionId, { lastActivity: Date.now() })
+}
\ No newline at end of file