Compare commits
No commits in common. "637cc4a3d08a41eb9bfd97f342a07fa965424d98" and "892d14c13ad9de1f85e7c9ca61596a375c41d759" have entirely different histories.
637cc4a3d0
...
892d14c13a
@ -1,80 +0,0 @@
|
|||||||
---
|
|
||||||
name: socialite-development
|
|
||||||
description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication."
|
|
||||||
license: MIT
|
|
||||||
metadata:
|
|
||||||
author: laravel
|
|
||||||
---
|
|
||||||
|
|
||||||
# Socialite Authentication
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth).
|
|
||||||
|
|
||||||
## Available Providers
|
|
||||||
|
|
||||||
Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch`
|
|
||||||
|
|
||||||
Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`.
|
|
||||||
|
|
||||||
Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`.
|
|
||||||
|
|
||||||
Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand.
|
|
||||||
|
|
||||||
Community providers differ from built-in providers in the following ways:
|
|
||||||
- Installed via `composer require socialiteproviders/{name}`
|
|
||||||
- Must register via event listener — NOT auto-discovered like built-in providers
|
|
||||||
- Use `search-docs` for the registration pattern
|
|
||||||
|
|
||||||
## Adding a Provider
|
|
||||||
|
|
||||||
### 1. Configure the provider
|
|
||||||
|
|
||||||
Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly.
|
|
||||||
|
|
||||||
### 2. Create redirect and callback routes
|
|
||||||
|
|
||||||
Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details.
|
|
||||||
|
|
||||||
### 3. Authenticate and store the user
|
|
||||||
|
|
||||||
In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`.
|
|
||||||
|
|
||||||
### 4. Customize the redirect (optional)
|
|
||||||
|
|
||||||
- `scopes()` — merge additional scopes with the provider's defaults
|
|
||||||
- `setScopes()` — replace all scopes entirely
|
|
||||||
- `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google)
|
|
||||||
- `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object.
|
|
||||||
- `stateless()` — for API/SPA contexts where session state is not maintained
|
|
||||||
|
|
||||||
### 5. Verify
|
|
||||||
|
|
||||||
1. Config key matches driver name exactly (check the list above for hyphenated names)
|
|
||||||
2. `client_id`, `client_secret`, and `redirect` are all present
|
|
||||||
3. Redirect URL matches what is registered in the provider's OAuth dashboard
|
|
||||||
4. Callback route handles denied grants (when user declines authorization)
|
|
||||||
|
|
||||||
Use `search-docs` for complete code examples of each step.
|
|
||||||
|
|
||||||
## Additional Features
|
|
||||||
|
|
||||||
Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details.
|
|
||||||
|
|
||||||
User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes`
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods.
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
- Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails.
|
|
||||||
- Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors.
|
|
||||||
- `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely.
|
|
||||||
- Missing `stateless()` in API/SPA contexts causes `InvalidStateException`.
|
|
||||||
- Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol).
|
|
||||||
- Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved.
|
|
||||||
- Community providers require event listener registration via `SocialiteWasCalled`.
|
|
||||||
- `user()` throws when the user declines authorization. Always handle denied grants.
|
|
||||||
@ -8,10 +8,6 @@ APP_LOCALE=en
|
|||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
APP_FAKER_LOCALE=en_US
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID=
|
|
||||||
GOOGLE_CLIENT_SECRET=
|
|
||||||
GOOGLE_REDIRECT_URI="${APP_URL}/auth/google/callback"
|
|
||||||
|
|
||||||
APP_MAINTENANCE_DRIVER=file
|
APP_MAINTENANCE_DRIVER=file
|
||||||
# APP_MAINTENANCE_STORE=database
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@ -26,8 +26,10 @@ jobs:
|
|||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Node
|
||||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
- name: Setup Application
|
- name: Setup Application
|
||||||
run: composer setup
|
run: composer setup
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,7 +19,8 @@
|
|||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
bun-debug.log
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
/auth.json
|
/auth.json
|
||||||
/.fleet
|
/.fleet
|
||||||
/.idea
|
/.idea
|
||||||
|
|||||||
@ -14,7 +14,6 @@ ## Foundational Context
|
|||||||
- laravel/fortify (FORTIFY) - v1
|
- laravel/fortify (FORTIFY) - v1
|
||||||
- laravel/framework (LARAVEL) - v13
|
- laravel/framework (LARAVEL) - v13
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
- laravel/socialite (SOCIALITE) - v5
|
|
||||||
- laravel/wayfinder (WAYFINDER) - v0
|
- laravel/wayfinder (WAYFINDER) - v0
|
||||||
- larastan/larastan (LARASTAN) - v3
|
- larastan/larastan (LARASTAN) - v3
|
||||||
- laravel/boost (BOOST) - v2
|
- laravel/boost (BOOST) - v2
|
||||||
@ -52,7 +51,7 @@ ## Application Structure & Architecture
|
|||||||
|
|
||||||
## Frontend Bundling
|
## Frontend Bundling
|
||||||
|
|
||||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `bun run build`, `bun run dev`, or `composer run dev`. Ask them.
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||||
|
|
||||||
## Documentation Files
|
## Documentation Files
|
||||||
|
|
||||||
@ -175,7 +174,7 @@ ## Testing
|
|||||||
|
|
||||||
## Vite Error
|
## Vite Error
|
||||||
|
|
||||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `bun run build` or ask the user to run `bun run dev` or `composer run dev`.
|
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||||
|
|
||||||
=== wayfinder/core rules ===
|
=== wayfinder/core rules ===
|
||||||
|
|
||||||
|
|||||||
@ -23,27 +23,15 @@ public function __construct(
|
|||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
$academicTermSelected = $request->has('academic_term_id');
|
|
||||||
$academicTermId = $academicTermSelected
|
|
||||||
? ((int) $request->validated('academic_term_id') ?: null)
|
|
||||||
: $this->academicTermService->getActive()?->id;
|
|
||||||
|
|
||||||
return Inertia::render('admin/academic-classes/assignments/index', [
|
return Inertia::render('admin/academic-classes/assignments/index', [
|
||||||
'assignments' => Inertia::scroll(fn () => $this->service->paginated(
|
'assignments' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
courseClassId: $request->validated('course_class_id'),
|
courseClassId: $request->validated('course_class_id'),
|
||||||
academicTermId: $academicTermId,
|
academicTermId: $this->academicTermService->getActive()?->id,
|
||||||
)),
|
)),
|
||||||
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
||||||
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
|
'filters' => $request->only(['course_class_id']),
|
||||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
|
||||||
'filters' => Inertia::always([
|
|
||||||
'course_class_id' => $request->validated('course_class_id'),
|
|
||||||
'academic_term_id' => $academicTermSelected
|
|
||||||
? (string) ($academicTermId ?? 0)
|
|
||||||
: ($academicTermId ? (string) $academicTermId : null),
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,27 +23,15 @@ public function __construct(
|
|||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
$academicTermSelected = $request->has('academic_term_id');
|
|
||||||
$academicTermId = $academicTermSelected
|
|
||||||
? ((int) $request->validated('academic_term_id') ?: null)
|
|
||||||
: $this->academicTermService->getActive()?->id;
|
|
||||||
|
|
||||||
return Inertia::render('admin/academic-classes/materials/index', [
|
return Inertia::render('admin/academic-classes/materials/index', [
|
||||||
'materials' => Inertia::scroll(fn () => $this->service->paginated(
|
'materials' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
courseClassId: $request->validated('course_class_id'),
|
courseClassId: $request->validated('course_class_id'),
|
||||||
academicTermId: $academicTermId,
|
academicTermId: $this->academicTermService->getActive()?->id,
|
||||||
)),
|
)),
|
||||||
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
||||||
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
|
'filters' => $request->only(['course_class_id']),
|
||||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
|
||||||
'filters' => Inertia::always([
|
|
||||||
'course_class_id' => $request->validated('course_class_id'),
|
|
||||||
'academic_term_id' => $academicTermSelected
|
|
||||||
? (string) ($academicTermId ?? 0)
|
|
||||||
: ($academicTermId ? (string) $academicTermId : null),
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Redirect;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
use Laravel\Socialite\Facades\Socialite;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
|
|
||||||
|
|
||||||
class GoogleAuthController extends Controller
|
|
||||||
{
|
|
||||||
public function redirect(): SymfonyRedirectResponse
|
|
||||||
{
|
|
||||||
return Socialite::driver('google')->redirect();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function callback(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$googleUser = Socialite::driver('google')->user();
|
|
||||||
|
|
||||||
$user = User::where('email', $googleUser->getEmail())->first();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
Inertia::flash('toast', [
|
|
||||||
'type' => 'error',
|
|
||||||
'message' => 'Akun dengan email Google tersebut tidak ditemukan. Silakan hubungi administrator.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return Redirect::route('login');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $user->is_active) {
|
|
||||||
Inertia::flash('toast', [
|
|
||||||
'type' => 'error',
|
|
||||||
'message' => 'Akun Anda tidak aktif. Silakan hubungi administrator.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return Redirect::route('login');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user->google_id !== $googleUser->getId()) {
|
|
||||||
$user->update(['google_id' => $googleUser->getId()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Auth::login($user, remember: true);
|
|
||||||
|
|
||||||
$request->session()->regenerate();
|
|
||||||
|
|
||||||
return Redirect::intended(route('dashboard', absolute: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -17,8 +17,8 @@
|
|||||||
use Laravel\Passkeys\PasskeyAuthenticatable;
|
use Laravel\Passkeys\PasskeyAuthenticatable;
|
||||||
use Spatie\Permission\Traits\HasRoles;
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
#[Hidden(['password', 'remember_token'])]
|
#[Hidden(['password'])]
|
||||||
#[Guarded(['id', 'last_login_at', 'remember_token'])]
|
#[Guarded(['id', 'last_login_at'])]
|
||||||
#[Appends(['full_name'])]
|
#[Appends(['full_name'])]
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Admin\Master\AcademicTermService;
|
use App\Services\Admin\Master\AcademicTermService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
@ -117,29 +116,8 @@ public function duplicateFromTerm(int $sourceAcademicTermId, int $targetAcademic
|
|||||||
public function getAllForSelect(User $user): Collection
|
public function getAllForSelect(User $user): Collection
|
||||||
{
|
{
|
||||||
$isLecturer = $user->hasRole(UserRole::Dosen->value);
|
$isLecturer = $user->hasRole(UserRole::Dosen->value);
|
||||||
$academicTermId = $isLecturer ? $this->academicTermService->getActive()?->id : null;
|
$activeAcademicTermId = $isLecturer ? $this->academicTermService->getActive()?->id : null;
|
||||||
|
|
||||||
return $this->queryForSelect($user, $academicTermId)
|
|
||||||
->get(['course_classes.id', 'course_classes.course_id', 'course_classes.lecturer_id', 'course_classes.academic_term_id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strictly scoped to a single academic term, e.g. for a filter that
|
|
||||||
* should only offer classes once a specific term has been chosen.
|
|
||||||
* Returns an empty collection when no term is given.
|
|
||||||
*/
|
|
||||||
public function getAllForSelectByAcademicTerm(User $user, ?int $academicTermId): Collection
|
|
||||||
{
|
|
||||||
if (! $academicTermId) {
|
|
||||||
return new Collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->queryForSelect($user, $academicTermId)
|
|
||||||
->get(['course_classes.id', 'course_classes.course_id', 'course_classes.lecturer_id', 'course_classes.academic_term_id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function queryForSelect(User $user, ?int $academicTermId): Builder
|
|
||||||
{
|
|
||||||
return CourseClass::query()
|
return CourseClass::query()
|
||||||
->join('courses', 'courses.id', '=', 'course_classes.course_id')
|
->join('courses', 'courses.id', '=', 'course_classes.course_id')
|
||||||
->join('departments', 'departments.id', '=', 'courses.department_id')
|
->join('departments', 'departments.id', '=', 'courses.department_id')
|
||||||
@ -147,11 +125,12 @@ private function queryForSelect(User $user, ?int $academicTermId): Builder
|
|||||||
'course:id,code,name,semester_number,department_id',
|
'course:id,code,name,semester_number,department_id',
|
||||||
'course.department:id,name',
|
'course.department:id,name',
|
||||||
])
|
])
|
||||||
->when($user->hasRole(UserRole::Dosen->value), fn ($q) => $q->where('course_classes.lecturer_id', $user->lecturer?->id))
|
->when($isLecturer, fn ($q) => $q->where('course_classes.lecturer_id', $user->lecturer?->id)
|
||||||
->when($academicTermId, fn ($q) => $q->where('course_classes.academic_term_id', $academicTermId))
|
->where('course_classes.academic_term_id', $activeAcademicTermId))
|
||||||
->when($user->hasRole(UserRole::Mahasiswa->value), fn ($q) => $q->where('courses.department_id', $user->student?->department_id))
|
->when($user->hasRole(UserRole::Mahasiswa->value), fn ($q) => $q->where('courses.department_id', $user->student?->department_id))
|
||||||
->orderBy('departments.name')
|
->orderBy('departments.name')
|
||||||
->orderBy('courses.semester_number')
|
->orderBy('courses.semester_number')
|
||||||
->orderBy('courses.name');
|
->orderBy('courses.name')
|
||||||
|
->get(['course_classes.id', 'course_classes.course_id', 'course_classes.lecturer_id', 'course_classes.academic_term_id']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"fortify-development",
|
"fortify-development",
|
||||||
"laravel-best-practices",
|
"laravel-best-practices",
|
||||||
"socialite-development",
|
|
||||||
"wayfinder-development",
|
"wayfinder-development",
|
||||||
"pest-testing",
|
"pest-testing",
|
||||||
"inertia-react-development",
|
"inertia-react-development",
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
[install]
|
|
||||||
ignoreScripts = true
|
|
||||||
@ -14,7 +14,6 @@
|
|||||||
"laravel/chisel": "^0.1.0",
|
"laravel/chisel": "^0.1.0",
|
||||||
"laravel/fortify": "^1.37.2",
|
"laravel/fortify": "^1.37.2",
|
||||||
"laravel/framework": "^13.17",
|
"laravel/framework": "^13.17",
|
||||||
"laravel/socialite": "^5.31",
|
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"laravel/wayfinder": "^0.1.14",
|
"laravel/wayfinder": "^0.1.14",
|
||||||
"maatwebsite/excel": "^4.0",
|
"maatwebsite/excel": "^4.0",
|
||||||
@ -53,8 +52,8 @@
|
|||||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
"@php artisan key:generate",
|
"@php artisan key:generate",
|
||||||
"@php artisan migrate --force",
|
"@php artisan migrate --force",
|
||||||
"bun install",
|
"npm install",
|
||||||
"bun run build"
|
"npm run build"
|
||||||
],
|
],
|
||||||
"dev": [
|
"dev": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
@ -68,9 +67,9 @@
|
|||||||
],
|
],
|
||||||
"ci:check": [
|
"ci:check": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
"bun run lint:check",
|
"npm run lint:check",
|
||||||
"bun run format:check",
|
"npm run format:check",
|
||||||
"bun run types:check",
|
"npm run types:check",
|
||||||
"@test"
|
"@test"
|
||||||
],
|
],
|
||||||
"types:check": [
|
"types:check": [
|
||||||
|
|||||||
416
composer.lock
generated
416
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "96c16aa645077a07df0727ce7f38e195",
|
"content-hash": "379c97a9e3b22c91d1427f44e711a975",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@ -813,72 +813,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-06T22:45:56+00:00"
|
"time": "2025-03-06T22:45:56+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "firebase/php-jwt",
|
|
||||||
"version": "v7.1.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/googleapis/php-jwt.git",
|
|
||||||
"reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0",
|
|
||||||
"reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"guzzlehttp/guzzle": "^7.4",
|
|
||||||
"phpfastcache/phpfastcache": "^9.2",
|
|
||||||
"phpseclib/phpseclib": "~3.0",
|
|
||||||
"phpspec/prophecy-phpunit": "^2.0",
|
|
||||||
"phpunit/phpunit": "^9.5",
|
|
||||||
"psr/cache": "^2.0||^3.0",
|
|
||||||
"psr/http-client": "^1.0",
|
|
||||||
"psr/http-factory": "^1.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-sodium": "Support EdDSA (Ed25519) signatures",
|
|
||||||
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
|
|
||||||
"phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Firebase\\JWT\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-3-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Neuman Vong",
|
|
||||||
"email": "neuman+pear@twilio.com",
|
|
||||||
"role": "Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Anant Narayanan",
|
|
||||||
"email": "anant@php.net",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
|
|
||||||
"homepage": "https://github.com/googleapis/php-jwt",
|
|
||||||
"keywords": [
|
|
||||||
"jwt",
|
|
||||||
"php"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/googleapis/php-jwt/issues",
|
|
||||||
"source": "https://github.com/googleapis/php-jwt/tree/v7.1.0"
|
|
||||||
},
|
|
||||||
"time": "2026-06-11T17:54:14+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "fruitcake/php-cors",
|
"name": "fruitcake/php-cors",
|
||||||
"version": "v1.4.0",
|
"version": "v1.4.0",
|
||||||
@ -2032,78 +1966,6 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-07-21T16:49:22+00:00"
|
"time": "2026-07-21T16:49:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "laravel/socialite",
|
|
||||||
"version": "v5.31.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/laravel/socialite.git",
|
|
||||||
"reference": "f721b2cbec327ab820bd6aabea6ab211cfcc9f08"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/laravel/socialite/zipball/f721b2cbec327ab820bd6aabea6ab211cfcc9f08",
|
|
||||||
"reference": "f721b2cbec327ab820bd6aabea6ab211cfcc9f08",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-json": "*",
|
|
||||||
"firebase/php-jwt": "^6.4|^7.0",
|
|
||||||
"guzzlehttp/guzzle": "^6.0|^7.0|^8.0",
|
|
||||||
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
|
|
||||||
"league/oauth1-client": "^1.11",
|
|
||||||
"php": "^8.1",
|
|
||||||
"phpseclib/phpseclib": "^4.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"mockery/mockery": "^1.0",
|
|
||||||
"orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8|^11.0",
|
|
||||||
"phpstan/phpstan": "^1.12.23",
|
|
||||||
"phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"laravel": {
|
|
||||||
"aliases": {
|
|
||||||
"Socialite": "Laravel\\Socialite\\Facades\\Socialite"
|
|
||||||
},
|
|
||||||
"providers": [
|
|
||||||
"Laravel\\Socialite\\SocialiteServiceProvider"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "5.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Laravel\\Socialite\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Taylor Otwell",
|
|
||||||
"email": "taylor@laravel.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.",
|
|
||||||
"homepage": "https://laravel.com",
|
|
||||||
"keywords": [
|
|
||||||
"laravel",
|
|
||||||
"oauth"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/laravel/socialite/issues",
|
|
||||||
"source": "https://github.com/laravel/socialite"
|
|
||||||
},
|
|
||||||
"time": "2026-08-31T13:49:19+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "laravel/tinker",
|
"name": "laravel/tinker",
|
||||||
"version": "v3.0.2",
|
"version": "v3.0.2",
|
||||||
@ -2613,82 +2475,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-07-09T11:49:27+00:00"
|
"time": "2026-07-09T11:49:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "league/oauth1-client",
|
|
||||||
"version": "v1.11.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/thephpleague/oauth1-client.git",
|
|
||||||
"reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055",
|
|
||||||
"reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-json": "*",
|
|
||||||
"ext-openssl": "*",
|
|
||||||
"guzzlehttp/guzzle": "^6.0|^7.0",
|
|
||||||
"guzzlehttp/psr7": "^1.7|^2.0",
|
|
||||||
"php": ">=7.1||>=8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"ext-simplexml": "*",
|
|
||||||
"friendsofphp/php-cs-fixer": "^2.17",
|
|
||||||
"mockery/mockery": "^1.3.3",
|
|
||||||
"phpstan/phpstan": "^0.12.42",
|
|
||||||
"phpunit/phpunit": "^7.5||9.5"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-simplexml": "For decoding XML-based responses."
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "1.0-dev",
|
|
||||||
"dev-develop": "2.0-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"League\\OAuth1\\Client\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Ben Corlett",
|
|
||||||
"email": "bencorlett@me.com",
|
|
||||||
"homepage": "http://www.webcomm.com.au",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "OAuth 1.0 Client Library",
|
|
||||||
"keywords": [
|
|
||||||
"Authentication",
|
|
||||||
"SSO",
|
|
||||||
"authorization",
|
|
||||||
"bitbucket",
|
|
||||||
"identity",
|
|
||||||
"idp",
|
|
||||||
"oauth",
|
|
||||||
"oauth1",
|
|
||||||
"single sign on",
|
|
||||||
"trello",
|
|
||||||
"tumblr",
|
|
||||||
"twitter"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/thephpleague/oauth1-client/issues",
|
|
||||||
"source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0"
|
|
||||||
},
|
|
||||||
"time": "2024-12-10T19:59:05+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "league/uri",
|
"name": "league/uri",
|
||||||
"version": "7.8.1",
|
"version": "7.8.1",
|
||||||
@ -4084,126 +3870,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-27T19:41:33+00:00"
|
"time": "2025-12-27T19:41:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "phpseclib/phpseclib",
|
|
||||||
"version": "4.0.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
|
||||||
"reference": "bb7b959c8159957edae6f5084ebbac765d310e16"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/bb7b959c8159957edae6f5084ebbac765d310e16",
|
|
||||||
"reference": "bb7b959c8159957edae6f5084ebbac765d310e16",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"paragonie/constant_time_encoding": "^2|^3",
|
|
||||||
"php": ">=8.1",
|
|
||||||
"symfony/polyfill-php82": "^1.26"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"brianium/paratest": "^7.22",
|
|
||||||
"ext-xml": "*",
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.3",
|
|
||||||
"phpunit/phpunit": "^13",
|
|
||||||
"squizlabs/php_codesniffer": "^3.7",
|
|
||||||
"vimeo/psalm": "*"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-dom": "Install the DOM extension to load XML formatted public keys.",
|
|
||||||
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
|
|
||||||
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
|
|
||||||
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"phpseclib/bootstrap.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"phpseclib4\\": "phpseclib/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Jim Wigginton",
|
|
||||||
"email": "terrafrost@php.net",
|
|
||||||
"role": "Lead Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Patrick Monnerat",
|
|
||||||
"email": "pm@datasphere.ch",
|
|
||||||
"role": "Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Andreas Fischer",
|
|
||||||
"email": "bantu@phpbb.com",
|
|
||||||
"role": "Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hans-Jürgen Petrich",
|
|
||||||
"email": "petrich@tronic-media.com",
|
|
||||||
"role": "Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Graham Campbell",
|
|
||||||
"email": "graham@alt-three.com",
|
|
||||||
"role": "Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Jack Worman",
|
|
||||||
"email": "jack.worman@gmail.com",
|
|
||||||
"homepage": "https://jackworman.com",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
|
|
||||||
"homepage": "https://phpseclib.com/",
|
|
||||||
"keywords": [
|
|
||||||
"BigInteger",
|
|
||||||
"aes",
|
|
||||||
"asn.1",
|
|
||||||
"asn1",
|
|
||||||
"blowfish",
|
|
||||||
"crypto",
|
|
||||||
"cryptography",
|
|
||||||
"encryption",
|
|
||||||
"rsa",
|
|
||||||
"security",
|
|
||||||
"sftp",
|
|
||||||
"signature",
|
|
||||||
"signing",
|
|
||||||
"ssh",
|
|
||||||
"twofish",
|
|
||||||
"x.509",
|
|
||||||
"x509"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
|
||||||
"source": "https://github.com/phpseclib/phpseclib/tree/4.0.1"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/terrafrost",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://www.patreon.com/phpseclib",
|
|
||||||
"type": "patreon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-08-26T12:15:13+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpdoc-parser",
|
"name": "phpstan/phpdoc-parser",
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
@ -7188,86 +6854,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
"time": "2026-04-10T16:19:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "symfony/polyfill-php82",
|
|
||||||
"version": "v1.38.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/polyfill-php82.git",
|
|
||||||
"reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
|
|
||||||
"reference": "002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=7.2"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"thanks": {
|
|
||||||
"url": "https://github.com/symfony/polyfill",
|
|
||||||
"name": "symfony/polyfill"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"bootstrap.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Polyfill\\Php82\\": ""
|
|
||||||
},
|
|
||||||
"classmap": [
|
|
||||||
"Resources/stubs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Nicolas Grekas",
|
|
||||||
"email": "p@tchwork.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"keywords": [
|
|
||||||
"compatibility",
|
|
||||||
"polyfill",
|
|
||||||
"portable",
|
|
||||||
"shim"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.38.1"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://symfony.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/fabpot",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/nicolas-grekas",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-05-26T12:45:58+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php84",
|
"name": "symfony/polyfill-php84",
|
||||||
"version": "v1.38.1",
|
"version": "v1.38.1",
|
||||||
|
|||||||
@ -35,10 +35,4 @@
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'google' => [
|
|
||||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
|
||||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
|
||||||
'redirect' => env('GOOGLE_REDIRECT_URI'),
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->rememberToken();
|
|
||||||
$table->string('google_id')->nullable()->unique()->after('email');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->dropColumn(['remember_token', 'google_id']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
12718
package-lock.json
generated
Normal file
12718
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
8492
pnpm-lock.yaml
generated
Normal file
8492
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
packages:
|
||||||
|
- '.'
|
||||||
|
|
||||||
|
publicHoistPattern:
|
||||||
|
- '@inertiajs/core'
|
||||||
@ -45,7 +45,6 @@ export type FilterField =
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
type?: 'select';
|
type?: 'select';
|
||||||
options: FilterOption[];
|
options: FilterOption[];
|
||||||
disabled?: boolean;
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
key: string;
|
key: string;
|
||||||
@ -53,7 +52,6 @@ export type FilterField =
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
type: 'combobox';
|
type: 'combobox';
|
||||||
groups: FilterOptionGroup[];
|
groups: FilterOptionGroup[];
|
||||||
disabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type FilterDialogProps = {
|
type FilterDialogProps = {
|
||||||
@ -87,12 +85,10 @@ function ComboboxFilterField({
|
|||||||
isItemEqualToValue={(a: FilterOption, b: FilterOption) =>
|
isItemEqualToValue={(a: FilterOption, b: FilterOption) =>
|
||||||
a.value === b.value
|
a.value === b.value
|
||||||
}
|
}
|
||||||
disabled={field.disabled}
|
|
||||||
>
|
>
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
placeholder={field.placeholder ?? 'Semua'}
|
placeholder={field.placeholder ?? 'Semua'}
|
||||||
showClear
|
showClear
|
||||||
disabled={field.disabled}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<ComboboxContent>
|
<ComboboxContent>
|
||||||
@ -194,7 +190,6 @@ export function FilterDialog({
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleChange(field.key, value)
|
handleChange(field.key, value)
|
||||||
}
|
}
|
||||||
disabled={field.disabled}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useFlashToast } from '@/hooks/use-flash-toast';
|
|
||||||
import AuthLayoutTemplate from '@/layouts/auth/auth-split-layout';
|
import AuthLayoutTemplate from '@/layouts/auth/auth-split-layout';
|
||||||
|
|
||||||
export default function AuthLayout({
|
export default function AuthLayout({
|
||||||
@ -10,8 +9,6 @@ export default function AuthLayout({
|
|||||||
description?: string;
|
description?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
useFlashToast();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayoutTemplate title={title} description={description}>
|
<AuthLayoutTemplate title={title} description={description}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -45,7 +45,6 @@ import {
|
|||||||
submit,
|
submit,
|
||||||
update,
|
update,
|
||||||
} from '@/routes/admin/academic-classes/assignments';
|
} from '@/routes/admin/academic-classes/assignments';
|
||||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
|
||||||
import type { Assignment } from '@/types/assignment';
|
import type { Assignment } from '@/types/assignment';
|
||||||
import { AssignmentStatuses, AssignmentStatusLabels } from '@/types/assignment';
|
import { AssignmentStatuses, AssignmentStatusLabels } from '@/types/assignment';
|
||||||
import { createAssignmentCard } from './card';
|
import { createAssignmentCard } from './card';
|
||||||
@ -64,12 +63,6 @@ type CourseClassOption = {
|
|||||||
|
|
||||||
type CourseClassGroup = { value: string; items: CourseClassOption[] };
|
type CourseClassGroup = { value: string; items: CourseClassOption[] };
|
||||||
|
|
||||||
type AcademicTermOption = {
|
|
||||||
id: number;
|
|
||||||
academic_year: string;
|
|
||||||
semester: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
assignments: {
|
assignments: {
|
||||||
data: Assignment[];
|
data: Assignment[];
|
||||||
@ -79,12 +72,9 @@ type Props = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
courseClasses: CourseClassOption[];
|
courseClasses: CourseClassOption[];
|
||||||
filterCourseClasses: CourseClassOption[];
|
|
||||||
academicTerms: AcademicTermOption[];
|
|
||||||
highlight?: number;
|
highlight?: number;
|
||||||
filters: {
|
filters: {
|
||||||
course_class_id?: string;
|
course_class_id?: string;
|
||||||
academic_term_id?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -171,8 +161,6 @@ function CourseClassField({
|
|||||||
export default function AssignmentIndex({
|
export default function AssignmentIndex({
|
||||||
assignments,
|
assignments,
|
||||||
courseClasses,
|
courseClasses,
|
||||||
filterCourseClasses,
|
|
||||||
academicTerms,
|
|
||||||
highlight,
|
highlight,
|
||||||
filters,
|
filters,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@ -189,24 +177,11 @@ export default function AssignmentIndex({
|
|||||||
const canSubmit = hasPermission('submit-assignments');
|
const canSubmit = hasPermission('submit-assignments');
|
||||||
|
|
||||||
const filterFields = [
|
const filterFields = [
|
||||||
{
|
|
||||||
key: 'academic_term_id',
|
|
||||||
label: 'Periode Akademik',
|
|
||||||
options: academicTerms.map((term) => ({
|
|
||||||
value: String(term.id),
|
|
||||||
label: formatAcademicTermLabel(term),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'course_class_id',
|
key: 'course_class_id',
|
||||||
label: 'Kelas',
|
label: 'Kelas',
|
||||||
type: 'combobox' as const,
|
type: 'combobox' as const,
|
||||||
groups: courseClassFilterGroups(filterCourseClasses),
|
groups: courseClassFilterGroups(courseClasses),
|
||||||
disabled: filterCourseClasses.length === 0,
|
|
||||||
placeholder:
|
|
||||||
filterCourseClasses.length === 0
|
|
||||||
? 'Pilih periode akademik dulu'
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -224,42 +199,6 @@ export default function AssignmentIndex({
|
|||||||
resetKeys: ['assignments'],
|
resetKeys: ['assignments'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleApplyFilters(newFilters: Record<string, string>) {
|
|
||||||
// Without an explicit `academic_term_id`, the backend falls back to
|
|
||||||
// the active term, so clearing this filter needs to be sent
|
|
||||||
// explicitly (value '0') rather than just dropping the key.
|
|
||||||
const clearedAcademicTerm =
|
|
||||||
Boolean(filters.academic_term_id) && !newFilters.academic_term_id;
|
|
||||||
|
|
||||||
const nextFilters: Record<string, string> = {
|
|
||||||
...newFilters,
|
|
||||||
...(clearedAcademicTerm ? { academic_term_id: '0' } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// The class list depends on the selected term, so a previously
|
|
||||||
// picked class is no longer relevant once the term changes.
|
|
||||||
const academicTermChanged =
|
|
||||||
(nextFilters.academic_term_id ?? '0') !==
|
|
||||||
(filters.academic_term_id ?? '0');
|
|
||||||
|
|
||||||
if (academicTermChanged) {
|
|
||||||
delete nextFilters.course_class_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilters(nextFilters);
|
|
||||||
}
|
|
||||||
|
|
||||||
// '0' explicitly means "all terms" — treated as no active filter from
|
|
||||||
// FilterDialog's point of view, so its Select shows "Semua" (not blank)
|
|
||||||
// as selected.
|
|
||||||
const filterDialogActiveFilters = {
|
|
||||||
...filters,
|
|
||||||
academic_term_id:
|
|
||||||
filters.academic_term_id === '0'
|
|
||||||
? undefined
|
|
||||||
: filters.academic_term_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
if (!deleting) {
|
if (!deleting) {
|
||||||
return;
|
return;
|
||||||
@ -294,8 +233,8 @@ export default function AssignmentIndex({
|
|||||||
<>
|
<>
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
activeFilters={filterDialogActiveFilters}
|
activeFilters={filters}
|
||||||
onApply={handleApplyFilters}
|
onApply={applyFilters}
|
||||||
/>
|
/>
|
||||||
<ViewToggle value={view} onChange={setView} />
|
<ViewToggle value={view} onChange={setView} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -36,7 +36,6 @@ import {
|
|||||||
store,
|
store,
|
||||||
update,
|
update,
|
||||||
} from '@/routes/admin/academic-classes/materials';
|
} from '@/routes/admin/academic-classes/materials';
|
||||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
|
||||||
import type { Material } from '@/types/material';
|
import type { Material } from '@/types/material';
|
||||||
import { createMaterialCard } from './card';
|
import { createMaterialCard } from './card';
|
||||||
import { createMaterialColumns } from './columns';
|
import { createMaterialColumns } from './columns';
|
||||||
@ -54,12 +53,6 @@ type CourseClassOption = {
|
|||||||
|
|
||||||
type CourseClassGroup = { value: string; items: CourseClassOption[] };
|
type CourseClassGroup = { value: string; items: CourseClassOption[] };
|
||||||
|
|
||||||
type AcademicTermOption = {
|
|
||||||
id: number;
|
|
||||||
academic_year: string;
|
|
||||||
semester: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
materials: {
|
materials: {
|
||||||
data: Material[];
|
data: Material[];
|
||||||
@ -69,12 +62,9 @@ type Props = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
courseClasses: CourseClassOption[];
|
courseClasses: CourseClassOption[];
|
||||||
filterCourseClasses: CourseClassOption[];
|
|
||||||
academicTerms: AcademicTermOption[];
|
|
||||||
highlight?: number;
|
highlight?: number;
|
||||||
filters: {
|
filters: {
|
||||||
course_class_id?: string;
|
course_class_id?: string;
|
||||||
academic_term_id?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -161,8 +151,6 @@ function courseClassFilterGroups(
|
|||||||
export default function MaterialIndex({
|
export default function MaterialIndex({
|
||||||
materials,
|
materials,
|
||||||
courseClasses,
|
courseClasses,
|
||||||
filterCourseClasses,
|
|
||||||
academicTerms,
|
|
||||||
highlight,
|
highlight,
|
||||||
filters,
|
filters,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@ -176,24 +164,11 @@ export default function MaterialIndex({
|
|||||||
const canDelete = hasPermission('delete-materials');
|
const canDelete = hasPermission('delete-materials');
|
||||||
|
|
||||||
const filterFields = [
|
const filterFields = [
|
||||||
{
|
|
||||||
key: 'academic_term_id',
|
|
||||||
label: 'Periode Akademik',
|
|
||||||
options: academicTerms.map((term) => ({
|
|
||||||
value: String(term.id),
|
|
||||||
label: formatAcademicTermLabel(term),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'course_class_id',
|
key: 'course_class_id',
|
||||||
label: 'Kelas',
|
label: 'Kelas',
|
||||||
type: 'combobox' as const,
|
type: 'combobox' as const,
|
||||||
groups: courseClassFilterGroups(filterCourseClasses),
|
groups: courseClassFilterGroups(courseClasses),
|
||||||
disabled: filterCourseClasses.length === 0,
|
|
||||||
placeholder:
|
|
||||||
filterCourseClasses.length === 0
|
|
||||||
? 'Pilih periode akademik dulu'
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -211,39 +186,6 @@ export default function MaterialIndex({
|
|||||||
resetKeys: ['materials'],
|
resetKeys: ['materials'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleApplyFilters(newFilters: Record<string, string>) {
|
|
||||||
const clearedAcademicTerm =
|
|
||||||
Boolean(filters.academic_term_id) && !newFilters.academic_term_id;
|
|
||||||
|
|
||||||
const nextFilters: Record<string, string> = {
|
|
||||||
...newFilters,
|
|
||||||
...(clearedAcademicTerm ? { academic_term_id: '0' } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// The class list depends on the selected term, so a previously
|
|
||||||
// picked class is no longer relevant once the term changes.
|
|
||||||
const academicTermChanged =
|
|
||||||
(nextFilters.academic_term_id ?? '0') !==
|
|
||||||
(filters.academic_term_id ?? '0');
|
|
||||||
|
|
||||||
if (academicTermChanged) {
|
|
||||||
delete nextFilters.course_class_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilters(nextFilters);
|
|
||||||
}
|
|
||||||
|
|
||||||
// '0' explicitly means "all terms" — treated as no active filter from
|
|
||||||
// FilterDialog's point of view, so its Select shows "Semua" (not blank)
|
|
||||||
// as selected.
|
|
||||||
const filterDialogActiveFilters = {
|
|
||||||
...filters,
|
|
||||||
academic_term_id:
|
|
||||||
filters.academic_term_id === '0'
|
|
||||||
? undefined
|
|
||||||
: filters.academic_term_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
if (!deleting) {
|
if (!deleting) {
|
||||||
return;
|
return;
|
||||||
@ -272,8 +214,8 @@ export default function MaterialIndex({
|
|||||||
<>
|
<>
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
activeFilters={filterDialogActiveFilters}
|
activeFilters={filters}
|
||||||
onApply={handleApplyFilters}
|
onApply={applyFilters}
|
||||||
/>
|
/>
|
||||||
<ViewToggle value={view} onChange={setView} />
|
<ViewToggle value={view} onChange={setView} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import google from '@/routes/auth/google';
|
|
||||||
import { store } from '@/routes/login';
|
import { store } from '@/routes/login';
|
||||||
import { request } from '@/routes/password';
|
import { request } from '@/routes/password';
|
||||||
|
|
||||||
@ -106,11 +105,10 @@ export default function Login({ status, canResetPassword }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<a href={google.redirect.url()}>
|
|
||||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
||||||
@ -130,7 +128,6 @@ export default function Login({ status, canResetPassword }: Props) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Masuk dengan Google
|
Masuk dengan Google
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,17 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Auth\GoogleAuthController;
|
|
||||||
use App\Http\Controllers\MediaUploadController;
|
use App\Http\Controllers\MediaUploadController;
|
||||||
use App\Http\Controllers\NotificationController;
|
use App\Http\Controllers\NotificationController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::inertia('/', 'welcome')->name('home');
|
Route::inertia('/', 'welcome')->name('home');
|
||||||
|
|
||||||
Route::middleware('guest')->prefix('auth/google')->name('auth.google.')->group(function () {
|
|
||||||
Route::get('redirect', [GoogleAuthController::class, 'redirect'])->name('redirect');
|
|
||||||
Route::get('callback', [GoogleAuthController::class, 'callback'])->name('callback');
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::middleware(['auth', 'verified'])->group(function () {
|
Route::middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::inertia('dashboard', 'dashboard')->name('dashboard');
|
Route::inertia('dashboard', 'dashboard')->name('dashboard');
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user