Initial commit: OAuth IAM Laravel application

Laravel-based OAuth/IAM service with Passport, Vue 3 frontend, and
Docker Compose setup for local and production deployment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 01:02:41 +02:00
commit c2b5291a74
174 changed files with 29318 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
// Equivalente al prefijo `dark:` de Tailwind: el toggle de tema pone la clase
// `.dark` en <html> (ver resources/js/composables/useTheme.ts), así que
// cualquier regla dentro de este mixin solo aplica bajo esa clase ancestro.
// Uso en los partials globales de components/ (no pasan por el compilador
// "scoped" de Vue, así que acá ".dark &" alcanza). Los dos componentes que
// SÍ usan <style scoped> (DailyLoginsChart, Analytics) escriben
// ":global(.dark) &" directo en vez de este mixin — ver nota en esos archivos.
@mixin dark {
.dark & {
@content;
}
}
@mixin focus-ring {
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
}
@mixin truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin respond-up($breakpoint) {
@media (min-width: $breakpoint) {
@content;
}
}

View File

@@ -0,0 +1,32 @@
// Escala de espaciado (coincide con la que ya se usaba vía utilities de Tailwind).
$space-1: 0.25rem; // 4px
$space-2: 0.5rem; // 8px
$space-3: 0.75rem; // 12px
$space-4: 1rem; // 16px
$space-5: 1.25rem; // 20px
$space-6: 1.5rem; // 24px
$space-8: 2rem; // 32px
$space-10: 2.5rem; // 40px
$radius-sm: 0.375rem; // 6px
$radius-md: 0.5rem; // 8px
$radius-lg: 0.75rem; // 12px
$radius-xl: 1rem; // 16px
$radius-2xl: 1.25rem; // 20px
$radius-full: 9999px;
$font-xs: 0.75rem;
$font-sm: 0.875rem;
$font-base: 1rem;
$font-lg: 1.125rem;
$font-xl: 1.25rem;
$font-2xl: 1.5rem;
$page-max-width: 72rem; // 1152px, equivalente a max-w-6xl
$auth-card-width: 24rem; // 384px, equivalente a max-w-sm
$breakpoint-sm: 640px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$transition-fast: 150ms ease;

18
resources/scss/app.scss Normal file
View File

@@ -0,0 +1,18 @@
@use 'base/tokens';
@use 'base/reset';
@use 'base/typography';
@use 'components/page-header';
@use 'components/button';
@use 'components/link';
@use 'components/card';
@use 'components/field';
@use 'components/table';
@use 'components/badge';
@use 'components/banner';
@use 'components/pill-select';
@use 'components/chip';
@use 'components/stat-tile';
@use 'components/bar-list';
@use 'components/auth-card';
@use 'components/app-nav';

View File

@@ -0,0 +1,50 @@
// Reset mínimo moderno — sin Tailwind Preflight, hace falta esto a mano.
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
tab-size: 4;
}
img,
svg,
video,
canvas {
display: block;
max-width: 100%;
}
button,
input,
select,
textarea {
font: inherit;
color: inherit;
background: transparent;
border: none;
}
button {
cursor: pointer;
}
a {
color: inherit;
text-decoration: none;
}
ul,
ol {
list-style: none;
}
table {
border-collapse: collapse;
width: 100%;
}

View File

