Files
crewli/apps/app/.eslintrc.cjs
bert.hausmans f2b08ecb21 refactor(auth): merge usePortalAuthStore into useAuthStore with context-aware getters
usePortalAuthStore is deleted — its 114 lines were a slim wrapper over
the same /auth/me endpoint useAuthStore already consumes. The merged
store gains the full set of additions Bert specified for B2a:

State:
- availableContexts / defaultContext (from /auth/me contexts block)
- lastContext (localStorage-persisted)
- portalToken (in-memory only, for the bearer-axios flavour)

Getters: isPortalUser, isOrganizerUser, isPlatformAdmin (alias of
isSuperAdmin), showContextSwitcher, hasRole(), hasAnyRole().

Actions: login(), verifyMfa() — both return typed discriminated
unions so login.vue (Phase H) consumes results without branching on
raw API response shapes. setLastContext, setPortalToken,
resolveLandingRoute, clearAll. clearAll dynamically imports
usePortalStore.reset() to clear portal sessionStorage on session-end —
this is the canonical session-cleanup hub now that the merge has
happened.

5 source files migrated from usePortalAuthStore → useAuthStore. The
PortalLayout.spec.ts mock follows. The boundaries matrix gains a
single new edge (`stores → stores-portal`) replacing the deleted
stores-portal/usePortalAuthStore which previously owned that
cross-zone call.

Adds 16 vitest specs in src/stores/__tests__/useAuthStore.spec.ts
covering setUser context hydration, hasRole/hasAnyRole, lastContext
localStorage persistence, resolveLandingRoute precedence
(portal/organizer/super_admin/multi-role/forceContext/forbidden
fallback), portalToken state, and clearAll cleanup.

Test count 162 → 178 (16 new). Frontend lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:25:24 +02:00

327 lines
14 KiB
JavaScript

