mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-11 02:50:29 +00:00
a3062f6c4e
Implements Sanctum-authenticated write endpoints so a lecturer can create and update their own courses and dated course events programmatically (e.g. to keep the portal's course events in sync with an external system). - CourseController@store / @update implemented (validation mirrors the Livewire course create form; create requires is_lecturer, update is restricted to the owner or a super-admin). - New CourseEventController with index/store/update. index returns only the authenticated user's own events (optional ?course_id= filter) for idempotent syncing; validation mirrors the Livewire course event form. - Public `courses` API resource narrowed to index/show; all writes moved behind an `auth:sanctum` route group (the previous store/update/destroy actions were empty no-ops). - Pest feature test covering auth (401), authorization (403/is_lecturer/ ownership), creation (201), validation (422) and ownership-scoped listing. Ported from Einundzwanzig-Podcast/einundzwanzig-portal#25, adapted to this repo's conventions (inline authorization instead of policies, Pest tests, validation mirroring the current Livewire forms) while keeping the same endpoint outputs. Co-authored-by: schnuartz-ai <schnuartz@gmail.com>
220 lines
9.5 KiB
PHP
220 lines
9.5 KiB
PHP
<?php
|
|
|
|
use App\Http\Controllers\Api\CityController;
|
|
use App\Http\Controllers\Api\CountryController;
|
|
use App\Http\Controllers\Api\CourseController;
|
|
use App\Http\Controllers\Api\CourseEventController;
|
|
use App\Http\Controllers\Api\HighscoreController;
|
|
use App\Http\Controllers\Api\LecturerController;
|
|
use App\Http\Controllers\Api\MeetupController;
|
|
use App\Http\Controllers\Api\VenueController;
|
|
use App\Http\Controllers\LnurlAuthController;
|
|
use App\Models\LibraryItem;
|
|
use App\Models\Meetup;
|
|
use App\Models\MeetupEvent;
|
|
use App\Models\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
Route::middleware(['throttle:60,1'])
|
|
->as('api.')
|
|
->group(function () {
|
|
Route::resource('countries', CountryController::class);
|
|
Route::get('meetup/ical', [MeetupController::class, 'ical'])->name('api.meetup.ical');
|
|
Route::resource('meetup', MeetupController::class);
|
|
Route::resource('lecturers', LecturerController::class);
|
|
Route::resource('courses', CourseController::class)
|
|
->only(['index', 'show']);
|
|
Route::resource('cities', CityController::class);
|
|
Route::resource('venues', VenueController::class);
|
|
Route::get('highscores', [HighscoreController::class, 'index'])->name('highscores.index');
|
|
Route::post('highscores', [HighscoreController::class, 'store'])
|
|
->middleware('throttle:10,1')
|
|
->name('highscores.store');
|
|
Route::get('nostrplebs', function () {
|
|
return User::query()
|
|
->select([
|
|
'email',
|
|
'public_key',
|
|
'lightning_address',
|
|
'lnurl',
|
|
'node_id',
|
|
'paynym',
|
|
'lnbits',
|
|
'nostr',
|
|
'id',
|
|
])
|
|
->whereNotNull('nostr')
|
|
->where('nostr', 'like', 'npub1%')
|
|
->orderByDesc('id')
|
|
->get()
|
|
->unique('nostr')
|
|
->pluck('nostr');
|
|
});
|
|
Route::get('bindles', function () {
|
|
return LibraryItem::query()
|
|
->where('type', 'bindle')
|
|
->with([
|
|
'media',
|
|
])
|
|
->orderByDesc('id')
|
|
->get()
|
|
->map(fn ($item) => [
|
|
'id' => $item->id,
|
|
'name' => $item->name,
|
|
'link' => strtok($item->value, '?'),
|
|
'image' => $item->getFirstMediaUrl('main'),
|
|
]);
|
|
});
|
|
Route::get('meetups', function (Request $request) {
|
|
return Meetup::query()
|
|
->where('visible_on_map', true)
|
|
->with([
|
|
'meetupEvents',
|
|
'city.country',
|
|
'media',
|
|
])
|
|
->get()
|
|
->map(fn ($meetup) => [
|
|
'name' => $meetup->name,
|
|
'portalLink' => url()->route(
|
|
'meetups.landingpage',
|
|
['country' => $meetup->city->country, 'meetup' => $meetup],
|
|
),
|
|
'url' => $meetup->telegram_link ?? $meetup->webpage,
|
|
'top' => $meetup->github_data['top'] ?? null,
|
|
'left' => $meetup->github_data['left'] ?? null,
|
|
'country' => str($meetup->city->country->code)->upper(),
|
|
'state' => $meetup->github_data['state'] ?? null,
|
|
'city' => $meetup->city->name,
|
|
'longitude' => (float) $meetup->city->longitude,
|
|
'latitude' => (float) $meetup->city->latitude,
|
|
'twitter_username' => $meetup->twitter_username,
|
|
'website' => $meetup->webpage,
|
|
'simplex' => $meetup->simplex,
|
|
'signal' => $meetup->signal,
|
|
'nostr' => $meetup->nostr,
|
|
'next_event' => $meetup->nextEvent,
|
|
'intro' => $request->has('withIntro') ? $meetup->intro : null,
|
|
'logo' => $request->has('withLogos') ? $meetup->getFirstMediaUrl('logo') : null,
|
|
]);
|
|
});
|
|
Route::get('meetup-events/{date?}', function ($date = null) {
|
|
if ($date) {
|
|
$date = Carbon::parse($date);
|
|
}
|
|
$events = MeetupEvent::query()
|
|
->with([
|
|
'meetup.city.country',
|
|
'meetup.media',
|
|
])
|
|
->when(
|
|
$date,
|
|
fn ($query) => $query
|
|
->where('start', '>=', $date)
|
|
->where('start', '<=', $date->copy()->endOfMonth()),
|
|
)
|
|
->get();
|
|
|
|
return $events->map(fn ($event) => [
|
|
'start' => $event->start->format('Y-m-d H:i'),
|
|
'location' => $event->location,
|
|
'description' => $event->description,
|
|
'link' => $event->link,
|
|
'meetup.name' => $event->meetup->name,
|
|
'meetup.portalLink' => url()->route(
|
|
'meetups.landingpage',
|
|
[
|
|
'country' => $event->meetup->city->country,
|
|
'meetup' => $event->meetup,
|
|
],
|
|
),
|
|
'meetup.url' => $event->meetup->telegram_link ?? $event->meetup->webpage,
|
|
'meetup.country' => str($event->meetup->city->country->code)->upper(),
|
|
'meetup.city' => $event->meetup->city->name,
|
|
'meetup.longitude' => (float) $event->meetup->city->longitude,
|
|
'meetup.latitude' => (float) $event->meetup->city->latitude,
|
|
'meetup.twitter_username' => $event->meetup->twitter_username,
|
|
'meetup.website' => $event->meetup->webpage,
|
|
'meetup.simplex' => $event->meetup->simplex,
|
|
'meetup.signal' => $event->meetup->signal,
|
|
'meetup.nostr' => $event->meetup->nostr,
|
|
'meetup.logo' => $event->meetup->getFirstMediaUrl('logo'),
|
|
],
|
|
);
|
|
});
|
|
Route::get('btc-map-communities', function () {
|
|
return response()->json(
|
|
Meetup::query()
|
|
->with([
|
|
'media',
|
|
'city.country',
|
|
])
|
|
->where('community', '=', 'einundzwanzig')
|
|
->when(
|
|
app()->environment('production'),
|
|
fn ($query) => $query->whereHas(
|
|
'city',
|
|
fn ($query) => $query
|
|
->whereNotNull('cities.simplified_geojson')
|
|
->whereNotNull('cities.population')
|
|
->whereNotNull('cities.population_date'),
|
|
),
|
|
)
|
|
->get()
|
|
->map(fn ($meetup) => [
|
|
'id' => $meetup->slug,
|
|
'tags' => [
|
|
'type' => 'community',
|
|
'name' => $meetup->name,
|
|
'continent' => 'europe',
|
|
'icon:square' => $meetup->logoSquare,
|
|
// 'contact:email' => null,
|
|
'contact:twitter' => $meetup->twitter_username ? 'https://twitter.com/'.$meetup->twitter_username : null,
|
|
'contact:website' => $meetup->webpage,
|
|
'contact:telegram' => $meetup->telegram_link,
|
|
'contact:nostr' => $meetup->nostr,
|
|
// 'tips:lightning_address' => null,
|
|
'organization' => 'einundzwanzig',
|
|
'language' => $meetup->city->country->language_codes[0] ?? 'de',
|
|
'geo_json' => $meetup->city->simplified_geojson,
|
|
'population' => $meetup->city->population,
|
|
'population:date' => $meetup->city->population_date,
|
|
],
|
|
])
|
|
->toArray(),
|
|
200,
|
|
['Content-Type' => 'application/json;charset=UTF-8', 'Charset' => 'utf-8'],
|
|
JSON_UNESCAPED_SLASHES,
|
|
);
|
|
});
|
|
});
|
|
|
|
/*
|
|
* Authenticated write endpoints (Sanctum token auth).
|
|
* Lets a lecturer create/update their own courses and course events
|
|
* programmatically, e.g. to sync events from an external system.
|
|
*/
|
|
Route::middleware('auth:sanctum')
|
|
->as('api.')
|
|
->group(function () {
|
|
Route::post('courses', [CourseController::class, 'store'])
|
|
->name('courses.store');
|
|
Route::patch('courses/{course}', [CourseController::class, 'update'])
|
|
->name('courses.update');
|
|
|
|
Route::get('course-events', [CourseEventController::class, 'index'])
|
|
->name('course-events.index');
|
|
Route::post('course-events', [CourseEventController::class, 'store'])
|
|
->name('course-events.store');
|
|
Route::patch('course-events/{courseEvent}', [CourseEventController::class, 'update'])
|
|
->name('course-events.update');
|
|
});
|
|
|
|
Route::get('/lnurl-auth-callback', [LnurlAuthController::class, 'callback'])
|
|
->name('auth.ln.callback');
|
|
|
|
Route::post('/check-auth-error', [LnurlAuthController::class, 'checkError'])
|
|
->name('auth.check-error');
|