@@ -0,0 +1,70 @@
// Tokens de color como custom properties CSS (no variables SCSS): así el
// toggle de tema (clase .dark en <html>) los puede resolver en runtime sin
// recompilar nada — SCSS solo aporta el anidado/organización de las reglas.
:root {
--color-page-bg: #f9fafb; // gray-50
--color-surface: #ffffff;
--color-surface-muted: #f9fafb; // gray-50
--color-code-bg: #f3f4f6; // gray-100
--color-border: #e5e7eb; // gray-200
--color-border-strong: #d1d5db; // gray-300
--color-text-primary: #111827; // gray-900
--color-text-secondary: #6b7280; // gray-500
--color-text-muted: #9ca3af; // gray-400
--color-text-on-accent: #ffffff;
--color-accent: #4f46e5; // indigo-600
--color-accent-strong: #6366f1; // indigo-500
--color-accent-soft: #eef2ff; // indigo-50
--color-accent-soft-text: #4338ca; // indigo-700
--gradient-accent: linear-gradient(to right, #4f46e5, #7c3aed);
--gradient-glow: linear-gradient(to bottom right, rgb(129 140 248 / 30%), rgb(167 139 250 / 20%));
--color-success: #059669; // emerald-600
--color-success-dot: #10b981; // emerald-500
--color-danger: #dc2626; // red-600
--color-warning-border: #fcd34d; // amber-300
--color-warning-bg: #fffbeb; // amber-50
--color-warning-title: #92400e; // amber-800
--color-warning-text: #78350f; // amber-900
--color-warning-code-bg: #ffffff;
--shadow-card: 0 1px 2px 0 rgb(0 0 0 / 4%);
--shadow-elevated: 0 10px 25px -5px rgb(17 24 39 / 8%), 0 8px 10px -6px rgb(17 24 39 / 5%);
}
.dark {
--color-page-bg: #09090b; // zinc-950
--color-surface: #18181b; // zinc-900
--color-surface-muted: rgb(255 255 255 / 5%);
--color-code-bg: rgb(255 255 255 / 10%);
--color-border: rgb(255 255 255 / 10%);
--color-border-strong: rgb(255 255 255 / 15%);
--color-text-primary: #ffffff;
--color-text-secondary: #9ca3af; // gray-400
--color-text-muted: #6b7280; // gray-500
--color-text-on-accent: #ffffff;
--color-accent: #6366f1; // indigo-500
--color-accent-strong: #818cf8; // indigo-400
--color-accent-soft: rgb(99 102 241 / 15%);
--color-accent-soft-text: #a5b4fc; // indigo-300
--gradient-accent: linear-gradient(to right, #4f46e5, #7c3aed);
--gradient-glow: linear-gradient(to bottom right, rgb(99 102 241 / 20%), rgb(139 92 246 / 10%));
--color-success: #34d399; // emerald-400
--color-success-dot: #10b981;
--color-danger: #f87171; // red-400
--color-warning-border: rgb(245 158 11 / 30%);
--color-warning-bg: rgb(245 158 11 / 10%);
--color-warning-title: #fcd34d; // amber-300
--color-warning-text: #fde68a; // amber-200
--color-warning-code-bg: rgb(0 0 0 / 20%);
--shadow-card: none;
--shadow-elevated: 0 20px 25px -5px rgb(0 0 0 / 30%), 0 8px 10px -6px rgb(0 0 0 / 30%);
}

View File

@@ -0,0 +1,26 @@
// La familia tipográfica la sirve el plugin `laravel-vite-plugin/fonts`
// (ver vite.config.ts) con su propio @font-face, independiente de este archivo.
body {
font-family:
'Instrument Sans',
ui-sans-serif,
system-ui,
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Noto Color Emoji';
font-size: 1rem;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
background-color: var(--color-page-bg);
color: var(--color-text-primary);
}
code {
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
background: var(--color-code-bg);
border-radius: 0.25rem;
padding: 0 0.25rem;
font-size: 0.85em;
}

View File

@@ -0,0 +1,131 @@
@use '../abstracts/variables' as *;
@use '../abstracts/mixins' as *;
.app-nav {
position: sticky;
top: 0;
z-index: 20;
border-bottom: 1px solid var(--color-border);
background: rgb(255 255 255 / 80%);
backdrop-filter: blur(8px);
@include dark {
background: rgb(9 9 11 / 80%);
}
}
.app-nav__inner {
max-width: $page-max-width;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: $space-4;
padding: $space-3 $space-6;
}
.app-nav__section {
display: flex;
align-items: center;
gap: $space-6;
}
.app-nav__brand {
display: flex;
align-items: center;
gap: $space-2;
}
.app-nav__logo {
display: grid;
place-items: center;
height: 1.75rem;
width: 1.75rem;
border-radius: $radius-md;
background: var(--gradient-accent);
color: #fff;
font-size: 0.7rem;
font-weight: 700;
}
.app-nav__brand-name {
font-weight: 600;
letter-spacing: -0.01em;
color: var(--color-text-primary);
}
.app-nav__links {
display: none;
align-items: center;
gap: $space-1;
@include respond-up($breakpoint-md) {
display: flex;
}
}
.app-nav__link {
border-radius: $radius-full;
padding: $space-1 * 1.5 $space-3;
font-size: $font-sm;
font-weight: 500;
color: var(--color-text-secondary);
transition: $transition-fast;
&:hover {
background: var(--color-surface-muted);
color: var(--color-text-primary);
}
&--active {
background: var(--color-accent-soft);
color: var(--color-accent-soft-text);
}
}
.app-nav__controls {
display: flex;
align-items: center;
gap: $space-2;
}
.app-nav__theme-toggle {
display: grid;
place-items: center;
height: 2rem;
width: 2rem;
border-radius: $radius-full;
border: 1px solid var(--color-border-strong);
color: var(--color-text-secondary);
transition: $transition-fast;
@include dark {
border-color: var(--color-border);
}
&:hover {
background: var(--color-surface-muted);
color: var(--color-text-primary);
}
svg {
height: 1rem;
width: 1rem;
}
}
.app-nav__user {
display: none;
font-size: $font-sm;
color: var(--color-text-secondary);
@include respond-up($breakpoint-sm) {
display: inline;
}
}
.page {
max-width: $page-max-width;
margin: 0 auto;
padding: $space-10 $space-6;
}

View File

@@ -0,0 +1,130 @@
@use '../abstracts/variables' as *;
.auth-page {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0 $space-4;
background: var(--color-page-bg);
}
.auth-page__glow {
pointer-events: none;
position: absolute;
top: -8rem;
left: 50%;
transform: translateX(-50%);
height: 20rem;
width: 36rem;
border-radius: $radius-full;
background: var(--gradient-glow);
filter: blur(64px);
}
.auth-card {
position: relative;
width: 100%;
max-width: $auth-card-width;
border-radius: $radius-2xl;
border: 1px solid var(--color-border);
background: var(--color-surface);
padding: $space-8;
box-shadow: var(--shadow-elevated);
}
.auth-card--wide {
max-width: 28rem;
}
.auth-card__logo {
display: grid;
place-items: center;
height: 2.25rem;
width: 2.25rem;
border-radius: $radius-lg;
background: var(--gradient-accent);
color: #fff;
font-size: $font-xs;
font-weight: 700;
box-shadow: 0 1px 2px 0 rgb(79 70 229 / 20%);
}
.auth-card__title {
margin-top: $space-4;
font-size: $font-xl;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--color-text-primary);
}
.auth-card__subtitle {
margin-top: $space-1;
font-size: $font-sm;
color: var(--color-text-secondary);
}
.auth-card__form {
margin-top: $space-6;
display: flex;
flex-direction: column;
gap: $space-4;
}
.auth-card__footer {
margin-top: $space-5;
text-align: center;
font-size: $font-sm;
color: var(--color-text-secondary);
}
.auth-card__banner {
margin-top: $space-4;
border-radius: $radius-xl;
border: 1px solid var(--color-border);
background: var(--color-surface-muted);
padding: $space-3;
font-size: $font-sm;
color: var(--color-text-secondary);
}
.auth-card__hint {
margin-top: $space-2;
text-align: center;
font-size: $font-xs;
color: var(--color-text-muted);
}
.auth-card__scopes {
margin-top: $space-4;
display: flex;
flex-direction: column;
gap: $space-2;
border-radius: $radius-xl;
border: 1px solid var(--color-border);
background: var(--color-surface-muted);
padding: $space-3;
}
.auth-card__scope {
display: flex;
gap: $space-2;
font-size: $font-sm;
color: var(--color-text-secondary);
}
.auth-card__scope-check {
color: var(--color-accent-strong);
}
.auth-card__actions {
margin-top: $space-6;
display: flex;
gap: $space-3;
form {
flex: 1;
}
}

View File

@@ -0,0 +1,24 @@
@use '../abstracts/variables' as *;
.badge {
display: inline-flex;
align-items: center;
gap: $space-1 * 1.5;
font-size: $font-xs;
font-weight: 500;
}
.badge__dot {
width: 0.375rem;
height: 0.375rem;
border-radius: $radius-full;
background: currentColor;
}
.badge--active {
color: var(--color-success);
}
.badge--inactive {
color: var(--color-text-muted);
}

View File

@@ -0,0 +1,36 @@
@use '../abstracts/variables' as *;
.banner {
margin-top: $space-6;
border-radius: $radius-xl;
padding: $space-4;
font-size: $font-sm;
}
.banner--warning {
border: 1px solid var(--color-warning-border);
background: var(--color-warning-bg);
}
.banner__title {
font-weight: 500;
color: var(--color-warning-title);
}
.banner__line {
margin-top: $space-1;
color: var(--color-warning-text);
}
.banner__code {
background: var(--color-warning-code-bg);
border-radius: $radius-sm;
padding: 0 $space-1;
}
.banner__close {
margin-top: $space-2;
color: var(--color-warning-title);
text-decoration: underline;
font-size: $font-sm;
}

View File

@@ -0,0 +1,68 @@
@use '../abstracts/variables' as *;
.bar-list {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: $radius-xl;
padding: $space-5;
}
.bar-list__title {
font-size: $font-sm;
font-weight: 500;
color: var(--color-text-primary);
}
.bar-list__empty {
margin-top: $space-3;
font-size: $font-sm;
color: var(--color-text-muted);
}
.bar-list__items {
margin-top: $space-4;
display: flex;
flex-direction: column;
gap: $space-3;
}
.bar-list__row-head {
margin-bottom: $space-1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: $font-xs;
}
.bar-list__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--color-text-secondary);
}
.bar-list__value {
margin-left: $space-2;
flex-shrink: 0;
color: var(--color-text-secondary);
font-variant-numeric: tabular-nums;
}
.bar-list__track {
height: 0.875rem;
width: 100%;
border-radius: $radius-full;
background: var(--color-surface-muted);
}
.bar-list__fill {
height: 0.875rem;
border-radius: 0 $radius-full $radius-full 0;
background: var(--series-1, var(--color-accent));
transition: filter 100ms;
}
.bar-list__row:hover .bar-list__fill,
.bar-list__row:focus-within .bar-list__fill {
filter: brightness(1.1);
}

