diff --git a/.idea/trwl.iml b/.idea/trwl.iml index d2eeaea44..d2bc5e7d8 100644 --- a/.idea/trwl.iml +++ b/.idea/trwl.iml @@ -189,4 +189,4 @@ - \ No newline at end of file + diff --git a/app/Http/Controllers/API/v1/Controller.php b/app/Http/Controllers/API/v1/Controller.php index 2a1e42626..e408bdf8e 100644 --- a/app/Http/Controllers/API/v1/Controller.php +++ b/app/Http/Controllers/API/v1/Controller.php @@ -10,7 +10,8 @@ * title="Träwelling API", * description="Träwelling user API description. This is an incomplete documentation with still many errors. The * API is currently not yet stable. Endpoints are still being restructured. Both the URL and the request or body - * can be changed. Breaking changes will be announced on the Discord server: https://discord.gg/72t7564ZbV", + * can be changed. Breaking changes will be announced on GitHub: + * https://github.com/Traewelling/traewelling/blob/develop/API_CHANGELOG.md", * @OA\Contact( * email="support@traewelling.de" * ), @@ -90,7 +91,11 @@ public function sendResponse( int $code = 200, array $additional = null ): JsonResponse { - $disclaimer = 'APIv1 is not officially released for use and is also not fully documented. You can find the documentation at https://traewelling.de/api/documentation. Use at your own risk. Data fields may change at any time without notice.'; + $disclaimer = [ + 'message' => 'APIv1 is not officially released for use and is also not fully documented. Use at your own risk. Data fields may change at any time without notice.', + 'documentation' => 'https://traewelling.de/api/documentation', + 'changelog' => 'https://github.com/Traewelling/traewelling/blob/develop/API_CHANGELOG.md', + ]; if ($data === null) { return response()->json( data: [ diff --git a/app/Http/Controllers/API/v1/StatusController.php b/app/Http/Controllers/API/v1/StatusController.php index e6dbd080e..952581549 100644 --- a/app/Http/Controllers/API/v1/StatusController.php +++ b/app/Http/Controllers/API/v1/StatusController.php @@ -289,9 +289,9 @@ public function getLivePositionForStatus($ids): AnonymousResourceCollection { * * @param int $id * - * @return StatusResource|Response + * @return StatusResource */ - public function show(int $id): StatusResource|Response { + public function show(int $id): StatusResource { $status = StatusBackend::getStatus($id); try { $this->authorize('view', $status); diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index a80a7fdd4..0b0338477 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -60,22 +60,22 @@ public static function getStatus(int $statusId): Status { */ public static function getActiveStatuses(): ?Collection { return Status::with([ - 'event', 'likes', 'user.blockedByUsers', 'user.blockedUsers', 'user.followers', - 'checkin.originStation', 'checkin.destinationStation', - 'checkin.Trip.stopovers.station', - 'checkin.Trip.polyline', - ]) - ->whereHas('checkin', function($query) { - $query->where('departure', '<', date('Y-m-d H:i:s')) - ->where('arrival', '>', date('Y-m-d H:i:s')); - }) - ->get() - ->filter(function(Status $status) { - return Gate::allows('view', $status) && !$status->user->shadow_banned && $status->visibility !== StatusVisibility::UNLISTED; - }) - ->sortByDesc(function(Status $status) { - return $status->checkin->departure; - })->values(); + 'event', 'likes', 'user.blockedByUsers', 'user.blockedUsers', 'user.followers', + 'checkin.originStation', 'checkin.destinationStation', + 'checkin.Trip.stopovers.station', + 'checkin.Trip.polyline', + ]) + ->whereHas('checkin', function($query) { + $query->where('departure', '<', date('Y-m-d H:i:s')) + ->where('arrival', '>', date('Y-m-d H:i:s')); + }) + ->get() + ->filter(function(Status $status) { + return Gate::allows('view', $status) && !$status->user->shadow_banned && $status->visibility !== StatusVisibility::UNLISTED; + }) + ->sortByDesc(function(Status $status) { + return $status->checkin->departure; + })->values(); } public static function getLivePositions(): array { @@ -283,7 +283,8 @@ public static function createStatus( 'body' => $body, 'business' => $business, 'visibility' => $visibility, - 'event_id' => $event?->id + 'event_id' => $event?->id, + 'client_id' => request()?->user()?->token()?->client?->id, ]); } } diff --git a/app/Http/Resources/ClientResource.php b/app/Http/Resources/ClientResource.php new file mode 100644 index 000000000..ca2087ba2 --- /dev/null +++ b/app/Http/Resources/ClientResource.php @@ -0,0 +1,19 @@ + OAuthClient + */ +class ClientResource extends JsonResource +{ + public function toArray($request) { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'privacyPolicyUrl' => $this->privacy_policy_url, + ]; + } +} diff --git a/app/Http/Resources/StatusResource.php b/app/Http/Resources/StatusResource.php index e5fbde6e0..b35d0c272 100644 --- a/app/Http/Resources/StatusResource.php +++ b/app/Http/Resources/StatusResource.php @@ -30,22 +30,23 @@ public function toArray($request): array { 'likes' => (int) $this->likes->count(), 'liked' => (bool) $this->favorited, 'isLikable' => Gate::allows('like', $this->resource), + 'client' => new ClientResource($this->client), 'createdAt' => $this->created_at->toIso8601String(), 'train' => [ - 'trip' => (int) $this->checkin->trip->id, - 'hafasId' => (string) $this->checkin->trip->trip_id, - 'category' => (string) $this->checkin->trip->category->value, - 'number' => (string) $this->checkin->trip->number, - 'lineName' => (string) $this->checkin->trip->linename, - 'journeyNumber' => $this->checkin->trip->journey_number, - 'distance' => (int) $this->checkin->distance, - 'points' => (int) $this->checkin->points, - 'duration' => (int) $this->checkin->duration, - 'manualDeparture' => $this->checkin->manual_departure?->toIso8601String(), - 'manualArrival' => $this->checkin->manual_arrival?->toIso8601String(), - 'origin' => new StopoverResource($this->checkin->originStopover), - 'destination' => new StopoverResource($this->checkin->destinationStopover), - 'operator' => new OperatorResource($this?->checkin->trip->operator) + 'trip' => (int) $this->checkin->trip->id, + 'hafasId' => (string) $this->checkin->trip->trip_id, + 'category' => (string) $this->checkin->trip->category->value, + 'number' => (string) $this->checkin->trip->number, + 'lineName' => (string) $this->checkin->trip->linename, + 'journeyNumber' => $this->checkin->trip->journey_number, + 'distance' => (int) $this->checkin->distance, + 'points' => (int) $this->checkin->points, + 'duration' => (int) $this->checkin->duration, + 'manualDeparture' => $this->checkin->manual_departure?->toIso8601String(), + 'manualArrival' => $this->checkin->manual_arrival?->toIso8601String(), + 'origin' => new StopoverResource($this->checkin->originStopover), + 'destination' => new StopoverResource($this->checkin->destinationStopover), + 'operator' => new OperatorResource($this?->checkin->trip->operator) ], 'event' => new EventResource($this?->event), ]; diff --git a/app/Models/Status.php b/app/Models/Status.php index 6c1959b02..31d607734 100644 --- a/app/Models/Status.php +++ b/app/Models/Status.php @@ -16,8 +16,10 @@ * @property int user_id * @property string body * @property Business business - * @property int event_id * @property StatusVisibility visibility + * @property int event_id + * @property string tweet_id + * @property string mastodon_post_id * @property Checkin $checkin * * @todo merge model with "Checkin" (later only "Checkin") because the difference between trip sources (HAFAS, @@ -28,7 +30,7 @@ class Status extends Model use HasFactory; - protected $fillable = ['user_id', 'body', 'business', 'visibility', 'event_id', 'tweet_id', 'mastodon_post_id']; + protected $fillable = ['user_id', 'body', 'business', 'visibility', 'event_id', 'tweet_id', 'mastodon_post_id', 'client_id']; protected $hidden = ['user_id', 'business']; protected $appends = ['favorited', 'socialText', 'statusInvisibleToMe', 'description']; protected $casts = [ @@ -39,6 +41,7 @@ class Status extends Model 'event_id' => 'integer', 'tweet_id' => 'string', 'mastodon_post_id' => 'string', + 'client_id' => 'integer' ]; public function user(): BelongsTo { @@ -53,6 +56,10 @@ public function checkin(): HasOne { return $this->hasOne(Checkin::class); } + public function client(): BelongsTo { + return $this->belongsTo(OAuthClient::class, 'client_id', 'id'); + } + /** * @return HasOne * @deprecated use ->checkin instead diff --git a/app/Virtual/Models/Client.php b/app/Virtual/Models/Client.php new file mode 100644 index 000000000..8a7e72eff --- /dev/null +++ b/app/Virtual/Models/Client.php @@ -0,0 +1,49 @@ +unsignedBigInteger('client_id')->nullable()->after('tweet_id'); + $table->foreign('client_id')->references('id')->on('oauth_clients')->nullOnDelete(); + }); + } + + public function down(): void { + Schema::table('statuses', function(Blueprint $table) { + $table->dropForeign(['client_id']); + $table->dropColumn('client_id'); + }); + } +}; diff --git a/resources/views/admin/status/edit.blade.php b/resources/views/admin/status/edit.blade.php index eb1e8b10c..79f073fc1 100644 --- a/resources/views/admin/status/edit.blade.php +++ b/resources/views/admin/status/edit.blade.php @@ -34,11 +34,21 @@ @isset($status->checkin->trip->operator?->name) (Betreiber: {{$status->checkin->trip->operator?->name}}) @endisset -
+
{{ $status->checkin->trip_id }} + +
+ +
+
+ @isset($status?->client) + {{$status->client->name}} (#{{$status->client->id}}) + @endisset +
+
diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json index 03982685a..f8969dd14 100644 --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Träwelling API", - "description": "Träwelling user API description. This is an incomplete documentation with still many errors. The\n * API is currently not yet stable. Endpoints are still being restructured. Both the URL and the request or body\n * can be changed. Breaking changes will be announced on the Discord server: https://discord.gg/72t7564ZbV", + "description": "Träwelling user API description. This is an incomplete documentation with still many errors. The\n * API is currently not yet stable. Endpoints are still being restructured. Both the URL and the request or body\n * can be changed. Breaking changes will be announced on GitHub:\n * https://github.com/Traewelling/traewelling/blob/develop/API_CHANGELOG.md", "contact": { "email": "support@traewelling.de" }, @@ -18,7 +18,7 @@ "description": "Production Server" }, { - "url": "http://localhost:8000/api/v1", + "url": "https://traewelling.de/api/v1", "description": "This instance" } ], @@ -4378,13 +4378,6 @@ "type": "integer", "nullable": true }, - "tweet": { - "title": "tweet", - "description": "The tweet flag is deprecated and will be removed in a future version. For now, it is ignored and check-ins are not tweeted.", - "type": "boolean", - "example": "false", - "nullable": true - }, "toot": { "title": "toot", "description": "Should this status be posted to mastodon?", @@ -4477,6 +4470,35 @@ "name": "CheckinResponse" } }, + "Client": { + "title": "Client", + "description": "Client model", + "properties": { + "id": { + "title": "ID", + "description": "ID", + "type": "integer", + "format": "int64", + "example": 39 + }, + "name": { + "title": "name", + "description": "Name of client", + "type": "string", + "example": "Träwelling App" + }, + "privacyPolicyUrl": { + "title": "privacyPolicyUrl", + "description": "URL to privacy policy", + "type": "string", + "example": "https://traewelling.de/privacy-policy" + } + }, + "type": "object", + "xml": { + "name": "Client" + } + }, "Event": { "title": "Event", "description": "Event model", @@ -5072,6 +5094,9 @@ "type": "boolean", "example": true }, + "client": { + "$ref": "#/components/schemas/Client" + }, "createdAt": { "title": "createdAt", "description": "creation date of this status", @@ -5637,13 +5662,6 @@ "type": "boolean", "example": false }, - "twitter": { - "title": "twitter", - "description": "Deprecated. Always null, since Träwelling doesn't support twitter anymore.", - "type": "string", - "example": "null", - "nullable": true - }, "mastodon": { "title": "mastodon", "description": "Mastodon URL of user", @@ -5871,9 +5889,9 @@ "scheme": "https", "flows": { "authorizationCode": { - "authorizationUrl": "http://localhost/oauth/authorize", - "tokenUrl": "http://localhost/oauth/token", - "refreshUrl": "http://localhost/auth/refresh", + "authorizationUrl": "http://127.0.0.1:8000//oauth/authorize", + "tokenUrl": "http://127.0.0.1:8000//oauth/token", + "refreshUrl": "http://127.0.0.1:8000//auth/refresh", "scopes": { "read-statuses": "see all statuses", "read-notifications": "see your notifications", diff --git a/tests/Feature/CheckinTest.php b/tests/Feature/CheckinTest.php index f38d7cd06..6afe9220e 100644 --- a/tests/Feature/CheckinTest.php +++ b/tests/Feature/CheckinTest.php @@ -14,6 +14,7 @@ use App\Models\Trip; use App\Models\Station; use App\Models\User; +use App\Providers\AuthServiceProvider; use Carbon\Carbon; use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -342,4 +343,27 @@ public function testCheckinSuccessFlash(): void { $response->assertSee(__('stationboard.where-are-you'), false); $response->assertSee(__('menu.developed'), false); } + + public function testOauthClientIdIsSavedOnApiCheckins(): void { + $this->artisan('passport:install'); + $this->artisan('passport:keys', ['--no-interaction' => true]); + + $user = User::factory()->create(); + $token = $user->createToken('token', array_keys(AuthServiceProvider::$scopes)); + $trip = Trip::factory()->create(); + + $response = $this->postJson( + uri: '/api/v1/trains/checkin', + data: [ + 'tripId' => $trip->trip_id, + 'lineName' => $trip->linename, + 'start' => $trip->originStation->id, + 'departure' => $trip->departure, + 'destination' => $trip->destinationStation->id, + 'arrival' => $trip->arrival, + ], + headers: ['Authorization' => 'Bearer ' . $token->accessToken], + ); + $this->assertEquals(1, $response->json('data.status.client.id')); + } }