mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-11 02:50:29 +00:00
✨ **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:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
->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',
|
||||
fn (Builder $query) => $query->whereIn('id',
|
||||
$request->input('selected', [])),
|
||||
fn(Builder $query) => $query->limit(10)
|
||||
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)
|
||||
{
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
//
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -4,20 +4,26 @@ 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', )
|
||||
->select('id', 'name')
|
||||
->orderBy('name')
|
||||
// ->when($request->has('user_id'),
|
||||
// fn(Builder $query) => $query->where('created_by', $request->user_id))
|
||||
@@ -43,9 +49,8 @@ class LecturerController extends Controller
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
//
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
//
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\LoginKey;
|
||||
use App\Models\User;
|
||||
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
|
||||
use eza\lnurl;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
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.
|
||||
* It validates the signature provided by wallet against the stored challenge (k1).
|
||||
*/
|
||||
#[ExcludeRouteFromDocs]
|
||||
public function callback(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
@@ -230,6 +232,7 @@ final class LnurlAuthController extends Controller
|
||||
*
|
||||
* This endpoint is polled by the frontend to detect authentication failures.
|
||||
*/
|
||||
#[ExcludeRouteFromDocs]
|
||||
public function checkError(Request $request): JsonResponse
|
||||
{
|
||||
$k1 = $request->input('k1');
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"akuechler/laravel-geoly": "^1.0",
|
||||
"dedoc/scramble": "^0.13.26",
|
||||
"druc/laravel-langscanner": "dev-master",
|
||||
"ezadr/lnurl-php": "^1.0",
|
||||
"laravel/framework": "^13.0",
|
||||
|
||||
Generated
+188
-108
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "5b201cdc2189923877272b6f6a80ba97",
|
||||
"content-hash": "470341cb1de08f2116075c47f24dd54a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "akuechler/laravel-geoly",
|
||||
@@ -640,6 +640,86 @@
|
||||
},
|
||||
"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",
|
||||
"version": "v3.0.3",
|
||||
@@ -3727,6 +3807,66 @@
|
||||
],
|
||||
"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",
|
||||
"version": "3.11.4",
|
||||
@@ -4675,6 +4815,53 @@
|
||||
],
|
||||
"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",
|
||||
"version": "1.4.1",
|
||||
@@ -13058,66 +13245,6 @@
|
||||
},
|
||||
"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",
|
||||
"version": "v8.9.4",
|
||||
@@ -14056,53 +14183,6 @@
|
||||
},
|
||||
"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",
|
||||
"version": "12.5.7",
|
||||
|
||||
@@ -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
@@ -674,5 +674,28 @@
|
||||
"Öffnen/RSVP": "",
|
||||
"Über den Dozenten": "",
|
||||
"Ü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
@@ -673,5 +673,28 @@
|
||||
"Öffnen/RSVP": "Open/RSVP",
|
||||
"Über den Dozenten": "About the lecturer",
|
||||
"Ü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
@@ -674,5 +674,28 @@
|
||||
"Öffnen/RSVP": "Abrir/RSVP",
|
||||
"Über den Dozenten": "Sobre el profesor",
|
||||
"Ü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
@@ -670,5 +670,28 @@
|
||||
"Öffnen/RSVP": "Megnyitás/RSVP",
|
||||
"Über den Dozenten": "Az oktatóró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
@@ -645,5 +645,28 @@
|
||||
"Öffnen/RSVP": "Atvērt/apstiprināt",
|
||||
"Über den Dozenten": "Par pasniedzēju",
|
||||
"Ü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
@@ -672,5 +672,28 @@
|
||||
"Öffnen/RSVP": "Openen/RSVP",
|
||||
"Über den Dozenten": "Over de docent",
|
||||
"Ü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
@@ -668,5 +668,28 @@
|
||||
"Öffnen/RSVP": "Otwórz/RSVP",
|
||||
"Über den Dozenten": "O wykładowcy",
|
||||
"Ü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
@@ -670,5 +670,28 @@
|
||||
"Öffnen/RSVP": "Abrir/RSVP",
|
||||
"Über den Dozenten": "Sobre o professor",
|
||||
"Ü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.": ""
|
||||
}
|
||||
+112
@@ -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>
|
||||
@@ -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
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\BindleController;
|
||||
use App\Http\Controllers\Api\BtcMapCommunityController;
|
||||
use App\Http\Controllers\Api\CityController;
|
||||
use App\Http\Controllers\Api\CountryController;
|
||||
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\LecturerController;
|
||||
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\LnurlAuthController;
|
||||
use App\Models\LibraryItem;
|
||||
use App\Models\Meetup;
|
||||
use App\Models\MeetupEvent;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['throttle:60,1'])
|
||||
@@ -32,163 +31,11 @@ Route::middleware(['throttle:60,1'])
|
||||
Route::post('highscores', [HighscoreController::class, 'store'])
|
||||
->middleware('throttle:10,1')
|
||||
->name('highscores.store');
|
||||
Route::get('nostrplebs', function () {
|
||||
return User::query()
|
||||
->select([
|
||||
'email',
|
||||
'public_key',
|
||||
'lightning_address',
|
||||
'lnurl',
|
||||
'node_id',
|
||||
'paynym',
|
||||
'lnbits',
|
||||
'nostr',
|
||||
'id',
|
||||
])
|
||||
->whereNotNull('nostr')
|
||||
->where('nostr', 'like', 'npub1%')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->unique('nostr')
|
||||
->pluck('nostr');
|
||||
});
|
||||
Route::get('bindles', function () {
|
||||
return LibraryItem::query()
|
||||
->where('type', 'bindle')
|
||||
->with([
|
||||
'media',
|
||||
])
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'link' => strtok($item->value, '?'),
|
||||
'image' => $item->getFirstMediaUrl('main'),
|
||||
]);
|
||||
});
|
||||
Route::get('meetups', function (Request $request) {
|
||||
return Meetup::query()
|
||||
->where('visible_on_map', true)
|
||||
->with([
|
||||
'meetupEvents',
|
||||
'city.country',
|
||||
'media',
|
||||
])
|
||||
->get()
|
||||
->map(fn ($meetup) => [
|
||||
'name' => $meetup->name,
|
||||
'portalLink' => url()->route(
|
||||
'meetups.landingpage',
|
||||
['country' => $meetup->city->country, 'meetup' => $meetup],
|
||||
),
|
||||
'url' => $meetup->telegram_link ?? $meetup->webpage,
|
||||
'top' => $meetup->github_data['top'] ?? null,
|
||||
'left' => $meetup->github_data['left'] ?? null,
|
||||
'country' => str($meetup->city->country->code)->upper(),
|
||||
'state' => $meetup->github_data['state'] ?? null,
|
||||
'city' => $meetup->city->name,
|
||||
'longitude' => (float) $meetup->city->longitude,
|
||||
'latitude' => (float) $meetup->city->latitude,
|
||||
'twitter_username' => $meetup->twitter_username,
|
||||
'website' => $meetup->webpage,
|
||||
'simplex' => $meetup->simplex,
|
||||
'signal' => $meetup->signal,
|
||||
'nostr' => $meetup->nostr,
|
||||
'next_event' => $meetup->nextEvent,
|
||||
'intro' => $request->has('withIntro') ? $meetup->intro : null,
|
||||
'logo' => $request->has('withLogos') ? $meetup->getFirstMediaUrl('logo') : null,
|
||||
]);
|
||||
});
|
||||
Route::get('meetup-events/{date?}', function ($date = null) {
|
||||
if ($date) {
|
||||
$date = Carbon::parse($date);
|
||||
}
|
||||
$events = MeetupEvent::query()
|
||||
->with([
|
||||
'meetup.city.country',
|
||||
'meetup.media',
|
||||
])
|
||||
->when(
|
||||
$date,
|
||||
fn ($query) => $query
|
||||
->where('start', '>=', $date)
|
||||
->where('start', '<=', $date->copy()->endOfMonth()),
|
||||
)
|
||||
->get();
|
||||
|
||||
return $events->map(fn ($event) => [
|
||||
'start' => $event->start->format('Y-m-d H:i'),
|
||||
'location' => $event->location,
|
||||
'description' => $event->description,
|
||||
'link' => $event->link,
|
||||
'meetup.name' => $event->meetup->name,
|
||||
'meetup.portalLink' => url()->route(
|
||||
'meetups.landingpage',
|
||||
[
|
||||
'country' => $event->meetup->city->country,
|
||||
'meetup' => $event->meetup,
|
||||
],
|
||||
),
|
||||
'meetup.url' => $event->meetup->telegram_link ?? $event->meetup->webpage,
|
||||
'meetup.country' => str($event->meetup->city->country->code)->upper(),
|
||||
'meetup.city' => $event->meetup->city->name,
|
||||
'meetup.longitude' => (float) $event->meetup->city->longitude,
|
||||
'meetup.latitude' => (float) $event->meetup->city->latitude,
|
||||
'meetup.twitter_username' => $event->meetup->twitter_username,
|
||||
'meetup.website' => $event->meetup->webpage,
|
||||
'meetup.simplex' => $event->meetup->simplex,
|
||||
'meetup.signal' => $event->meetup->signal,
|
||||
'meetup.nostr' => $event->meetup->nostr,
|
||||
'meetup.logo' => $event->meetup->getFirstMediaUrl('logo'),
|
||||
],
|
||||
);
|
||||
});
|
||||
Route::get('btc-map-communities', function () {
|
||||
return response()->json(
|
||||
Meetup::query()
|
||||
->with([
|
||||
'media',
|
||||
'city.country',
|
||||
])
|
||||
->where('community', '=', 'einundzwanzig')
|
||||
->when(
|
||||
app()->environment('production'),
|
||||
fn ($query) => $query->whereHas(
|
||||
'city',
|
||||
fn ($query) => $query
|
||||
->whereNotNull('cities.simplified_geojson')
|
||||
->whereNotNull('cities.population')
|
||||
->whereNotNull('cities.population_date'),
|
||||
),
|
||||
)
|
||||
->get()
|
||||
->map(fn ($meetup) => [
|
||||
'id' => $meetup->slug,
|
||||
'tags' => [
|
||||
'type' => 'community',
|
||||
'name' => $meetup->name,
|
||||
'continent' => 'europe',
|
||||
'icon:square' => $meetup->logoSquare,
|
||||
// 'contact:email' => null,
|
||||
'contact:twitter' => $meetup->twitter_username ? 'https://twitter.com/'.$meetup->twitter_username : null,
|
||||
'contact:website' => $meetup->webpage,
|
||||
'contact:telegram' => $meetup->telegram_link,
|
||||
'contact:nostr' => $meetup->nostr,
|
||||
// 'tips:lightning_address' => null,
|
||||
'organization' => 'einundzwanzig',
|
||||
'language' => $meetup->city->country->language_codes[0] ?? 'de',
|
||||
'geo_json' => $meetup->city->simplified_geojson,
|
||||
'population' => $meetup->city->population,
|
||||
'population:date' => $meetup->city->population_date,
|
||||
],
|
||||
])
|
||||
->toArray(),
|
||||
200,
|
||||
['Content-Type' => 'application/json;charset=UTF-8', 'Charset' => 'utf-8'],
|
||||
JSON_UNESCAPED_SLASHES,
|
||||
);
|
||||
});
|
||||
Route::get('nostrplebs', NostrPlebController::class);
|
||||
Route::get('bindles', BindleController::class);
|
||||
Route::get('meetups', MeetupMapController::class);
|
||||
Route::get('meetup-events/{date?}', MeetupEventController::class);
|
||||
Route::get('btc-map-communities', BtcMapCommunityController::class);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user