View File

@@ -0,0 +1,107 @@
@use '../abstracts/variables' as *;
@use '../abstracts/mixins' as *;
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: $space-2;
padding: $space-2 $space-4;
border-radius: $radius-lg;
font-size: $font-sm;
font-weight: 500;
transition: $transition-fast;
white-space: nowrap;
@include focus-ring;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--full {
width: 100%;
}
&--pill {
border-radius: $radius-full;
}
}
.btn--primary {
background: var(--gradient-accent);
color: var(--color-text-on-accent);
box-shadow: 0 1px 2px 0 rgb(79 70 229 / 20%);
&:hover:not(:disabled) {
opacity: 0.9;
}
}
// Variante sólida (no-gradiente) del acento, para acciones dentro de tarjetas
// donde el gradiente sería demasiado.
.btn--accent {
background: var(--color-accent);
color: var(--color-text-on-accent);
border-radius: $radius-md;
padding: $space-2 $space-4;
&:hover:not(:disabled) {
background: var(--color-accent-strong);
}
}
.btn--secondary {
background: transparent;
border: 1px solid var(--color-border-strong);
color: var(--color-text-primary);
@include dark {
border-color: var(--color-border);
}
&:hover:not(:disabled) {
background: var(--color-surface-muted);
}
}
// Botón oscuro/claro invertido (usado para "Salir" en la nav).
.btn--invert {
background: #111827;
color: #ffffff;
border-radius: $radius-full;
&:hover:not(:disabled) {
background: #374151;
}
@include dark {
background: #ffffff;
color: #111827;
&:hover:not(:disabled) {
background: #e5e7eb;
}
}
}
// Botón oscuro sólido (asignar rol dentro de Usuarios).
.btn--dark {
background: #111827;
color: #ffffff;
border-radius: $radius-md;
&:hover:not(:disabled) {
background: #374151;
}
@include dark {
background: #ffffff;
color: #111827;
&:hover:not(:disabled) {
background: #e5e7eb;
}
}
}

