Spaces:
Sleeping
Sleeping
add: page /mahasiswa
Browse files- app/Providers/FortifyServiceProvider.php +1 -1
- resources/css/app.css +755 -28
- resources/js/app.tsx +8 -1
- resources/js/components/appearance-toggle.tsx +34 -0
- resources/js/components/auth/register-role-form.tsx +177 -0
- resources/js/layouts/app-layout.tsx +2 -6
- resources/js/lib/api.ts +46 -5
- resources/js/lib/auth.ts +126 -0
- resources/js/lib/rag.ts +109 -0
- resources/js/pages/auth/login.tsx +114 -78
- resources/js/pages/auth/register-choice.tsx +82 -0
- resources/js/pages/auth/register-dosen.tsx +5 -0
- resources/js/pages/auth/register-mahasiswa.tsx +5 -0
- resources/js/pages/mahasiswa.tsx +523 -0
- resources/js/pages/welcome.tsx +93 -15
- routes/web.php +7 -3
- sevima-raghub.json +2060 -0
- tests/Feature/Auth/RegistrationTest.php +17 -1
- vite.config.ts +2 -2
app/Providers/FortifyServiceProvider.php
CHANGED
|
@@ -66,7 +66,7 @@ private function configureViews(): void
|
|
| 66 |
'status' => $request->session()->get('status'),
|
| 67 |
]));
|
| 68 |
|
| 69 |
-
Fortify::registerView(fn () => Inertia::render('auth/register'));
|
| 70 |
|
| 71 |
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge'));
|
| 72 |
|
|
|
|
| 66 |
'status' => $request->session()->get('status'),
|
| 67 |
]));
|
| 68 |
|
| 69 |
+
Fortify::registerView(fn () => Inertia::render('auth/register-choice'));
|
| 70 |
|
| 71 |
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge'));
|
| 72 |
|
resources/css/app.css
CHANGED
|
@@ -9,9 +9,8 @@
|
|
| 9 |
|
| 10 |
@theme {
|
| 11 |
--font-sans:
|
| 12 |
-
'
|
| 13 |
-
'
|
| 14 |
-
'Noto Color Emoji';
|
| 15 |
|
| 16 |
--radius-lg: var(--radius);
|
| 17 |
--radius-md: calc(var(--radius) - 2px);
|
|
@@ -95,41 +94,169 @@ :root {
|
|
| 95 |
--sidebar-accent-foreground: oklch(0.205 0 0);
|
| 96 |
--sidebar-border: oklch(0.922 0 0);
|
| 97 |
--sidebar-ring: oklch(0.87 0 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
.dark {
|
| 101 |
-
--background:
|
| 102 |
-
--foreground:
|
| 103 |
-
--card:
|
| 104 |
-
--card-foreground:
|
| 105 |
-
--popover:
|
| 106 |
-
--popover-foreground:
|
| 107 |
-
--primary:
|
| 108 |
-
--primary-foreground:
|
| 109 |
-
--secondary:
|
| 110 |
-
--secondary-foreground:
|
| 111 |
-
--muted:
|
| 112 |
-
--muted-foreground:
|
| 113 |
-
--accent:
|
| 114 |
-
--accent-foreground:
|
| 115 |
--destructive: oklch(0.396 0.141 25.723);
|
| 116 |
--destructive-foreground: oklch(0.637 0.237 25.331);
|
| 117 |
-
--border:
|
| 118 |
-
--input:
|
| 119 |
-
--ring:
|
| 120 |
--chart-1: oklch(0.488 0.243 264.376);
|
| 121 |
--chart-2: oklch(0.696 0.17 162.48);
|
| 122 |
--chart-3: oklch(0.769 0.188 70.08);
|
| 123 |
--chart-4: oklch(0.627 0.265 303.9);
|
| 124 |
--chart-5: oklch(0.645 0.246 16.439);
|
| 125 |
-
--sidebar:
|
| 126 |
-
--sidebar-foreground:
|
| 127 |
-
--sidebar-primary:
|
| 128 |
-
--sidebar-primary-foreground:
|
| 129 |
-
--sidebar-accent:
|
| 130 |
-
--sidebar-accent-foreground:
|
| 131 |
-
--sidebar-border:
|
| 132 |
-
--sidebar-ring:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
@layer base {
|
|
@@ -141,3 +268,603 @@ @layer base {
|
|
| 141 |
@apply bg-background text-foreground;
|
| 142 |
}
|
| 143 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
@theme {
|
| 11 |
--font-sans:
|
| 12 |
+
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
| 13 |
+
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
|
|
| 14 |
|
| 15 |
--radius-lg: var(--radius);
|
| 16 |
--radius-md: calc(var(--radius) - 2px);
|
|
|
|
| 94 |
--sidebar-accent-foreground: oklch(0.205 0 0);
|
| 95 |
--sidebar-border: oklch(0.922 0 0);
|
| 96 |
--sidebar-ring: oklch(0.87 0 0);
|
| 97 |
+
|
| 98 |
+
--app-bg: #fafbff;
|
| 99 |
+
--app-sidebar-bg: #ffffff;
|
| 100 |
+
--app-surface: #ffffff;
|
| 101 |
+
--app-surface-soft: #eef4ff;
|
| 102 |
+
--app-surface-muted: rgba(217, 217, 217, 0.32);
|
| 103 |
+
--app-border: rgba(0, 0, 0, 0.14);
|
| 104 |
+
--app-border-soft: rgba(0, 0, 0, 0.08);
|
| 105 |
+
--app-border-strong: #739af1;
|
| 106 |
+
--app-text: #0a0a0a;
|
| 107 |
+
--app-muted: #737373;
|
| 108 |
+
--app-primary: #2764eb;
|
| 109 |
+
--app-primary-soft: #ccdcff;
|
| 110 |
+
--app-primary-foreground: #ffffff;
|
| 111 |
+
--app-danger: #ef4444;
|
| 112 |
+
--app-placeholder: rgba(115, 115, 115, 0.42);
|
| 113 |
+
--app-toggle-bg: rgba(255, 255, 255, 0.9);
|
| 114 |
+
--app-shadow: 1px 1px 4.5px rgba(39, 100, 235, 0.36);
|
| 115 |
+
--app-composer-shadow: 0 10px 24px rgba(39, 100, 235, 0.28);
|
| 116 |
+
|
| 117 |
+
--welcome-shell-bg: var(--app-bg);
|
| 118 |
+
--welcome-title: var(--app-text);
|
| 119 |
+
--welcome-subtitle: var(--app-muted);
|
| 120 |
+
--welcome-accent: var(--app-primary);
|
| 121 |
+
--welcome-accent-foreground: var(--app-primary-foreground);
|
| 122 |
+
--welcome-accent-soft: var(--app-primary-soft);
|
| 123 |
+
--welcome-toggle-bg: var(--app-toggle-bg);
|
| 124 |
+
--welcome-card-bg: var(--app-surface);
|
| 125 |
+
--welcome-card-border: var(--app-border);
|
| 126 |
+
--welcome-card-active-border: var(--app-border-strong);
|
| 127 |
+
--welcome-card-active-shadow: var(--app-shadow);
|
| 128 |
+
--welcome-icon-muted-bg: var(--app-surface-muted);
|
| 129 |
+
--welcome-icon-muted-border: var(--app-border-soft);
|
| 130 |
+
--welcome-icon-active-border: color-mix(
|
| 131 |
+
in oklab,
|
| 132 |
+
var(--app-primary) 26%,
|
| 133 |
+
transparent
|
| 134 |
+
);
|
| 135 |
+
--register-shell-bg: var(--welcome-shell-bg);
|
| 136 |
+
--register-card-bg: var(--welcome-card-bg);
|
| 137 |
+
--register-card-border: var(--welcome-card-active-border);
|
| 138 |
+
--register-card-shadow: var(--welcome-card-active-shadow);
|
| 139 |
+
--register-input-bg: color-mix(
|
| 140 |
+
in oklab,
|
| 141 |
+
var(--app-primary) 13%,
|
| 142 |
+
var(--app-surface)
|
| 143 |
+
);
|
| 144 |
+
--register-input-border: var(--app-border-soft);
|
| 145 |
+
--register-button-border: var(--app-border-soft);
|
| 146 |
+
--register-placeholder: var(--welcome-subtitle);
|
| 147 |
+
--student-bg: var(--app-bg);
|
| 148 |
+
--student-sidebar-bg: var(--app-sidebar-bg);
|
| 149 |
+
--student-surface: var(--app-surface);
|
| 150 |
+
--student-surface-soft: var(--app-surface-soft);
|
| 151 |
+
--student-border: var(--app-border);
|
| 152 |
+
--student-border-strong: var(--app-border-strong);
|
| 153 |
+
--student-text: var(--app-text);
|
| 154 |
+
--student-muted: var(--app-muted);
|
| 155 |
+
--student-primary: var(--app-primary);
|
| 156 |
+
--student-primary-soft: var(--app-primary-soft);
|
| 157 |
+
--student-primary-foreground: var(--app-primary-foreground);
|
| 158 |
+
--student-danger: var(--app-danger);
|
| 159 |
+
--student-placeholder: var(--app-placeholder);
|
| 160 |
+
--student-shadow: 0 24px 70px rgba(15, 23, 42, 0.08);
|
| 161 |
+
--student-sidebar-shadow: 1px 0 0 rgba(15, 23, 42, 0.08);
|
| 162 |
+
--student-composer-shadow: var(--app-composer-shadow);
|
| 163 |
}
|
| 164 |
|
| 165 |
.dark {
|
| 166 |
+
--background: #1f1f1f;
|
| 167 |
+
--foreground: #f4f4f5;
|
| 168 |
+
--card: #171717;
|
| 169 |
+
--card-foreground: #f4f4f5;
|
| 170 |
+
--popover: #171717;
|
| 171 |
+
--popover-foreground: #f4f4f5;
|
| 172 |
+
--primary: #3478ff;
|
| 173 |
+
--primary-foreground: #ffffff;
|
| 174 |
+
--secondary: #262626;
|
| 175 |
+
--secondary-foreground: #f4f4f5;
|
| 176 |
+
--muted: #262626;
|
| 177 |
+
--muted-foreground: #737373;
|
| 178 |
+
--accent: #262626;
|
| 179 |
+
--accent-foreground: #f4f4f5;
|
| 180 |
--destructive: oklch(0.396 0.141 25.723);
|
| 181 |
--destructive-foreground: oklch(0.637 0.237 25.331);
|
| 182 |
+
--border: rgba(255, 255, 255, 0.12);
|
| 183 |
+
--input: rgba(255, 255, 255, 0.12);
|
| 184 |
+
--ring: #3478ff;
|
| 185 |
--chart-1: oklch(0.488 0.243 264.376);
|
| 186 |
--chart-2: oklch(0.696 0.17 162.48);
|
| 187 |
--chart-3: oklch(0.769 0.188 70.08);
|
| 188 |
--chart-4: oklch(0.627 0.265 303.9);
|
| 189 |
--chart-5: oklch(0.645 0.246 16.439);
|
| 190 |
+
--sidebar: #171717;
|
| 191 |
+
--sidebar-foreground: #f4f4f5;
|
| 192 |
+
--sidebar-primary: #3478ff;
|
| 193 |
+
--sidebar-primary-foreground: #ffffff;
|
| 194 |
+
--sidebar-accent: #262626;
|
| 195 |
+
--sidebar-accent-foreground: #f4f4f5;
|
| 196 |
+
--sidebar-border: rgba(255, 255, 255, 0.12);
|
| 197 |
+
--sidebar-ring: #3478ff;
|
| 198 |
+
|
| 199 |
+
--app-bg: #1f1f1f;
|
| 200 |
+
--app-sidebar-bg: #171717;
|
| 201 |
+
--app-surface: #171717;
|
| 202 |
+
--app-surface-soft: #262626;
|
| 203 |
+
--app-surface-muted: rgba(255, 255, 255, 0.08);
|
| 204 |
+
--app-border: rgba(255, 255, 255, 0.12);
|
| 205 |
+
--app-border-soft: rgba(255, 255, 255, 0.1);
|
| 206 |
+
--app-border-strong: #3478ff;
|
| 207 |
+
--app-text: #f4f4f5;
|
| 208 |
+
--app-muted: #737373;
|
| 209 |
+
--app-primary: #3478ff;
|
| 210 |
+
--app-primary-soft: #c8d7ff;
|
| 211 |
+
--app-primary-foreground: #ffffff;
|
| 212 |
+
--app-danger: #ff4545;
|
| 213 |
+
--app-placeholder: rgba(244, 244, 245, 0.42);
|
| 214 |
+
--app-toggle-bg: #171717;
|
| 215 |
+
--app-shadow: 0 0 20px rgba(52, 120, 255, 0.34);
|
| 216 |
+
--app-composer-shadow: 0 0 28px rgba(52, 120, 255, 0.36);
|
| 217 |
+
|
| 218 |
+
--welcome-shell-bg: var(--app-bg);
|
| 219 |
+
--welcome-title: var(--app-text);
|
| 220 |
+
--welcome-subtitle: var(--app-muted);
|
| 221 |
+
--welcome-accent: var(--app-primary);
|
| 222 |
+
--welcome-accent-foreground: var(--app-primary-foreground);
|
| 223 |
+
--welcome-accent-soft: var(--app-primary-soft);
|
| 224 |
+
--welcome-toggle-bg: var(--app-toggle-bg);
|
| 225 |
+
--welcome-card-bg: var(--app-surface);
|
| 226 |
+
--welcome-card-border: var(--app-border);
|
| 227 |
+
--welcome-card-active-border: var(--app-border-strong);
|
| 228 |
+
--welcome-card-active-shadow: var(--app-shadow);
|
| 229 |
+
--welcome-icon-muted-bg: var(--app-surface-muted);
|
| 230 |
+
--welcome-icon-muted-border: var(--app-border-soft);
|
| 231 |
+
--welcome-icon-active-border: color-mix(
|
| 232 |
+
in oklab,
|
| 233 |
+
var(--app-primary) 42%,
|
| 234 |
+
transparent
|
| 235 |
+
);
|
| 236 |
+
--register-shell-bg: var(--welcome-shell-bg);
|
| 237 |
+
--register-card-bg: var(--welcome-card-bg);
|
| 238 |
+
--register-card-border: var(--welcome-card-active-border);
|
| 239 |
+
--register-card-shadow: var(--welcome-card-active-shadow);
|
| 240 |
+
--register-input-bg: var(--app-surface);
|
| 241 |
+
--register-input-border: var(--app-border);
|
| 242 |
+
--register-button-border: var(--app-border);
|
| 243 |
+
--register-placeholder: var(--welcome-subtitle);
|
| 244 |
+
--student-bg: var(--app-bg);
|
| 245 |
+
--student-sidebar-bg: var(--app-sidebar-bg);
|
| 246 |
+
--student-surface: var(--app-surface);
|
| 247 |
+
--student-surface-soft: var(--app-surface-soft);
|
| 248 |
+
--student-border: var(--app-border);
|
| 249 |
+
--student-border-strong: var(--app-border-strong);
|
| 250 |
+
--student-text: var(--app-text);
|
| 251 |
+
--student-muted: var(--app-muted);
|
| 252 |
+
--student-primary: var(--app-primary);
|
| 253 |
+
--student-primary-soft: var(--app-primary-soft);
|
| 254 |
+
--student-primary-foreground: var(--app-primary-foreground);
|
| 255 |
+
--student-danger: var(--app-danger);
|
| 256 |
+
--student-placeholder: var(--app-placeholder);
|
| 257 |
+
--student-shadow: 0 24px 70px rgba(0, 0, 0, 0.18);
|
| 258 |
+
--student-sidebar-shadow: 1px 0 0 var(--app-border);
|
| 259 |
+
--student-composer-shadow: var(--app-composer-shadow);
|
| 260 |
}
|
| 261 |
|
| 262 |
@layer base {
|
|
|
|
| 268 |
@apply bg-background text-foreground;
|
| 269 |
}
|
| 270 |
}
|
| 271 |
+
|
| 272 |
+
@layer components {
|
| 273 |
+
.welcome-shell {
|
| 274 |
+
@apply relative flex min-h-svh items-center justify-center px-4 py-8 sm:px-6;
|
| 275 |
+
background-color: var(--welcome-shell-bg);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.welcome-content {
|
| 279 |
+
@apply w-full max-w-[561px] space-y-7;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.welcome-theme-toggle {
|
| 283 |
+
@apply absolute top-4 right-4 h-10 w-10 rounded-[10px] shadow-none backdrop-blur sm:top-8 sm:right-8;
|
| 284 |
+
background-color: var(--welcome-toggle-bg);
|
| 285 |
+
border-color: var(--welcome-card-active-border);
|
| 286 |
+
color: var(--welcome-accent);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.welcome-logo {
|
| 290 |
+
@apply mx-auto flex h-[77px] w-[77px] items-center justify-center rounded-[15px] border border-white;
|
| 291 |
+
background-color: var(--welcome-accent);
|
| 292 |
+
color: var(--welcome-accent-foreground);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.welcome-title {
|
| 296 |
+
@apply text-center text-[34px] leading-tight font-bold tracking-[0.01em] sm:text-[40px];
|
| 297 |
+
color: var(--welcome-title);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.welcome-subtitle {
|
| 301 |
+
@apply text-center text-[15px] leading-normal sm:text-[20px];
|
| 302 |
+
color: var(--welcome-subtitle);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.welcome-entries {
|
| 306 |
+
@apply space-y-3;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.welcome-entry-card {
|
| 310 |
+
@apply gap-0 overflow-hidden rounded-[17px] border py-0 transition-[border-color,box-shadow];
|
| 311 |
+
background-color: var(--welcome-card-bg);
|
| 312 |
+
border-color: var(--welcome-card-border);
|
| 313 |
+
box-shadow: none;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.welcome-entry-card:hover,
|
| 317 |
+
.welcome-entry-card:focus-within {
|
| 318 |
+
border-color: var(--welcome-card-active-border);
|
| 319 |
+
box-shadow: var(--welcome-card-active-shadow);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.welcome-entry-link {
|
| 323 |
+
@apply flex h-[90px] items-center justify-between gap-3 px-3.5 py-2.5 outline-none focus-visible:ring-2 sm:px-4;
|
| 324 |
+
--tw-ring-color: color-mix(
|
| 325 |
+
in oklab,
|
| 326 |
+
var(--welcome-accent) 35%,
|
| 327 |
+
transparent
|
| 328 |
+
);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.welcome-entry-icon {
|
| 332 |
+
@apply flex h-[52px] w-[52px] shrink-0 items-center justify-center rounded-[10px] border transition-colors;
|
| 333 |
+
background-color: var(--welcome-icon-muted-bg);
|
| 334 |
+
border-color: var(--welcome-icon-muted-border);
|
| 335 |
+
color: var(--welcome-subtitle);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.welcome-entry-card:hover .welcome-entry-icon,
|
| 339 |
+
.welcome-entry-card:focus-within .welcome-entry-icon {
|
| 340 |
+
background-color: var(--welcome-accent-soft);
|
| 341 |
+
border-color: var(--welcome-icon-active-border);
|
| 342 |
+
color: var(--welcome-accent);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.welcome-entry-title {
|
| 346 |
+
@apply truncate text-left text-lg leading-tight font-semibold;
|
| 347 |
+
color: var(--welcome-title);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.welcome-entry-description {
|
| 351 |
+
@apply mt-1 truncate text-left text-[13px] leading-tight;
|
| 352 |
+
color: var(--welcome-subtitle);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.welcome-entry-arrow {
|
| 356 |
+
@apply h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5;
|
| 357 |
+
color: var(--welcome-title);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.welcome-register {
|
| 361 |
+
@apply text-center text-[13px];
|
| 362 |
+
color: var(--welcome-subtitle);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
.welcome-register-link {
|
| 366 |
+
@apply font-medium underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:outline-none;
|
| 367 |
+
color: var(--welcome-accent);
|
| 368 |
+
--tw-ring-color: color-mix(
|
| 369 |
+
in oklab,
|
| 370 |
+
var(--welcome-accent) 35%,
|
| 371 |
+
transparent
|
| 372 |
+
);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.register-shell {
|
| 376 |
+
@apply relative flex min-h-svh items-center justify-center px-4 py-8 sm:px-6;
|
| 377 |
+
background-color: var(--register-shell-bg);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.register-choice-content {
|
| 381 |
+
@apply w-full max-w-[561px];
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.register-choice-back {
|
| 385 |
+
@apply mb-3;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.register-choice-options {
|
| 389 |
+
@apply space-y-3;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.register-choice-card[data-highlighted='true'] {
|
| 393 |
+
border-color: var(--welcome-card-active-border);
|
| 394 |
+
box-shadow: var(--welcome-card-active-shadow);
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.register-choice-card[data-highlighted='true'] .welcome-entry-icon {
|
| 398 |
+
background-color: var(--welcome-accent-soft);
|
| 399 |
+
border-color: var(--welcome-icon-active-border);
|
| 400 |
+
color: var(--welcome-accent);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.register-card {
|
| 404 |
+
@apply w-full max-w-[559px] gap-0 rounded-[17px] border py-0;
|
| 405 |
+
background-color: var(--register-card-bg) !important;
|
| 406 |
+
border-color: var(--register-card-border) !important;
|
| 407 |
+
box-shadow: var(--register-card-shadow) !important;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.register-card-content {
|
| 411 |
+
@apply px-5 py-6 sm:px-[38px] sm:py-7;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.register-back-link {
|
| 415 |
+
@apply inline-flex text-sm font-semibold underline-offset-4 transition-colors hover:underline focus-visible:ring-2 focus-visible:outline-none;
|
| 416 |
+
color: var(--welcome-subtitle);
|
| 417 |
+
--tw-ring-color: color-mix(
|
| 418 |
+
in oklab,
|
| 419 |
+
var(--welcome-accent) 35%,
|
| 420 |
+
transparent
|
| 421 |
+
);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.register-heading {
|
| 425 |
+
@apply mt-2 text-center text-2xl leading-tight font-semibold;
|
| 426 |
+
color: var(--welcome-title);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.register-form {
|
| 430 |
+
@apply mt-7 flex flex-col gap-4;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.register-field {
|
| 434 |
+
@apply grid gap-1.5;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.register-label {
|
| 438 |
+
@apply text-base leading-none font-medium;
|
| 439 |
+
color: var(--welcome-title);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.register-input {
|
| 443 |
+
@apply h-[43px] rounded-[6px] px-3.5 text-sm shadow-none;
|
| 444 |
+
background-color: var(--register-input-bg) !important;
|
| 445 |
+
border-color: var(--register-input-border) !important;
|
| 446 |
+
color: var(--welcome-title) !important;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.register-input::placeholder {
|
| 450 |
+
color: var(--register-placeholder);
|
| 451 |
+
opacity: 1;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.register-input:focus-visible {
|
| 455 |
+
border-color: var(--register-card-border) !important;
|
| 456 |
+
--tw-ring-color: color-mix(
|
| 457 |
+
in oklab,
|
| 458 |
+
var(--welcome-accent) 35%,
|
| 459 |
+
transparent
|
| 460 |
+
);
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.register-submit {
|
| 464 |
+
@apply mt-1 h-[43px] w-full rounded-[6px] border text-base font-semibold shadow-none;
|
| 465 |
+
background-color: var(--welcome-accent) !important;
|
| 466 |
+
border-color: var(--register-button-border) !important;
|
| 467 |
+
color: var(--welcome-accent-foreground) !important;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.register-submit:hover {
|
| 471 |
+
background-color: color-mix(
|
| 472 |
+
in oklab,
|
| 473 |
+
var(--welcome-accent) 88%,
|
| 474 |
+
var(--register-card-bg)
|
| 475 |
+
) !important;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.login-card {
|
| 479 |
+
@apply max-w-[561px];
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.login-card-content {
|
| 483 |
+
@apply px-5 py-7 sm:px-[38px] sm:py-8;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.login-heading {
|
| 487 |
+
@apply mt-2;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.login-form {
|
| 491 |
+
@apply mt-8 flex flex-col gap-4;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.login-remember {
|
| 495 |
+
@apply flex items-center gap-3;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.login-remember-label {
|
| 499 |
+
@apply text-sm leading-none font-normal;
|
| 500 |
+
color: var(--welcome-subtitle);
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.login-submit {
|
| 504 |
+
@apply mt-2;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.login-register-text {
|
| 508 |
+
@apply mt-4;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
.login-status {
|
| 512 |
+
@apply mt-4 text-center text-sm font-medium;
|
| 513 |
+
color: var(--welcome-accent);
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.student-dashboard-shell {
|
| 517 |
+
@apply min-h-svh;
|
| 518 |
+
background-color: var(--student-bg);
|
| 519 |
+
color: var(--student-text);
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.student-sidebar {
|
| 523 |
+
border-color: var(--student-border) !important;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.student-sidebar [data-sidebar='sidebar'] {
|
| 527 |
+
background-color: var(--student-sidebar-bg);
|
| 528 |
+
border-color: var(--student-border);
|
| 529 |
+
box-shadow: var(--student-sidebar-shadow);
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.student-sidebar-top {
|
| 533 |
+
@apply border-b;
|
| 534 |
+
border-color: var(--student-border);
|
| 535 |
+
display: flex !important;
|
| 536 |
+
height: 72px !important;
|
| 537 |
+
flex-direction: row !important;
|
| 538 |
+
align-items: center !important;
|
| 539 |
+
justify-content: space-between !important;
|
| 540 |
+
gap: 0 !important;
|
| 541 |
+
padding: 0 20px !important;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
.student-logo-action {
|
| 545 |
+
@apply flex size-8 shrink-0 items-center justify-center rounded-[8px] transition-transform outline-none focus-visible:ring-2;
|
| 546 |
+
--tw-ring-color: color-mix(
|
| 547 |
+
in oklab,
|
| 548 |
+
var(--student-primary) 45%,
|
| 549 |
+
transparent
|
| 550 |
+
);
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.student-logo-action:hover {
|
| 554 |
+
@apply scale-[1.03];
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.student-logo-mark {
|
| 558 |
+
@apply flex size-8 items-center justify-center rounded-[8px];
|
| 559 |
+
background-color: var(--student-primary);
|
| 560 |
+
color: var(--student-primary-foreground);
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.student-sidebar-trigger {
|
| 564 |
+
@apply rounded-[8px] shadow-none;
|
| 565 |
+
color: var(--student-text);
|
| 566 |
+
width: 32px !important;
|
| 567 |
+
height: 32px !important;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.student-sidebar-trigger:hover {
|
| 571 |
+
background-color: var(--student-surface-soft);
|
| 572 |
+
color: var(--student-text);
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.student-sidebar-content {
|
| 576 |
+
gap: 0 !important;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.student-history-group {
|
| 580 |
+
padding: 0 !important;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
.student-history-label {
|
| 584 |
+
@apply border-b text-xs font-medium;
|
| 585 |
+
border-color: var(--student-border);
|
| 586 |
+
color: var(--student-text);
|
| 587 |
+
display: flex !important;
|
| 588 |
+
height: 44px !important;
|
| 589 |
+
align-items: center !important;
|
| 590 |
+
gap: 6px !important;
|
| 591 |
+
border-radius: 0 !important;
|
| 592 |
+
padding: 0 20px !important;
|
| 593 |
+
margin: 0 !important;
|
| 594 |
+
width: 100% !important;
|
| 595 |
+
background-color: transparent !important;
|
| 596 |
+
cursor: pointer;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.student-history-label:hover {
|
| 600 |
+
background-color: var(--student-surface-soft) !important;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.student-history-chevron {
|
| 604 |
+
@apply ml-auto size-3 transition-transform;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
.student-history-label[aria-expanded='false'] .student-history-chevron {
|
| 608 |
+
transform: rotate(-90deg);
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.student-history-list {
|
| 612 |
+
gap: 4px !important;
|
| 613 |
+
padding: 12px 20px !important;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.student-history-menu-button {
|
| 617 |
+
@apply rounded-[8px] text-left shadow-none;
|
| 618 |
+
color: var(--student-muted);
|
| 619 |
+
height: auto !important;
|
| 620 |
+
min-height: 42px !important;
|
| 621 |
+
padding: 8px 4px !important;
|
| 622 |
+
align-items: flex-start !important;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.student-history-menu-button:hover,
|
| 626 |
+
.student-history-menu-button[data-active='true'] {
|
| 627 |
+
background-color: var(--student-surface-soft);
|
| 628 |
+
color: var(--student-text);
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.student-history-menu-icon {
|
| 632 |
+
@apply hidden;
|
| 633 |
+
color: var(--student-text);
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.student-history-copy,
|
| 637 |
+
.student-user-copy {
|
| 638 |
+
@apply min-w-0;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
.student-history-title {
|
| 642 |
+
@apply block truncate text-[11px] leading-tight font-medium;
|
| 643 |
+
color: var(--student-text);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.student-history-meta {
|
| 647 |
+
@apply mt-1 block truncate text-[9px] leading-tight;
|
| 648 |
+
color: var(--student-muted);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.student-sidebar-footer {
|
| 652 |
+
@apply mt-auto border-t;
|
| 653 |
+
border-color: var(--student-border);
|
| 654 |
+
padding: 16px 20px !important;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
.student-user-menu-button {
|
| 658 |
+
@apply mb-2 w-fit gap-2 rounded-[6px] shadow-none;
|
| 659 |
+
background-color: var(--student-surface-soft);
|
| 660 |
+
color: var(--student-text);
|
| 661 |
+
height: auto !important;
|
| 662 |
+
padding: 6px 8px !important;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
.student-user-menu-button:hover {
|
| 666 |
+
background-color: var(--student-surface-soft);
|
| 667 |
+
color: var(--student-text);
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.student-user-avatar {
|
| 671 |
+
@apply size-6 text-[8px];
|
| 672 |
+
background-color: var(--student-surface);
|
| 673 |
+
color: var(--student-text);
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.student-user-name {
|
| 677 |
+
@apply block truncate text-[11px] leading-tight font-semibold;
|
| 678 |
+
color: var(--student-text);
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
.student-user-role {
|
| 682 |
+
@apply block truncate text-[10px] leading-tight;
|
| 683 |
+
color: var(--student-muted);
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
.student-logout-button {
|
| 687 |
+
@apply justify-start gap-1 rounded-none text-[11px] font-normal shadow-none;
|
| 688 |
+
color: var(--student-danger);
|
| 689 |
+
height: auto !important;
|
| 690 |
+
padding: 0 !important;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
.student-logout-button:hover {
|
| 694 |
+
background-color: transparent;
|
| 695 |
+
color: var(--student-danger);
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
.group[data-collapsible='icon'] .student-sidebar-top {
|
| 699 |
+
justify-content: center !important;
|
| 700 |
+
padding: 0 !important;
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
.group[data-collapsible='icon'] .student-sidebar-trigger {
|
| 704 |
+
display: none !important;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.group[data-collapsible='icon'] .student-history-list {
|
| 708 |
+
padding: 12px 8px !important;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
.group[data-collapsible='icon'] .student-history-label {
|
| 712 |
+
display: none !important;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.group[data-collapsible='icon'] .student-history-menu-button,
|
| 716 |
+
.group[data-collapsible='icon'] .student-user-menu-button,
|
| 717 |
+
.group[data-collapsible='icon'] .student-logout-button {
|
| 718 |
+
@apply mx-auto flex items-center justify-center rounded-[8px];
|
| 719 |
+
background-color: transparent;
|
| 720 |
+
width: 32px !important;
|
| 721 |
+
height: 32px !important;
|
| 722 |
+
min-height: 32px !important;
|
| 723 |
+
padding: 0 !important;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
.group[data-collapsible='icon'] .student-history-menu-icon {
|
| 727 |
+
@apply block;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.group[data-collapsible='icon'] .student-history-copy,
|
| 731 |
+
.group[data-collapsible='icon'] .student-user-copy,
|
| 732 |
+
.group[data-collapsible='icon'] .student-logout-button span {
|
| 733 |
+
@apply hidden;
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
.group[data-collapsible='icon'] .student-sidebar-footer {
|
| 737 |
+
@apply px-2;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
.student-workspace {
|
| 741 |
+
@apply flex min-w-0 flex-1 flex-col;
|
| 742 |
+
background-color: var(--student-bg);
|
| 743 |
+
color: var(--student-text);
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
.student-topbar {
|
| 747 |
+
@apply flex h-[72px] shrink-0 items-center justify-between border-b px-5 sm:px-6;
|
| 748 |
+
background-color: var(--student-surface);
|
| 749 |
+
border-color: var(--student-border);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.student-brand-title {
|
| 753 |
+
@apply text-base font-bold;
|
| 754 |
+
color: var(--student-text);
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.student-topbar-actions {
|
| 758 |
+
@apply flex items-center gap-3;
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
.student-theme-toggle {
|
| 762 |
+
@apply static size-8 rounded-[8px] shadow-none sm:static;
|
| 763 |
+
background-color: var(--student-surface) !important;
|
| 764 |
+
border-color: var(--student-border-strong) !important;
|
| 765 |
+
color: var(--student-primary) !important;
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
.student-new-session-button {
|
| 769 |
+
@apply h-8 rounded-[8px] px-4 text-xs font-semibold shadow-none;
|
| 770 |
+
background-color: var(--student-primary) !important;
|
| 771 |
+
color: var(--student-primary-foreground) !important;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
.student-chat-stage {
|
| 775 |
+
@apply flex flex-1 items-center justify-center px-4 py-10;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
.student-empty-state {
|
| 779 |
+
@apply flex w-full max-w-[610px] -translate-y-8 flex-col items-center;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
.student-empty-icon {
|
| 783 |
+
@apply mb-5 flex size-[52px] items-center justify-center rounded-[12px];
|
| 784 |
+
background-color: var(--student-primary-soft);
|
| 785 |
+
color: var(--student-primary);
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
.student-greeting {
|
| 789 |
+
@apply mb-9 text-center;
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
.student-greeting h2 {
|
| 793 |
+
@apply text-3xl leading-tight font-bold tracking-[-0.02em];
|
| 794 |
+
color: var(--student-text);
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
.student-greeting p {
|
| 798 |
+
@apply mt-2 text-sm;
|
| 799 |
+
color: var(--student-muted);
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.student-composer-card {
|
| 803 |
+
@apply w-full gap-0 rounded-[14px] border py-0 shadow-none;
|
| 804 |
+
background-color: var(--student-surface);
|
| 805 |
+
border-color: var(--student-border-strong);
|
| 806 |
+
box-shadow: var(--student-composer-shadow);
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.student-composer-content {
|
| 810 |
+
@apply relative min-h-[225px] px-5 py-4;
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
.student-composer-form {
|
| 814 |
+
display: contents;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.student-status {
|
| 818 |
+
@apply -mt-5 mb-5 text-center text-xs font-medium;
|
| 819 |
+
color: var(--student-primary);
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.student-course-row {
|
| 823 |
+
@apply flex flex-wrap items-center gap-2 text-[11px] font-medium;
|
| 824 |
+
color: var(--student-text);
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
.student-course-icon,
|
| 828 |
+
.student-question-icon {
|
| 829 |
+
color: var(--student-primary);
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.student-course-label {
|
| 833 |
+
@apply whitespace-nowrap;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.student-course-select {
|
| 837 |
+
@apply h-6 w-[180px] rounded-[5px] px-2 text-[10px] shadow-none;
|
| 838 |
+
background-color: var(--student-surface) !important;
|
| 839 |
+
border-color: var(--student-border-strong) !important;
|
| 840 |
+
color: var(--student-text) !important;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.student-course-select svg {
|
| 844 |
+
@apply size-3;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.student-composer-divider {
|
| 848 |
+
@apply my-4 h-px w-full;
|
| 849 |
+
background-color: var(--student-border);
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
.student-question-row {
|
| 853 |
+
@apply flex items-start gap-3;
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
.student-question-row textarea {
|
| 857 |
+
@apply min-h-[118px] flex-1 resize-none bg-transparent pt-0 text-[11px] outline-none;
|
| 858 |
+
color: var(--student-text);
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.student-question-row textarea::placeholder {
|
| 862 |
+
color: var(--student-placeholder);
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
.student-floating-send {
|
| 866 |
+
@apply absolute right-5 bottom-4 size-8 rounded-[8px] shadow-none;
|
| 867 |
+
background-color: var(--student-primary) !important;
|
| 868 |
+
color: var(--student-primary-foreground) !important;
|
| 869 |
+
}
|
| 870 |
+
}
|
resources/js/app.tsx
CHANGED
|
@@ -7,12 +7,19 @@ import AuthLayout from '@/layouts/auth-layout';
|
|
| 7 |
import SettingsLayout from '@/layouts/settings/layout';
|
| 8 |
|
| 9 |
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
createInertiaApp({
|
| 12 |
title: (title) => (title ? `${title} - ${appName}` : appName),
|
| 13 |
layout: (name) => {
|
| 14 |
switch (true) {
|
| 15 |
-
case name
|
| 16 |
return null;
|
| 17 |
case name.startsWith('auth/'):
|
| 18 |
return AuthLayout;
|
|
|
|
| 7 |
import SettingsLayout from '@/layouts/settings/layout';
|
| 8 |
|
| 9 |
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
| 10 |
+
const fullScreenPages = new Set([
|
| 11 |
+
'welcome',
|
| 12 |
+
'auth/login',
|
| 13 |
+
'auth/register-choice',
|
| 14 |
+
'auth/register-dosen',
|
| 15 |
+
'auth/register-mahasiswa',
|
| 16 |
+
]);
|
| 17 |
|
| 18 |
createInertiaApp({
|
| 19 |
title: (title) => (title ? `${title} - ${appName}` : appName),
|
| 20 |
layout: (name) => {
|
| 21 |
switch (true) {
|
| 22 |
+
case fullScreenPages.has(name):
|
| 23 |
return null;
|
| 24 |
case name.startsWith('auth/'):
|
| 25 |
return AuthLayout;
|
resources/js/components/appearance-toggle.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Moon, Sun } from 'lucide-react';
|
| 2 |
+
import { Button } from '@/components/ui/button';
|
| 3 |
+
import { useAppearance } from '@/hooks/use-appearance';
|
| 4 |
+
import { cn } from '@/lib/utils';
|
| 5 |
+
|
| 6 |
+
type AppearanceToggleProps = {
|
| 7 |
+
className?: string;
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
export default function AppearanceToggle({ className }: AppearanceToggleProps) {
|
| 11 |
+
const { resolvedAppearance, updateAppearance } = useAppearance();
|
| 12 |
+
const isDarkMode = resolvedAppearance === 'dark';
|
| 13 |
+
|
| 14 |
+
const handleToggle = (): void => {
|
| 15 |
+
updateAppearance(isDarkMode ? 'light' : 'dark');
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
return (
|
| 19 |
+
<Button
|
| 20 |
+
variant="outline"
|
| 21 |
+
size="icon"
|
| 22 |
+
type="button"
|
| 23 |
+
onClick={handleToggle}
|
| 24 |
+
className={cn('welcome-theme-toggle', className)}
|
| 25 |
+
aria-label="Toggle appearance"
|
| 26 |
+
>
|
| 27 |
+
{isDarkMode ? (
|
| 28 |
+
<Sun className="h-4 w-4" />
|
| 29 |
+
) : (
|
| 30 |
+
<Moon className="h-4 w-4" />
|
| 31 |
+
)}
|
| 32 |
+
</Button>
|
| 33 |
+
);
|
| 34 |
+
}
|
resources/js/components/auth/register-role-form.tsx
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Head, Link } from '@inertiajs/react';
|
| 2 |
+
import type { FormEvent } from 'react';
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import AppearanceToggle from '@/components/appearance-toggle';
|
| 5 |
+
import { Button } from '@/components/ui/button';
|
| 6 |
+
import { Card, CardContent } from '@/components/ui/card';
|
| 7 |
+
import { Input } from '@/components/ui/input';
|
| 8 |
+
import { Label } from '@/components/ui/label';
|
| 9 |
+
import { authApi, getAuthErrorMessage } from '@/lib/auth';
|
| 10 |
+
import { register } from '@/routes';
|
| 11 |
+
|
| 12 |
+
const roleContent = {
|
| 13 |
+
mahasiswa: {
|
| 14 |
+
apiRole: 'student',
|
| 15 |
+
heading: 'Registrasi Mahasiswa',
|
| 16 |
+
identifierLabel: 'NIM',
|
| 17 |
+
identifierPlaceholder: 'NIM',
|
| 18 |
+
},
|
| 19 |
+
dosen: {
|
| 20 |
+
apiRole: 'lecturer',
|
| 21 |
+
heading: 'Registrasi Dosen',
|
| 22 |
+
identifierLabel: 'NIDN',
|
| 23 |
+
identifierPlaceholder: 'NIDN',
|
| 24 |
+
},
|
| 25 |
+
} as const;
|
| 26 |
+
|
| 27 |
+
export type RegisterRole = keyof typeof roleContent;
|
| 28 |
+
|
| 29 |
+
type RegisterRoleFormProps = {
|
| 30 |
+
role: RegisterRole;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
type RegisterFieldProps = {
|
| 34 |
+
autoComplete?: string;
|
| 35 |
+
autoFocus?: boolean;
|
| 36 |
+
label: string;
|
| 37 |
+
name: string;
|
| 38 |
+
placeholder: string;
|
| 39 |
+
required?: boolean;
|
| 40 |
+
tabIndex: number;
|
| 41 |
+
type?: string;
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
function RegisterField({
|
| 45 |
+
autoComplete,
|
| 46 |
+
autoFocus = false,
|
| 47 |
+
label,
|
| 48 |
+
name,
|
| 49 |
+
placeholder,
|
| 50 |
+
required = false,
|
| 51 |
+
tabIndex,
|
| 52 |
+
type = 'text',
|
| 53 |
+
}: RegisterFieldProps) {
|
| 54 |
+
return (
|
| 55 |
+
<div className="register-field">
|
| 56 |
+
<Label htmlFor={name} className="register-label">
|
| 57 |
+
{label}
|
| 58 |
+
</Label>
|
| 59 |
+
<Input
|
| 60 |
+
id={name}
|
| 61 |
+
name={name}
|
| 62 |
+
type={type}
|
| 63 |
+
autoComplete={autoComplete}
|
| 64 |
+
autoFocus={autoFocus}
|
| 65 |
+
placeholder={placeholder}
|
| 66 |
+
required={required}
|
| 67 |
+
tabIndex={tabIndex}
|
| 68 |
+
className="register-input"
|
| 69 |
+
/>
|
| 70 |
+
</div>
|
| 71 |
+
);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
export default function RegisterRoleForm({ role }: RegisterRoleFormProps) {
|
| 75 |
+
const content = roleContent[role];
|
| 76 |
+
const [backendMessage, setBackendMessage] = useState<string>();
|
| 77 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 78 |
+
|
| 79 |
+
const handleSubmit = async (
|
| 80 |
+
event: FormEvent<HTMLFormElement>,
|
| 81 |
+
): Promise<void> => {
|
| 82 |
+
event.preventDefault();
|
| 83 |
+
setBackendMessage(undefined);
|
| 84 |
+
setIsProcessing(true);
|
| 85 |
+
|
| 86 |
+
const formData = new FormData(event.currentTarget);
|
| 87 |
+
|
| 88 |
+
try {
|
| 89 |
+
const response = await authApi.register({
|
| 90 |
+
email: String(formData.get('email') ?? ''),
|
| 91 |
+
password: String(formData.get('password') ?? ''),
|
| 92 |
+
name: String(formData.get('name') ?? ''),
|
| 93 |
+
role: content.apiRole,
|
| 94 |
+
identity_number: String(formData.get('identity_number') ?? ''),
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
setBackendMessage(response.message);
|
| 98 |
+
} catch (error) {
|
| 99 |
+
setBackendMessage(getAuthErrorMessage(error));
|
| 100 |
+
} finally {
|
| 101 |
+
setIsProcessing(false);
|
| 102 |
+
}
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
return (
|
| 106 |
+
<>
|
| 107 |
+
<Head title={content.heading} />
|
| 108 |
+
|
| 109 |
+
<main className="register-shell">
|
| 110 |
+
<AppearanceToggle />
|
| 111 |
+
|
| 112 |
+
<Card className="register-card">
|
| 113 |
+
<CardContent className="register-card-content">
|
| 114 |
+
<Link href={register()} className="register-back-link">
|
| 115 |
+
← Kembali
|
| 116 |
+
</Link>
|
| 117 |
+
|
| 118 |
+
<h1 className="register-heading">{content.heading}</h1>
|
| 119 |
+
|
| 120 |
+
<form className="register-form" onSubmit={handleSubmit}>
|
| 121 |
+
<input type="hidden" name="role" value={role} />
|
| 122 |
+
|
| 123 |
+
<RegisterField
|
| 124 |
+
name="name"
|
| 125 |
+
label="Nama Lengkap"
|
| 126 |
+
placeholder="Masukkan nama lengkap"
|
| 127 |
+
autoComplete="name"
|
| 128 |
+
autoFocus
|
| 129 |
+
required
|
| 130 |
+
tabIndex={1}
|
| 131 |
+
/>
|
| 132 |
+
<RegisterField
|
| 133 |
+
name="identity_number"
|
| 134 |
+
label={content.identifierLabel}
|
| 135 |
+
placeholder={content.identifierPlaceholder}
|
| 136 |
+
required
|
| 137 |
+
tabIndex={2}
|
| 138 |
+
/>
|
| 139 |
+
<RegisterField
|
| 140 |
+
name="email"
|
| 141 |
+
type="email"
|
| 142 |
+
label="Email"
|
| 143 |
+
placeholder="contoh@kampus.ac.id"
|
| 144 |
+
autoComplete="email"
|
| 145 |
+
required
|
| 146 |
+
tabIndex={3}
|
| 147 |
+
/>
|
| 148 |
+
<RegisterField
|
| 149 |
+
name="password"
|
| 150 |
+
type="password"
|
| 151 |
+
label="Password"
|
| 152 |
+
placeholder="Minimal 6 karakter"
|
| 153 |
+
autoComplete="new-password"
|
| 154 |
+
required
|
| 155 |
+
tabIndex={4}
|
| 156 |
+
/>
|
| 157 |
+
|
| 158 |
+
<Button
|
| 159 |
+
type="submit"
|
| 160 |
+
className="register-submit"
|
| 161 |
+
tabIndex={5}
|
| 162 |
+
disabled={isProcessing}
|
| 163 |
+
data-test="register-role-button"
|
| 164 |
+
>
|
| 165 |
+
Register
|
| 166 |
+
</Button>
|
| 167 |
+
</form>
|
| 168 |
+
|
| 169 |
+
{backendMessage && (
|
| 170 |
+
<p className="login-status">{backendMessage}</p>
|
| 171 |
+
)}
|
| 172 |
+
</CardContent>
|
| 173 |
+
</Card>
|
| 174 |
+
</main>
|
| 175 |
+
</>
|
| 176 |
+
);
|
| 177 |
+
}
|
resources/js/layouts/app-layout.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout';
|
| 2 |
import type { BreadcrumbItem } from '@/types';
|
| 3 |
|
| 4 |
export default function AppLayout({
|
|
@@ -8,9 +8,5 @@ export default function AppLayout({
|
|
| 8 |
breadcrumbs?: BreadcrumbItem[];
|
| 9 |
children: React.ReactNode;
|
| 10 |
}) {
|
| 11 |
-
return
|
| 12 |
-
<AppLayoutTemplate breadcrumbs={breadcrumbs}>
|
| 13 |
-
{children}
|
| 14 |
-
</AppLayoutTemplate>
|
| 15 |
-
);
|
| 16 |
}
|
|
|
|
| 1 |
+
// import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout';
|
| 2 |
import type { BreadcrumbItem } from '@/types';
|
| 3 |
|
| 4 |
export default function AppLayout({
|
|
|
|
| 8 |
breadcrumbs?: BreadcrumbItem[];
|
| 9 |
children: React.ReactNode;
|
| 10 |
}) {
|
| 11 |
+
return <div>{children}</div>;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
}
|
resources/js/lib/api.ts
CHANGED
|
@@ -91,7 +91,11 @@ async function parseResponseBody(response: Response): Promise<unknown> {
|
|
| 91 |
return text.length > 0 ? text : null;
|
| 92 |
}
|
| 93 |
|
| 94 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
if (
|
| 96 |
typeof data === 'object' &&
|
| 97 |
data !== null &&
|
|
@@ -101,15 +105,52 @@ function resolveErrorMessage(status: number, data: unknown): string {
|
|
| 101 |
return (data as { message: string }).message;
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
|
| 107 |
export async function apiFetch<TResponse>(
|
| 108 |
path: string,
|
| 109 |
options: ApiRequestOptions = {},
|
| 110 |
): Promise<TResponse> {
|
| 111 |
-
const {
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
const headers = new Headers(initialHeaders);
|
| 115 |
headers.set('Accept', 'application/json');
|
|
@@ -128,7 +169,7 @@ export async function apiFetch<TResponse>(
|
|
| 128 |
|
| 129 |
if (!response.ok) {
|
| 130 |
throw new ApiError(
|
| 131 |
-
|
| 132 |
response.status,
|
| 133 |
data,
|
| 134 |
);
|
|
|
|
| 91 |
return text.length > 0 ? text : null;
|
| 92 |
}
|
| 93 |
|
| 94 |
+
export function getBackendErrorMessage(data: unknown): string | undefined {
|
| 95 |
+
if (typeof data === 'string') {
|
| 96 |
+
return data;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
if (
|
| 100 |
typeof data === 'object' &&
|
| 101 |
data !== null &&
|
|
|
|
| 105 |
return (data as { message: string }).message;
|
| 106 |
}
|
| 107 |
|
| 108 |
+
if (
|
| 109 |
+
typeof data === 'object' &&
|
| 110 |
+
data !== null &&
|
| 111 |
+
'error' in data &&
|
| 112 |
+
typeof (data as { error?: unknown }).error === 'string'
|
| 113 |
+
) {
|
| 114 |
+
return (data as { error: string }).error;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
if (typeof data === 'object' && data !== null && 'detail' in data) {
|
| 118 |
+
const detail = (data as { detail?: unknown }).detail;
|
| 119 |
+
|
| 120 |
+
if (typeof detail === 'string') {
|
| 121 |
+
return detail;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
if (Array.isArray(detail)) {
|
| 125 |
+
const firstError = detail.find(
|
| 126 |
+
(item): item is { msg?: unknown; message?: unknown } =>
|
| 127 |
+
typeof item === 'object' && item !== null,
|
| 128 |
+
);
|
| 129 |
+
|
| 130 |
+
if (typeof firstError?.msg === 'string') {
|
| 131 |
+
return firstError.msg;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
if (typeof firstError?.message === 'string') {
|
| 135 |
+
return firstError.message;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return undefined;
|
| 141 |
}
|
| 142 |
|
| 143 |
export async function apiFetch<TResponse>(
|
| 144 |
path: string,
|
| 145 |
options: ApiRequestOptions = {},
|
| 146 |
): Promise<TResponse> {
|
| 147 |
+
const {
|
| 148 |
+
body,
|
| 149 |
+
query,
|
| 150 |
+
token,
|
| 151 |
+
headers: initialHeaders,
|
| 152 |
+
...requestInit
|
| 153 |
+
} = options;
|
| 154 |
|
| 155 |
const headers = new Headers(initialHeaders);
|
| 156 |
headers.set('Accept', 'application/json');
|
|
|
|
| 169 |
|
| 170 |
if (!response.ok) {
|
| 171 |
throw new ApiError(
|
| 172 |
+
getBackendErrorMessage(data) ?? '',
|
| 173 |
response.status,
|
| 174 |
data,
|
| 175 |
);
|
resources/js/lib/auth.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { api, ApiError, getBackendErrorMessage } from '@/lib/api';
|
| 2 |
+
|
| 3 |
+
const AUTH_TOKEN_KEY = 'sevima_raghub_auth_token';
|
| 4 |
+
const AUTH_USER_KEY = 'sevima_raghub_auth_user';
|
| 5 |
+
|
| 6 |
+
export type AuthRole = 'admin' | 'student' | 'lecturer';
|
| 7 |
+
|
| 8 |
+
export type AuthUser = {
|
| 9 |
+
id: number;
|
| 10 |
+
identity_number: string;
|
| 11 |
+
name?: string;
|
| 12 |
+
role: AuthRole;
|
| 13 |
+
email: string;
|
| 14 |
+
status: 'active' | 'inactive';
|
| 15 |
+
created_at: string;
|
| 16 |
+
updated_at: string;
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
export type LoginRequest = {
|
| 20 |
+
email: string;
|
| 21 |
+
password: string;
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
export type LoginResponse = {
|
| 25 |
+
token: string;
|
| 26 |
+
user: AuthUser;
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
export type RegisterRequest = {
|
| 30 |
+
email: string;
|
| 31 |
+
password: string;
|
| 32 |
+
name: string;
|
| 33 |
+
role: AuthRole;
|
| 34 |
+
identity_number: string;
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
export type RegisterResponse = {
|
| 38 |
+
message: string;
|
| 39 |
+
user: AuthUser;
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
export const authApi = {
|
| 43 |
+
login(payload: LoginRequest) {
|
| 44 |
+
return api.post<LoginResponse>('/auth/login', payload);
|
| 45 |
+
},
|
| 46 |
+
|
| 47 |
+
register(payload: RegisterRequest) {
|
| 48 |
+
return api.post<RegisterResponse>('/auth/register', payload);
|
| 49 |
+
},
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
function getBrowserStorage(remember: boolean): Storage | null {
|
| 53 |
+
if (typeof window === 'undefined') {
|
| 54 |
+
return null;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return remember ? window.localStorage : window.sessionStorage;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export function saveAuthSession(
|
| 61 |
+
response: LoginResponse,
|
| 62 |
+
remember: boolean,
|
| 63 |
+
): void {
|
| 64 |
+
const storage = getBrowserStorage(remember);
|
| 65 |
+
const staleStorage = getBrowserStorage(!remember);
|
| 66 |
+
|
| 67 |
+
if (!storage) {
|
| 68 |
+
return;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
storage.setItem(AUTH_TOKEN_KEY, response.token);
|
| 72 |
+
storage.setItem(AUTH_USER_KEY, JSON.stringify(response.user));
|
| 73 |
+
|
| 74 |
+
staleStorage?.removeItem(AUTH_TOKEN_KEY);
|
| 75 |
+
staleStorage?.removeItem(AUTH_USER_KEY);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export function getStoredAuthToken(): string | null {
|
| 79 |
+
if (typeof window === 'undefined') {
|
| 80 |
+
return null;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
return (
|
| 84 |
+
window.localStorage.getItem(AUTH_TOKEN_KEY) ??
|
| 85 |
+
window.sessionStorage.getItem(AUTH_TOKEN_KEY)
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export function getStoredAuthUser(): AuthUser | null {
|
| 90 |
+
if (typeof window === 'undefined') {
|
| 91 |
+
return null;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const rawUser =
|
| 95 |
+
window.localStorage.getItem(AUTH_USER_KEY) ??
|
| 96 |
+
window.sessionStorage.getItem(AUTH_USER_KEY);
|
| 97 |
+
|
| 98 |
+
if (!rawUser) {
|
| 99 |
+
return null;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
try {
|
| 103 |
+
return JSON.parse(rawUser) as AuthUser;
|
| 104 |
+
} catch {
|
| 105 |
+
return null;
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export function clearAuthSession(): void {
|
| 110 |
+
if (typeof window === 'undefined') {
|
| 111 |
+
return;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
window.localStorage.removeItem(AUTH_TOKEN_KEY);
|
| 115 |
+
window.localStorage.removeItem(AUTH_USER_KEY);
|
| 116 |
+
window.sessionStorage.removeItem(AUTH_TOKEN_KEY);
|
| 117 |
+
window.sessionStorage.removeItem(AUTH_USER_KEY);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
export function getAuthErrorMessage(error: unknown): string | undefined {
|
| 121 |
+
if (error instanceof ApiError) {
|
| 122 |
+
return getBackendErrorMessage(error.data);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return undefined;
|
| 126 |
+
}
|
resources/js/lib/rag.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { api, ApiError, getBackendErrorMessage } from '@/lib/api';
|
| 2 |
+
|
| 3 |
+
export type CourseResponse = {
|
| 4 |
+
id: number;
|
| 5 |
+
title: string;
|
| 6 |
+
description?: string;
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
export type CourseListResponse = {
|
| 10 |
+
data: CourseResponse[];
|
| 11 |
+
total: number;
|
| 12 |
+
page: number;
|
| 13 |
+
limit: number;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
export type ChatSessionResponse = {
|
| 17 |
+
uuid_id: string;
|
| 18 |
+
user_id: string;
|
| 19 |
+
user_name?: string;
|
| 20 |
+
course_id: string;
|
| 21 |
+
course_name?: string;
|
| 22 |
+
title: string;
|
| 23 |
+
message_count: number;
|
| 24 |
+
last_message_at?: string;
|
| 25 |
+
created_at: string;
|
| 26 |
+
updated_at: string;
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
export type ChatSessionListResponse = {
|
| 30 |
+
data: ChatSessionResponse[];
|
| 31 |
+
pagination: unknown;
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
export type CreateChatSessionRequest = {
|
| 35 |
+
course_id: string;
|
| 36 |
+
title: string;
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
export type ChatRestRequest = {
|
| 40 |
+
content: string;
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
export type ChatMessageResponse = {
|
| 44 |
+
uuid_id: string;
|
| 45 |
+
role: string;
|
| 46 |
+
content: string;
|
| 47 |
+
sources: {
|
| 48 |
+
document_id: string;
|
| 49 |
+
filename: string;
|
| 50 |
+
excerpt: string;
|
| 51 |
+
}[];
|
| 52 |
+
created_at: string;
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
type ListCoursesOptions = {
|
| 56 |
+
page?: number;
|
| 57 |
+
limit?: number;
|
| 58 |
+
signal?: AbortSignal;
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
type AuthenticatedListOptions = ListCoursesOptions & {
|
| 62 |
+
token?: string;
|
| 63 |
+
courseId?: string;
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
export const ragApi = {
|
| 67 |
+
listCourses({ page = 1, limit = 100, signal }: ListCoursesOptions = {}) {
|
| 68 |
+
return api.get<CourseListResponse>('/courses', {
|
| 69 |
+
query: { page, limit },
|
| 70 |
+
signal,
|
| 71 |
+
});
|
| 72 |
+
},
|
| 73 |
+
|
| 74 |
+
listChatSessions({
|
| 75 |
+
page = 1,
|
| 76 |
+
limit = 20,
|
| 77 |
+
token,
|
| 78 |
+
courseId,
|
| 79 |
+
signal,
|
| 80 |
+
}: AuthenticatedListOptions = {}) {
|
| 81 |
+
return api.get<ChatSessionListResponse>('/chats/sessions', {
|
| 82 |
+
query: { page, limit, course_id: courseId },
|
| 83 |
+
signal,
|
| 84 |
+
token,
|
| 85 |
+
});
|
| 86 |
+
},
|
| 87 |
+
|
| 88 |
+
createChatSession(payload: CreateChatSessionRequest, token?: string) {
|
| 89 |
+
return api.post<ChatSessionResponse>('/chats/sessions', payload, {
|
| 90 |
+
token,
|
| 91 |
+
});
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
sendMessage(sessionId: string, payload: ChatRestRequest, token?: string) {
|
| 95 |
+
return api.post<ChatMessageResponse>(
|
| 96 |
+
`/chats/sessions/${sessionId}/messages`,
|
| 97 |
+
payload,
|
| 98 |
+
{ token },
|
| 99 |
+
);
|
| 100 |
+
},
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
export function getRagErrorMessage(error: unknown): string | undefined {
|
| 104 |
+
if (error instanceof ApiError) {
|
| 105 |
+
return getBackendErrorMessage(error.data);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return undefined;
|
| 109 |
+
}
|
resources/js/pages/auth/login.tsx
CHANGED
|
@@ -1,121 +1,157 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
import { Button } from '@/components/ui/button';
|
|
|
|
| 6 |
import { Checkbox } from '@/components/ui/checkbox';
|
| 7 |
import { Input } from '@/components/ui/input';
|
| 8 |
import { Label } from '@/components/ui/label';
|
| 9 |
-
import {
|
| 10 |
-
import { register } from '@/routes';
|
| 11 |
-
import { store } from '@/routes/login';
|
| 12 |
-
import { request } from '@/routes/password';
|
| 13 |
|
| 14 |
-
type
|
|
|
|
| 15 |
status?: string;
|
| 16 |
-
canResetPassword: boolean;
|
| 17 |
-
canRegister: boolean;
|
| 18 |
};
|
| 19 |
|
| 20 |
-
export default function Login({
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
return (
|
| 26 |
<>
|
| 27 |
-
<Head title="
|
| 28 |
-
|
| 29 |
-
<
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
className="
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
<
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
<Input
|
| 40 |
id="email"
|
| 41 |
-
type="email"
|
| 42 |
name="email"
|
| 43 |
-
|
|
|
|
| 44 |
autoFocus
|
|
|
|
| 45 |
tabIndex={1}
|
| 46 |
-
|
| 47 |
-
placeholder="email@example.com"
|
| 48 |
/>
|
| 49 |
-
<InputError message={errors.email} />
|
| 50 |
</div>
|
| 51 |
|
| 52 |
-
<div className="
|
| 53 |
-
<
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
>
|
| 61 |
-
Forgot password?
|
| 62 |
-
</TextLink>
|
| 63 |
-
)}
|
| 64 |
-
</div>
|
| 65 |
-
<PasswordInput
|
| 66 |
id="password"
|
| 67 |
name="password"
|
| 68 |
-
|
| 69 |
-
tabIndex={2}
|
| 70 |
autoComplete="current-password"
|
| 71 |
-
placeholder="
|
|
|
|
|
|
|
| 72 |
/>
|
| 73 |
-
<InputError message={errors.password} />
|
| 74 |
</div>
|
| 75 |
|
| 76 |
-
<div className="
|
| 77 |
<Checkbox
|
| 78 |
id="remember"
|
| 79 |
name="remember"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
tabIndex={3}
|
| 81 |
/>
|
| 82 |
-
<Label
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
</div>
|
| 84 |
|
| 85 |
<Button
|
| 86 |
type="submit"
|
| 87 |
-
className="
|
| 88 |
tabIndex={4}
|
| 89 |
-
disabled={
|
| 90 |
data-test="login-button"
|
| 91 |
>
|
| 92 |
-
|
| 93 |
-
Log in
|
| 94 |
</Button>
|
| 95 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
{canRegister && (
|
| 98 |
-
<
|
| 99 |
-
|
| 100 |
-
<
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
)}
|
| 105 |
-
</>
|
| 106 |
-
|
| 107 |
-
</
|
| 108 |
-
|
| 109 |
-
{status && (
|
| 110 |
-
<div className="mb-4 text-center text-sm font-medium text-green-600">
|
| 111 |
-
{status}
|
| 112 |
-
</div>
|
| 113 |
-
)}
|
| 114 |
</>
|
| 115 |
);
|
| 116 |
}
|
| 117 |
-
|
| 118 |
-
Login.layout = {
|
| 119 |
-
title: 'Log in to your account',
|
| 120 |
-
description: 'Enter your email and password below to log in',
|
| 121 |
-
};
|
|
|
|
| 1 |
+
import { Head, Link, router } from '@inertiajs/react';
|
| 2 |
+
import type { FormEvent } from 'react';
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import AppearanceToggle from '@/components/appearance-toggle';
|
| 5 |
import { Button } from '@/components/ui/button';
|
| 6 |
+
import { Card, CardContent } from '@/components/ui/card';
|
| 7 |
import { Checkbox } from '@/components/ui/checkbox';
|
| 8 |
import { Input } from '@/components/ui/input';
|
| 9 |
import { Label } from '@/components/ui/label';
|
| 10 |
+
import { authApi, getAuthErrorMessage, saveAuthSession } from '@/lib/auth';
|
| 11 |
+
import { dashboard, home, mahasiswa, register } from '@/routes';
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
type LoginProps = {
|
| 14 |
+
canRegister?: boolean;
|
| 15 |
status?: string;
|
|
|
|
|
|
|
| 16 |
};
|
| 17 |
|
| 18 |
+
export default function Login({ canRegister = true, status }: LoginProps) {
|
| 19 |
+
const [backendMessage, setBackendMessage] = useState<string>();
|
| 20 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 21 |
+
const [remember, setRemember] = useState(true);
|
| 22 |
+
|
| 23 |
+
const handleSubmit = async (
|
| 24 |
+
event: FormEvent<HTMLFormElement>,
|
| 25 |
+
): Promise<void> => {
|
| 26 |
+
event.preventDefault();
|
| 27 |
+
setBackendMessage(undefined);
|
| 28 |
+
setIsProcessing(true);
|
| 29 |
+
|
| 30 |
+
const formData = new FormData(event.currentTarget);
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
const response = await authApi.login({
|
| 34 |
+
email: String(formData.get('email') ?? ''),
|
| 35 |
+
password: String(formData.get('password') ?? ''),
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
saveAuthSession(response, remember);
|
| 39 |
+
|
| 40 |
+
router.visit(
|
| 41 |
+
response.user.role === 'student' ? mahasiswa() : dashboard(),
|
| 42 |
+
);
|
| 43 |
+
} catch (error) {
|
| 44 |
+
setBackendMessage(getAuthErrorMessage(error));
|
| 45 |
+
} finally {
|
| 46 |
+
setIsProcessing(false);
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
return (
|
| 51 |
<>
|
| 52 |
+
<Head title="Login" />
|
| 53 |
+
|
| 54 |
+
<main className="register-shell">
|
| 55 |
+
<AppearanceToggle />
|
| 56 |
+
|
| 57 |
+
<Card className="register-card login-card">
|
| 58 |
+
<CardContent className="login-card-content">
|
| 59 |
+
<Link href={home()} className="register-back-link">
|
| 60 |
+
← Kembali
|
| 61 |
+
</Link>
|
| 62 |
+
|
| 63 |
+
<h1 className="register-heading login-heading">
|
| 64 |
+
Login
|
| 65 |
+
</h1>
|
| 66 |
+
|
| 67 |
+
<form className="login-form" onSubmit={handleSubmit}>
|
| 68 |
+
<div className="register-field">
|
| 69 |
+
<Label
|
| 70 |
+
htmlFor="email"
|
| 71 |
+
className="register-label"
|
| 72 |
+
>
|
| 73 |
+
Email
|
| 74 |
+
</Label>
|
| 75 |
<Input
|
| 76 |
id="email"
|
|
|
|
| 77 |
name="email"
|
| 78 |
+
type="email"
|
| 79 |
+
autoComplete="email"
|
| 80 |
autoFocus
|
| 81 |
+
placeholder="Masukkan email disini"
|
| 82 |
tabIndex={1}
|
| 83 |
+
className="register-input"
|
|
|
|
| 84 |
/>
|
|
|
|
| 85 |
</div>
|
| 86 |
|
| 87 |
+
<div className="register-field">
|
| 88 |
+
<Label
|
| 89 |
+
htmlFor="password"
|
| 90 |
+
className="register-label"
|
| 91 |
+
>
|
| 92 |
+
Password
|
| 93 |
+
</Label>
|
| 94 |
+
<Input
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
id="password"
|
| 96 |
name="password"
|
| 97 |
+
type="password"
|
|
|
|
| 98 |
autoComplete="current-password"
|
| 99 |
+
placeholder="********"
|
| 100 |
+
tabIndex={2}
|
| 101 |
+
className="register-input"
|
| 102 |
/>
|
|
|
|
| 103 |
</div>
|
| 104 |
|
| 105 |
+
<div className="login-remember">
|
| 106 |
<Checkbox
|
| 107 |
id="remember"
|
| 108 |
name="remember"
|
| 109 |
+
checked={remember}
|
| 110 |
+
onCheckedChange={(checked) => {
|
| 111 |
+
setRemember(checked === true);
|
| 112 |
+
}}
|
| 113 |
tabIndex={3}
|
| 114 |
/>
|
| 115 |
+
<Label
|
| 116 |
+
htmlFor="remember"
|
| 117 |
+
className="login-remember-label"
|
| 118 |
+
>
|
| 119 |
+
Remember me
|
| 120 |
+
</Label>
|
| 121 |
</div>
|
| 122 |
|
| 123 |
<Button
|
| 124 |
type="submit"
|
| 125 |
+
className="register-submit login-submit"
|
| 126 |
tabIndex={4}
|
| 127 |
+
disabled={isProcessing}
|
| 128 |
data-test="login-button"
|
| 129 |
>
|
| 130 |
+
Sign In
|
|
|
|
| 131 |
</Button>
|
| 132 |
+
</form>
|
| 133 |
+
|
| 134 |
+
{(backendMessage ?? status) && (
|
| 135 |
+
<p className="login-status">
|
| 136 |
+
{backendMessage ?? status}
|
| 137 |
+
</p>
|
| 138 |
+
)}
|
| 139 |
|
| 140 |
{canRegister && (
|
| 141 |
+
<p className="welcome-register login-register-text">
|
| 142 |
+
Belum punya akun?{' '}
|
| 143 |
+
<Link
|
| 144 |
+
href={register()}
|
| 145 |
+
className="welcome-register-link"
|
| 146 |
+
tabIndex={5}
|
| 147 |
+
>
|
| 148 |
+
Register
|
| 149 |
+
</Link>
|
| 150 |
+
</p>
|
| 151 |
)}
|
| 152 |
+
</CardContent>
|
| 153 |
+
</Card>
|
| 154 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
</>
|
| 156 |
);
|
| 157 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resources/js/pages/auth/register-choice.tsx
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Head, Link } from '@inertiajs/react';
|
| 2 |
+
import type { LucideIcon } from 'lucide-react';
|
| 3 |
+
import { BookOpen, ChevronRight, MessageSquare } from 'lucide-react';
|
| 4 |
+
import type { ComponentProps } from 'react';
|
| 5 |
+
import AppearanceToggle from '@/components/appearance-toggle';
|
| 6 |
+
import { Card, CardContent } from '@/components/ui/card';
|
| 7 |
+
import { home } from '@/routes';
|
| 8 |
+
import { dosen, mahasiswa } from '@/routes/register';
|
| 9 |
+
|
| 10 |
+
type RegisterOptionCardProps = {
|
| 11 |
+
description: string;
|
| 12 |
+
href: NonNullable<ComponentProps<typeof Link>['href']>;
|
| 13 |
+
icon: LucideIcon;
|
| 14 |
+
isHighlighted?: boolean;
|
| 15 |
+
title: string;
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
function RegisterOptionCard({
|
| 19 |
+
description,
|
| 20 |
+
href,
|
| 21 |
+
icon: Icon,
|
| 22 |
+
isHighlighted = false,
|
| 23 |
+
title,
|
| 24 |
+
}: RegisterOptionCardProps) {
|
| 25 |
+
return (
|
| 26 |
+
<Card className="welcome-entry-card register-choice-card px-1 py-0">
|
| 27 |
+
<CardContent className="p-0">
|
| 28 |
+
<Link href={href} className="group welcome-entry-link">
|
| 29 |
+
<div className="flex min-w-0 items-start gap-3">
|
| 30 |
+
<div className="welcome-entry-icon">
|
| 31 |
+
<Icon className="h-5 w-5" />
|
| 32 |
+
</div>
|
| 33 |
+
<div className="min-w-0">
|
| 34 |
+
<p className="welcome-entry-title">{title}</p>
|
| 35 |
+
<p className="welcome-entry-description">
|
| 36 |
+
{description}
|
| 37 |
+
</p>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
<ChevronRight className="welcome-entry-arrow" />
|
| 41 |
+
</Link>
|
| 42 |
+
</CardContent>
|
| 43 |
+
</Card>
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export default function RegisterChoice() {
|
| 48 |
+
return (
|
| 49 |
+
<>
|
| 50 |
+
<Head title="Register" />
|
| 51 |
+
|
| 52 |
+
<main className="register-shell">
|
| 53 |
+
<AppearanceToggle />
|
| 54 |
+
|
| 55 |
+
<section className="register-choice-content">
|
| 56 |
+
<Link
|
| 57 |
+
href={home()}
|
| 58 |
+
className="register-back-link register-choice-back"
|
| 59 |
+
>
|
| 60 |
+
← Kembali
|
| 61 |
+
</Link>
|
| 62 |
+
|
| 63 |
+
<div className="register-choice-options">
|
| 64 |
+
<RegisterOptionCard
|
| 65 |
+
href={dosen()}
|
| 66 |
+
icon={BookOpen}
|
| 67 |
+
title="Register sebagai Dosen"
|
| 68 |
+
description="Kelola knowledge base dan dokumen"
|
| 69 |
+
/>
|
| 70 |
+
<RegisterOptionCard
|
| 71 |
+
href={mahasiswa()}
|
| 72 |
+
icon={MessageSquare}
|
| 73 |
+
title="Register sebagai Mahasiswa"
|
| 74 |
+
description="Tanya jawab materi kuliah dengan AI"
|
| 75 |
+
isHighlighted
|
| 76 |
+
/>
|
| 77 |
+
</div>
|
| 78 |
+
</section>
|
| 79 |
+
</main>
|
| 80 |
+
</>
|
| 81 |
+
);
|
| 82 |
+
}
|
resources/js/pages/auth/register-dosen.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import RegisterRoleForm from '@/components/auth/register-role-form';
|
| 2 |
+
|
| 3 |
+
export default function RegisterDosen() {
|
| 4 |
+
return <RegisterRoleForm role="dosen" />;
|
| 5 |
+
}
|
resources/js/pages/auth/register-mahasiswa.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import RegisterRoleForm from '@/components/auth/register-role-form';
|
| 2 |
+
|
| 3 |
+
export default function RegisterMahasiswa() {
|
| 4 |
+
return <RegisterRoleForm role="mahasiswa" />;
|
| 5 |
+
}
|
resources/js/pages/mahasiswa.tsx
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Head, router } from '@inertiajs/react';
|
| 2 |
+
import {
|
| 3 |
+
BookOpen,
|
| 4 |
+
ChevronDown,
|
| 5 |
+
Clock3,
|
| 6 |
+
LogOut,
|
| 7 |
+
MessageSquare,
|
| 8 |
+
Send,
|
| 9 |
+
Sparkles,
|
| 10 |
+
} from 'lucide-react';
|
| 11 |
+
import type { CSSProperties, FormEvent } from 'react';
|
| 12 |
+
import { useEffect, useState } from 'react';
|
| 13 |
+
|
| 14 |
+
import AppearanceToggle from '@/components/appearance-toggle';
|
| 15 |
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
| 16 |
+
import { Button } from '@/components/ui/button';
|
| 17 |
+
import { Card, CardContent } from '@/components/ui/card';
|
| 18 |
+
import {
|
| 19 |
+
Select,
|
| 20 |
+
SelectContent,
|
| 21 |
+
SelectItem,
|
| 22 |
+
SelectTrigger,
|
| 23 |
+
SelectValue,
|
| 24 |
+
} from '@/components/ui/select';
|
| 25 |
+
import {
|
| 26 |
+
Sidebar,
|
| 27 |
+
SidebarContent,
|
| 28 |
+
SidebarFooter,
|
| 29 |
+
SidebarGroup,
|
| 30 |
+
SidebarGroupContent,
|
| 31 |
+
SidebarGroupLabel,
|
| 32 |
+
SidebarHeader,
|
| 33 |
+
SidebarInset,
|
| 34 |
+
SidebarMenu,
|
| 35 |
+
SidebarMenuButton,
|
| 36 |
+
SidebarMenuItem,
|
| 37 |
+
SidebarProvider,
|
| 38 |
+
SidebarRail,
|
| 39 |
+
SidebarTrigger,
|
| 40 |
+
useSidebar,
|
| 41 |
+
} from '@/components/ui/sidebar';
|
| 42 |
+
import {
|
| 43 |
+
clearAuthSession,
|
| 44 |
+
getStoredAuthToken,
|
| 45 |
+
getStoredAuthUser,
|
| 46 |
+
} from '@/lib/auth';
|
| 47 |
+
import type { ChatSessionResponse, CourseResponse } from '@/lib/rag';
|
| 48 |
+
import { getRagErrorMessage, ragApi } from '@/lib/rag';
|
| 49 |
+
import { login } from '@/routes';
|
| 50 |
+
|
| 51 |
+
const sidebarStyle = {
|
| 52 |
+
'--sidebar-width': '212px',
|
| 53 |
+
'--sidebar-width-icon': '64px',
|
| 54 |
+
} as CSSProperties;
|
| 55 |
+
|
| 56 |
+
function getStoredStudentName(): string {
|
| 57 |
+
return getStoredAuthUser()?.name ?? 'Mahasiswa';
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function getInitials(name: string): string {
|
| 61 |
+
return name
|
| 62 |
+
.split(' ')
|
| 63 |
+
.filter(Boolean)
|
| 64 |
+
.slice(0, 2)
|
| 65 |
+
.map((part) => part[0]?.toUpperCase())
|
| 66 |
+
.join('');
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function handleLogout(): void {
|
| 70 |
+
clearAuthSession();
|
| 71 |
+
router.visit(login());
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function isAbortError(error: unknown): boolean {
|
| 75 |
+
return error instanceof DOMException && error.name === 'AbortError';
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function formatSessionTime(value?: string): string {
|
| 79 |
+
if (!value) {
|
| 80 |
+
return '';
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const date = new Date(value);
|
| 84 |
+
|
| 85 |
+
if (Number.isNaN(date.getTime())) {
|
| 86 |
+
return value;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
return new Intl.DateTimeFormat('en-US', {
|
| 90 |
+
day: 'numeric',
|
| 91 |
+
hour: 'numeric',
|
| 92 |
+
minute: '2-digit',
|
| 93 |
+
month: 'short',
|
| 94 |
+
}).format(date);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function StudentSidebarLogo() {
|
| 98 |
+
const { state, toggleSidebar } = useSidebar();
|
| 99 |
+
|
| 100 |
+
const handleLogoClick = (): void => {
|
| 101 |
+
if (state === 'collapsed') {
|
| 102 |
+
toggleSidebar();
|
| 103 |
+
}
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
return (
|
| 107 |
+
<button
|
| 108 |
+
aria-label="Buka sidebar"
|
| 109 |
+
className="student-logo-action"
|
| 110 |
+
onClick={handleLogoClick}
|
| 111 |
+
type="button"
|
| 112 |
+
>
|
| 113 |
+
<span className="student-logo-mark">
|
| 114 |
+
<MessageSquare className="size-5" />
|
| 115 |
+
</span>
|
| 116 |
+
</button>
|
| 117 |
+
);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function StudentSidebar({
|
| 121 |
+
chatSessions,
|
| 122 |
+
isLoading,
|
| 123 |
+
studentName,
|
| 124 |
+
}: {
|
| 125 |
+
chatSessions: ChatSessionResponse[];
|
| 126 |
+
isLoading: boolean;
|
| 127 |
+
studentName: string;
|
| 128 |
+
}) {
|
| 129 |
+
const [isHistoryOpen, setIsHistoryOpen] = useState(true);
|
| 130 |
+
const { state } = useSidebar();
|
| 131 |
+
const isSidebarCollapsed = state === 'collapsed';
|
| 132 |
+
|
| 133 |
+
return (
|
| 134 |
+
<Sidebar className="student-sidebar" collapsible="icon">
|
| 135 |
+
<SidebarHeader className="student-sidebar-top">
|
| 136 |
+
<StudentSidebarLogo />
|
| 137 |
+
<SidebarTrigger className="student-sidebar-trigger" />
|
| 138 |
+
</SidebarHeader>
|
| 139 |
+
|
| 140 |
+
<SidebarContent className="student-sidebar-content">
|
| 141 |
+
<SidebarGroup className="student-history-group">
|
| 142 |
+
{isSidebarCollapsed ? (
|
| 143 |
+
<SidebarGroupContent>
|
| 144 |
+
<SidebarMenu className="student-history-list">
|
| 145 |
+
<SidebarMenuItem>
|
| 146 |
+
<SidebarMenuButton
|
| 147 |
+
className="student-history-menu-button"
|
| 148 |
+
tooltip="Riwayat Chat"
|
| 149 |
+
>
|
| 150 |
+
<Clock3 className="student-history-menu-icon size-4" />
|
| 151 |
+
<span className="student-history-copy">
|
| 152 |
+
<span className="student-history-title">
|
| 153 |
+
Riwayat Chat
|
| 154 |
+
</span>
|
| 155 |
+
</span>
|
| 156 |
+
</SidebarMenuButton>
|
| 157 |
+
</SidebarMenuItem>
|
| 158 |
+
</SidebarMenu>
|
| 159 |
+
</SidebarGroupContent>
|
| 160 |
+
) : (
|
| 161 |
+
<>
|
| 162 |
+
<SidebarGroupLabel asChild>
|
| 163 |
+
<button
|
| 164 |
+
aria-expanded={isHistoryOpen}
|
| 165 |
+
className="student-history-label"
|
| 166 |
+
onClick={() =>
|
| 167 |
+
setIsHistoryOpen(
|
| 168 |
+
(currentState) => !currentState,
|
| 169 |
+
)
|
| 170 |
+
}
|
| 171 |
+
type="button"
|
| 172 |
+
>
|
| 173 |
+
<Clock3 className="size-4" />
|
| 174 |
+
<span>Riwayat Chat</span>
|
| 175 |
+
<ChevronDown className="student-history-chevron" />
|
| 176 |
+
</button>
|
| 177 |
+
</SidebarGroupLabel>
|
| 178 |
+
|
| 179 |
+
{isHistoryOpen && (
|
| 180 |
+
<SidebarGroupContent>
|
| 181 |
+
<SidebarMenu className="student-history-list">
|
| 182 |
+
{isLoading && (
|
| 183 |
+
<SidebarMenuItem>
|
| 184 |
+
<SidebarMenuButton
|
| 185 |
+
className="student-history-menu-button"
|
| 186 |
+
disabled
|
| 187 |
+
>
|
| 188 |
+
<Clock3 className="student-history-menu-icon size-4" />
|
| 189 |
+
<span className="student-history-copy">
|
| 190 |
+
<span className="student-history-title">
|
| 191 |
+
Memuat riwayat
|
| 192 |
+
</span>
|
| 193 |
+
</span>
|
| 194 |
+
</SidebarMenuButton>
|
| 195 |
+
</SidebarMenuItem>
|
| 196 |
+
)}
|
| 197 |
+
|
| 198 |
+
{chatSessions.map((session) => (
|
| 199 |
+
<SidebarMenuItem
|
| 200 |
+
key={session.uuid_id}
|
| 201 |
+
>
|
| 202 |
+
<SidebarMenuButton
|
| 203 |
+
className="student-history-menu-button"
|
| 204 |
+
tooltip={session.title}
|
| 205 |
+
>
|
| 206 |
+
<Clock3 className="student-history-menu-icon size-4" />
|
| 207 |
+
<span className="student-history-copy">
|
| 208 |
+
<span className="student-history-title">
|
| 209 |
+
{session.title}
|
| 210 |
+
</span>
|
| 211 |
+
{formatSessionTime(
|
| 212 |
+
session.last_message_at ??
|
| 213 |
+
session.updated_at ??
|
| 214 |
+
session.created_at,
|
| 215 |
+
) && (
|
| 216 |
+
<span className="student-history-meta">
|
| 217 |
+
{formatSessionTime(
|
| 218 |
+
session.last_message_at ??
|
| 219 |
+
session.updated_at ??
|
| 220 |
+
session.created_at,
|
| 221 |
+
)}
|
| 222 |
+
</span>
|
| 223 |
+
)}
|
| 224 |
+
</span>
|
| 225 |
+
</SidebarMenuButton>
|
| 226 |
+
</SidebarMenuItem>
|
| 227 |
+
))}
|
| 228 |
+
</SidebarMenu>
|
| 229 |
+
</SidebarGroupContent>
|
| 230 |
+
)}
|
| 231 |
+
</>
|
| 232 |
+
)}
|
| 233 |
+
</SidebarGroup>
|
| 234 |
+
</SidebarContent>
|
| 235 |
+
|
| 236 |
+
<SidebarFooter className="student-sidebar-footer">
|
| 237 |
+
<SidebarMenu>
|
| 238 |
+
<SidebarMenuItem>
|
| 239 |
+
<SidebarMenuButton
|
| 240 |
+
className="student-user-menu-button"
|
| 241 |
+
tooltip={studentName}
|
| 242 |
+
>
|
| 243 |
+
<Avatar className="student-user-avatar">
|
| 244 |
+
<AvatarFallback>
|
| 245 |
+
{getInitials(studentName) || 'AK'}
|
| 246 |
+
</AvatarFallback>
|
| 247 |
+
</Avatar>
|
| 248 |
+
<span className="student-user-copy">
|
| 249 |
+
<span className="student-user-name">
|
| 250 |
+
{studentName}
|
| 251 |
+
</span>
|
| 252 |
+
<span className="student-user-role">
|
| 253 |
+
Mahasiswa
|
| 254 |
+
</span>
|
| 255 |
+
</span>
|
| 256 |
+
</SidebarMenuButton>
|
| 257 |
+
</SidebarMenuItem>
|
| 258 |
+
|
| 259 |
+
<SidebarMenuItem>
|
| 260 |
+
<SidebarMenuButton
|
| 261 |
+
className="student-logout-button"
|
| 262 |
+
onClick={handleLogout}
|
| 263 |
+
tooltip="Logout"
|
| 264 |
+
>
|
| 265 |
+
<LogOut className="size-4" />
|
| 266 |
+
<span>Logout</span>
|
| 267 |
+
</SidebarMenuButton>
|
| 268 |
+
</SidebarMenuItem>
|
| 269 |
+
</SidebarMenu>
|
| 270 |
+
</SidebarFooter>
|
| 271 |
+
|
| 272 |
+
<SidebarRail />
|
| 273 |
+
</Sidebar>
|
| 274 |
+
);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
function QuestionComposer({
|
| 278 |
+
courses,
|
| 279 |
+
isLoadingCourses,
|
| 280 |
+
isProcessing,
|
| 281 |
+
onCourseChange,
|
| 282 |
+
onQuestionChange,
|
| 283 |
+
onSubmit,
|
| 284 |
+
question,
|
| 285 |
+
selectedCourseId,
|
| 286 |
+
}: {
|
| 287 |
+
courses: CourseResponse[];
|
| 288 |
+
isLoadingCourses: boolean;
|
| 289 |
+
isProcessing: boolean;
|
| 290 |
+
onCourseChange: (courseId: string) => void;
|
| 291 |
+
onQuestionChange: (question: string) => void;
|
| 292 |
+
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
| 293 |
+
question: string;
|
| 294 |
+
selectedCourseId: string;
|
| 295 |
+
}) {
|
| 296 |
+
return (
|
| 297 |
+
<Card className="student-composer-card py-0">
|
| 298 |
+
<CardContent className="student-composer-content">
|
| 299 |
+
<form className="student-composer-form" onSubmit={onSubmit}>
|
| 300 |
+
<div className="student-course-row">
|
| 301 |
+
<BookOpen className="student-course-icon size-5" />
|
| 302 |
+
<span className="student-course-label text-sm">
|
| 303 |
+
Mata Kuliah:
|
| 304 |
+
</span>
|
| 305 |
+
|
| 306 |
+
<Select
|
| 307 |
+
disabled={isLoadingCourses || courses.length === 0}
|
| 308 |
+
onValueChange={onCourseChange}
|
| 309 |
+
value={selectedCourseId}
|
| 310 |
+
>
|
| 311 |
+
<SelectTrigger
|
| 312 |
+
className="student-course-select"
|
| 313 |
+
size="sm"
|
| 314 |
+
>
|
| 315 |
+
<SelectValue placeholder="Pilih mata kuliah" />
|
| 316 |
+
</SelectTrigger>
|
| 317 |
+
<SelectContent>
|
| 318 |
+
{courses.map((course) => (
|
| 319 |
+
<SelectItem
|
| 320 |
+
key={course.id}
|
| 321 |
+
value={String(course.id)}
|
| 322 |
+
>
|
| 323 |
+
{course.title}
|
| 324 |
+
</SelectItem>
|
| 325 |
+
))}
|
| 326 |
+
</SelectContent>
|
| 327 |
+
</Select>
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<div className="student-composer-divider" />
|
| 331 |
+
|
| 332 |
+
<div className="student-question-row">
|
| 333 |
+
<Sparkles className="student-question-icon size-5" />
|
| 334 |
+
<textarea
|
| 335 |
+
className="text-base"
|
| 336 |
+
aria-label="Pertanyaan"
|
| 337 |
+
onChange={(event) =>
|
| 338 |
+
onQuestionChange(event.target.value)
|
| 339 |
+
}
|
| 340 |
+
placeholder="Tulis pertanyaanmu disini..."
|
| 341 |
+
value={question}
|
| 342 |
+
/>
|
| 343 |
+
</div>
|
| 344 |
+
|
| 345 |
+
<Button
|
| 346 |
+
className="student-floating-send"
|
| 347 |
+
disabled={
|
| 348 |
+
isProcessing ||
|
| 349 |
+
selectedCourseId.length === 0 ||
|
| 350 |
+
question.trim().length === 0
|
| 351 |
+
}
|
| 352 |
+
size="icon"
|
| 353 |
+
type="submit"
|
| 354 |
+
>
|
| 355 |
+
<Send className="size-5" />
|
| 356 |
+
</Button>
|
| 357 |
+
</form>
|
| 358 |
+
</CardContent>
|
| 359 |
+
</Card>
|
| 360 |
+
);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
export default function Mahasiswa() {
|
| 364 |
+
const [backendMessage, setBackendMessage] = useState<string>();
|
| 365 |
+
const [chatSessions, setChatSessions] = useState<ChatSessionResponse[]>([]);
|
| 366 |
+
const [courses, setCourses] = useState<CourseResponse[]>([]);
|
| 367 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 368 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 369 |
+
const [question, setQuestion] = useState('');
|
| 370 |
+
const [selectedCourseId, setSelectedCourseId] = useState('');
|
| 371 |
+
const [studentName] = useState(getStoredStudentName);
|
| 372 |
+
|
| 373 |
+
useEffect(() => {
|
| 374 |
+
const controller = new AbortController();
|
| 375 |
+
|
| 376 |
+
async function loadDashboardData() {
|
| 377 |
+
const token = getStoredAuthToken() ?? undefined;
|
| 378 |
+
const [coursesResult, sessionsResult] = await Promise.allSettled([
|
| 379 |
+
ragApi.listCourses({ signal: controller.signal }),
|
| 380 |
+
ragApi.listChatSessions({ signal: controller.signal, token }),
|
| 381 |
+
]);
|
| 382 |
+
|
| 383 |
+
if (controller.signal.aborted) {
|
| 384 |
+
return;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
if (coursesResult.status === 'fulfilled') {
|
| 388 |
+
const nextCourses = coursesResult.value.data;
|
| 389 |
+
|
| 390 |
+
setCourses(nextCourses);
|
| 391 |
+
setSelectedCourseId((currentCourseId) => {
|
| 392 |
+
if (currentCourseId || nextCourses.length === 0) {
|
| 393 |
+
return currentCourseId;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
return String(nextCourses[0].id);
|
| 397 |
+
});
|
| 398 |
+
} else if (!isAbortError(coursesResult.reason)) {
|
| 399 |
+
setBackendMessage(getRagErrorMessage(coursesResult.reason));
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
if (sessionsResult.status === 'fulfilled') {
|
| 403 |
+
setChatSessions(sessionsResult.value.data);
|
| 404 |
+
} else if (!isAbortError(sessionsResult.reason)) {
|
| 405 |
+
setBackendMessage(getRagErrorMessage(sessionsResult.reason));
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
setIsLoading(false);
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
void loadDashboardData();
|
| 412 |
+
|
| 413 |
+
return () => {
|
| 414 |
+
controller.abort();
|
| 415 |
+
};
|
| 416 |
+
}, []);
|
| 417 |
+
|
| 418 |
+
const handleQuestionSubmit = async (
|
| 419 |
+
event: FormEvent<HTMLFormElement>,
|
| 420 |
+
): Promise<void> => {
|
| 421 |
+
event.preventDefault();
|
| 422 |
+
|
| 423 |
+
const trimmedQuestion = question.trim();
|
| 424 |
+
|
| 425 |
+
if (!selectedCourseId || !trimmedQuestion) {
|
| 426 |
+
return;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
setIsProcessing(true);
|
| 430 |
+
setBackendMessage(undefined);
|
| 431 |
+
|
| 432 |
+
try {
|
| 433 |
+
const token = getStoredAuthToken() ?? undefined;
|
| 434 |
+
const session = await ragApi.createChatSession(
|
| 435 |
+
{
|
| 436 |
+
course_id: selectedCourseId,
|
| 437 |
+
title: trimmedQuestion,
|
| 438 |
+
},
|
| 439 |
+
token,
|
| 440 |
+
);
|
| 441 |
+
|
| 442 |
+
await ragApi.sendMessage(
|
| 443 |
+
session.uuid_id,
|
| 444 |
+
{ content: trimmedQuestion },
|
| 445 |
+
token,
|
| 446 |
+
);
|
| 447 |
+
|
| 448 |
+
setChatSessions((currentSessions) => [session, ...currentSessions]);
|
| 449 |
+
setQuestion('');
|
| 450 |
+
} catch (error) {
|
| 451 |
+
setBackendMessage(getRagErrorMessage(error));
|
| 452 |
+
} finally {
|
| 453 |
+
setIsProcessing(false);
|
| 454 |
+
}
|
| 455 |
+
};
|
| 456 |
+
|
| 457 |
+
return (
|
| 458 |
+
<>
|
| 459 |
+
<Head title="RAG Hub" />
|
| 460 |
+
|
| 461 |
+
<SidebarProvider
|
| 462 |
+
className="student-dashboard-shell"
|
| 463 |
+
defaultOpen
|
| 464 |
+
style={sidebarStyle}
|
| 465 |
+
>
|
| 466 |
+
<StudentSidebar
|
| 467 |
+
chatSessions={chatSessions}
|
| 468 |
+
isLoading={isLoading}
|
| 469 |
+
studentName={studentName}
|
| 470 |
+
/>
|
| 471 |
+
|
| 472 |
+
<SidebarInset className="student-workspace">
|
| 473 |
+
<header className="student-topbar">
|
| 474 |
+
<h1 className="student-brand-title">RAG Hub</h1>
|
| 475 |
+
|
| 476 |
+
<div className="student-topbar-actions">
|
| 477 |
+
<AppearanceToggle className="student-theme-toggle" />
|
| 478 |
+
<Button
|
| 479 |
+
className="student-new-session-button"
|
| 480 |
+
type="button"
|
| 481 |
+
>
|
| 482 |
+
+ Sesi Baru
|
| 483 |
+
</Button>
|
| 484 |
+
</div>
|
| 485 |
+
</header>
|
| 486 |
+
|
| 487 |
+
<div className="student-chat-stage">
|
| 488 |
+
<section className="student-empty-state">
|
| 489 |
+
<div className="student-empty-icon">
|
| 490 |
+
<MessageSquare className="size-8" />
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
+
<div className="student-greeting">
|
| 494 |
+
<h2>Good Morning, {studentName}</h2>
|
| 495 |
+
<p>
|
| 496 |
+
Pilih mata kuliah, lalu kirim pertanyaan
|
| 497 |
+
pertama untuk membuat sesi baru
|
| 498 |
+
</p>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
{backendMessage && (
|
| 502 |
+
<p className="student-status">
|
| 503 |
+
{backendMessage}
|
| 504 |
+
</p>
|
| 505 |
+
)}
|
| 506 |
+
|
| 507 |
+
<QuestionComposer
|
| 508 |
+
courses={courses}
|
| 509 |
+
isLoadingCourses={isLoading}
|
| 510 |
+
isProcessing={isProcessing}
|
| 511 |
+
onCourseChange={setSelectedCourseId}
|
| 512 |
+
onQuestionChange={setQuestion}
|
| 513 |
+
onSubmit={handleQuestionSubmit}
|
| 514 |
+
question={question}
|
| 515 |
+
selectedCourseId={selectedCourseId}
|
| 516 |
+
/>
|
| 517 |
+
</section>
|
| 518 |
+
</div>
|
| 519 |
+
</SidebarInset>
|
| 520 |
+
</SidebarProvider>
|
| 521 |
+
</>
|
| 522 |
+
);
|
| 523 |
+
}
|
resources/js/pages/welcome.tsx
CHANGED
|
@@ -1,21 +1,99 @@
|
|
| 1 |
-
import { Head } from '@inertiajs/react';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
return (
|
| 5 |
<>
|
| 6 |
<Head title="Welcome" />
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
<
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
<
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</>
|
| 18 |
);
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
export default Welcome;
|
|
|
|
| 1 |
+
import { Head, Link } from '@inertiajs/react';
|
| 2 |
+
import type { LucideIcon } from 'lucide-react';
|
| 3 |
+
import { BookOpen, ChevronRight, MessageSquare } from 'lucide-react';
|
| 4 |
+
import type { ComponentProps } from 'react';
|
| 5 |
+
import AppearanceToggle from '@/components/appearance-toggle';
|
| 6 |
+
import { Card, CardContent } from '@/components/ui/card';
|
| 7 |
+
import { login, mahasiswa, register } from '@/routes';
|
| 8 |
|
| 9 |
+
type WelcomeProps = {
|
| 10 |
+
canRegister?: boolean;
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
type AuthEntryCardProps = {
|
| 14 |
+
description: string;
|
| 15 |
+
href: NonNullable<ComponentProps<typeof Link>['href']>;
|
| 16 |
+
icon: LucideIcon;
|
| 17 |
+
title: string;
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
function AuthEntryCard({
|
| 21 |
+
description,
|
| 22 |
+
href,
|
| 23 |
+
icon: Icon,
|
| 24 |
+
title,
|
| 25 |
+
}: AuthEntryCardProps) {
|
| 26 |
+
return (
|
| 27 |
+
<Card className="welcome-entry-card px-1 py-0">
|
| 28 |
+
<CardContent className="p-0">
|
| 29 |
+
<Link href={href} className="group welcome-entry-link">
|
| 30 |
+
<div className="flex min-w-0 items-start gap-3">
|
| 31 |
+
<div className="welcome-entry-icon">
|
| 32 |
+
<Icon className="h-5 w-5" />
|
| 33 |
+
</div>
|
| 34 |
+
<div className="min-w-0">
|
| 35 |
+
<p className="welcome-entry-title">{title}</p>
|
| 36 |
+
<p className="welcome-entry-description">
|
| 37 |
+
{description}
|
| 38 |
+
</p>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
<ChevronRight className="welcome-entry-arrow" />
|
| 42 |
+
</Link>
|
| 43 |
+
</CardContent>
|
| 44 |
+
</Card>
|
| 45 |
+
);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export default function Welcome({ canRegister = true }: WelcomeProps) {
|
| 49 |
return (
|
| 50 |
<>
|
| 51 |
<Head title="Welcome" />
|
| 52 |
+
|
| 53 |
+
<main className="welcome-shell">
|
| 54 |
+
<AppearanceToggle />
|
| 55 |
+
|
| 56 |
+
<section className="welcome-content">
|
| 57 |
+
<div className="welcome-logo">
|
| 58 |
+
<MessageSquare className="h-9 w-9" />
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<header className="space-y-2 text-center">
|
| 62 |
+
<h1 className="welcome-title">Sevima RAG Hub</h1>
|
| 63 |
+
<p className="welcome-subtitle text-base">
|
| 64 |
+
Pusat kelola dokumen dan Q&A berbasis AI untuk
|
| 65 |
+
lingkungan kampus
|
| 66 |
+
</p>
|
| 67 |
+
</header>
|
| 68 |
+
|
| 69 |
+
<div className="welcome-entries">
|
| 70 |
+
<AuthEntryCard
|
| 71 |
+
href={login()}
|
| 72 |
+
icon={BookOpen}
|
| 73 |
+
title="Masuk sebagai Dosen"
|
| 74 |
+
description="Kelola knowledge base dan dokumen"
|
| 75 |
+
/>
|
| 76 |
+
<AuthEntryCard
|
| 77 |
+
href={login()}
|
| 78 |
+
icon={MessageSquare}
|
| 79 |
+
title="Masuk sebagai Mahasiswa"
|
| 80 |
+
description="Tanya jawab materi kuliah dengan AI"
|
| 81 |
+
/>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
{canRegister && (
|
| 85 |
+
<p className="welcome-register text-base">
|
| 86 |
+
Belum punya akun?{' '}
|
| 87 |
+
<Link
|
| 88 |
+
href={register()}
|
| 89 |
+
className="welcome-register-link"
|
| 90 |
+
>
|
| 91 |
+
Register
|
| 92 |
+
</Link>
|
| 93 |
+
</p>
|
| 94 |
+
)}
|
| 95 |
+
</section>
|
| 96 |
+
</main>
|
| 97 |
</>
|
| 98 |
);
|
| 99 |
+
}
|
|
|
|
|
|
routes/web.php
CHANGED
|
@@ -7,8 +7,12 @@
|
|
| 7 |
'canRegister' => Features::enabled(Features::registration()),
|
| 8 |
])->name('home');
|
| 9 |
|
| 10 |
-
Route::
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
require __DIR__.'/settings.php';
|
|
|
|
| 7 |
'canRegister' => Features::enabled(Features::registration()),
|
| 8 |
])->name('home');
|
| 9 |
|
| 10 |
+
Route::inertia('dashboard', 'dashboard')->name('dashboard');
|
| 11 |
+
|
| 12 |
+
Route::inertia('register/mahasiswa', 'auth/register-mahasiswa')->name('register.mahasiswa');
|
| 13 |
+
|
| 14 |
+
Route::inertia('register/dosen', 'auth/register-dosen')->name('register.dosen');
|
| 15 |
+
|
| 16 |
+
Route::inertia('mahasiswa', 'mahasiswa')->name('mahasiswa');
|
| 17 |
|
| 18 |
require __DIR__.'/settings.php';
|
sevima-raghub.json
ADDED
|
@@ -0,0 +1,2060 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"openapi": "3.1.0",
|
| 3 |
+
"info": {
|
| 4 |
+
"title": "fca-api",
|
| 5 |
+
"version": "0.0.1"
|
| 6 |
+
},
|
| 7 |
+
"paths": {
|
| 8 |
+
"/": {
|
| 9 |
+
"get": {
|
| 10 |
+
"summary": "Root",
|
| 11 |
+
"operationId": "root__get",
|
| 12 |
+
"responses": {
|
| 13 |
+
"200": {
|
| 14 |
+
"description": "Successful Response",
|
| 15 |
+
"content": {
|
| 16 |
+
"application/json": {
|
| 17 |
+
"schema": {}
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"/api/v1/auth/register": {
|
| 25 |
+
"post": {
|
| 26 |
+
"tags": ["v1", "auth"],
|
| 27 |
+
"summary": "Register",
|
| 28 |
+
"description": "Register a new user (student or lecturer). Source: openapi-spec /auth/register",
|
| 29 |
+
"operationId": "register_api_v1_auth_register_post",
|
| 30 |
+
"requestBody": {
|
| 31 |
+
"content": {
|
| 32 |
+
"application/json": {
|
| 33 |
+
"schema": {
|
| 34 |
+
"$ref": "#/components/schemas/RegisterRequest"
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"required": true
|
| 39 |
+
},
|
| 40 |
+
"responses": {
|
| 41 |
+
"201": {
|
| 42 |
+
"description": "Successful Response",
|
| 43 |
+
"content": {
|
| 44 |
+
"application/json": {
|
| 45 |
+
"schema": {
|
| 46 |
+
"$ref": "#/components/schemas/RegisterResponse"
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
"422": {
|
| 52 |
+
"description": "Validation Error",
|
| 53 |
+
"content": {
|
| 54 |
+
"application/json": {
|
| 55 |
+
"schema": {
|
| 56 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"/api/v1/auth/login": {
|
| 65 |
+
"post": {
|
| 66 |
+
"tags": ["v1", "auth"],
|
| 67 |
+
"summary": "Login",
|
| 68 |
+
"description": "Login with email + password and receive a JWT. Source: openapi-spec /auth/login",
|
| 69 |
+
"operationId": "login_api_v1_auth_login_post",
|
| 70 |
+
"requestBody": {
|
| 71 |
+
"content": {
|
| 72 |
+
"application/json": {
|
| 73 |
+
"schema": {
|
| 74 |
+
"$ref": "#/components/schemas/LoginRequest"
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
"required": true
|
| 79 |
+
},
|
| 80 |
+
"responses": {
|
| 81 |
+
"200": {
|
| 82 |
+
"description": "Successful Response",
|
| 83 |
+
"content": {
|
| 84 |
+
"application/json": {
|
| 85 |
+
"schema": {
|
| 86 |
+
"$ref": "#/components/schemas/LoginResponse"
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
},
|
| 91 |
+
"422": {
|
| 92 |
+
"description": "Validation Error",
|
| 93 |
+
"content": {
|
| 94 |
+
"application/json": {
|
| 95 |
+
"schema": {
|
| 96 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
},
|
| 104 |
+
"/api/v1/auth/login/api-key": {
|
| 105 |
+
"post": {
|
| 106 |
+
"tags": ["v1", "auth"],
|
| 107 |
+
"summary": "Login With Api Key",
|
| 108 |
+
"description": "Exchange an API key for a JWT. Source: openapi-spec /auth/login/api-key",
|
| 109 |
+
"operationId": "login_with_api_key_api_v1_auth_login_api_key_post",
|
| 110 |
+
"requestBody": {
|
| 111 |
+
"content": {
|
| 112 |
+
"application/json": {
|
| 113 |
+
"schema": {
|
| 114 |
+
"$ref": "#/components/schemas/ApiKeyLoginRequest"
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
},
|
| 118 |
+
"required": true
|
| 119 |
+
},
|
| 120 |
+
"responses": {
|
| 121 |
+
"200": {
|
| 122 |
+
"description": "Successful Response",
|
| 123 |
+
"content": {
|
| 124 |
+
"application/json": {
|
| 125 |
+
"schema": {
|
| 126 |
+
"$ref": "#/components/schemas/LoginResponse"
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
"422": {
|
| 132 |
+
"description": "Validation Error",
|
| 133 |
+
"content": {
|
| 134 |
+
"application/json": {
|
| 135 |
+
"schema": {
|
| 136 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
"/api/v1/auth/api-keys": {
|
| 145 |
+
"post": {
|
| 146 |
+
"tags": ["v1", "auth"],
|
| 147 |
+
"summary": "Generate Api Key",
|
| 148 |
+
"description": "Generate a new API key. Restricted to lecturers and admins. Source: openapi-spec /auth/api-keys",
|
| 149 |
+
"operationId": "generate_api_key_api_v1_auth_api_keys_post",
|
| 150 |
+
"responses": {
|
| 151 |
+
"201": {
|
| 152 |
+
"description": "Successful Response",
|
| 153 |
+
"content": {
|
| 154 |
+
"application/json": {
|
| 155 |
+
"schema": {
|
| 156 |
+
"$ref": "#/components/schemas/ApiKeyGenerateResponse"
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
"security": [
|
| 163 |
+
{
|
| 164 |
+
"JWTBearer": []
|
| 165 |
+
}
|
| 166 |
+
]
|
| 167 |
+
}
|
| 168 |
+
},
|
| 169 |
+
"/api/v1/documents/upload": {
|
| 170 |
+
"post": {
|
| 171 |
+
"tags": ["v1", "documents"],
|
| 172 |
+
"summary": "Upload course material document",
|
| 173 |
+
"description": "Upload a file to be used as a knowledge base. Supported formats: PDF, PPT, PPTX, TXT, XLS, XLSX. Maximum size: 100 MB. Asynchronous â use the status endpoint to check progress.",
|
| 174 |
+
"operationId": "upload_document_api_v1_documents_upload_post",
|
| 175 |
+
"requestBody": {
|
| 176 |
+
"content": {
|
| 177 |
+
"multipart/form-data": {
|
| 178 |
+
"schema": {
|
| 179 |
+
"$ref": "#/components/schemas/Body_upload_document_api_v1_documents_upload_post"
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
"required": true
|
| 184 |
+
},
|
| 185 |
+
"responses": {
|
| 186 |
+
"202": {
|
| 187 |
+
"description": "Successful Response",
|
| 188 |
+
"content": {
|
| 189 |
+
"application/json": {
|
| 190 |
+
"schema": {
|
| 191 |
+
"$ref": "#/components/schemas/UploadDocumentResponse"
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
},
|
| 196 |
+
"422": {
|
| 197 |
+
"description": "Validation Error",
|
| 198 |
+
"content": {
|
| 199 |
+
"application/json": {
|
| 200 |
+
"schema": {
|
| 201 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
},
|
| 207 |
+
"security": [
|
| 208 |
+
{
|
| 209 |
+
"JWTBearer": []
|
| 210 |
+
}
|
| 211 |
+
]
|
| 212 |
+
}
|
| 213 |
+
},
|
| 214 |
+
"/api/v1/courses/{course_id}/documents": {
|
| 215 |
+
"get": {
|
| 216 |
+
"tags": ["v1", "documents"],
|
| 217 |
+
"summary": "List documents per course",
|
| 218 |
+
"description": "Get all knowledge base documents for a specific course.",
|
| 219 |
+
"operationId": "list_documents_by_course_api_v1_courses__course_id__documents_get",
|
| 220 |
+
"parameters": [
|
| 221 |
+
{
|
| 222 |
+
"required": true,
|
| 223 |
+
"schema": {
|
| 224 |
+
"type": "string",
|
| 225 |
+
"title": "Course Id"
|
| 226 |
+
},
|
| 227 |
+
"name": "course_id",
|
| 228 |
+
"in": "path"
|
| 229 |
+
}
|
| 230 |
+
],
|
| 231 |
+
"responses": {
|
| 232 |
+
"200": {
|
| 233 |
+
"description": "Successful Response",
|
| 234 |
+
"content": {
|
| 235 |
+
"application/json": {
|
| 236 |
+
"schema": {
|
| 237 |
+
"$ref": "#/components/schemas/DocumentListResponse"
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
},
|
| 242 |
+
"422": {
|
| 243 |
+
"description": "Validation Error",
|
| 244 |
+
"content": {
|
| 245 |
+
"application/json": {
|
| 246 |
+
"schema": {
|
| 247 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"security": [
|
| 254 |
+
{
|
| 255 |
+
"JWTBearer": []
|
| 256 |
+
}
|
| 257 |
+
]
|
| 258 |
+
}
|
| 259 |
+
},
|
| 260 |
+
"/api/v1/courses": {
|
| 261 |
+
"get": {
|
| 262 |
+
"tags": ["v1", "courses"],
|
| 263 |
+
"summary": "List courses",
|
| 264 |
+
"operationId": "get_courses_api_v1_courses_get",
|
| 265 |
+
"parameters": [
|
| 266 |
+
{
|
| 267 |
+
"required": false,
|
| 268 |
+
"schema": {
|
| 269 |
+
"type": "integer",
|
| 270 |
+
"minimum": 1.0,
|
| 271 |
+
"title": "Page",
|
| 272 |
+
"default": 1
|
| 273 |
+
},
|
| 274 |
+
"name": "page",
|
| 275 |
+
"in": "query"
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"required": false,
|
| 279 |
+
"schema": {
|
| 280 |
+
"type": "integer",
|
| 281 |
+
"minimum": 1.0,
|
| 282 |
+
"title": "Limit",
|
| 283 |
+
"default": 10
|
| 284 |
+
},
|
| 285 |
+
"name": "limit",
|
| 286 |
+
"in": "query"
|
| 287 |
+
}
|
| 288 |
+
],
|
| 289 |
+
"responses": {
|
| 290 |
+
"200": {
|
| 291 |
+
"description": "Successful Response",
|
| 292 |
+
"content": {
|
| 293 |
+
"application/json": {
|
| 294 |
+
"schema": {
|
| 295 |
+
"$ref": "#/components/schemas/CourseListResponse"
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
},
|
| 300 |
+
"422": {
|
| 301 |
+
"description": "Validation Error",
|
| 302 |
+
"content": {
|
| 303 |
+
"application/json": {
|
| 304 |
+
"schema": {
|
| 305 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
},
|
| 312 |
+
"post": {
|
| 313 |
+
"tags": ["v1", "courses"],
|
| 314 |
+
"summary": "Create course",
|
| 315 |
+
"operationId": "create_course_api_v1_courses_post",
|
| 316 |
+
"requestBody": {
|
| 317 |
+
"content": {
|
| 318 |
+
"application/json": {
|
| 319 |
+
"schema": {
|
| 320 |
+
"$ref": "#/components/schemas/CreateCourseRequest"
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
},
|
| 324 |
+
"required": true
|
| 325 |
+
},
|
| 326 |
+
"responses": {
|
| 327 |
+
"201": {
|
| 328 |
+
"description": "Successful Response",
|
| 329 |
+
"content": {
|
| 330 |
+
"application/json": {
|
| 331 |
+
"schema": {
|
| 332 |
+
"$ref": "#/components/schemas/CourseResponse"
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
"422": {
|
| 338 |
+
"description": "Validation Error",
|
| 339 |
+
"content": {
|
| 340 |
+
"application/json": {
|
| 341 |
+
"schema": {
|
| 342 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
},
|
| 350 |
+
"/api/v1/courses/{course_id}": {
|
| 351 |
+
"get": {
|
| 352 |
+
"tags": ["v1", "courses"],
|
| 353 |
+
"summary": "Get course detail",
|
| 354 |
+
"operationId": "get_course_api_v1_courses__course_id__get",
|
| 355 |
+
"parameters": [
|
| 356 |
+
{
|
| 357 |
+
"required": true,
|
| 358 |
+
"schema": {
|
| 359 |
+
"type": "integer",
|
| 360 |
+
"title": "Course Id"
|
| 361 |
+
},
|
| 362 |
+
"name": "course_id",
|
| 363 |
+
"in": "path"
|
| 364 |
+
}
|
| 365 |
+
],
|
| 366 |
+
"responses": {
|
| 367 |
+
"200": {
|
| 368 |
+
"description": "Successful Response",
|
| 369 |
+
"content": {
|
| 370 |
+
"application/json": {
|
| 371 |
+
"schema": {
|
| 372 |
+
"$ref": "#/components/schemas/CourseResponse"
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
},
|
| 377 |
+
"422": {
|
| 378 |
+
"description": "Validation Error",
|
| 379 |
+
"content": {
|
| 380 |
+
"application/json": {
|
| 381 |
+
"schema": {
|
| 382 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
},
|
| 389 |
+
"put": {
|
| 390 |
+
"tags": ["v1", "courses"],
|
| 391 |
+
"summary": "Update course",
|
| 392 |
+
"operationId": "update_course_api_v1_courses__course_id__put",
|
| 393 |
+
"parameters": [
|
| 394 |
+
{
|
| 395 |
+
"required": true,
|
| 396 |
+
"schema": {
|
| 397 |
+
"type": "integer",
|
| 398 |
+
"title": "Course Id"
|
| 399 |
+
},
|
| 400 |
+
"name": "course_id",
|
| 401 |
+
"in": "path"
|
| 402 |
+
}
|
| 403 |
+
],
|
| 404 |
+
"requestBody": {
|
| 405 |
+
"content": {
|
| 406 |
+
"application/json": {
|
| 407 |
+
"schema": {
|
| 408 |
+
"$ref": "#/components/schemas/UpdateCourseRequest"
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
},
|
| 412 |
+
"required": true
|
| 413 |
+
},
|
| 414 |
+
"responses": {
|
| 415 |
+
"200": {
|
| 416 |
+
"description": "Successful Response",
|
| 417 |
+
"content": {
|
| 418 |
+
"application/json": {
|
| 419 |
+
"schema": {
|
| 420 |
+
"$ref": "#/components/schemas/CourseResponse"
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
},
|
| 425 |
+
"422": {
|
| 426 |
+
"description": "Validation Error",
|
| 427 |
+
"content": {
|
| 428 |
+
"application/json": {
|
| 429 |
+
"schema": {
|
| 430 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"delete": {
|
| 438 |
+
"tags": ["v1", "courses"],
|
| 439 |
+
"summary": "Delete course",
|
| 440 |
+
"operationId": "delete_course_api_v1_courses__course_id__delete",
|
| 441 |
+
"parameters": [
|
| 442 |
+
{
|
| 443 |
+
"required": true,
|
| 444 |
+
"schema": {
|
| 445 |
+
"type": "integer",
|
| 446 |
+
"title": "Course Id"
|
| 447 |
+
},
|
| 448 |
+
"name": "course_id",
|
| 449 |
+
"in": "path"
|
| 450 |
+
}
|
| 451 |
+
],
|
| 452 |
+
"responses": {
|
| 453 |
+
"200": {
|
| 454 |
+
"description": "Successful Response",
|
| 455 |
+
"content": {
|
| 456 |
+
"application/json": {
|
| 457 |
+
"schema": {}
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
},
|
| 461 |
+
"422": {
|
| 462 |
+
"description": "Validation Error",
|
| 463 |
+
"content": {
|
| 464 |
+
"application/json": {
|
| 465 |
+
"schema": {
|
| 466 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
},
|
| 474 |
+
"/api/v1/user": {
|
| 475 |
+
"get": {
|
| 476 |
+
"tags": ["v1", "user"],
|
| 477 |
+
"summary": "Get User List",
|
| 478 |
+
"operationId": "get_user_list_api_v1_user_get",
|
| 479 |
+
"parameters": [
|
| 480 |
+
{
|
| 481 |
+
"required": false,
|
| 482 |
+
"schema": {
|
| 483 |
+
"type": "string",
|
| 484 |
+
"title": "Email"
|
| 485 |
+
},
|
| 486 |
+
"name": "email",
|
| 487 |
+
"in": "query"
|
| 488 |
+
},
|
| 489 |
+
{
|
| 490 |
+
"required": false,
|
| 491 |
+
"schema": {
|
| 492 |
+
"type": "string",
|
| 493 |
+
"title": "User Token"
|
| 494 |
+
},
|
| 495 |
+
"name": "user_token",
|
| 496 |
+
"in": "query"
|
| 497 |
+
},
|
| 498 |
+
{
|
| 499 |
+
"required": false,
|
| 500 |
+
"schema": {
|
| 501 |
+
"type": "string",
|
| 502 |
+
"title": "Name"
|
| 503 |
+
},
|
| 504 |
+
"name": "name",
|
| 505 |
+
"in": "query"
|
| 506 |
+
},
|
| 507 |
+
{
|
| 508 |
+
"required": false,
|
| 509 |
+
"schema": {
|
| 510 |
+
"$ref": "#/components/schemas/app__schema__user_schema__Role"
|
| 511 |
+
},
|
| 512 |
+
"name": "role",
|
| 513 |
+
"in": "query"
|
| 514 |
+
},
|
| 515 |
+
{
|
| 516 |
+
"required": false,
|
| 517 |
+
"schema": {
|
| 518 |
+
"type": "string",
|
| 519 |
+
"title": "Identity Number"
|
| 520 |
+
},
|
| 521 |
+
"name": "identity_number",
|
| 522 |
+
"in": "query"
|
| 523 |
+
},
|
| 524 |
+
{
|
| 525 |
+
"required": false,
|
| 526 |
+
"schema": {
|
| 527 |
+
"type": "boolean",
|
| 528 |
+
"title": "Is Active"
|
| 529 |
+
},
|
| 530 |
+
"name": "is_active",
|
| 531 |
+
"in": "query"
|
| 532 |
+
},
|
| 533 |
+
{
|
| 534 |
+
"required": false,
|
| 535 |
+
"schema": {
|
| 536 |
+
"type": "boolean",
|
| 537 |
+
"title": "Is Superuser"
|
| 538 |
+
},
|
| 539 |
+
"name": "is_superuser",
|
| 540 |
+
"in": "query"
|
| 541 |
+
},
|
| 542 |
+
{
|
| 543 |
+
"required": false,
|
| 544 |
+
"schema": {
|
| 545 |
+
"type": "string",
|
| 546 |
+
"title": "Ordering"
|
| 547 |
+
},
|
| 548 |
+
"name": "ordering",
|
| 549 |
+
"in": "query"
|
| 550 |
+
},
|
| 551 |
+
{
|
| 552 |
+
"required": false,
|
| 553 |
+
"schema": {
|
| 554 |
+
"type": "integer",
|
| 555 |
+
"title": "Page"
|
| 556 |
+
},
|
| 557 |
+
"name": "page",
|
| 558 |
+
"in": "query"
|
| 559 |
+
},
|
| 560 |
+
{
|
| 561 |
+
"required": false,
|
| 562 |
+
"schema": {
|
| 563 |
+
"anyOf": [
|
| 564 |
+
{
|
| 565 |
+
"type": "integer"
|
| 566 |
+
},
|
| 567 |
+
{
|
| 568 |
+
"type": "string"
|
| 569 |
+
}
|
| 570 |
+
],
|
| 571 |
+
"title": "Page Size"
|
| 572 |
+
},
|
| 573 |
+
"name": "page_size",
|
| 574 |
+
"in": "query"
|
| 575 |
+
},
|
| 576 |
+
{
|
| 577 |
+
"required": false,
|
| 578 |
+
"schema": {
|
| 579 |
+
"type": "string",
|
| 580 |
+
"title": "Email Eq"
|
| 581 |
+
},
|
| 582 |
+
"name": "email__eq",
|
| 583 |
+
"in": "query"
|
| 584 |
+
},
|
| 585 |
+
{
|
| 586 |
+
"required": false,
|
| 587 |
+
"schema": {
|
| 588 |
+
"type": "string",
|
| 589 |
+
"title": "Identity Number Eq"
|
| 590 |
+
},
|
| 591 |
+
"name": "identity_number__eq",
|
| 592 |
+
"in": "query"
|
| 593 |
+
}
|
| 594 |
+
],
|
| 595 |
+
"responses": {
|
| 596 |
+
"200": {
|
| 597 |
+
"description": "Successful Response",
|
| 598 |
+
"content": {
|
| 599 |
+
"application/json": {
|
| 600 |
+
"schema": {
|
| 601 |
+
"$ref": "#/components/schemas/FindUserResult"
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
},
|
| 606 |
+
"422": {
|
| 607 |
+
"description": "Validation Error",
|
| 608 |
+
"content": {
|
| 609 |
+
"application/json": {
|
| 610 |
+
"schema": {
|
| 611 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
}
|
| 615 |
+
}
|
| 616 |
+
},
|
| 617 |
+
"security": [
|
| 618 |
+
{
|
| 619 |
+
"JWTBearer": []
|
| 620 |
+
},
|
| 621 |
+
{
|
| 622 |
+
"JWTBearer": []
|
| 623 |
+
}
|
| 624 |
+
]
|
| 625 |
+
},
|
| 626 |
+
"post": {
|
| 627 |
+
"tags": ["v1", "user"],
|
| 628 |
+
"summary": "Create User",
|
| 629 |
+
"operationId": "create_user_api_v1_user_post",
|
| 630 |
+
"requestBody": {
|
| 631 |
+
"content": {
|
| 632 |
+
"application/json": {
|
| 633 |
+
"schema": {
|
| 634 |
+
"$ref": "#/components/schemas/UpsertUser"
|
| 635 |
+
}
|
| 636 |
+
}
|
| 637 |
+
},
|
| 638 |
+
"required": true
|
| 639 |
+
},
|
| 640 |
+
"responses": {
|
| 641 |
+
"200": {
|
| 642 |
+
"description": "Successful Response",
|
| 643 |
+
"content": {
|
| 644 |
+
"application/json": {
|
| 645 |
+
"schema": {
|
| 646 |
+
"$ref": "#/components/schemas/User"
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
},
|
| 651 |
+
"422": {
|
| 652 |
+
"description": "Validation Error",
|
| 653 |
+
"content": {
|
| 654 |
+
"application/json": {
|
| 655 |
+
"schema": {
|
| 656 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 657 |
+
}
|
| 658 |
+
}
|
| 659 |
+
}
|
| 660 |
+
}
|
| 661 |
+
},
|
| 662 |
+
"security": [
|
| 663 |
+
{
|
| 664 |
+
"JWTBearer": []
|
| 665 |
+
},
|
| 666 |
+
{
|
| 667 |
+
"JWTBearer": []
|
| 668 |
+
}
|
| 669 |
+
]
|
| 670 |
+
}
|
| 671 |
+
},
|
| 672 |
+
"/api/v1/user/{user_id}": {
|
| 673 |
+
"get": {
|
| 674 |
+
"tags": ["v1", "user"],
|
| 675 |
+
"summary": "Get User",
|
| 676 |
+
"operationId": "get_user_api_v1_user__user_id__get",
|
| 677 |
+
"parameters": [
|
| 678 |
+
{
|
| 679 |
+
"required": true,
|
| 680 |
+
"schema": {
|
| 681 |
+
"type": "integer",
|
| 682 |
+
"title": "User Id"
|
| 683 |
+
},
|
| 684 |
+
"name": "user_id",
|
| 685 |
+
"in": "path"
|
| 686 |
+
}
|
| 687 |
+
],
|
| 688 |
+
"responses": {
|
| 689 |
+
"200": {
|
| 690 |
+
"description": "Successful Response",
|
| 691 |
+
"content": {
|
| 692 |
+
"application/json": {
|
| 693 |
+
"schema": {
|
| 694 |
+
"$ref": "#/components/schemas/User"
|
| 695 |
+
}
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
},
|
| 699 |
+
"422": {
|
| 700 |
+
"description": "Validation Error",
|
| 701 |
+
"content": {
|
| 702 |
+
"application/json": {
|
| 703 |
+
"schema": {
|
| 704 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
}
|
| 708 |
+
}
|
| 709 |
+
},
|
| 710 |
+
"security": [
|
| 711 |
+
{
|
| 712 |
+
"JWTBearer": []
|
| 713 |
+
},
|
| 714 |
+
{
|
| 715 |
+
"JWTBearer": []
|
| 716 |
+
}
|
| 717 |
+
]
|
| 718 |
+
},
|
| 719 |
+
"delete": {
|
| 720 |
+
"tags": ["v1", "user"],
|
| 721 |
+
"summary": "Delete User",
|
| 722 |
+
"operationId": "delete_user_api_v1_user__user_id__delete",
|
| 723 |
+
"parameters": [
|
| 724 |
+
{
|
| 725 |
+
"required": true,
|
| 726 |
+
"schema": {
|
| 727 |
+
"type": "integer",
|
| 728 |
+
"title": "User Id"
|
| 729 |
+
},
|
| 730 |
+
"name": "user_id",
|
| 731 |
+
"in": "path"
|
| 732 |
+
}
|
| 733 |
+
],
|
| 734 |
+
"responses": {
|
| 735 |
+
"200": {
|
| 736 |
+
"description": "Successful Response",
|
| 737 |
+
"content": {
|
| 738 |
+
"application/json": {
|
| 739 |
+
"schema": {
|
| 740 |
+
"$ref": "#/components/schemas/Blank"
|
| 741 |
+
}
|
| 742 |
+
}
|
| 743 |
+
}
|
| 744 |
+
},
|
| 745 |
+
"422": {
|
| 746 |
+
"description": "Validation Error",
|
| 747 |
+
"content": {
|
| 748 |
+
"application/json": {
|
| 749 |
+
"schema": {
|
| 750 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 751 |
+
}
|
| 752 |
+
}
|
| 753 |
+
}
|
| 754 |
+
}
|
| 755 |
+
},
|
| 756 |
+
"security": [
|
| 757 |
+
{
|
| 758 |
+
"JWTBearer": []
|
| 759 |
+
},
|
| 760 |
+
{
|
| 761 |
+
"JWTBearer": []
|
| 762 |
+
}
|
| 763 |
+
]
|
| 764 |
+
},
|
| 765 |
+
"patch": {
|
| 766 |
+
"tags": ["v1", "user"],
|
| 767 |
+
"summary": "Update User",
|
| 768 |
+
"operationId": "update_user_api_v1_user__user_id__patch",
|
| 769 |
+
"parameters": [
|
| 770 |
+
{
|
| 771 |
+
"required": true,
|
| 772 |
+
"schema": {
|
| 773 |
+
"type": "integer",
|
| 774 |
+
"title": "User Id"
|
| 775 |
+
},
|
| 776 |
+
"name": "user_id",
|
| 777 |
+
"in": "path"
|
| 778 |
+
}
|
| 779 |
+
],
|
| 780 |
+
"requestBody": {
|
| 781 |
+
"content": {
|
| 782 |
+
"application/json": {
|
| 783 |
+
"schema": {
|
| 784 |
+
"$ref": "#/components/schemas/UpsertUser"
|
| 785 |
+
}
|
| 786 |
+
}
|
| 787 |
+
},
|
| 788 |
+
"required": true
|
| 789 |
+
},
|
| 790 |
+
"responses": {
|
| 791 |
+
"200": {
|
| 792 |
+
"description": "Successful Response",
|
| 793 |
+
"content": {
|
| 794 |
+
"application/json": {
|
| 795 |
+
"schema": {
|
| 796 |
+
"$ref": "#/components/schemas/User"
|
| 797 |
+
}
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
},
|
| 801 |
+
"422": {
|
| 802 |
+
"description": "Validation Error",
|
| 803 |
+
"content": {
|
| 804 |
+
"application/json": {
|
| 805 |
+
"schema": {
|
| 806 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 807 |
+
}
|
| 808 |
+
}
|
| 809 |
+
}
|
| 810 |
+
}
|
| 811 |
+
},
|
| 812 |
+
"security": [
|
| 813 |
+
{
|
| 814 |
+
"JWTBearer": []
|
| 815 |
+
},
|
| 816 |
+
{
|
| 817 |
+
"JWTBearer": []
|
| 818 |
+
}
|
| 819 |
+
]
|
| 820 |
+
}
|
| 821 |
+
},
|
| 822 |
+
"/api/v1/chats/sessions": {
|
| 823 |
+
"get": {
|
| 824 |
+
"tags": ["v1", "Chat"],
|
| 825 |
+
"summary": "Get Chat Sessions",
|
| 826 |
+
"description": "List chat sessions with pagination.",
|
| 827 |
+
"operationId": "get_chat_sessions_api_v1_chats_sessions_get",
|
| 828 |
+
"parameters": [
|
| 829 |
+
{
|
| 830 |
+
"required": false,
|
| 831 |
+
"schema": {
|
| 832 |
+
"type": "integer",
|
| 833 |
+
"minimum": 1.0,
|
| 834 |
+
"title": "Page",
|
| 835 |
+
"default": 1
|
| 836 |
+
},
|
| 837 |
+
"name": "page",
|
| 838 |
+
"in": "query"
|
| 839 |
+
},
|
| 840 |
+
{
|
| 841 |
+
"required": false,
|
| 842 |
+
"schema": {
|
| 843 |
+
"type": "integer",
|
| 844 |
+
"minimum": 1.0,
|
| 845 |
+
"title": "Limit",
|
| 846 |
+
"default": 10
|
| 847 |
+
},
|
| 848 |
+
"name": "limit",
|
| 849 |
+
"in": "query"
|
| 850 |
+
},
|
| 851 |
+
{
|
| 852 |
+
"required": false,
|
| 853 |
+
"schema": {
|
| 854 |
+
"type": "string",
|
| 855 |
+
"title": "Course Id"
|
| 856 |
+
},
|
| 857 |
+
"name": "course_id",
|
| 858 |
+
"in": "query"
|
| 859 |
+
}
|
| 860 |
+
],
|
| 861 |
+
"responses": {
|
| 862 |
+
"200": {
|
| 863 |
+
"description": "Successful Response",
|
| 864 |
+
"content": {
|
| 865 |
+
"application/json": {
|
| 866 |
+
"schema": {
|
| 867 |
+
"$ref": "#/components/schemas/ChatSessionListResponse"
|
| 868 |
+
}
|
| 869 |
+
}
|
| 870 |
+
}
|
| 871 |
+
},
|
| 872 |
+
"422": {
|
| 873 |
+
"description": "Validation Error",
|
| 874 |
+
"content": {
|
| 875 |
+
"application/json": {
|
| 876 |
+
"schema": {
|
| 877 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 878 |
+
}
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
}
|
| 882 |
+
},
|
| 883 |
+
"security": [
|
| 884 |
+
{
|
| 885 |
+
"JWTBearer": []
|
| 886 |
+
}
|
| 887 |
+
]
|
| 888 |
+
},
|
| 889 |
+
"post": {
|
| 890 |
+
"tags": ["v1", "Chat"],
|
| 891 |
+
"summary": "Create Chat Session",
|
| 892 |
+
"description": "Create a new chat session.",
|
| 893 |
+
"operationId": "create_chat_session_api_v1_chats_sessions_post",
|
| 894 |
+
"requestBody": {
|
| 895 |
+
"content": {
|
| 896 |
+
"application/json": {
|
| 897 |
+
"schema": {
|
| 898 |
+
"$ref": "#/components/schemas/CreateChatSessionRequest"
|
| 899 |
+
}
|
| 900 |
+
}
|
| 901 |
+
},
|
| 902 |
+
"required": true
|
| 903 |
+
},
|
| 904 |
+
"responses": {
|
| 905 |
+
"201": {
|
| 906 |
+
"description": "Successful Response",
|
| 907 |
+
"content": {
|
| 908 |
+
"application/json": {
|
| 909 |
+
"schema": {
|
| 910 |
+
"$ref": "#/components/schemas/ChatSessionResponse"
|
| 911 |
+
}
|
| 912 |
+
}
|
| 913 |
+
}
|
| 914 |
+
},
|
| 915 |
+
"422": {
|
| 916 |
+
"description": "Validation Error",
|
| 917 |
+
"content": {
|
| 918 |
+
"application/json": {
|
| 919 |
+
"schema": {
|
| 920 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 921 |
+
}
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
}
|
| 925 |
+
},
|
| 926 |
+
"security": [
|
| 927 |
+
{
|
| 928 |
+
"JWTBearer": []
|
| 929 |
+
}
|
| 930 |
+
]
|
| 931 |
+
}
|
| 932 |
+
},
|
| 933 |
+
"/api/v1/chats/sessions/{session_id}": {
|
| 934 |
+
"get": {
|
| 935 |
+
"tags": ["v1", "Chat"],
|
| 936 |
+
"summary": "Get Chat Session",
|
| 937 |
+
"description": "Get chat session details.",
|
| 938 |
+
"operationId": "get_chat_session_api_v1_chats_sessions__session_id__get",
|
| 939 |
+
"parameters": [
|
| 940 |
+
{
|
| 941 |
+
"required": true,
|
| 942 |
+
"schema": {
|
| 943 |
+
"type": "string",
|
| 944 |
+
"title": "Session Id"
|
| 945 |
+
},
|
| 946 |
+
"name": "session_id",
|
| 947 |
+
"in": "path"
|
| 948 |
+
}
|
| 949 |
+
],
|
| 950 |
+
"responses": {
|
| 951 |
+
"200": {
|
| 952 |
+
"description": "Successful Response",
|
| 953 |
+
"content": {
|
| 954 |
+
"application/json": {
|
| 955 |
+
"schema": {
|
| 956 |
+
"$ref": "#/components/schemas/ChatSessionResponse"
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
},
|
| 961 |
+
"422": {
|
| 962 |
+
"description": "Validation Error",
|
| 963 |
+
"content": {
|
| 964 |
+
"application/json": {
|
| 965 |
+
"schema": {
|
| 966 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 967 |
+
}
|
| 968 |
+
}
|
| 969 |
+
}
|
| 970 |
+
}
|
| 971 |
+
},
|
| 972 |
+
"security": [
|
| 973 |
+
{
|
| 974 |
+
"JWTBearer": []
|
| 975 |
+
}
|
| 976 |
+
]
|
| 977 |
+
},
|
| 978 |
+
"delete": {
|
| 979 |
+
"tags": ["v1", "Chat"],
|
| 980 |
+
"summary": "Delete Chat Session",
|
| 981 |
+
"description": "Delete a chat session.",
|
| 982 |
+
"operationId": "delete_chat_session_api_v1_chats_sessions__session_id__delete",
|
| 983 |
+
"parameters": [
|
| 984 |
+
{
|
| 985 |
+
"required": true,
|
| 986 |
+
"schema": {
|
| 987 |
+
"type": "string",
|
| 988 |
+
"title": "Session Id"
|
| 989 |
+
},
|
| 990 |
+
"name": "session_id",
|
| 991 |
+
"in": "path"
|
| 992 |
+
}
|
| 993 |
+
],
|
| 994 |
+
"responses": {
|
| 995 |
+
"200": {
|
| 996 |
+
"description": "Successful Response",
|
| 997 |
+
"content": {
|
| 998 |
+
"application/json": {
|
| 999 |
+
"schema": {}
|
| 1000 |
+
}
|
| 1001 |
+
}
|
| 1002 |
+
},
|
| 1003 |
+
"422": {
|
| 1004 |
+
"description": "Validation Error",
|
| 1005 |
+
"content": {
|
| 1006 |
+
"application/json": {
|
| 1007 |
+
"schema": {
|
| 1008 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 1009 |
+
}
|
| 1010 |
+
}
|
| 1011 |
+
}
|
| 1012 |
+
}
|
| 1013 |
+
},
|
| 1014 |
+
"security": [
|
| 1015 |
+
{
|
| 1016 |
+
"JWTBearer": []
|
| 1017 |
+
}
|
| 1018 |
+
]
|
| 1019 |
+
}
|
| 1020 |
+
},
|
| 1021 |
+
"/api/v1/chats/sessions/{session_id}/messages": {
|
| 1022 |
+
"get": {
|
| 1023 |
+
"tags": ["v1", "Chat"],
|
| 1024 |
+
"summary": "Get Chat History",
|
| 1025 |
+
"description": "Get all messages in a session.",
|
| 1026 |
+
"operationId": "get_chat_history_api_v1_chats_sessions__session_id__messages_get",
|
| 1027 |
+
"parameters": [
|
| 1028 |
+
{
|
| 1029 |
+
"required": true,
|
| 1030 |
+
"schema": {
|
| 1031 |
+
"type": "string",
|
| 1032 |
+
"title": "Session Id"
|
| 1033 |
+
},
|
| 1034 |
+
"name": "session_id",
|
| 1035 |
+
"in": "path"
|
| 1036 |
+
}
|
| 1037 |
+
],
|
| 1038 |
+
"responses": {
|
| 1039 |
+
"200": {
|
| 1040 |
+
"description": "Successful Response",
|
| 1041 |
+
"content": {
|
| 1042 |
+
"application/json": {
|
| 1043 |
+
"schema": {
|
| 1044 |
+
"$ref": "#/components/schemas/ChatHistoryResponse"
|
| 1045 |
+
}
|
| 1046 |
+
}
|
| 1047 |
+
}
|
| 1048 |
+
},
|
| 1049 |
+
"422": {
|
| 1050 |
+
"description": "Validation Error",
|
| 1051 |
+
"content": {
|
| 1052 |
+
"application/json": {
|
| 1053 |
+
"schema": {
|
| 1054 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 1055 |
+
}
|
| 1056 |
+
}
|
| 1057 |
+
}
|
| 1058 |
+
}
|
| 1059 |
+
},
|
| 1060 |
+
"security": [
|
| 1061 |
+
{
|
| 1062 |
+
"JWTBearer": []
|
| 1063 |
+
}
|
| 1064 |
+
]
|
| 1065 |
+
},
|
| 1066 |
+
"post": {
|
| 1067 |
+
"tags": ["v1", "Chat"],
|
| 1068 |
+
"summary": "Send Message Rest",
|
| 1069 |
+
"description": "Send a message to AI via REST fallback.",
|
| 1070 |
+
"operationId": "send_message_rest_api_v1_chats_sessions__session_id__messages_post",
|
| 1071 |
+
"parameters": [
|
| 1072 |
+
{
|
| 1073 |
+
"required": true,
|
| 1074 |
+
"schema": {
|
| 1075 |
+
"type": "string",
|
| 1076 |
+
"title": "Session Id"
|
| 1077 |
+
},
|
| 1078 |
+
"name": "session_id",
|
| 1079 |
+
"in": "path"
|
| 1080 |
+
}
|
| 1081 |
+
],
|
| 1082 |
+
"requestBody": {
|
| 1083 |
+
"content": {
|
| 1084 |
+
"application/json": {
|
| 1085 |
+
"schema": {
|
| 1086 |
+
"$ref": "#/components/schemas/ChatRESTRequest"
|
| 1087 |
+
}
|
| 1088 |
+
}
|
| 1089 |
+
},
|
| 1090 |
+
"required": true
|
| 1091 |
+
},
|
| 1092 |
+
"responses": {
|
| 1093 |
+
"200": {
|
| 1094 |
+
"description": "Successful Response",
|
| 1095 |
+
"content": {
|
| 1096 |
+
"application/json": {
|
| 1097 |
+
"schema": {
|
| 1098 |
+
"$ref": "#/components/schemas/ChatMessageResponse"
|
| 1099 |
+
}
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
+
},
|
| 1103 |
+
"422": {
|
| 1104 |
+
"description": "Validation Error",
|
| 1105 |
+
"content": {
|
| 1106 |
+
"application/json": {
|
| 1107 |
+
"schema": {
|
| 1108 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 1109 |
+
}
|
| 1110 |
+
}
|
| 1111 |
+
}
|
| 1112 |
+
}
|
| 1113 |
+
},
|
| 1114 |
+
"security": [
|
| 1115 |
+
{
|
| 1116 |
+
"JWTBearer": []
|
| 1117 |
+
}
|
| 1118 |
+
]
|
| 1119 |
+
}
|
| 1120 |
+
},
|
| 1121 |
+
"/api/v1/ai/query": {
|
| 1122 |
+
"post": {
|
| 1123 |
+
"tags": ["v1", "Chat"],
|
| 1124 |
+
"summary": "Query Ai Direct",
|
| 1125 |
+
"description": "Send a direct question to AI (without session history).",
|
| 1126 |
+
"operationId": "query_ai_direct_api_v1_ai_query_post",
|
| 1127 |
+
"requestBody": {
|
| 1128 |
+
"content": {
|
| 1129 |
+
"application/json": {
|
| 1130 |
+
"schema": {
|
| 1131 |
+
"$ref": "#/components/schemas/ChatQueryRequest"
|
| 1132 |
+
}
|
| 1133 |
+
}
|
| 1134 |
+
},
|
| 1135 |
+
"required": true
|
| 1136 |
+
},
|
| 1137 |
+
"responses": {
|
| 1138 |
+
"200": {
|
| 1139 |
+
"description": "Successful Response",
|
| 1140 |
+
"content": {
|
| 1141 |
+
"application/json": {
|
| 1142 |
+
"schema": {
|
| 1143 |
+
"$ref": "#/components/schemas/ChatQueryResponse"
|
| 1144 |
+
}
|
| 1145 |
+
}
|
| 1146 |
+
}
|
| 1147 |
+
},
|
| 1148 |
+
"422": {
|
| 1149 |
+
"description": "Validation Error",
|
| 1150 |
+
"content": {
|
| 1151 |
+
"application/json": {
|
| 1152 |
+
"schema": {
|
| 1153 |
+
"$ref": "#/components/schemas/HTTPValidationError"
|
| 1154 |
+
}
|
| 1155 |
+
}
|
| 1156 |
+
}
|
| 1157 |
+
}
|
| 1158 |
+
},
|
| 1159 |
+
"security": [
|
| 1160 |
+
{
|
| 1161 |
+
"JWTBearer": []
|
| 1162 |
+
}
|
| 1163 |
+
]
|
| 1164 |
+
}
|
| 1165 |
+
}
|
| 1166 |
+
},
|
| 1167 |
+
"components": {
|
| 1168 |
+
"schemas": {
|
| 1169 |
+
"ApiKeyGenerateResponse": {
|
| 1170 |
+
"properties": {
|
| 1171 |
+
"api_key": {
|
| 1172 |
+
"type": "string",
|
| 1173 |
+
"title": "Api Key"
|
| 1174 |
+
},
|
| 1175 |
+
"expires_at": {
|
| 1176 |
+
"type": "string",
|
| 1177 |
+
"format": "date-time",
|
| 1178 |
+
"title": "Expires At"
|
| 1179 |
+
}
|
| 1180 |
+
},
|
| 1181 |
+
"type": "object",
|
| 1182 |
+
"required": ["api_key"],
|
| 1183 |
+
"title": "ApiKeyGenerateResponse",
|
| 1184 |
+
"description": "POST /auth/api-keys â 201 response. Raw key returned ONCE."
|
| 1185 |
+
},
|
| 1186 |
+
"ApiKeyLoginRequest": {
|
| 1187 |
+
"properties": {
|
| 1188 |
+
"api_key": {
|
| 1189 |
+
"type": "string",
|
| 1190 |
+
"title": "Api Key"
|
| 1191 |
+
}
|
| 1192 |
+
},
|
| 1193 |
+
"type": "object",
|
| 1194 |
+
"required": ["api_key"],
|
| 1195 |
+
"title": "ApiKeyLoginRequest",
|
| 1196 |
+
"description": "POST /auth/login/api-key â body."
|
| 1197 |
+
},
|
| 1198 |
+
"Blank": {
|
| 1199 |
+
"properties": {},
|
| 1200 |
+
"type": "object",
|
| 1201 |
+
"title": "Blank"
|
| 1202 |
+
},
|
| 1203 |
+
"Body_upload_document_api_v1_documents_upload_post": {
|
| 1204 |
+
"properties": {
|
| 1205 |
+
"file": {
|
| 1206 |
+
"type": "string",
|
| 1207 |
+
"format": "binary",
|
| 1208 |
+
"title": "File",
|
| 1209 |
+
"description": "Course material file"
|
| 1210 |
+
},
|
| 1211 |
+
"course_id": {
|
| 1212 |
+
"type": "string",
|
| 1213 |
+
"title": "Course Id",
|
| 1214 |
+
"description": "Target course UUID"
|
| 1215 |
+
}
|
| 1216 |
+
},
|
| 1217 |
+
"type": "object",
|
| 1218 |
+
"required": ["file", "course_id"],
|
| 1219 |
+
"title": "Body_upload_document_api_v1_documents_upload_post"
|
| 1220 |
+
},
|
| 1221 |
+
"ChatHistoryResponse": {
|
| 1222 |
+
"properties": {
|
| 1223 |
+
"data": {
|
| 1224 |
+
"items": {
|
| 1225 |
+
"$ref": "#/components/schemas/ChatMessageResponse"
|
| 1226 |
+
},
|
| 1227 |
+
"type": "array",
|
| 1228 |
+
"title": "Data"
|
| 1229 |
+
}
|
| 1230 |
+
},
|
| 1231 |
+
"type": "object",
|
| 1232 |
+
"required": ["data"],
|
| 1233 |
+
"title": "ChatHistoryResponse",
|
| 1234 |
+
"description": "Response format for GET /sessions/{session_id}/messages."
|
| 1235 |
+
},
|
| 1236 |
+
"ChatMessageResponse": {
|
| 1237 |
+
"properties": {
|
| 1238 |
+
"uuid_id": {
|
| 1239 |
+
"type": "string",
|
| 1240 |
+
"title": "Uuid Id",
|
| 1241 |
+
"description": "Public UUID of the message"
|
| 1242 |
+
},
|
| 1243 |
+
"role": {
|
| 1244 |
+
"type": "string",
|
| 1245 |
+
"title": "Role"
|
| 1246 |
+
},
|
| 1247 |
+
"content": {
|
| 1248 |
+
"type": "string",
|
| 1249 |
+
"title": "Content"
|
| 1250 |
+
},
|
| 1251 |
+
"sources": {
|
| 1252 |
+
"items": {
|
| 1253 |
+
"$ref": "#/components/schemas/DocumentSourceDTO"
|
| 1254 |
+
},
|
| 1255 |
+
"type": "array",
|
| 1256 |
+
"title": "Sources",
|
| 1257 |
+
"default": []
|
| 1258 |
+
},
|
| 1259 |
+
"created_at": {
|
| 1260 |
+
"type": "string",
|
| 1261 |
+
"format": "date-time",
|
| 1262 |
+
"title": "Created At"
|
| 1263 |
+
}
|
| 1264 |
+
},
|
| 1265 |
+
"type": "object",
|
| 1266 |
+
"required": ["uuid_id", "role", "content", "created_at"],
|
| 1267 |
+
"title": "ChatMessageResponse",
|
| 1268 |
+
"description": "Mirrors the message object in OpenAPI spec."
|
| 1269 |
+
},
|
| 1270 |
+
"ChatQueryRequest": {
|
| 1271 |
+
"properties": {
|
| 1272 |
+
"prompt": {
|
| 1273 |
+
"type": "string",
|
| 1274 |
+
"title": "Prompt",
|
| 1275 |
+
"description": "The user\u0027s prompt text."
|
| 1276 |
+
},
|
| 1277 |
+
"course_id": {
|
| 1278 |
+
"type": "string",
|
| 1279 |
+
"title": "Course Id",
|
| 1280 |
+
"description": "UUID of the course context."
|
| 1281 |
+
}
|
| 1282 |
+
},
|
| 1283 |
+
"type": "object",
|
| 1284 |
+
"required": ["prompt", "course_id"],
|
| 1285 |
+
"title": "ChatQueryRequest",
|
| 1286 |
+
"description": "Payload for direct AI query (non-session)."
|
| 1287 |
+
},
|
| 1288 |
+
"ChatQueryResponse": {
|
| 1289 |
+
"properties": {
|
| 1290 |
+
"message_id": {
|
| 1291 |
+
"type": "string",
|
| 1292 |
+
"title": "Message Id"
|
| 1293 |
+
},
|
| 1294 |
+
"role": {
|
| 1295 |
+
"type": "string",
|
| 1296 |
+
"title": "Role",
|
| 1297 |
+
"default": "assistant"
|
| 1298 |
+
},
|
| 1299 |
+
"content": {
|
| 1300 |
+
"type": "string",
|
| 1301 |
+
"title": "Content"
|
| 1302 |
+
},
|
| 1303 |
+
"sources": {
|
| 1304 |
+
"items": {
|
| 1305 |
+
"$ref": "#/components/schemas/DocumentSourceDTO"
|
| 1306 |
+
},
|
| 1307 |
+
"type": "array",
|
| 1308 |
+
"title": "Sources",
|
| 1309 |
+
"default": []
|
| 1310 |
+
},
|
| 1311 |
+
"created_at": {
|
| 1312 |
+
"type": "string",
|
| 1313 |
+
"format": "date-time",
|
| 1314 |
+
"title": "Created At"
|
| 1315 |
+
}
|
| 1316 |
+
},
|
| 1317 |
+
"type": "object",
|
| 1318 |
+
"required": ["message_id", "content", "created_at"],
|
| 1319 |
+
"title": "ChatQueryResponse",
|
| 1320 |
+
"description": "Response format for POST /ai/query (direct query without session)."
|
| 1321 |
+
},
|
| 1322 |
+
"ChatRESTRequest": {
|
| 1323 |
+
"properties": {
|
| 1324 |
+
"content": {
|
| 1325 |
+
"type": "string",
|
| 1326 |
+
"title": "Content",
|
| 1327 |
+
"description": "The user\u0027s prompt text."
|
| 1328 |
+
}
|
| 1329 |
+
},
|
| 1330 |
+
"type": "object",
|
| 1331 |
+
"required": ["content"],
|
| 1332 |
+
"title": "ChatRESTRequest",
|
| 1333 |
+
"description": "Payload for the REST fallback endpoint sending a message to a session."
|
| 1334 |
+
},
|
| 1335 |
+
"ChatSessionListResponse": {
|
| 1336 |
+
"properties": {
|
| 1337 |
+
"data": {
|
| 1338 |
+
"items": {
|
| 1339 |
+
"$ref": "#/components/schemas/ChatSessionResponse"
|
| 1340 |
+
},
|
| 1341 |
+
"type": "array",
|
| 1342 |
+
"title": "Data"
|
| 1343 |
+
},
|
| 1344 |
+
"pagination": {
|
| 1345 |
+
"$ref": "#/components/schemas/PaginationDTO"
|
| 1346 |
+
}
|
| 1347 |
+
},
|
| 1348 |
+
"type": "object",
|
| 1349 |
+
"required": ["data", "pagination"],
|
| 1350 |
+
"title": "ChatSessionListResponse"
|
| 1351 |
+
},
|
| 1352 |
+
"ChatSessionResponse": {
|
| 1353 |
+
"properties": {
|
| 1354 |
+
"uuid_id": {
|
| 1355 |
+
"type": "string",
|
| 1356 |
+
"title": "Uuid Id",
|
| 1357 |
+
"description": "Public UUID of the session."
|
| 1358 |
+
},
|
| 1359 |
+
"user_id": {
|
| 1360 |
+
"type": "string",
|
| 1361 |
+
"title": "User Id",
|
| 1362 |
+
"description": "User\u0027s public UUID (mocked or stringified internally if missing)."
|
| 1363 |
+
},
|
| 1364 |
+
"user_name": {
|
| 1365 |
+
"type": "string",
|
| 1366 |
+
"title": "User Name"
|
| 1367 |
+
},
|
| 1368 |
+
"course_id": {
|
| 1369 |
+
"type": "string",
|
| 1370 |
+
"title": "Course Id"
|
| 1371 |
+
},
|
| 1372 |
+
"course_name": {
|
| 1373 |
+
"type": "string",
|
| 1374 |
+
"title": "Course Name"
|
| 1375 |
+
},
|
| 1376 |
+
"title": {
|
| 1377 |
+
"type": "string",
|
| 1378 |
+
"title": "Title"
|
| 1379 |
+
},
|
| 1380 |
+
"message_count": {
|
| 1381 |
+
"type": "integer",
|
| 1382 |
+
"title": "Message Count"
|
| 1383 |
+
},
|
| 1384 |
+
"last_message_at": {
|
| 1385 |
+
"type": "string",
|
| 1386 |
+
"format": "date-time",
|
| 1387 |
+
"title": "Last Message At"
|
| 1388 |
+
},
|
| 1389 |
+
"created_at": {
|
| 1390 |
+
"type": "string",
|
| 1391 |
+
"format": "date-time",
|
| 1392 |
+
"title": "Created At"
|
| 1393 |
+
},
|
| 1394 |
+
"updated_at": {
|
| 1395 |
+
"type": "string",
|
| 1396 |
+
"format": "date-time",
|
| 1397 |
+
"title": "Updated At"
|
| 1398 |
+
}
|
| 1399 |
+
},
|
| 1400 |
+
"type": "object",
|
| 1401 |
+
"required": [
|
| 1402 |
+
"uuid_id",
|
| 1403 |
+
"user_id",
|
| 1404 |
+
"course_id",
|
| 1405 |
+
"title",
|
| 1406 |
+
"message_count",
|
| 1407 |
+
"created_at",
|
| 1408 |
+
"updated_at"
|
| 1409 |
+
],
|
| 1410 |
+
"title": "ChatSessionResponse",
|
| 1411 |
+
"description": "Mirrors the ChatSession schema in OpenAPI spec."
|
| 1412 |
+
},
|
| 1413 |
+
"CourseListResponse": {
|
| 1414 |
+
"properties": {
|
| 1415 |
+
"data": {
|
| 1416 |
+
"items": {
|
| 1417 |
+
"$ref": "#/components/schemas/CourseResponse"
|
| 1418 |
+
},
|
| 1419 |
+
"type": "array",
|
| 1420 |
+
"title": "Data"
|
| 1421 |
+
},
|
| 1422 |
+
"total": {
|
| 1423 |
+
"type": "integer",
|
| 1424 |
+
"title": "Total"
|
| 1425 |
+
},
|
| 1426 |
+
"page": {
|
| 1427 |
+
"type": "integer",
|
| 1428 |
+
"title": "Page"
|
| 1429 |
+
},
|
| 1430 |
+
"limit": {
|
| 1431 |
+
"type": "integer",
|
| 1432 |
+
"title": "Limit"
|
| 1433 |
+
}
|
| 1434 |
+
},
|
| 1435 |
+
"type": "object",
|
| 1436 |
+
"required": ["data", "total", "page", "limit"],
|
| 1437 |
+
"title": "CourseListResponse"
|
| 1438 |
+
},
|
| 1439 |
+
"CourseResponse": {
|
| 1440 |
+
"properties": {
|
| 1441 |
+
"id": {
|
| 1442 |
+
"type": "integer",
|
| 1443 |
+
"title": "Id"
|
| 1444 |
+
},
|
| 1445 |
+
"title": {
|
| 1446 |
+
"type": "string",
|
| 1447 |
+
"title": "Title"
|
| 1448 |
+
},
|
| 1449 |
+
"description": {
|
| 1450 |
+
"type": "string",
|
| 1451 |
+
"title": "Description"
|
| 1452 |
+
}
|
| 1453 |
+
},
|
| 1454 |
+
"type": "object",
|
| 1455 |
+
"required": ["id", "title"],
|
| 1456 |
+
"title": "CourseResponse"
|
| 1457 |
+
},
|
| 1458 |
+
"CreateChatSessionRequest": {
|
| 1459 |
+
"properties": {
|
| 1460 |
+
"course_id": {
|
| 1461 |
+
"type": "string",
|
| 1462 |
+
"title": "Course Id",
|
| 1463 |
+
"description": "UUID of the course"
|
| 1464 |
+
},
|
| 1465 |
+
"title": {
|
| 1466 |
+
"type": "string",
|
| 1467 |
+
"title": "Title",
|
| 1468 |
+
"description": "Session title"
|
| 1469 |
+
}
|
| 1470 |
+
},
|
| 1471 |
+
"type": "object",
|
| 1472 |
+
"required": ["course_id", "title"],
|
| 1473 |
+
"title": "CreateChatSessionRequest"
|
| 1474 |
+
},
|
| 1475 |
+
"CreateCourseRequest": {
|
| 1476 |
+
"properties": {
|
| 1477 |
+
"title": {
|
| 1478 |
+
"type": "string",
|
| 1479 |
+
"title": "Title"
|
| 1480 |
+
},
|
| 1481 |
+
"description": {
|
| 1482 |
+
"type": "string",
|
| 1483 |
+
"title": "Description"
|
| 1484 |
+
}
|
| 1485 |
+
},
|
| 1486 |
+
"type": "object",
|
| 1487 |
+
"required": ["title"],
|
| 1488 |
+
"title": "CreateCourseRequest"
|
| 1489 |
+
},
|
| 1490 |
+
"DocumentListResponse": {
|
| 1491 |
+
"properties": {
|
| 1492 |
+
"data": {
|
| 1493 |
+
"items": {
|
| 1494 |
+
"$ref": "#/components/schemas/DocumentResponse"
|
| 1495 |
+
},
|
| 1496 |
+
"type": "array",
|
| 1497 |
+
"title": "Data"
|
| 1498 |
+
}
|
| 1499 |
+
},
|
| 1500 |
+
"type": "object",
|
| 1501 |
+
"required": ["data"],
|
| 1502 |
+
"title": "DocumentListResponse",
|
| 1503 |
+
"description": "GET /courses/{course_id}/documents â 200 response wrapper."
|
| 1504 |
+
},
|
| 1505 |
+
"DocumentResponse": {
|
| 1506 |
+
"properties": {
|
| 1507 |
+
"id": {
|
| 1508 |
+
"type": "string",
|
| 1509 |
+
"title": "Id"
|
| 1510 |
+
},
|
| 1511 |
+
"filename": {
|
| 1512 |
+
"type": "string",
|
| 1513 |
+
"title": "Filename"
|
| 1514 |
+
},
|
| 1515 |
+
"file_type": {
|
| 1516 |
+
"$ref": "#/components/schemas/FileType"
|
| 1517 |
+
},
|
| 1518 |
+
"file_size": {
|
| 1519 |
+
"type": "integer",
|
| 1520 |
+
"title": "File Size"
|
| 1521 |
+
},
|
| 1522 |
+
"status": {
|
| 1523 |
+
"$ref": "#/components/schemas/DocumentStatus"
|
| 1524 |
+
},
|
| 1525 |
+
"course_id": {
|
| 1526 |
+
"type": "string",
|
| 1527 |
+
"title": "Course Id"
|
| 1528 |
+
},
|
| 1529 |
+
"course_name": {
|
| 1530 |
+
"type": "string",
|
| 1531 |
+
"title": "Course Name"
|
| 1532 |
+
},
|
| 1533 |
+
"uploaded_by": {
|
| 1534 |
+
"type": "string",
|
| 1535 |
+
"title": "Uploaded By"
|
| 1536 |
+
},
|
| 1537 |
+
"uploader_name": {
|
| 1538 |
+
"type": "string",
|
| 1539 |
+
"title": "Uploader Name"
|
| 1540 |
+
},
|
| 1541 |
+
"summary": {
|
| 1542 |
+
"type": "string",
|
| 1543 |
+
"title": "Summary"
|
| 1544 |
+
},
|
| 1545 |
+
"chunk_count": {
|
| 1546 |
+
"type": "integer",
|
| 1547 |
+
"title": "Chunk Count"
|
| 1548 |
+
},
|
| 1549 |
+
"error": {
|
| 1550 |
+
"type": "string",
|
| 1551 |
+
"title": "Error"
|
| 1552 |
+
},
|
| 1553 |
+
"created_at": {
|
| 1554 |
+
"type": "string",
|
| 1555 |
+
"format": "date-time",
|
| 1556 |
+
"title": "Created At"
|
| 1557 |
+
},
|
| 1558 |
+
"updated_at": {
|
| 1559 |
+
"type": "string",
|
| 1560 |
+
"format": "date-time",
|
| 1561 |
+
"title": "Updated At"
|
| 1562 |
+
}
|
| 1563 |
+
},
|
| 1564 |
+
"type": "object",
|
| 1565 |
+
"required": [
|
| 1566 |
+
"id",
|
| 1567 |
+
"filename",
|
| 1568 |
+
"file_type",
|
| 1569 |
+
"file_size",
|
| 1570 |
+
"status",
|
| 1571 |
+
"course_id",
|
| 1572 |
+
"uploaded_by",
|
| 1573 |
+
"created_at",
|
| 1574 |
+
"updated_at"
|
| 1575 |
+
],
|
| 1576 |
+
"title": "DocumentResponse",
|
| 1577 |
+
"description": "Mirrors the OpenAPI `Document` schema exactly (snake_case, all fields).\nConsumers should use `from_document()` to build from a DB record."
|
| 1578 |
+
},
|
| 1579 |
+
"DocumentSourceDTO": {
|
| 1580 |
+
"properties": {
|
| 1581 |
+
"document_id": {
|
| 1582 |
+
"type": "string",
|
| 1583 |
+
"title": "Document Id"
|
| 1584 |
+
},
|
| 1585 |
+
"filename": {
|
| 1586 |
+
"type": "string",
|
| 1587 |
+
"title": "Filename"
|
| 1588 |
+
},
|
| 1589 |
+
"excerpt": {
|
| 1590 |
+
"type": "string",
|
| 1591 |
+
"title": "Excerpt"
|
| 1592 |
+
}
|
| 1593 |
+
},
|
| 1594 |
+
"type": "object",
|
| 1595 |
+
"required": ["document_id", "filename", "excerpt"],
|
| 1596 |
+
"title": "DocumentSourceDTO"
|
| 1597 |
+
},
|
| 1598 |
+
"DocumentStatus": {
|
| 1599 |
+
"type": "string",
|
| 1600 |
+
"enum": ["processing", "ready", "failed"],
|
| 1601 |
+
"title": "DocumentStatus",
|
| 1602 |
+
"description": "An enumeration."
|
| 1603 |
+
},
|
| 1604 |
+
"FileType": {
|
| 1605 |
+
"type": "string",
|
| 1606 |
+
"enum": ["pdf", "ppt", "pptx", "txt", "xls", "xlsx"],
|
| 1607 |
+
"title": "FileType",
|
| 1608 |
+
"description": "An enumeration."
|
| 1609 |
+
},
|
| 1610 |
+
"FindUserResult": {
|
| 1611 |
+
"properties": {
|
| 1612 |
+
"founds": {
|
| 1613 |
+
"items": {
|
| 1614 |
+
"$ref": "#/components/schemas/User"
|
| 1615 |
+
},
|
| 1616 |
+
"type": "array",
|
| 1617 |
+
"title": "Founds"
|
| 1618 |
+
},
|
| 1619 |
+
"search_options": {
|
| 1620 |
+
"$ref": "#/components/schemas/SearchOptions"
|
| 1621 |
+
}
|
| 1622 |
+
},
|
| 1623 |
+
"type": "object",
|
| 1624 |
+
"title": "FindUserResult"
|
| 1625 |
+
},
|
| 1626 |
+
"HTTPValidationError": {
|
| 1627 |
+
"properties": {
|
| 1628 |
+
"detail": {
|
| 1629 |
+
"items": {
|
| 1630 |
+
"$ref": "#/components/schemas/ValidationError"
|
| 1631 |
+
},
|
| 1632 |
+
"type": "array",
|
| 1633 |
+
"title": "Detail"
|
| 1634 |
+
}
|
| 1635 |
+
},
|
| 1636 |
+
"type": "object",
|
| 1637 |
+
"title": "HTTPValidationError"
|
| 1638 |
+
},
|
| 1639 |
+
"LoginRequest": {
|
| 1640 |
+
"properties": {
|
| 1641 |
+
"email": {
|
| 1642 |
+
"type": "string",
|
| 1643 |
+
"title": "Email"
|
| 1644 |
+
},
|
| 1645 |
+
"password": {
|
| 1646 |
+
"type": "string",
|
| 1647 |
+
"title": "Password"
|
| 1648 |
+
}
|
| 1649 |
+
},
|
| 1650 |
+
"type": "object",
|
| 1651 |
+
"required": ["email", "password"],
|
| 1652 |
+
"title": "LoginRequest",
|
| 1653 |
+
"description": "POST /auth/login â body."
|
| 1654 |
+
},
|
| 1655 |
+
"LoginResponse": {
|
| 1656 |
+
"properties": {
|
| 1657 |
+
"token": {
|
| 1658 |
+
"type": "string",
|
| 1659 |
+
"title": "Token"
|
| 1660 |
+
},
|
| 1661 |
+
"user": {
|
| 1662 |
+
"$ref": "#/components/schemas/UserResponse"
|
| 1663 |
+
}
|
| 1664 |
+
},
|
| 1665 |
+
"type": "object",
|
| 1666 |
+
"required": ["token", "user"],
|
| 1667 |
+
"title": "LoginResponse",
|
| 1668 |
+
"description": "POST /auth/login and POST /auth/login/api-key â 200 response."
|
| 1669 |
+
},
|
| 1670 |
+
"PaginationDTO": {
|
| 1671 |
+
"properties": {
|
| 1672 |
+
"page": {
|
| 1673 |
+
"type": "integer",
|
| 1674 |
+
"title": "Page"
|
| 1675 |
+
},
|
| 1676 |
+
"limit": {
|
| 1677 |
+
"type": "integer",
|
| 1678 |
+
"title": "Limit"
|
| 1679 |
+
},
|
| 1680 |
+
"total": {
|
| 1681 |
+
"type": "integer",
|
| 1682 |
+
"title": "Total"
|
| 1683 |
+
},
|
| 1684 |
+
"total_pages": {
|
| 1685 |
+
"type": "integer",
|
| 1686 |
+
"title": "Total Pages"
|
| 1687 |
+
}
|
| 1688 |
+
},
|
| 1689 |
+
"type": "object",
|
| 1690 |
+
"required": ["page", "limit", "total", "total_pages"],
|
| 1691 |
+
"title": "PaginationDTO"
|
| 1692 |
+
},
|
| 1693 |
+
"RegisterRequest": {
|
| 1694 |
+
"properties": {
|
| 1695 |
+
"email": {
|
| 1696 |
+
"type": "string",
|
| 1697 |
+
"title": "Email"
|
| 1698 |
+
},
|
| 1699 |
+
"password": {
|
| 1700 |
+
"type": "string",
|
| 1701 |
+
"minLength": 6,
|
| 1702 |
+
"title": "Password"
|
| 1703 |
+
},
|
| 1704 |
+
"name": {
|
| 1705 |
+
"type": "string",
|
| 1706 |
+
"title": "Name"
|
| 1707 |
+
},
|
| 1708 |
+
"role": {
|
| 1709 |
+
"$ref": "#/components/schemas/app__schema__auth_schema__Role"
|
| 1710 |
+
},
|
| 1711 |
+
"identity_number": {
|
| 1712 |
+
"type": "string",
|
| 1713 |
+
"title": "Identity Number",
|
| 1714 |
+
"description": "Student ID (NIM), Lecturer ID (NIP), or uppercase code followed by digits"
|
| 1715 |
+
}
|
| 1716 |
+
},
|
| 1717 |
+
"type": "object",
|
| 1718 |
+
"required": [
|
| 1719 |
+
"email",
|
| 1720 |
+
"password",
|
| 1721 |
+
"name",
|
| 1722 |
+
"role",
|
| 1723 |
+
"identity_number"
|
| 1724 |
+
],
|
| 1725 |
+
"title": "RegisterRequest",
|
| 1726 |
+
"description": "POST /auth/register â body."
|
| 1727 |
+
},
|
| 1728 |
+
"RegisterResponse": {
|
| 1729 |
+
"properties": {
|
| 1730 |
+
"message": {
|
| 1731 |
+
"type": "string",
|
| 1732 |
+
"title": "Message"
|
| 1733 |
+
},
|
| 1734 |
+
"user": {
|
| 1735 |
+
"$ref": "#/components/schemas/UserResponse"
|
| 1736 |
+
}
|
| 1737 |
+
},
|
| 1738 |
+
"type": "object",
|
| 1739 |
+
"required": ["message", "user"],
|
| 1740 |
+
"title": "RegisterResponse",
|
| 1741 |
+
"description": "POST /auth/register â 201 response."
|
| 1742 |
+
},
|
| 1743 |
+
"SearchOptions": {
|
| 1744 |
+
"properties": {
|
| 1745 |
+
"ordering": {
|
| 1746 |
+
"type": "string",
|
| 1747 |
+
"title": "Ordering"
|
| 1748 |
+
},
|
| 1749 |
+
"page": {
|
| 1750 |
+
"type": "integer",
|
| 1751 |
+
"title": "Page"
|
| 1752 |
+
},
|
| 1753 |
+
"page_size": {
|
| 1754 |
+
"anyOf": [
|
| 1755 |
+
{
|
| 1756 |
+
"type": "integer"
|
| 1757 |
+
},
|
| 1758 |
+
{
|
| 1759 |
+
"type": "string"
|
| 1760 |
+
}
|
| 1761 |
+
],
|
| 1762 |
+
"title": "Page Size"
|
| 1763 |
+
},
|
| 1764 |
+
"total_count": {
|
| 1765 |
+
"type": "integer",
|
| 1766 |
+
"title": "Total Count"
|
| 1767 |
+
}
|
| 1768 |
+
},
|
| 1769 |
+
"type": "object",
|
| 1770 |
+
"title": "SearchOptions"
|
| 1771 |
+
},
|
| 1772 |
+
"SignIn": {
|
| 1773 |
+
"properties": {
|
| 1774 |
+
"email__eq": {
|
| 1775 |
+
"type": "string",
|
| 1776 |
+
"title": "Email Eq"
|
| 1777 |
+
},
|
| 1778 |
+
"password": {
|
| 1779 |
+
"type": "string",
|
| 1780 |
+
"minLength": 6,
|
| 1781 |
+
"title": "Password"
|
| 1782 |
+
}
|
| 1783 |
+
},
|
| 1784 |
+
"type": "object",
|
| 1785 |
+
"required": ["email__eq", "password"],
|
| 1786 |
+
"title": "SignIn"
|
| 1787 |
+
},
|
| 1788 |
+
"SignInResponse": {
|
| 1789 |
+
"properties": {
|
| 1790 |
+
"access_token": {
|
| 1791 |
+
"type": "string",
|
| 1792 |
+
"title": "Access Token"
|
| 1793 |
+
},
|
| 1794 |
+
"expiration": {
|
| 1795 |
+
"type": "string",
|
| 1796 |
+
"format": "date-time",
|
| 1797 |
+
"title": "Expiration"
|
| 1798 |
+
},
|
| 1799 |
+
"user_info": {
|
| 1800 |
+
"title": "User Info"
|
| 1801 |
+
}
|
| 1802 |
+
},
|
| 1803 |
+
"type": "object",
|
| 1804 |
+
"required": ["access_token", "expiration"],
|
| 1805 |
+
"title": "SignInResponse"
|
| 1806 |
+
},
|
| 1807 |
+
"SignUp": {
|
| 1808 |
+
"properties": {
|
| 1809 |
+
"email": {
|
| 1810 |
+
"type": "string",
|
| 1811 |
+
"title": "Email"
|
| 1812 |
+
},
|
| 1813 |
+
"password": {
|
| 1814 |
+
"type": "string",
|
| 1815 |
+
"minLength": 6,
|
| 1816 |
+
"title": "Password"
|
| 1817 |
+
},
|
| 1818 |
+
"name": {
|
| 1819 |
+
"type": "string",
|
| 1820 |
+
"title": "Name"
|
| 1821 |
+
},
|
| 1822 |
+
"role": {
|
| 1823 |
+
"$ref": "#/components/schemas/app__schema__auth_schema__Role"
|
| 1824 |
+
},
|
| 1825 |
+
"identity_number": {
|
| 1826 |
+
"type": "string",
|
| 1827 |
+
"title": "Identity Number"
|
| 1828 |
+
}
|
| 1829 |
+
},
|
| 1830 |
+
"type": "object",
|
| 1831 |
+
"required": [
|
| 1832 |
+
"email",
|
| 1833 |
+
"password",
|
| 1834 |
+
"name",
|
| 1835 |
+
"role",
|
| 1836 |
+
"identity_number"
|
| 1837 |
+
],
|
| 1838 |
+
"title": "SignUp"
|
| 1839 |
+
},
|
| 1840 |
+
"UpdateCourseRequest": {
|
| 1841 |
+
"properties": {
|
| 1842 |
+
"title": {
|
| 1843 |
+
"type": "string",
|
| 1844 |
+
"title": "Title"
|
| 1845 |
+
},
|
| 1846 |
+
"description": {
|
| 1847 |
+
"type": "string",
|
| 1848 |
+
"title": "Description"
|
| 1849 |
+
}
|
| 1850 |
+
},
|
| 1851 |
+
"type": "object",
|
| 1852 |
+
"title": "UpdateCourseRequest"
|
| 1853 |
+
},
|
| 1854 |
+
"UploadDocumentResponse": {
|
| 1855 |
+
"properties": {
|
| 1856 |
+
"document_id": {
|
| 1857 |
+
"type": "string",
|
| 1858 |
+
"title": "Document Id"
|
| 1859 |
+
},
|
| 1860 |
+
"filename": {
|
| 1861 |
+
"type": "string",
|
| 1862 |
+
"title": "Filename"
|
| 1863 |
+
},
|
| 1864 |
+
"status": {
|
| 1865 |
+
"$ref": "#/components/schemas/DocumentStatus"
|
| 1866 |
+
},
|
| 1867 |
+
"message": {
|
| 1868 |
+
"type": "string",
|
| 1869 |
+
"title": "Message"
|
| 1870 |
+
}
|
| 1871 |
+
},
|
| 1872 |
+
"type": "object",
|
| 1873 |
+
"required": ["document_id", "filename", "status", "message"],
|
| 1874 |
+
"title": "UploadDocumentResponse",
|
| 1875 |
+
"description": "POST /documents/upload â 202 Accepted response.\nSource: openapi-spec.yml /documents/upload 202 schema"
|
| 1876 |
+
},
|
| 1877 |
+
"UpsertUser": {
|
| 1878 |
+
"properties": {
|
| 1879 |
+
"email": {
|
| 1880 |
+
"type": "string",
|
| 1881 |
+
"title": "Email"
|
| 1882 |
+
},
|
| 1883 |
+
"user_token": {
|
| 1884 |
+
"type": "string",
|
| 1885 |
+
"title": "User Token"
|
| 1886 |
+
},
|
| 1887 |
+
"name": {
|
| 1888 |
+
"type": "string",
|
| 1889 |
+
"title": "Name"
|
| 1890 |
+
},
|
| 1891 |
+
"role": {
|
| 1892 |
+
"$ref": "#/components/schemas/app__schema__user_schema__Role"
|
| 1893 |
+
},
|
| 1894 |
+
"identity_number": {
|
| 1895 |
+
"type": "string",
|
| 1896 |
+
"title": "Identity Number"
|
| 1897 |
+
},
|
| 1898 |
+
"is_active": {
|
| 1899 |
+
"type": "boolean",
|
| 1900 |
+
"title": "Is Active"
|
| 1901 |
+
},
|
| 1902 |
+
"is_superuser": {
|
| 1903 |
+
"type": "boolean",
|
| 1904 |
+
"title": "Is Superuser"
|
| 1905 |
+
}
|
| 1906 |
+
},
|
| 1907 |
+
"type": "object",
|
| 1908 |
+
"title": "UpsertUser"
|
| 1909 |
+
},
|
| 1910 |
+
"User": {
|
| 1911 |
+
"properties": {
|
| 1912 |
+
"email": {
|
| 1913 |
+
"type": "string",
|
| 1914 |
+
"title": "Email"
|
| 1915 |
+
},
|
| 1916 |
+
"user_token": {
|
| 1917 |
+
"type": "string",
|
| 1918 |
+
"title": "User Token"
|
| 1919 |
+
},
|
| 1920 |
+
"name": {
|
| 1921 |
+
"type": "string",
|
| 1922 |
+
"title": "Name"
|
| 1923 |
+
},
|
| 1924 |
+
"role": {
|
| 1925 |
+
"$ref": "#/components/schemas/app__schema__user_schema__Role"
|
| 1926 |
+
},
|
| 1927 |
+
"identity_number": {
|
| 1928 |
+
"type": "string",
|
| 1929 |
+
"title": "Identity Number"
|
| 1930 |
+
},
|
| 1931 |
+
"is_active": {
|
| 1932 |
+
"type": "boolean",
|
| 1933 |
+
"title": "Is Active"
|
| 1934 |
+
},
|
| 1935 |
+
"is_superuser": {
|
| 1936 |
+
"type": "boolean",
|
| 1937 |
+
"title": "Is Superuser"
|
| 1938 |
+
},
|
| 1939 |
+
"id": {
|
| 1940 |
+
"type": "integer",
|
| 1941 |
+
"title": "Id"
|
| 1942 |
+
},
|
| 1943 |
+
"created_at": {
|
| 1944 |
+
"type": "string",
|
| 1945 |
+
"format": "date-time",
|
| 1946 |
+
"title": "Created At"
|
| 1947 |
+
},
|
| 1948 |
+
"updated_at": {
|
| 1949 |
+
"type": "string",
|
| 1950 |
+
"format": "date-time",
|
| 1951 |
+
"title": "Updated At"
|
| 1952 |
+
}
|
| 1953 |
+
},
|
| 1954 |
+
"type": "object",
|
| 1955 |
+
"title": "User"
|
| 1956 |
+
},
|
| 1957 |
+
"UserResponse": {
|
| 1958 |
+
"properties": {
|
| 1959 |
+
"id": {
|
| 1960 |
+
"type": "integer",
|
| 1961 |
+
"title": "Id"
|
| 1962 |
+
},
|
| 1963 |
+
"identity_number": {
|
| 1964 |
+
"type": "string",
|
| 1965 |
+
"title": "Identity Number"
|
| 1966 |
+
},
|
| 1967 |
+
"name": {
|
| 1968 |
+
"type": "string",
|
| 1969 |
+
"title": "Name"
|
| 1970 |
+
},
|
| 1971 |
+
"role": {
|
| 1972 |
+
"$ref": "#/components/schemas/app__schema__auth_schema__Role"
|
| 1973 |
+
},
|
| 1974 |
+
"email": {
|
| 1975 |
+
"type": "string",
|
| 1976 |
+
"title": "Email"
|
| 1977 |
+
},
|
| 1978 |
+
"status": {
|
| 1979 |
+
"$ref": "#/components/schemas/UserStatus"
|
| 1980 |
+
},
|
| 1981 |
+
"created_at": {
|
| 1982 |
+
"type": "string",
|
| 1983 |
+
"format": "date-time",
|
| 1984 |
+
"title": "Created At"
|
| 1985 |
+
},
|
| 1986 |
+
"updated_at": {
|
| 1987 |
+
"type": "string",
|
| 1988 |
+
"format": "date-time",
|
| 1989 |
+
"title": "Updated At"
|
| 1990 |
+
}
|
| 1991 |
+
},
|
| 1992 |
+
"type": "object",
|
| 1993 |
+
"required": [
|
| 1994 |
+
"id",
|
| 1995 |
+
"identity_number",
|
| 1996 |
+
"role",
|
| 1997 |
+
"email",
|
| 1998 |
+
"status",
|
| 1999 |
+
"created_at",
|
| 2000 |
+
"updated_at"
|
| 2001 |
+
],
|
| 2002 |
+
"title": "UserResponse",
|
| 2003 |
+
"description": "Mirrors the OpenAPI `User` schema.\nMaps `is_active: bool` â `status: active|inactive` at serialization time.\n[ENGINEERING DECISION] DB column stays as `is_active` to avoid a migration\nbreaking change; the mapping is handled here in the Pydantic layer."
|
| 2004 |
+
},
|
| 2005 |
+
"UserStatus": {
|
| 2006 |
+
"type": "string",
|
| 2007 |
+
"enum": ["active", "inactive"],
|
| 2008 |
+
"title": "UserStatus",
|
| 2009 |
+
"description": "An enumeration."
|
| 2010 |
+
},
|
| 2011 |
+
"ValidationError": {
|
| 2012 |
+
"properties": {
|
| 2013 |
+
"loc": {
|
| 2014 |
+
"items": {
|
| 2015 |
+
"anyOf": [
|
| 2016 |
+
{
|
| 2017 |
+
"type": "string"
|
| 2018 |
+
},
|
| 2019 |
+
{
|
| 2020 |
+
"type": "integer"
|
| 2021 |
+
}
|
| 2022 |
+
]
|
| 2023 |
+
},
|
| 2024 |
+
"type": "array",
|
| 2025 |
+
"title": "Location"
|
| 2026 |
+
},
|
| 2027 |
+
"msg": {
|
| 2028 |
+
"type": "string",
|
| 2029 |
+
"title": "Message"
|
| 2030 |
+
},
|
| 2031 |
+
"type": {
|
| 2032 |
+
"type": "string",
|
| 2033 |
+
"title": "Error Type"
|
| 2034 |
+
}
|
| 2035 |
+
},
|
| 2036 |
+
"type": "object",
|
| 2037 |
+
"required": ["loc", "msg", "type"],
|
| 2038 |
+
"title": "ValidationError"
|
| 2039 |
+
},
|
| 2040 |
+
"app__schema__auth_schema__Role": {
|
| 2041 |
+
"type": "string",
|
| 2042 |
+
"enum": ["admin", "student", "lecturer"],
|
| 2043 |
+
"title": "Role",
|
| 2044 |
+
"description": "An enumeration."
|
| 2045 |
+
},
|
| 2046 |
+
"app__schema__user_schema__Role": {
|
| 2047 |
+
"type": "string",
|
| 2048 |
+
"enum": ["student", "lecturer", "admin"],
|
| 2049 |
+
"title": "Role",
|
| 2050 |
+
"description": "An enumeration."
|
| 2051 |
+
}
|
| 2052 |
+
},
|
| 2053 |
+
"securitySchemes": {
|
| 2054 |
+
"JWTBearer": {
|
| 2055 |
+
"type": "http",
|
| 2056 |
+
"scheme": "bearer"
|
| 2057 |
+
}
|
| 2058 |
+
}
|
| 2059 |
+
}
|
| 2060 |
+
}
|
tests/Feature/Auth/RegistrationTest.php
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
<?php
|
| 2 |
|
|
|
|
| 3 |
use Laravel\Fortify\Features;
|
| 4 |
|
| 5 |
beforeEach(function () {
|
|
@@ -10,8 +11,23 @@
|
|
| 10 |
$response = $this->get(route('register'));
|
| 11 |
|
| 12 |
$response->assertOk();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
});
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
test('new users can register', function () {
|
| 16 |
$response = $this->post(route('register.store'), [
|
| 17 |
'name' => 'Test User',
|
|
@@ -22,4 +38,4 @@
|
|
| 22 |
|
| 23 |
$this->assertAuthenticated();
|
| 24 |
$response->assertRedirect(route('dashboard', absolute: false));
|
| 25 |
-
});
|
|
|
|
| 1 |
<?php
|
| 2 |
|
| 3 |
+
use Inertia\Testing\AssertableInertia as Assert;
|
| 4 |
use Laravel\Fortify\Features;
|
| 5 |
|
| 6 |
beforeEach(function () {
|
|
|
|
| 11 |
$response = $this->get(route('register'));
|
| 12 |
|
| 13 |
$response->assertOk();
|
| 14 |
+
|
| 15 |
+
$response->assertInertia(fn (Assert $page) => $page
|
| 16 |
+
->component('auth/register-choice'),
|
| 17 |
+
);
|
| 18 |
});
|
| 19 |
|
| 20 |
+
test('role registration screens can be rendered', function (string $routeName, string $component) {
|
| 21 |
+
$this->get(route($routeName))
|
| 22 |
+
->assertOk()
|
| 23 |
+
->assertInertia(fn (Assert $page) => $page
|
| 24 |
+
->component($component),
|
| 25 |
+
);
|
| 26 |
+
})->with([
|
| 27 |
+
'mahasiswa' => ['register.mahasiswa', 'auth/register-mahasiswa'],
|
| 28 |
+
'dosen' => ['register.dosen', 'auth/register-dosen'],
|
| 29 |
+
]);
|
| 30 |
+
|
| 31 |
test('new users can register', function () {
|
| 32 |
$response = $this->post(route('register.store'), [
|
| 33 |
'name' => 'Test User',
|
|
|
|
| 38 |
|
| 39 |
$this->assertAuthenticated();
|
| 40 |
$response->assertRedirect(route('dashboard', absolute: false));
|
| 41 |
+
});
|
vite.config.ts
CHANGED
|
@@ -12,8 +12,8 @@ export default defineConfig({
|
|
| 12 |
input: ['resources/css/app.css', 'resources/js/app.tsx'],
|
| 13 |
refresh: true,
|
| 14 |
fonts: [
|
| 15 |
-
bunny('
|
| 16 |
-
weights: [400, 500, 600],
|
| 17 |
}),
|
| 18 |
],
|
| 19 |
}),
|
|
|
|
| 12 |
input: ['resources/css/app.css', 'resources/js/app.tsx'],
|
| 13 |
refresh: true,
|
| 14 |
fonts: [
|
| 15 |
+
bunny('Inter', {
|
| 16 |
+
weights: [400, 500, 600, 700],
|
| 17 |
}),
|
| 18 |
],
|
| 19 |
}),
|