**Enhance API functionality and localizations**

- 🌐 Added API documentation annotations for multiple controllers (Meetups, Cities, Countries, Courses, Highscores, Venues), improving public and developer-facing endpoint clarity.
-  Integrated and configured the `dedoc/scramble` package for automated OpenAPI documentation generation.
- 🔒 Excluded internal routes and actions from API documentation using `ExcludeRouteFromDocs` attributes.
- 🌍 Added new localization keys for API Token features across multiple languages (`lv`, `es`, etc.).
- 🛠️ Introduced `Group`, `Response`, and `QueryParameter` attributes for better request descriptions and structured documentation.
- 🚀 Enhanced functionality for listing operations in controllers with filters and query parameters like `search` and `selected`.
This commit is contained in:
HolgerHatGarKeineNode
2026-06-08 00:09:59 +02:00
parent 5a325b1b28
commit 351dd87fa9
29 changed files with 1178 additions and 421 deletions
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\LibraryItem;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Support\Collection;
#[Group(name: 'Community', weight: 7)]
class BindleController extends Controller
{
/**
* Bindles (Bibliotheks-Einträge) auflisten
*
* Liefert die Bibliothekseinträge vom Typ 'bindle' mit id, name, link und image.
*
* @return Collection<int, array{id: int, name: string, link: string, image: string}>
*/
public function __invoke(): Collection
{
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'),
]);
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Meetup;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\JsonResponse;
#[Group(name: 'Community', weight: 7)]
class BtcMapCommunityController extends Controller
{
/**
* Einundzwanzig-Communities für BTC Map
*
* Liefert die Einundzwanzig-Communities im BTC-Map-Format (GeoJSON-Tags).
*/
public function __invoke(): JsonResponse
{
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,
);
}
}
+25 -30
View File
@@ -5,66 +5,61 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\City;
use App\Models\Lecturer;
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
#[Group(name: 'Stammdaten', weight: 5)]
class CityController extends Controller
{
/**
* Display a listing of the resource.
* @return \Illuminate\Http\Response
* Städte auflisten und durchsuchen
*
* Öffentlicher Endpunkt; liefert id, name und das zugehörige Land, alphabetisch sortiert. Ohne 'selected' wird die Liste auf 10 Einträge begrenzt.
*/
#[QueryParameter(name: 'search', description: 'Teilstring-Suche im Namen der Stadt.', required: false, type: 'string')]
#[QueryParameter(name: 'selected', description: 'Lädt gezielt die angegebenen IDs.', required: false, type: 'array')]
public function index(Request $request)
{
return City::query()
->with(['country:id,name'])
->select('id', 'name','country_id')
->orderBy('name')
->when(
->with(['country:id,name'])
->select('id', 'name', 'country_id')
->orderBy('name')
->when(
$request->search,
fn(Builder $query) => $query
fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%")
)
->when(
$request->exists('selected'),
fn(Builder $query) => $query->whereIn('id',
$request->input('selected', [])),
fn(Builder $query) => $query->limit(10)
)
->get();
->when(
$request->exists('selected'),
fn (Builder $query) => $query->whereIn('id',
$request->input('selected', [])),
fn (Builder $query) => $query->limit(10)
)
->get();
}
/**
* Store a newly created resource in storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function show(Lecturer $lecturer)
{
//
}
/**
* Update the specified resource in storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function update(Request $request, Lecturer $lecturer)
{
//
}
/**
* Remove the specified resource from storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function destroy(Lecturer $lecturer)
{
//
+18 -21
View File
@@ -4,11 +4,22 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Country;
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
#[Group(name: 'Stammdaten', weight: 5)]
class CountryController extends Controller
{
/**
* Länder auflisten und durchsuchen
*
* Öffentlicher Endpunkt; liefert id, name und code (Ländercode), alphabetisch sortiert. Ohne 'selected' wird das Ergebnis auf 10 Einträge begrenzt. Jedes Land enthält zusätzlich eine 'flag' (SVG-URL).
*/
#[QueryParameter(name: 'search', description: 'Suche in Name oder Code (Ländercode).', required: false, type: 'string')]
#[QueryParameter(name: 'selected', description: 'Lädt gezielt die angegebenen Codes oder IDs.', required: false, type: 'array')]
public function index(Request $request)
{
return Country::query()
@@ -16,19 +27,17 @@ class CountryController extends Controller
->orderBy('name')
->when(
$request->search,
fn(Builder $query)
=> $query
fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%")
->orWhere('code', 'ilike', "%{$request->search}%"),
)
->when(
$request->exists('selected'),
fn(Builder $query)
=> $query
fn (Builder $query) => $query
->whereIn('code', $request->input('selected', []))
->orWhereIn('id',
$request->input('selected', [])),
fn(Builder $query) => $query->limit(10),
fn (Builder $query) => $query->limit(10),
)
->get()
->map(function (Country $country) {
@@ -38,37 +47,25 @@ class CountryController extends Controller
});
}
/**
* Store a newly created resource in storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function show(Country $country)
{
//
}
/**
* Update the specified resource in storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function update(Request $request, Country $country)
{
//
}
/**
* Remove the specified resource from storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function destroy(Country $country)
{
//
+21 -14
View File
@@ -4,17 +4,28 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Course;
use App\Models\Lecturer;
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Dedoc\Scramble\Attributes\Response as ResponseAttribute;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
#[Group(name: 'Kurse', weight: 1)]
class CourseController extends Controller
{
/**
* Display a listing of the resource.
* Kurse auflisten und durchsuchen
*
* Öffentlicher Endpunkt; liefert id und name, alphabetisch sortiert. Ohne den Parameter
* 'selected' wird das Ergebnis auf 10 Einträge begrenzt. Jeder Kurs enthält zusätzlich
* ein 'image' (Logo-Thumbnail-URL).
*/
#[QueryParameter(name: 'search', description: 'Teilstring-Suche im Namen des Kurses.', required: false, type: 'string')]
#[QueryParameter(name: 'user_id', description: 'Filtert die Kurse nach ihrem Ersteller.', required: false, type: 'integer')]
#[QueryParameter(name: 'selected', description: 'Lädt gezielt die angegebenen Kurs-IDs.', required: false, type: 'array')]
public function index(Request $request)
{
return Course::query()
@@ -43,12 +54,11 @@ class CourseController extends Controller
}
/**
* Store a newly created resource in storage.
* Kurs anlegen
*
* Allows an authenticated lecturer to create a course programmatically
* (e.g. to sync courses from an external system). Validation mirrors the
* Livewire course create form; `created_by` is set by the model's creating hook.
* Erlaubt einem authentifizierten Referenten, einen Kurs programmatisch anzulegen.
*/
#[ResponseAttribute(status: 403, description: 'Nur Referenten (is_lecturer) dürfen Kurse anlegen.')]
public function store(Request $request): JsonResponse
{
abort_unless((bool) $request->user()->is_lecturer, Response::HTTP_FORBIDDEN);
@@ -64,19 +74,18 @@ class CourseController extends Controller
return response()->json($course->fresh(), Response::HTTP_CREATED);
}
/**
* Display the specified resource.
*/
#[ExcludeRouteFromDocs]
public function show(Course $course)
{
//
}
/**
* Update the specified resource in storage.
* Kurs aktualisieren
*
* Authorized for the course owner (or a super-admin).
* Aktualisiert einen Kurs; nur für den Ersteller oder einen Super-Admin.
*/
#[ResponseAttribute(status: 403, description: 'Nur der Ersteller des Kurses oder ein Super-Admin darf ihn ändern.')]
public function update(Request $request, Course $course): JsonResponse
{
abort_unless(
@@ -95,9 +104,7 @@ class CourseController extends Controller
return response()->json($course->fresh());
}
/**
* Remove the specified resource from storage.
*/
#[ExcludeRouteFromDocs]
public function destroy(Course $course)
{
//
@@ -4,22 +4,28 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\CourseEvent;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Dedoc\Scramble\Attributes\Response as ResponseAttribute;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
#[Group(name: 'Kurs-Events', weight: 2)]
class CourseEventController extends Controller
{
/**
* Display a listing of the course events created by the authenticated user.
* Eigene Kurs-Events auflisten
*
* Useful for an external sync client to detect which events already exist
* (idempotent syncing). Optionally filtered by course_id.
* Liefert alle vom authentifizierten Nutzer erstellten Kurs-Events (inkl. zugehörigem
* Kurs und Veranstaltungsort), absteigend nach Startdatum. Ideal für idempotente
* Synchronisierung durch externe Clients.
*
* @return Collection<int, CourseEvent>
*/
#[QueryParameter(name: 'course_id', description: 'Filtert die Kurs-Events auf einen bestimmten Kurs.', required: false, type: 'integer')]
public function index(Request $request): Collection
{
return CourseEvent::query()
@@ -34,12 +40,11 @@ class CourseEventController extends Controller
}
/**
* Store a newly created course event in storage.
* Kurs-Event anlegen
*
* Allows an authenticated lecturer to create a dated course event
* programmatically. Validation mirrors the Livewire course event form;
* `created_by` is set by the model's creating hook.
* Erlaubt einem authentifizierten Referenten, ein datiertes Kurs-Event programmatisch anzulegen.
*/
#[ResponseAttribute(status: 403, description: 'Nur Referenten (is_lecturer) dürfen Kurs-Events anlegen.')]
public function store(Request $request): JsonResponse
{
abort_unless((bool) $request->user()->is_lecturer, Response::HTTP_FORBIDDEN);
@@ -58,10 +63,11 @@ class CourseEventController extends Controller
}
/**
* Update the specified course event in storage.
* Kurs-Event aktualisieren
*
* Authorized for the course event owner (or a super-admin).
* Aktualisiert ein Kurs-Event; nur für den Ersteller oder einen Super-Admin.
*/
#[ResponseAttribute(status: 403, description: 'Nur der Ersteller des Kurs-Events oder ein Super-Admin darf es ändern.')]
public function update(Request $request, CourseEvent $courseEvent): JsonResponse
{
abort_unless(
@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\StoreHighscoreRequest;
use App\Models\Highscore;
use Carbon\CarbonImmutable;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use swentel\nostr\Filter\Filter;
@@ -15,8 +17,15 @@ use swentel\nostr\Relay\RelaySet;
use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription;
#[Group(name: 'Highscores', weight: 6)]
class HighscoreController extends Controller
{
/**
* Highscore-Bestenliste abrufen
*
* Öffentliche Bestenliste des Spiels, absteigend nach Satoshis (dann nach Zeitpunkt).
* Die Antwort hat die Form { data: [ { npub, name, satoshis, blocks, datetime } ] }.
*/
public function index(): JsonResponse
{
// npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6
@@ -37,6 +46,15 @@ class HighscoreController extends Controller
]);
}
/**
* Highscore einreichen
*
* Reicht einen Highscore ein (idempotent pro npub und Zeitpunkt).
* Zusätzlich auf 10 Anfragen pro Minute begrenzt.
* Fehlt ein Name, versucht der Server, ihn über das Nostr-Profil zu ergänzen.
* Antwortet mit HTTP 202.
*/
#[Response(status: 429, description: 'Zu viele Anfragen (Limit: 10 pro Minute überschritten).')]
public function store(StoreHighscoreRequest $request): JsonResponse
{
$validated = $request->validated();
+31 -29
View File
@@ -4,48 +4,53 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Lecturer;
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
#[Group(name: 'Referenten', weight: 4)]
class LecturerController extends Controller
{
/**
* Display a listing of the resource.
* Referenten auflisten und durchsuchen
*
* @return \Illuminate\Http\Response
* Öffentlicher Endpunkt; liefert id und name, alphabetisch sortiert. Ohne den Parameter 'selected' wird die Liste auf 10 Einträge begrenzt. Jeder Referent enthält zusätzlich ein 'image' (Avatar-Thumbnail-URL).
*/
#[QueryParameter(name: 'search', description: 'Teilstring-Suche im Namen.', required: false, type: 'string')]
#[QueryParameter(name: 'selected', description: 'Lädt gezielt die angegebenen IDs.', required: false, type: 'array')]
public function index(Request $request)
{
return Lecturer::query()
->select('id', 'name', )
->orderBy('name')
->select('id', 'name')
->orderBy('name')
// ->when($request->has('user_id'),
// fn(Builder $query) => $query->where('created_by', $request->user_id))
->when(
$request->search,
fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%")
)
->when(
$request->exists('selected'),
fn (Builder $query) => $query->whereIn('id',
$request->input('selected', [])),
fn (Builder $query) => $query->limit(10)
)
->get()
->map(function (Lecturer $lecturer) {
$lecturer->image = $lecturer->getFirstMediaUrl('avatar',
'thumb');
->when(
$request->search,
fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%")
)
->when(
$request->exists('selected'),
fn (Builder $query) => $query->whereIn('id',
$request->input('selected', [])),
fn (Builder $query) => $query->limit(10)
)
->get()
->map(function (Lecturer $lecturer) {
$lecturer->image = $lecturer->getFirstMediaUrl('avatar',
'thumb');
return $lecturer;
});
return $lecturer;
});
}
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function store(Request $request)
{
//
@@ -53,9 +58,8 @@ class LecturerController extends Controller
/**
* Display the specified resource.
*
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function show(Lecturer $lecturer)
{
//
@@ -63,9 +67,8 @@ class LecturerController extends Controller
/**
* Update the specified resource in storage.
*
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function update(Request $request, Lecturer $lecturer)
{
//
@@ -73,9 +76,8 @@ class LecturerController extends Controller
/**
* Remove the specified resource from storage.
*
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function destroy(Lecturer $lecturer)
{
//
+26 -28
View File
@@ -4,16 +4,31 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Meetup;
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Dedoc\Scramble\Attributes\Response;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
#[Group(name: 'Meetups', weight: 3)]
class MeetupController extends Controller
{
#[ExcludeRouteFromDocs]
public function ical()
{
abort(404);
}
/**
* Eigene Meetups auflisten
*
* Liefert die Meetups des angemeldeten Nutzers (id, name, inklusive Stadt/Land und Profilbild),
* alphabetisch sortiert. Erfordert eine authentifizierte Sitzung (sonst 401).
*/
#[QueryParameter(name: 'search', description: 'Teilstring-Suche im Meetup- oder Stadtnamen.', required: false, type: 'string')]
#[QueryParameter(name: 'selected', description: 'Lädt gezielt die angegebenen Meetup-IDs.', required: false, type: 'array')]
#[Response(status: 401, description: 'Nicht authentifiziert.')]
public function index(Request $request)
{
$user = $request->user();
@@ -30,16 +45,15 @@ class MeetupController extends Controller
->orderBy('name')
->when(
$request->search,
fn(Builder $query)
=> $query
fn (Builder $query) => $query
->where('name', 'like', "%{$request->search}%")
->orWhereHas('city',
fn(Builder $query) => $query->where('cities.name', 'ilike', "%{$request->search}%")),
fn (Builder $query) => $query->where('cities.name', 'ilike', "%{$request->search}%")),
)
->when(
$request->exists('selected'),
fn(Builder $query) => $query->whereIn('id', $request->input('selected', [])),
fn(Builder $query) => $query->limit(10),
fn (Builder $query) => $query->whereIn('id', $request->input('selected', [])),
fn (Builder $query) => $query->limit(10),
)
->get()
->map(function (Meetup $meetup) {
@@ -49,42 +63,26 @@ class MeetupController extends Controller
});
}
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @return \Illuminate\Http\Response
*/
public function show(meetup $meetup)
#[ExcludeRouteFromDocs]
public function show(Meetup $meetup)
{
//
}
/**
* Update the specified resource in storage.
*
* @return \Illuminate\Http\Response
*/
public function update(Request $request, meetup $meetup)
#[ExcludeRouteFromDocs]
public function update(Request $request, Meetup $meetup)
{
//
}
/**
* Remove the specified resource from storage.
*
* @return \Illuminate\Http\Response
*/
public function destroy(meetup $meetup)
#[ExcludeRouteFromDocs]
public function destroy(Meetup $meetup)
{
//
}
@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\MeetupEvent;
use Carbon\Carbon;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\PathParameter;
use Illuminate\Support\Collection;
#[Group(name: 'Meetups', weight: 3)]
class MeetupEventController extends Controller
{
/**
* Meetup-Termine auflisten
*
* Liefert kommende/vergangene Meetup-Termine. Mit optionalem Datum wird auf den
* jeweiligen Monat dieses Datums gefiltert.
*
* @return Collection<int, array<string, mixed>>
*/
#[PathParameter(name: 'date', description: 'Optionales Datum (Y-m-d); filtert auf den Monat dieses Datums.', required: false, type: 'string')]
public function __invoke(?string $date = null): Collection
{
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'),
],
);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Meetup;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
#[Group(name: 'Meetups', weight: 3)]
class MeetupMapController extends Controller
{
/**
* Öffentliche Meetups für die Community-Karte
*
* Liefert alle auf der Karte sichtbaren Meetups mit Geo- und Kontaktdaten.
*
* @return Collection<int, array<string, mixed>>
*/
#[QueryParameter(name: 'withIntro', description: 'Presence-Flag: Bei Vorhandensein wird der Intro-Text mitgeliefert.', required: false, type: 'string')]
#[QueryParameter(name: 'withLogos', description: 'Presence-Flag: Bei Vorhandensein wird die Logo-URL mitgeliefert.', required: false, type: 'string')]
public function __invoke(Request $request): Collection
{
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,
]);
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Support\Collection;
#[Group(name: 'Community', weight: 7)]
class NostrPlebController extends Controller
{
/**
* Nostr-Pubkeys (npubs) der Community
*
* Liefert die eindeutigen npub-Public-Keys aller Nutzer mit hinterlegtem Nostr-Profil.
*
* @return Collection<int, string>
*/
public function __invoke(): Collection
{
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');
}
}
+20 -11
View File
@@ -5,15 +5,24 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Lecturer;
use App\Models\Venue;
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
#[Group(name: 'Stammdaten', weight: 5)]
class VenueController extends Controller
{
/**
* Display a listing of the resource.
* @return \Illuminate\Http\Response
* Veranstaltungsorte auflisten und durchsuchen
*
* Öffentlicher Endpunkt; liefert id, name und die zugehörige Stadt/Land, alphabetisch sortiert.
* Ohne 'selected' wird die Liste auf 10 Einträge begrenzt. Jeder Ort enthält zusätzlich
* 'flag' (SVG-URL der Landesflagge) und 'description' (Stadt + Straße).
*/
#[QueryParameter(name: 'search', description: 'Teilstring-Suche im Namen des Veranstaltungsortes.', required: false, type: 'string')]
#[QueryParameter(name: 'selected', description: 'Lädt gezielt die angegebenen Veranstaltungsort-IDs (umgeht die Begrenzung auf 10 Einträge).', required: false, type: 'array')]
public function index(Request $request)
{
return Venue::query()
@@ -22,19 +31,19 @@ class VenueController extends Controller
->orderBy('name')
->when(
$request->search,
fn(Builder $query) => $query
fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%")
)
->when(
$request->exists('selected'),
fn(Builder $query) => $query->whereIn('id',
fn (Builder $query) => $query->whereIn('id',
$request->input('selected', [])),
fn(Builder $query) => $query->limit(10)
fn (Builder $query) => $query->limit(10)
)
->get()
->map(function (Venue $venue) {
$venue->flag = asset('vendor/blade-country-flags/4x3-' . $venue->city->country->code . '.svg');
$venue->description = $venue->city->name . ', ' . $venue->street;
$venue->flag = asset('vendor/blade-country-flags/4x3-'.$venue->city->country->code.'.svg');
$venue->description = $venue->city->name.', '.$venue->street;
return $venue;
});
@@ -42,8 +51,8 @@ class VenueController extends Controller
/**
* Store a newly created resource in storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function store(Request $request)
{
//
@@ -51,8 +60,8 @@ class VenueController extends Controller
/**
* Display the specified resource.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function show(Lecturer $lecturer)
{
//
@@ -60,8 +69,8 @@ class VenueController extends Controller
/**
* Update the specified resource in storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function update(Request $request, Lecturer $lecturer)
{
//
@@ -69,8 +78,8 @@ class VenueController extends Controller
/**
* Remove the specified resource from storage.
* @return \Illuminate\Http\Response
*/
#[ExcludeRouteFromDocs]
public function destroy(Lecturer $lecturer)
{
//