View File

@@ -0,0 +1,35 @@
@use '../abstracts/variables' as *;
@use '../abstracts/mixins' as *;
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: $radius-xl;
padding: $space-5;
}
.card__title {
font-weight: 500;
color: var(--color-text-primary);
}
.card__subtitle {
margin-top: $space-1;
font-size: $font-sm;
color: var(--color-text-secondary);
}
// Tarjetas-link del dashboard: hover con leve elevación.
.card--interactive {
display: block;
transition:
transform $transition-fast,
border-color $transition-fast,
box-shadow $transition-fast;
&:hover {
transform: translateY(-2px);
border-color: var(--color-accent-strong);
box-shadow: var(--shadow-elevated);
}
}

View File

@@ -0,0 +1,21 @@
@use '../abstracts/variables' as *;
.chip {
display: inline-flex;
align-items: center;
gap: $space-1;
background: var(--color-accent-soft);
color: var(--color-accent-soft-text);
border-radius: $radius-full;
padding: $space-1 $space-2 * 1.25;
font-size: $font-xs;
}
.chip__remove {
color: var(--color-accent-strong);
opacity: 0.6;
&:hover {
opacity: 1;
}
}

View File

@@ -0,0 +1,80 @@
@use '../abstracts/variables' as *;
@use '../abstracts/mixins' as *;
.field {
display: flex;
flex-direction: column;
}
.field__label {
font-size: $font-sm;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: $space-1;
}
.field__hint {
margin-top: $space-1;
font-size: $font-xs;
color: var(--color-text-muted);
}
.field__error {
margin-top: $space-1;
font-size: $font-sm;
color: var(--color-danger);
}
.field__input,
.field__select {
width: 100%;
border: 1px solid var(--color-border-strong);
background: var(--color-surface);
color: var(--color-text-primary);
border-radius: $radius-md;
padding: $space-2 $space-3;
font-size: $font-sm;
transition: border-color $transition-fast;
@include dark {
border-color: var(--color-border);
background: var(--color-surface-muted);
}
&::placeholder {
color: var(--color-text-muted);
}
&:focus {
outline: none;
border-color: var(--color-accent-strong);
box-shadow: 0 0 0 3px rgb(99 102 241 / 15%);
}
}
.field--sm {
.field__label {
font-size: $font-xs;
color: var(--color-text-secondary);
}
.field__input,
.field__select {
padding: $space-1 * 1.5 $space-2;
}
}
.field__checkbox-row {
display: flex;
align-items: center;
gap: $space-2;
font-size: $font-sm;
color: var(--color-text-secondary);
}
.field__checkbox {
width: 1rem;
height: 1rem;
border-radius: $radius-sm;
accent-color: var(--color-accent);
}