// Sessie 3c (WS-6) — closes the apps/app ESLint config gap.
// Adapted from the Vuexy reference (resources/vuexy-admin-v10.11.1/.../full-version/.eslintrc.cjs)
// minus the Vuexy-internal lint rules (valid-appcardcode-*, internal regex
// rules) that don't apply outside the demo project. Plugin set matches
// what's installed in apps/app's package.json.
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2022: true,
},
extends: [
'@antfu/eslint-config-vue',
'plugin:vue/vue3-recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:promise/recommended',
'plugin:sonarjs/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:case-police/recommended',
'plugin:regexp/recommended',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 13,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: [
'vue',
'@typescript-eslint',
'regex',
'regexp',
'boundaries',
],
ignorePatterns: [
'src/plugins/iconify/*.js',
'node_modules',
'dist',
'*.d.ts',
'vendor',
'*.json',
'src/@core/**',
'src/@layouts/**',
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'comma-spacing': ['error', { before: false, after: true }],
'key-spacing': ['error', { afterColon: true }],
'n/prefer-global/process': ['off'],
'sonarjs/cognitive-complexity': ['off'],
'vue/first-attribute-linebreak': ['error', {
singleline: 'beside',
multiline: 'below',
}],
'antfu/top-level-function': 'off',
// Project rule (CLAUDE.md frontend rules): no `any`. Override the
// Vuexy reference (which sets this off) — Crewli's stricter posture.
'@typescript-eslint/no-explicit-any': 'error',
'indent': ['error', 2, { SwitchCase: 1 }],
'comma-dangle': ['error', 'always-multiline'],
'object-curly-spacing': ['error', 'always'],
'camelcase': 'error',
'max-len': 'off',
'semi': ['error', 'never'],
'arrow-parens': ['error', 'as-needed'],
'newline-before-return': 'error',
'lines-around-comment': [
'error',
{
beforeBlockComment: true,
beforeLineComment: true,
allowBlockStart: true,
allowClassStart: true,
allowObjectStart: true,
allowArrayStart: true,
ignorePattern: '!SECTION',
},
],
'@typescript-eslint/no-unused-vars': ['error', {
varsIgnorePattern: '^_+$',
argsIgnorePattern: '^_+$',
}],
'array-element-newline': ['error', 'consistent'],
'array-bracket-newline': ['error', 'consistent'],
'vue/multi-word-component-names': 'off',
'padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: 'expression', next: 'const' },
{ blankLine: 'always', prev: 'const', next: 'expression' },
{ blankLine: 'always', prev: 'multiline-const', next: '*' },
{ blankLine: 'always', prev: '*', next: 'multiline-const' },
],
'import/prefer-default-export': 'off',
'import/newline-after-import': ['error', { count: 1 }],
'no-restricted-imports': ['error', 'vuetify/components', {
name: 'vue3-apexcharts',
message: 'apexcharts are auto imported',
}],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-unresolved': [2, {
ignore: [
'~pages$',
'virtual:meta-layouts',
'#auth$',
'#components$',
'.*\\?raw',
],
}],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error'],
'@typescript-eslint/consistent-type-imports': 'error',
// CLAUDE.md frontend convention — backend enums are mirrored as
// `as const` objects WITH a same-named `type` alias. The two live
// in different namespaces (value vs. type) and are intentional;
// both base `no-redeclare` and the typed variant flag them anyway.
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'off',
'promise/always-return': 'off',
'promise/catch-or-return': 'off',
'vue/block-tag-newline': 'error',
'vue/component-api-style': 'error',
'vue/component-name-in-template-casing': ['error', 'PascalCase', {
registeredComponentsOnly: false,
ignores: ['/^swiper-/'],
}],
'vue/custom-event-name-casing': ['error', 'camelCase', {
ignores: [
'/^(click):[a-z]+((\\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?/',
],
}],
'vue/define-macros-order': 'error',
'vue/html-comment-content-newline': 'error',
'vue/html-comment-content-spacing': 'error',
'vue/html-comment-indent': 'error',
'vue/match-component-file-name': 'error',
'vue/no-child-content': 'error',
'vue/require-default-prop': 'off',
'vue/no-duplicate-attr-inheritance': 'error',
'vue/no-empty-component-block': 'error',
'vue/no-multiple-objects-in-class': 'error',
'vue/no-reserved-component-names': 'error',
'vue/no-template-target-blank': 'error',
'vue/no-useless-mustaches': 'error',
'vue/no-useless-v-bind': 'error',
'vue/padding-line-between-blocks': 'error',
'vue/prefer-separate-static-class': 'error',
'vue/prefer-true-attribute-shorthand': 'error',
'vue/v-on-function-call': 'error',
'vue/no-restricted-class': ['error', '/^(p|m)(l|r)-/'],
'vue/valid-v-slot': ['error', { allowModifiers: true }],
'vue/no-irregular-whitespace': 'error',
'vue/template-curly-spacing': 'error',
'sonarjs/no-duplicate-string': 'off',
'sonarjs/no-nested-template-literals': 'off',
'regex/invalid': [
'error',
[
{
regex: '@/assets/images',
replacement: '@images',
message: 'Use \'@images\' path alias for image imports',
},
{
regex: '@/assets/styles',
replacement: '@styles',
message: 'Use \'@styles\' path alias for importing styles from \'src/assets/styles\'',
},
],
'\\.eslintrc\\.cjs',
],
// Architectural import boundaries (WS-3 1c, audit:
// dev-docs/WS-3-SESSION-1C-AUDIT.md). The matrix is layered:
// types → utils → lib → composables → stores → components → layouts → pages.
// The `lib → stores` edge is intentionally disallowed; lib/axios.ts
// uses dynamic `await import('@/stores/...')` for its 4 store reads
// so the static-import surface stays clean.
//
// WS-3 PR-B1 activated TECH-WS3-BOUNDARIES-SUBZONES: components and
// pages now have organizer/portal/shared sub-zones (§4.2 charter).
// The cross-context edges (organizer ↛ portal, shared ↛ portal/organizer)
// are forbidden so a future portal-only refactor cannot leak into
// the organizer surface and vice versa.
//
// FORWARD-FLAG: when src/plugins/1.router/ migrates to src/router/
// in a later WS-3 PR (TECH-WS3-BOUNDARIES-ROUTER-ZONE), add
// `{ type: 'router', pattern: 'src/router/**' }` to boundaries/elements
// and `{ from: 'router', allow: ['types', 'utils', 'lib', 'plugins',
// 'stores'] }` to the rules.
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
{ from: 'types', allow: ['types'] },
{ from: 'utils', allow: ['types', 'utils'] },
{ from: 'lib', allow: ['types', 'utils', 'lib'] },
{ from: 'plugins', allow: ['types', 'utils', 'lib', 'plugins', 'stores', 'stores-portal'] },
{ from: 'composables-forms', allow: ['types', 'utils', 'lib', 'composables-forms'] },
{ from: 'composables', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal'] },
{ from: 'stores-portal', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal'] },
// useAuthStore.clearAll() / .logout() invokes usePortalStore.reset()
// via dynamic import to clear portal sessionStorage on session-end.
// The merged auth store is the canonical session-cleanup hub — this
// edge replaces the deleted stores-portal/usePortalAuthStore which
// previously owned the cross-zone call (WS-3 PR-B2a).
{ from: 'stores', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal'] },
{ from: 'navigation', allow: ['types', 'utils', 'navigation'] },
{ from: 'components-shared', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'components-shared'] },
{ from: 'components-portal', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'components-shared', 'components-portal'] },
{ from: 'components-organizer', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-shared', 'components-organizer'] },
{ from: 'components', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components', 'components-shared', 'components-organizer'] },
{ from: 'layouts', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components', 'components-shared', 'components-portal', 'components-organizer', 'layouts'] },
{ from: 'pages-register', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'plugins', 'components-shared', 'layouts'] },
{ from: 'pages-portal', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components-shared', 'components-portal', 'layouts', 'plugins'] },
{ from: 'pages-organizer', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components', 'components-shared', 'components-organizer', 'layouts', 'plugins'] },
{ from: 'pages-platform', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components', 'components-shared', 'components-organizer', 'layouts', 'plugins'] },
{ from: 'pages', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components', 'components-shared', 'components-portal', 'components-organizer', 'layouts'] },
],
}],
'boundaries/no-unknown': 'off', // External packages are fine.
'boundaries/no-unknown-files': 'off', // The ignore list handles vendor/generated.
},
settings: {
'import/resolver': {
node: true,
typescript: {},
},
// Element-type assignment: first-match-wins. Order matters — narrower
// sub-zones declared before their broader parents.
'boundaries/elements': [
{ type: 'types', pattern: 'src/types/**' },
{ type: 'utils', pattern: 'src/utils/**' },
{ type: 'lib', pattern: 'src/lib/**' },
{ type: 'plugins', pattern: 'src/plugins/**' },
{ type: 'composables-forms', pattern: 'src/composables/forms/**' },
{ type: 'composables', pattern: 'src/composables/**' },
{ type: 'stores-portal', pattern: 'src/stores/portal/**' },
{ type: 'stores', pattern: 'src/stores/**' },
{ type: 'navigation', pattern: 'src/navigation/**' },
// components/shared/** is the canonical sub-zone. components/auth/**
// and components/settings/** are folded in here as legacy cross-context
// siblings (MFA dialogs + password-requirements widgets used by both
// organizer reset-password and portal wachtwoord-instellen / profiel
// flows). PR-B2 may rehome these under components/shared/{auth,settings}/.
{ type: 'components-shared', pattern: 'src/components/{shared,auth,settings}/**' },
{ type: 'components-portal', pattern: 'src/components/portal/**' },
{ type: 'components-organizer', pattern: 'src/components/organizer/**' },
{ type: 'components', pattern: 'src/components/**' },
{ type: 'layouts', pattern: 'src/layouts/**' },
{ type: 'pages-register', pattern: 'src/pages/register/**' },
{ type: 'pages-portal', pattern: 'src/pages/portal/**' },
{ type: 'pages-platform', pattern: 'src/pages/platform/**' },
{ type: 'pages-organizer', pattern: 'src/pages/{events,members,organisation,account-settings,dashboard,invitations}/**' },
{ type: 'pages', pattern: 'src/pages/**' },
],
'boundaries/ignore': [
'src/@core/**', // vendored Vuexy
'src/@layouts/**', // vendored Vuexy
'src/App.vue', // orchestration root
'src/main.ts', // orchestration root
'src/assets/**', // static media
'src/styles/**', // SCSS
'**/*.d.ts', // generated declarations
],
'boundaries/include': ['src/**/*.{ts,vue,tsx}'],
},
overrides: [
// Vue SFCs: the base lines-around-comment rule conflicts with
// vue/block-tag-newline at the <script setup>/comment boundary
// (the <script> tag isn't a JS block-start so allowBlockStart
// doesn't kick in, while vue/block-tag-newline forbids the blank
// line that lines-around-comment wants). Disable both
// beforeBlockComment and beforeLineComment for *.vue so a leading
// comment in <script setup> is allowed without a preceding blank.
{
files: ['*.vue'],
rules: {
'lines-around-comment': ['error', {
beforeBlockComment: false,
beforeLineComment: false,
allowBlockStart: true,
allowClassStart: true,
allowObjectStart: true,
allowArrayStart: true,
ignorePattern: '!SECTION',
}],
},
},
],
}