**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\Http\Controllers\Controller;
use App\Models\City; use App\Models\City;
use App\Models\Lecturer; 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\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
#[Group(name: 'Stammdaten', weight: 5)]
class CityController extends Controller class CityController extends Controller
{ {
/** /**
* Display a listing of the resource. * Städte auflisten und durchsuchen
* @return \Illuminate\Http\Response *
* Ö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) public function index(Request $request)
{ {
return City::query() return City::query()
->with(['country:id,name']) ->with(['country:id,name'])
->select('id', 'name','country_id') ->select('id', 'name', 'country_id')
->orderBy('name') ->orderBy('name')
->when( ->when(
$request->search, $request->search,
fn(Builder $query) => $query fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%") ->where('name', 'ilike', "%{$request->search}%")
) )
->when( ->when(
$request->exists('selected'), $request->exists('selected'),
fn(Builder $query) => $query->whereIn('id', fn (Builder $query) => $query->whereIn('id',
$request->input('selected', [])), $request->input('selected', [])),
fn(Builder $query) => $query->limit(10) fn (Builder $query) => $query->limit(10)
) )
->get(); ->get();
} }
/** #[ExcludeRouteFromDocs]
* Store a newly created resource in storage.
* @return \Illuminate\Http\Response
*/
public function store(Request $request) public function store(Request $request)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Display the specified resource.
* @return \Illuminate\Http\Response
*/
public function show(Lecturer $lecturer) public function show(Lecturer $lecturer)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Update the specified resource in storage.
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Lecturer $lecturer) public function update(Request $request, Lecturer $lecturer)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Remove the specified resource from storage.
* @return \Illuminate\Http\Response
*/
public function destroy(Lecturer $lecturer) 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\Http\Controllers\Controller;
use App\Models\Country; 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\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
#[Group(name: 'Stammdaten', weight: 5)]
class CountryController extends Controller 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) public function index(Request $request)
{ {
return Country::query() return Country::query()
@@ -16,19 +27,17 @@ class CountryController extends Controller
->orderBy('name') ->orderBy('name')
->when( ->when(
$request->search, $request->search,
fn(Builder $query) fn (Builder $query) => $query
=> $query
->where('name', 'ilike', "%{$request->search}%") ->where('name', 'ilike', "%{$request->search}%")
->orWhere('code', 'ilike', "%{$request->search}%"), ->orWhere('code', 'ilike', "%{$request->search}%"),
) )
->when( ->when(
$request->exists('selected'), $request->exists('selected'),
fn(Builder $query) fn (Builder $query) => $query
=> $query
->whereIn('code', $request->input('selected', [])) ->whereIn('code', $request->input('selected', []))
->orWhereIn('id', ->orWhereIn('id',
$request->input('selected', [])), $request->input('selected', [])),
fn(Builder $query) => $query->limit(10), fn (Builder $query) => $query->limit(10),
) )
->get() ->get()
->map(function (Country $country) { ->map(function (Country $country) {
@@ -38,37 +47,25 @@ class CountryController extends Controller
}); });
} }
/** #[ExcludeRouteFromDocs]
* Store a newly created resource in storage.
* @return \Illuminate\Http\Response
*/
public function store(Request $request) public function store(Request $request)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Display the specified resource.
* @return \Illuminate\Http\Response
*/
public function show(Country $country) public function show(Country $country)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Update the specified resource in storage.
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Country $country) public function update(Request $request, Country $country)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Remove the specified resource from storage.
* @return \Illuminate\Http\Response
*/
public function destroy(Country $country) 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\Http\Controllers\Controller;
use App\Models\Course; 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\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
#[Group(name: 'Kurse', weight: 1)]
class CourseController extends Controller 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) public function index(Request $request)
{ {
return Course::query() 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 * Erlaubt einem authentifizierten Referenten, einen Kurs programmatisch anzulegen.
* (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.
*/ */
#[ResponseAttribute(status: 403, description: 'Nur Referenten (is_lecturer) dürfen Kurse anlegen.')]
public function store(Request $request): JsonResponse public function store(Request $request): JsonResponse
{ {
abort_unless((bool) $request->user()->is_lecturer, Response::HTTP_FORBIDDEN); 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); return response()->json($course->fresh(), Response::HTTP_CREATED);
} }
/** #[ExcludeRouteFromDocs]
* Display the specified resource.
*/
public function show(Course $course) 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 public function update(Request $request, Course $course): JsonResponse
{ {
abort_unless( abort_unless(
@@ -95,9 +104,7 @@ class CourseController extends Controller
return response()->json($course->fresh()); return response()->json($course->fresh());
} }
/** #[ExcludeRouteFromDocs]
* Remove the specified resource from storage.
*/
public function destroy(Course $course) public function destroy(Course $course)
{ {
// //
@@ -4,22 +4,28 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\CourseEvent; 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\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
#[Group(name: 'Kurs-Events', weight: 2)]
class CourseEventController extends Controller 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 * Liefert alle vom authentifizierten Nutzer erstellten Kurs-Events (inkl. zugehörigem
* (idempotent syncing). Optionally filtered by course_id. * Kurs und Veranstaltungsort), absteigend nach Startdatum. Ideal für idempotente
* Synchronisierung durch externe Clients.
* *
* @return Collection<int, CourseEvent> * @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 public function index(Request $request): Collection
{ {
return CourseEvent::query() 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 * Erlaubt einem authentifizierten Referenten, ein datiertes Kurs-Event programmatisch anzulegen.
* programmatically. Validation mirrors the Livewire course event form;
* `created_by` is set by the model's creating hook.
*/ */
#[ResponseAttribute(status: 403, description: 'Nur Referenten (is_lecturer) dürfen Kurs-Events anlegen.')]
public function store(Request $request): JsonResponse public function store(Request $request): JsonResponse
{ {
abort_unless((bool) $request->user()->is_lecturer, Response::HTTP_FORBIDDEN); 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 public function update(Request $request, CourseEvent $courseEvent): JsonResponse
{ {
abort_unless( abort_unless(
@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\StoreHighscoreRequest; use App\Http\Requests\StoreHighscoreRequest;
use App\Models\Highscore; use App\Models\Highscore;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\Response;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use swentel\nostr\Filter\Filter; use swentel\nostr\Filter\Filter;
@@ -15,8 +17,15 @@ use swentel\nostr\Relay\RelaySet;
use swentel\nostr\Request\Request; use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription; use swentel\nostr\Subscription\Subscription;
#[Group(name: 'Highscores', weight: 6)]
class HighscoreController extends Controller 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 public function index(): JsonResponse
{ {
// npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6 // 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 public function store(StoreHighscoreRequest $request): JsonResponse
{ {
$validated = $request->validated(); $validated = $request->validated();
+31 -29
View File
@@ -4,48 +4,53 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Lecturer; 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\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
#[Group(name: 'Referenten', weight: 4)]
class LecturerController extends Controller 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) public function index(Request $request)
{ {
return Lecturer::query() return Lecturer::query()
->select('id', 'name', ) ->select('id', 'name')
->orderBy('name') ->orderBy('name')
// ->when($request->has('user_id'), // ->when($request->has('user_id'),
// fn(Builder $query) => $query->where('created_by', $request->user_id)) // fn(Builder $query) => $query->where('created_by', $request->user_id))
->when( ->when(
$request->search, $request->search,
fn (Builder $query) => $query fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%") ->where('name', 'ilike', "%{$request->search}%")
) )
->when( ->when(
$request->exists('selected'), $request->exists('selected'),
fn (Builder $query) => $query->whereIn('id', fn (Builder $query) => $query->whereIn('id',
$request->input('selected', [])), $request->input('selected', [])),
fn (Builder $query) => $query->limit(10) fn (Builder $query) => $query->limit(10)
) )
->get() ->get()
->map(function (Lecturer $lecturer) { ->map(function (Lecturer $lecturer) {
$lecturer->image = $lecturer->getFirstMediaUrl('avatar', $lecturer->image = $lecturer->getFirstMediaUrl('avatar',
'thumb'); 'thumb');
return $lecturer; return $lecturer;
}); });
} }
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function store(Request $request) public function store(Request $request)
{ {
// //
@@ -53,9 +58,8 @@ class LecturerController extends Controller
/** /**
* Display the specified resource. * Display the specified resource.
*
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function show(Lecturer $lecturer) public function show(Lecturer $lecturer)
{ {
// //
@@ -63,9 +67,8 @@ class LecturerController extends Controller
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
*
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function update(Request $request, Lecturer $lecturer) public function update(Request $request, Lecturer $lecturer)
{ {
// //
@@ -73,9 +76,8 @@ class LecturerController extends Controller
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
*
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function destroy(Lecturer $lecturer) 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\Http\Controllers\Controller;
use App\Models\Meetup; 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\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
#[Group(name: 'Meetups', weight: 3)]
class MeetupController extends Controller class MeetupController extends Controller
{ {
#[ExcludeRouteFromDocs]
public function ical() public function ical()
{ {
abort(404); 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) public function index(Request $request)
{ {
$user = $request->user(); $user = $request->user();
@@ -30,16 +45,15 @@ class MeetupController extends Controller
->orderBy('name') ->orderBy('name')
->when( ->when(
$request->search, $request->search,
fn(Builder $query) fn (Builder $query) => $query
=> $query
->where('name', 'like', "%{$request->search}%") ->where('name', 'like', "%{$request->search}%")
->orWhereHas('city', ->orWhereHas('city',
fn(Builder $query) => $query->where('cities.name', 'ilike', "%{$request->search}%")), fn (Builder $query) => $query->where('cities.name', 'ilike', "%{$request->search}%")),
) )
->when( ->when(
$request->exists('selected'), $request->exists('selected'),
fn(Builder $query) => $query->whereIn('id', $request->input('selected', [])), fn (Builder $query) => $query->whereIn('id', $request->input('selected', [])),
fn(Builder $query) => $query->limit(10), fn (Builder $query) => $query->limit(10),
) )
->get() ->get()
->map(function (Meetup $meetup) { ->map(function (Meetup $meetup) {
@@ -49,42 +63,26 @@ class MeetupController extends Controller
}); });
} }
/** #[ExcludeRouteFromDocs]
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
*/
public function store(Request $request) public function store(Request $request)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Display the specified resource. public function show(Meetup $meetup)
*
* @return \Illuminate\Http\Response
*/
public function show(meetup $meetup)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Update the specified resource in storage. public function update(Request $request, Meetup $meetup)
*
* @return \Illuminate\Http\Response
*/
public function update(Request $request, meetup $meetup)
{ {
// //
} }
/** #[ExcludeRouteFromDocs]
* Remove the specified resource from storage. public function destroy(Meetup $meetup)
*
* @return \Illuminate\Http\Response
*/
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\Http\Controllers\Controller;
use App\Models\Lecturer; use App\Models\Lecturer;
use App\Models\Venue; 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\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
#[Group(name: 'Stammdaten', weight: 5)]
class VenueController extends Controller class VenueController extends Controller
{ {
/** /**
* Display a listing of the resource. * Veranstaltungsorte auflisten und durchsuchen
* @return \Illuminate\Http\Response *
* Ö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) public function index(Request $request)
{ {
return Venue::query() return Venue::query()
@@ -22,19 +31,19 @@ class VenueController extends Controller
->orderBy('name') ->orderBy('name')
->when( ->when(
$request->search, $request->search,
fn(Builder $query) => $query fn (Builder $query) => $query
->where('name', 'ilike', "%{$request->search}%") ->where('name', 'ilike', "%{$request->search}%")
) )
->when( ->when(
$request->exists('selected'), $request->exists('selected'),
fn(Builder $query) => $query->whereIn('id', fn (Builder $query) => $query->whereIn('id',
$request->input('selected', [])), $request->input('selected', [])),
fn(Builder $query) => $query->limit(10) fn (Builder $query) => $query->limit(10)
) )
->get() ->get()
->map(function (Venue $venue) { ->map(function (Venue $venue) {
$venue->flag = asset('vendor/blade-country-flags/4x3-' . $venue->city->country->code . '.svg'); $venue->flag = asset('vendor/blade-country-flags/4x3-'.$venue->city->country->code.'.svg');
$venue->description = $venue->city->name . ', ' . $venue->street; $venue->description = $venue->city->name.', '.$venue->street;
return $venue; return $venue;
}); });
@@ -42,8 +51,8 @@ class VenueController extends Controller
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function store(Request $request) public function store(Request $request)
{ {
// //
@@ -51,8 +60,8 @@ class VenueController extends Controller
/** /**
* Display the specified resource. * Display the specified resource.
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function show(Lecturer $lecturer) public function show(Lecturer $lecturer)
{ {
// //
@@ -60,8 +69,8 @@ class VenueController extends Controller
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function update(Request $request, Lecturer $lecturer) public function update(Request $request, Lecturer $lecturer)
{ {
// //
@@ -69,8 +78,8 @@ class VenueController extends Controller
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* @return \Illuminate\Http\Response
*/ */
#[ExcludeRouteFromDocs]
public function destroy(Lecturer $lecturer) public function destroy(Lecturer $lecturer)
{ {
// //
@@ -6,6 +6,7 @@ namespace App\Http\Controllers;
use App\Models\LoginKey; use App\Models\LoginKey;
use App\Models\User; use App\Models\User;
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
use eza\lnurl; use eza\lnurl;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@@ -22,6 +23,7 @@ final class LnurlAuthController extends Controller
* This endpoint is called by Lightning wallets during LNURL-Auth authentication flow. * This endpoint is called by Lightning wallets during LNURL-Auth authentication flow.
* It validates the signature provided by wallet against the stored challenge (k1). * It validates the signature provided by wallet against the stored challenge (k1).
*/ */
#[ExcludeRouteFromDocs]
public function callback(Request $request): JsonResponse public function callback(Request $request): JsonResponse
{ {
try { try {
@@ -230,6 +232,7 @@ final class LnurlAuthController extends Controller
* *
* This endpoint is polled by the frontend to detect authentication failures. * This endpoint is polled by the frontend to detect authentication failures.
*/ */
#[ExcludeRouteFromDocs]
public function checkError(Request $request): JsonResponse public function checkError(Request $request): JsonResponse
{ {
$k1 = $request->input('k1'); $k1 = $request->input('k1');
+1
View File
@@ -11,6 +11,7 @@
"require": { "require": {
"php": "^8.3", "php": "^8.3",
"akuechler/laravel-geoly": "^1.0", "akuechler/laravel-geoly": "^1.0",
"dedoc/scramble": "^0.13.26",
"druc/laravel-langscanner": "dev-master", "druc/laravel-langscanner": "dev-master",
"ezadr/lnurl-php": "^1.0", "ezadr/lnurl-php": "^1.0",
"laravel/framework": "^13.0", "laravel/framework": "^13.0",
Generated
+188 -108
View File
@@ -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": "5b201cdc2189923877272b6f6a80ba97", "content-hash": "470341cb1de08f2116075c47f24dd54a",
"packages": [ "packages": [
{ {
"name": "akuechler/laravel-geoly", "name": "akuechler/laravel-geoly",
@@ -640,6 +640,86 @@
}, },
"time": "2025-09-16T12:23:56+00:00" "time": "2025-09-16T12:23:56+00:00"
}, },
{
"name": "dedoc/scramble",
"version": "v0.13.26",
"source": {
"type": "git",
"url": "https://github.com/dedoc/scramble.git",
"reference": "5ca42b5e23b9d5c120607138f790b51e22d8b4a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/5ca42b5e23b9d5c120607138f790b51e22d8b4a1",
"reference": "5ca42b5e23b9d5c120607138f790b51e22d8b4a1",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"myclabs/deep-copy": "^1.12",
"nikic/php-parser": "^5.0",
"php": "^8.1",
"phpstan/phpdoc-parser": "^1.0|^2.0",
"spatie/laravel-package-tools": "^1.9.2"
},
"require-dev": {
"larastan/larastan": "^3.3",
"laravel/pint": "^v1.1.0",
"nunomaduro/collision": "^7.0|^8.0",
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
"pestphp/pest": "^2.34|^3.7|^4.4",
"pestphp/pest-plugin-laravel": "^2.3|^3.1|^4.1",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5|^11.5.3|^12.5.12",
"spatie/laravel-permission": "^6.10|^7.2",
"spatie/pest-plugin-snapshots": "^2.1"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Dedoc\\Scramble\\ScrambleServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Dedoc\\Scramble\\": "src",
"Dedoc\\Scramble\\Database\\Factories\\": "database/factories"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Lytvynenko",
"email": "litvinenko95@gmail.com",
"role": "Developer"
}
],
"description": "Automatic generation of API documentation for Laravel applications.",
"homepage": "https://github.com/dedoc/scramble",
"keywords": [
"documentation",
"laravel",
"openapi"
],
"support": {
"issues": "https://github.com/dedoc/scramble/issues",
"source": "https://github.com/dedoc/scramble/tree/v0.13.26"
},
"funding": [
{
"url": "https://github.com/romalytvynenko",
"type": "github"
}
],
"time": "2026-06-02T14:43:17+00:00"
},
{ {
"name": "dflydev/dot-access-data", "name": "dflydev/dot-access-data",
"version": "v3.0.3", "version": "v3.0.3",
@@ -3727,6 +3807,66 @@
], ],
"time": "2026-01-02T08:56:05+00:00" "time": "2026-01-02T08:56:05+00:00"
}, },
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{ {
"name": "nesbot/carbon", "name": "nesbot/carbon",
"version": "3.11.4", "version": "3.11.4",
@@ -4675,6 +4815,53 @@
], ],
"time": "2025-12-27T19:41:33+00:00" "time": "2025-12-27T19:41:33+00:00"
}, },
{
"name": "phpstan/phpdoc-parser",
"version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
"nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2"
},
"time": "2026-01-25T14:56:51+00:00"
},
{ {
"name": "phrity/comparison", "name": "phrity/comparison",
"version": "1.4.1", "version": "1.4.1",
@@ -13058,66 +13245,6 @@
}, },
"time": "2024-05-16T03:13:13+00:00" "time": "2024-05-16T03:13:13+00:00"
}, },
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{ {
"name": "nunomaduro/collision", "name": "nunomaduro/collision",
"version": "v8.9.4", "version": "v8.9.4",
@@ -14056,53 +14183,6 @@
}, },
"time": "2026-01-06T21:53:42+00:00" "time": "2026-01-06T21:53:42+00:00"
}, },
{
"name": "phpstan/phpdoc-parser",
"version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
"nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2"
},
"time": "2026-01-25T14:56:51+00:00"
},
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "12.5.7", "version": "12.5.7",
+198
View File
@@ -0,0 +1,198 @@
<?php
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
use Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy;
use Dedoc\Scramble\Support\Generator\SecurityScheme;
return [
/*
* Which routes to document. String or array form; use Scramble::routes() for custom selection.
*
* 'api_path' => [
* 'include' => 'api',
* 'exclude' => ['api/internal'],
* ],
*
* Without *, patterns match path segments (api matches api and api/users, not apiary).
* With *, Str::is is used (e.g. api/v*).
*
* One static include default server is /{include} and paths are stripped (/users).
* Multiple includes or wildcards server defaults to / and paths stay full (/api/users).
* Override with `servers`, or use Scramble::registerApi() for separate bases.
*/
'api_path' => 'api',
/*
* Your API domain. By default, app domain is used. This is also a part of the default API routes
* matcher, so when implementing your own, make sure you use this config if needed.
*/
'api_domain' => null,
/*
* The path where your OpenAPI specification will be exported.
*/
'export_path' => 'api.json',
'info' => [
/*
* API version.
*/
'version' => env('API_VERSION', '1.0.0'),
/*
* Description rendered on the home page of the API documentation (`/docs/api`).
*/
'description' => <<<'MARKDOWN'
Willkommen bei der **Einundzwanzig API** der öffentlichen Schnittstelle der
[Einundzwanzig](https://einundzwanzig.space) Bitcoin-Community-Plattform.
Über diese API erreichst du die Daten der dezentralen deutschsprachigen Bitcoin-Bewegung:
Meetups und ihre Termine, Kurse und Kurs-Events, Referenten, Veranstaltungsorte sowie die
Geo-Daten für die Community-Karte.
## Authentifizierung
Die meisten **Lese-Endpunkte** sind öffentlich und benötigen kein Token.
**Schreibende Endpunkte** (Kurse & Kurs-Events anlegen/aktualisieren) erfordern ein
persönliches Zugriffstoken. Erzeuge dir eines unter
*Einstellungen API Tokens* und sende es als Bearer-Token:
```http
Authorization: Bearer <dein-token>
```
## Rate Limiting
Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt, das Einreichen von
Highscores zusätzlich auf **10 Anfragen/Minute**.
MARKDOWN,
],
'ui' => [
'title' => 'Einundzwanzig API',
],
'renderer' => 'scalar',
'renderers' => [
/*
* Stoplight Elements config options: https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options
*/
'elements' => [
'view' => 'scramble::docs',
'theme' => 'light',
'hideTryIt' => false,
'hideSchemas' => false,
'logo' => '',
'tryItCredentialsPolicy' => 'include',
'layout' => 'responsive',
'router' => 'hash',
],
/*
* Scalar API reference config options: https://scalar.com/products/api-references/configuration
*/
'scalar' => [
'view' => 'scramble::scalar',
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
'theme' => 'laravel',
'proxyUrl' => 'https://proxy.scalar.com',
'darkMode' => true,
'showDeveloperTools' => 'never',
'agent' => ['disabled' => true],
'credentials' => 'include',
],
],
/*
* The list of servers of the API. By default, when `null`, server URL will be created from
* `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you
* will need to specify the local server URL manually (if needed).
*
* Example of non-default config (final URLs are generated using Laravel `url` helper):
*
* ```php
* 'servers' => [
* 'Live' => 'api',
* 'Prod' => 'https://scramble.dedoc.co/api',
* ],
* ```
*/
'servers' => [
'Production' => 'https://einundzwanzig.space/api',
'Local' => 'api',
],
/**
* Determines how Scramble stores the descriptions of enum cases.
* Available options:
* - 'description' Case descriptions are stored as the enum schema's description using table formatting.
* - 'extension' Case descriptions are stored in the `x-enumDescriptions` enum schema extension.
*
* @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions
* - false - Case descriptions are ignored.
*/
'enum_cases_description_strategy' => 'description',
/**
* Determines how Scramble stores the names of enum cases.
* Available options:
* - 'names' Case names are stored in the `x-enumNames` enum schema extension.
* - 'varnames' - Case names are stored in the `x-enum-varnames` enum schema extension.
* - false - Case names are not stored.
*/
'enum_cases_names_strategy' => false,
/**
* When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated
* OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until
* OpenAPI 3.2 is released and query string structure can be described properly.
*
* For example, this nested validation rule describes the object with `bar` property:
* `['foo.bar' => ['required', 'int']]`.
*
* When `flatten_deep_query_parameters` is `true`, Scramble will document the parameter like so:
* `{"name":"foo[bar]", "schema":{"type":"int"}, "required":true}`.
*
* When `flatten_deep_query_parameters` is `false`, Scramble will document the parameter like so:
* `{"name":"foo", "schema": {"type":"object", "properties":{"bar":{"type": "int"}}, "required": ["bar"]}, "required":true}`.
*/
'flatten_deep_query_parameters' => true,
'middleware' => [
'web',
RestrictedDocsAccess::class,
],
'extensions' => [],
/*
* Automatically document API security (OpenAPI `security` / `securitySchemes`) based on route
* middleware.
*
* Disabled by default. Uncomment the line below to enable `MiddlewareAuthSecurityStrategy`.
* When at least one documented route uses middleware matching the configured patterns (by default
* `auth` and `auth:*`), bearer auth is applied globally. Routes without matching middleware are
* marked as public (`security: []`).
*
* Set to `null` explicitly to disable. If you already configure security manually via
* `afterOpenApiGenerated` / `extendOpenApi`, keep this disabled to avoid duplicate schemes.
*
* Customize with a class-string or [class, options]:
*
* 'security_strategy' => [
* \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
* [
* 'middleware' => ['auth', 'auth:*'],
* 'scheme' => \Dedoc\Scramble\Support\Generator\SecurityScheme::http('bearer'),
* ],
* ],
*/
'security_strategy' => [
MiddlewareAuthSecurityStrategy::class,
[
'middleware' => ['auth', 'auth:*'],
'scheme' => SecurityScheme::http('bearer'),
],
],
];
+24 -1
View File
@@ -674,5 +674,28 @@
"Öffnen/RSVP": "", "Öffnen/RSVP": "",
"Über den Dozenten": "", "Über den Dozenten": "",
"Über den Kurs": "", "Über den Kurs": "",
"Über uns": "" "Über uns": "",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
+24 -1
View File
@@ -673,5 +673,28 @@
"Öffnen/RSVP": "Open/RSVP", "Öffnen/RSVP": "Open/RSVP",
"Über den Dozenten": "About the lecturer", "Über den Dozenten": "About the lecturer",
"Über den Kurs": "About the course", "Über den Kurs": "About the course",
"Über uns": "About us" "Über uns": "About us",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
+24 -1
View File
@@ -674,5 +674,28 @@
"Öffnen/RSVP": "Abrir/RSVP", "Öffnen/RSVP": "Abrir/RSVP",
"Über den Dozenten": "Sobre el profesor", "Über den Dozenten": "Sobre el profesor",
"Über den Kurs": "Sobre el curso", "Über den Kurs": "Sobre el curso",
"Über uns": "Sobre nosotros" "Über uns": "Sobre nosotros",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
+24 -1
View File
@@ -670,5 +670,28 @@
"Öffnen/RSVP": "Megnyitás/RSVP", "Öffnen/RSVP": "Megnyitás/RSVP",
"Über den Dozenten": "Az oktatóról", "Über den Dozenten": "Az oktatóról",
"Über den Kurs": "A kurzusról", "Über den Kurs": "A kurzusról",
"Über uns": "Rólunk" "Über uns": "Rólunk",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
+24 -1
View File
@@ -645,5 +645,28 @@
"Öffnen/RSVP": "Atvērt/apstiprināt", "Öffnen/RSVP": "Atvērt/apstiprināt",
"Über den Dozenten": "Par pasniedzēju", "Über den Dozenten": "Par pasniedzēju",
"Über den Kurs": "Par kursu", "Über den Kurs": "Par kursu",
"Über uns": "Par mums" "Über uns": "Par mums",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
+24 -1
View File
@@ -672,5 +672,28 @@
"Öffnen/RSVP": "Openen/RSVP", "Öffnen/RSVP": "Openen/RSVP",
"Über den Dozenten": "Over de docent", "Über den Dozenten": "Over de docent",
"Über den Kurs": "Over de cursus", "Über den Kurs": "Over de cursus",
"Über uns": "Over ons" "Über uns": "Over ons",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
+24 -1
View File
@@ -668,5 +668,28 @@
"Öffnen/RSVP": "Otwórz/RSVP", "Öffnen/RSVP": "Otwórz/RSVP",
"Über den Dozenten": "O wykładowcy", "Über den Dozenten": "O wykładowcy",
"Über den Kurs": "O kursie", "Über den Kurs": "O kursie",
"Über uns": "O nas" "Über uns": "O nas",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
+24 -1
View File
@@ -670,5 +670,28 @@
"Öffnen/RSVP": "Abrir/RSVP", "Öffnen/RSVP": "Abrir/RSVP",
"Über den Dozenten": "Sobre o professor", "Über den Dozenten": "Sobre o professor",
"Über den Kurs": "Sobre o curso", "Über den Kurs": "Sobre o curso",
"Über uns": "Sobre nós" "Über uns": "Sobre nós",
"API Tokens - Einstellungen": "",
"Verwalte deine persönlichen Zugriffstokens für den programmatischen API-Zugriff auf dein Bitcoin Meetup Konto.": "",
"API Tokens": "",
"Erstelle persönliche Zugriffstokens, um über die API auf dein Konto zuzugreifen.": "",
"Mit einem persönlichen Zugriffstoken kannst du deine Kurse und Kurs-Events programmatisch über die API verwalten (z. B. zum Synchronisieren aus einem externen System). Sende das Token als Bearer-Token im :header-Header.": "",
"Dein neues API Token": "",
"Kopiere dein Token jetzt. Aus Sicherheitsgründen wird es dir nur dieses eine Mal angezeigt.": "",
"Kopieren": "",
"Kopiert!": "",
"Verstanden": "",
"Token-Name": "",
"z. B. Externer Kurs-Sync": "",
"Ein aussagekräftiger Name hilft dir, das Token später wiederzuerkennen.": "",
"Token erstellen": "",
"Token erstellt.": "",
"Aktive Tokens": "",
"Du hast noch keine API Tokens erstellt.": "",
"Zuletzt verwendet": "",
"Erstellt": "",
"Nie": "",
"Widerrufen": "",
"Token widerrufen": "",
"Token „:name“ wirklich widerrufen? Anwendungen, die es nutzen, verlieren den Zugriff.": ""
} }
View File
+112
View File
@@ -0,0 +1,112 @@
<!doctype html>
<html lang="en" data-theme="{{ $config->renderer()->get('theme', 'light') }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="color-scheme" content="{{ $config->renderer()->get('theme', 'light') }}">
<title>{{ $config->get('ui.title') ?? config('app.name') . ' - API Docs' }}</title>
<script src="https://unpkg.com/@stoplight/elements@8.4.2/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements@8.4.2/styles.min.css">
<script>
const originalFetch = window.fetch;
// intercept TryIt requests and add the XSRF-TOKEN header,
// which is necessary for Sanctum cookie-based authentication to work correctly
window.fetch = (url, options) => {
const CSRF_TOKEN_COOKIE_KEY = "XSRF-TOKEN";
const CSRF_TOKEN_HEADER_KEY = "X-XSRF-TOKEN";
const getCookieValue = (key) => {
const cookie = document.cookie.split(';').find((cookie) => cookie.trim().startsWith(key));
return cookie?.split("=")[1];
};
const updateFetchHeaders = (
headers,
headerKey,
headerValue,
) => {
if (headers instanceof Headers) {
headers.set(headerKey, headerValue);
} else if (Array.isArray(headers)) {
headers.push([headerKey, headerValue]);
} else if (headers) {
headers[headerKey] = headerValue;
}
};
const csrfToken = getCookieValue(CSRF_TOKEN_COOKIE_KEY);
if (csrfToken) {
const { headers = new Headers() } = options || {};
updateFetchHeaders(headers, CSRF_TOKEN_HEADER_KEY, decodeURIComponent(csrfToken));
return originalFetch(url, {
...options,
headers,
});
}
return originalFetch(url, options);
};
</script>
<style>
html, body { margin:0; height:100%; }
body { background-color: var(--color-canvas); }
/* issues about the dark theme of stoplight/mosaic-code-viewer using web component:
* https://github.com/stoplightio/elements/issues/2188#issuecomment-1485461965
*/
[data-theme="dark"] .token.property {
color: rgb(128, 203, 196) !important;
}
[data-theme="dark"] .token.operator {
color: rgb(255, 123, 114) !important;
}
[data-theme="dark"] .token.number {
color: rgb(247, 140, 108) !important;
}
[data-theme="dark"] .token.string {
color: rgb(165, 214, 255) !important;
}
[data-theme="dark"] .token.boolean {
color: rgb(121, 192, 255) !important;
}
[data-theme="dark"] .token.punctuation {
color: #dbdbdb !important;
}
</style>
</head>
<body style="height: 100vh; overflow-y: hidden">
<elements-api
id="docs"
@foreach($config->renderer()->all(except: ['theme']) as $key => $value)
@continue(! $value)
{{ $key }}="{{ $value === true ? 'true' : ($value === false ? 'false' : $value) }}"
@endforeach
/>
<script>
(async () => {
const docs = document.getElementById('docs');
docs.apiDescriptionDocument = @json($spec);
})();
</script>
@if($config->renderer()->get('theme', 'light') === 'system')
<script>
var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
function updateTheme(e) {
if (e.matches) {
window.document.documentElement.setAttribute('data-theme', 'dark');
window.document.getElementsByName('color-scheme')[0].setAttribute('content', 'dark');
} else {
window.document.documentElement.setAttribute('data-theme', 'light');
window.document.getElementsByName('color-scheme')[0].setAttribute('content', 'light');
}
}
mediaQuery.addEventListener('change', updateTheme);
updateTheme(mediaQuery);
</script>
@endif
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ $config->get('ui.title') ?? config('app.name') . ' - API Docs' }}</title>
</head>
<body>
<div id="app"></div>
<script src="{{ $config->renderer()->get('cdn', 'https://cdn.jsdelivr.net/npm/@scalar/api-reference') }}"></script>
<script>
const CSRF_TOKEN_COOKIE_KEY = "XSRF-TOKEN";
const CSRF_TOKEN_HEADER_KEY = "X-XSRF-TOKEN";
const getCookieValue = (key) => {
const cookie = document.cookie.split(';').find((cookie) => cookie.trim().startsWith(key));
return cookie?.split("=")[1];
};
Scalar.createApiReference('#app', {
content: @json($spec),
...@json($config->renderer()->all(except: ['cdn', 'credentials'])),
onBeforeRequest: ({ requestBuilder }) => {
requestBuilder.headers.set(CSRF_TOKEN_HEADER_KEY, decodeURIComponent(getCookieValue(CSRF_TOKEN_COOKIE_KEY)))
},
customFetch: (input, init) => {
return window.fetch(input, { ...init, credentials: @json($config->renderer()->get('credentials', 'include')) })
}
})
</script>
</body>
</html>
+10 -163
View File
@@ -1,5 +1,7 @@
<?php <?php
use App\Http\Controllers\Api\BindleController;
use App\Http\Controllers\Api\BtcMapCommunityController;
use App\Http\Controllers\Api\CityController; use App\Http\Controllers\Api\CityController;
use App\Http\Controllers\Api\CountryController; use App\Http\Controllers\Api\CountryController;
use App\Http\Controllers\Api\CourseController; use App\Http\Controllers\Api\CourseController;
@@ -7,14 +9,11 @@ use App\Http\Controllers\Api\CourseEventController;
use App\Http\Controllers\Api\HighscoreController; use App\Http\Controllers\Api\HighscoreController;
use App\Http\Controllers\Api\LecturerController; use App\Http\Controllers\Api\LecturerController;
use App\Http\Controllers\Api\MeetupController; use App\Http\Controllers\Api\MeetupController;
use App\Http\Controllers\Api\MeetupEventController;
use App\Http\Controllers\Api\MeetupMapController;
use App\Http\Controllers\Api\NostrPlebController;
use App\Http\Controllers\Api\VenueController; use App\Http\Controllers\Api\VenueController;
use App\Http\Controllers\LnurlAuthController; 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; use Illuminate\Support\Facades\Route;
Route::middleware(['throttle:60,1']) Route::middleware(['throttle:60,1'])
@@ -32,163 +31,11 @@ Route::middleware(['throttle:60,1'])
Route::post('highscores', [HighscoreController::class, 'store']) Route::post('highscores', [HighscoreController::class, 'store'])
->middleware('throttle:10,1') ->middleware('throttle:10,1')
->name('highscores.store'); ->name('highscores.store');
Route::get('nostrplebs', function () { Route::get('nostrplebs', NostrPlebController::class);
return User::query() Route::get('bindles', BindleController::class);
->select([ Route::get('meetups', MeetupMapController::class);
'email', Route::get('meetup-events/{date?}', MeetupEventController::class);
'public_key', Route::get('btc-map-communities', BtcMapCommunityController::class);
'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,
);
});
}); });
/* /*