View File

@@ -0,0 +1,28 @@
@use '../abstracts/variables' as *;
.link {
color: var(--color-accent-strong);
font-weight: 500;
font-size: inherit;
&:hover {
text-decoration: underline;
}
}
.link--danger {
color: var(--color-danger);
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
.link--muted {
color: var(--color-text-secondary);
&:hover {
color: var(--color-text-primary);
}
}

View File

@@ -0,0 +1,20 @@
@use '../abstracts/variables' as *;
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $space-4;
}
.page-title {
font-size: $font-2xl;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--color-text-primary);
}
.page-subtitle {
margin-top: $space-1;
color: var(--color-text-secondary);
}

View File

@@ -0,0 +1,18 @@
@use '../abstracts/variables' as *;
@use '../abstracts/mixins' as *;
.pill-select {
border: 1px solid var(--color-border-strong);
background: var(--color-surface);
color: var(--color-text-secondary);
border-radius: $radius-full;
padding: $space-1 * 1.5 $space-3;
font-size: $font-sm;
font-weight: 500;
@include dark {
border-color: var(--color-border);
background: var(--color-surface-muted);
color: var(--color-text-secondary);
}
}

View File

@@ -0,0 +1,26 @@
@use '../abstracts/variables' as *;
.stat-tile {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: $radius-xl;
padding: $space-4;
}
.stat-tile__label {
font-size: $font-xs;
color: var(--color-text-secondary);
}
.stat-tile__value {
margin-top: $space-1;
font-size: $font-2xl;
font-weight: 600;
color: var(--color-text-primary);
}
.stat-tile__hint {
margin-top: $space-1;
font-size: $font-xs;
color: var(--color-text-muted);
}

View File

@@ -0,0 +1,62 @@
@use '../abstracts/variables' as *;
@use '../abstracts/mixins' as *;
.table-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: $radius-xl;
overflow-x: auto;
}
.table {
width: 100%;
font-size: $font-sm;
}
.table__head {
background: var(--color-surface-muted);
}
.table__head-cell {
text-align: left;
font-weight: 500;
color: var(--color-text-secondary);
padding: $space-2 $space-4;
}
.table__body {
tr {
border-top: 1px solid var(--color-border);
&:hover {
background: var(--color-surface-muted);
}
}
tr.table__row--edit:hover {
background: none;
}
}
.table__row--edit {
background: var(--color-surface-muted);
}
.table__cell {
padding: $space-2 $space-4;
color: var(--color-text-secondary);
&--primary {
color: var(--color-text-primary);
}
&--actions {
text-align: right;
}
}
.table__empty {
padding: $space-4;
font-size: $font-sm;
color: var(--color-text-secondary);
}