Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions docs/source/releases/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ brought up to date.
accepts legacy format-1 backups (created by Kegbot v1.1.x) and upgrades
their data in one step; no intermediate 1.2/1.3 install is needed.
* Time zone choices are now derived from the system time zone database.
* **A new REST API is served at ``/api/``** (previously the experimental
``/api/v2/``, now always enabled). An OpenAPI schema is available at
``/api/schema``, with interactive documentation at ``/api/docs``.
* **The legacy HTTP API is deprecated and now lives only at ``/api/v1/``.**
It serves only the endpoints used by kegbot-pycore (plus the events
feed); every other endpoint returns ``410 Gone``, and all legacy
responses carry a ``Deprecation`` header. Protocol Buffers are no longer
used anywhere in the server. **kegbot-pycore users:** set the api url to
``http://<server>/api/v1/`` (the bare ``/api/`` prefix no longer serves
the legacy endpoints).

**Upgrade notes**

Expand All @@ -40,6 +50,77 @@ brought up to date.
arm64 only).
* The legacy gflags-based Python API client was removed from the server
package; it lives in the separate kegbot-api project.
* **Breaking change: the following legacy API endpoints have been
removed** and now return ``410 Gone``:

* ``GET /api/v1/version``
* ``POST /api/v1/login``
* ``GET|POST /api/v1/logout``
* ``GET /api/v1/get-api-key``
* ``POST /api/v1/devices/link``
* ``GET /api/v1/devices/link/status``
* ``GET /api/v1/devices/link/status/:code``
* ``POST /api/v1/auth-tokens/:auth_device/:token_value/assign``
* ``GET|POST|DELETE /api/v1/controllers/:id``
* ``GET /api/v1/drinks``
* ``GET /api/v1/drinks/last``
* ``GET /api/v1/drinks/:id``
* ``POST /api/v1/drinks/:id/add-photo``
* ``GET|POST|DELETE /api/v1/flow-meters/:id``
* ``GET|POST /api/v1/flow-toggles``
* ``GET|POST|DELETE /api/v1/flow-toggles/:id``
* ``GET /api/v1/kegs``
* ``GET /api/v1/kegs/:id``
* ``POST /api/v1/kegs/:id/end``
* ``GET /api/v1/kegs/:id/drinks``
* ``GET /api/v1/kegs/:id/events``
* ``GET /api/v1/kegs/:id/sessions``
* ``GET /api/v1/kegs/:id/stats``
* ``GET /api/v1/keg-sizes``
* ``POST /api/v1/pictures``
* ``GET /api/v1/sessions``
* ``GET /api/v1/sessions/current``
* ``GET /api/v1/sessions/:id``
* ``GET /api/v1/sessions/:id/stats``
* ``DELETE /api/v1/taps/:meter_name_or_id``
* ``POST /api/v1/taps/:meter_name_or_id/activate``
* ``POST /api/v1/taps/:meter_name_or_id/calibrate``
* ``POST /api/v1/taps/:meter_name_or_id/spill``
* ``POST /api/v1/taps/:meter_name_or_id/connect-meter``
* ``POST /api/v1/taps/:meter_name_or_id/disconnect-meter``
* ``POST /api/v1/taps/:meter_name_or_id/connect-toggle``
* ``POST /api/v1/taps/:meter_name_or_id/disconnect-toggle``
* ``POST /api/v1/taps/:meter_name_or_id/connect-thermo``
* ``POST /api/v1/taps/:meter_name_or_id/disconnect-thermo``
* ``GET /api/v1/thermo-sensors``
* ``GET /api/v1/thermo-sensors/:sensor_name/logs``
* ``GET /api/v1/users``
* ``GET /api/v1/users/:username``
* ``GET /api/v1/users/:username/drinks``
* ``GET /api/v1/users/:username/events``
* ``GET /api/v1/users/:username/stats``
* ``GET|POST /api/v1/users/:username/photo``
* ``POST /api/v1/new-user``
* ``GET /api/v1/stats``
* ``GET /api/v1/sound-events``

* The following legacy API endpoints are deprecated, but remain supported
for compatibility with kegbot-pycore. They will be removed in a future
release:

* ``GET /api/v1/status``
* ``GET /api/v1/auth-tokens/:auth_device/:token_value``
* ``POST /api/v1/cancel-drink``
* ``GET|POST /api/v1/controllers``
* ``GET /api/v1/events``
* ``GET|POST /api/v1/flow-meters``
* ``GET|POST /api/v1/taps``
* ``GET|POST /api/v1/taps/:meter_name_or_id``
* ``GET|POST /api/v1/thermo-sensors/:sensor_name``

* The old Kegbot mobile apps depended on now-retired API endpoints (device
linking, registration, drink lists) and no longer work against this
server.


Version 1.3.0 (2022-08-10)
Expand Down
7 changes: 0 additions & 7 deletions docs/source/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,3 @@ change and should only be needed by advanced users.
be prefixed by this URL. Otherwise, media will be served from
the same host as the server itself, under ``/media``. You may use
this setting to e.g. serve media links through a CDN.

.. data:: KEGBOT_ENABLE_V2_API

If set to ``true``, the new Kegbot Server API will be enabled (at
path ``/api/v2/...``). This API will replace the existing API implementation
in a future major Kegbot release. It is currently unfinished. Enable this
if you are a developer intending to work on or with this API.
3 changes: 2 additions & 1 deletion pykeg/api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"
name = "pykeg.api"
label = "pykeg_api"
15 changes: 15 additions & 0 deletions pykeg/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from drf_spectacular.extensions import OpenApiAuthenticationExtension
from rest_framework import authentication
from rest_framework.exceptions import AuthenticationFailed

Expand Down Expand Up @@ -40,3 +41,17 @@ def authenticate_credentials(self, userid, password, request=None):
'For Basic Auth, provide "api" as the username and an API key as the password.'
)
return validate_api_key(password)


class ApiKeyBasicAuthScheme(OpenApiAuthenticationExtension):
"""Describes ApiKeyBasicAuth in the OpenAPI schema."""

target_class = "pykeg.api.auth.ApiKeyBasicAuth"
name = "apiKeyBasicAuth"

def get_security_definition(self, auto_schema):
return {
"type": "http",
"scheme": "basic",
"description": 'Use "api" as the username and an API key as the password.',
}
1 change: 0 additions & 1 deletion pykeg/api/models.py

This file was deleted.

17 changes: 12 additions & 5 deletions pykeg/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,22 @@ def has_permission(self, request, view):


class IsAuthenticated(DashboardViewer):
"""Permission for API resources which respects site-privacy.

Resources are readable without authentication, *unless* the site's
`.privacy` setting is non-public.
"""
"""Requires an authenticated user who also passes the site-privacy check."""

message = "You must log in to do that"

def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
return bool(request.user and request.user.is_authenticated)


class AdminWriteDashboardRead(DashboardViewer):
"""Reads follow site-privacy; writes require a staff user."""

message = "You must be an admin to do that"

def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return super().has_permission(request, view)
return bool(request.user and request.user.is_staff)
59 changes: 34 additions & 25 deletions pykeg/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from django.contrib.auth import authenticate
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

Expand Down Expand Up @@ -25,15 +28,19 @@ class Meta:
thumbnail_png_url = serializers.SerializerMethodField()
caption = serializers.CharField()

@extend_schema_field(OpenApiTypes.URI)
def get_resized_url(self, picture):
return picture.resized.url if picture else None

@extend_schema_field(OpenApiTypes.URI)
def get_resized_png_url(self, picture):
return picture.resized_png.url if picture else None

@extend_schema_field(OpenApiTypes.URI)
def get_thumbnail_url(self, picture):
return picture.thumbnail.url if picture else None

@extend_schema_field(OpenApiTypes.URI)
def get_thumbnail_png_url(self, picture):
return picture.thumbnail_png.url if picture else None

Expand All @@ -59,8 +66,8 @@ class Meta:
"stats",
]

background_image = PictureSerializer()
stats = serializers.JSONField(source="get_stats")
background_image = PictureSerializer(read_only=True)
stats = serializers.JSONField(source="get_stats", read_only=True)


class UserSerializer(serializers.ModelSerializer):
Expand All @@ -79,7 +86,7 @@ class Meta:
"is_active",
]

picture = PictureSerializer(source="mugshot")
picture = PictureSerializer(source="mugshot", read_only=True)


class InvitationSerializer(serializers.ModelSerializer):
Expand All @@ -93,6 +100,8 @@ class Meta:
"is_expired",
]

is_expired = serializers.BooleanField(read_only=True)


class DeviceSerializer(serializers.ModelSerializer):
class Meta:
Expand All @@ -118,6 +127,7 @@ class Meta:
]

is_active = serializers.BooleanField(source="active")
key = serializers.CharField(read_only=True)


class BeverageProducerSerializer(serializers.ModelSerializer):
Expand All @@ -143,6 +153,7 @@ class Meta:
"id",
"name",
"producer",
"producer_id",
"beverage_type",
"style",
"description",
Expand All @@ -160,7 +171,10 @@ class Meta:
"untappd_beer_id",
]

producer = BeverageProducerSerializer()
producer = BeverageProducerSerializer(read_only=True)
producer_id = serializers.PrimaryKeyRelatedField(
queryset=models.BeverageProducer.objects.all(), source="producer", write_only=True
)


class ControllerSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -230,10 +244,10 @@ class Meta:
"stats",
]

beverage = BeverageSerializer(source="type")
illustration = serializers.URLField(source="get_illustration")
illustration_thumbnail = serializers.URLField(source="get_illustration_thumb")
stats = serializers.JSONField(source="get_stats")
beverage = BeverageSerializer(source="type", read_only=True)
illustration = serializers.URLField(source="get_illustration", read_only=True)
illustration_thumbnail = serializers.URLField(source="get_illustration_thumb", read_only=True)
stats = serializers.JSONField(source="get_stats", read_only=True)


class KegTapSerializer(serializers.ModelSerializer):
Expand All @@ -249,7 +263,7 @@ class Meta:
"current_keg",
]

current_keg = KegSerializer()
current_keg = KegSerializer(read_only=True)


class DrinkSerializer(serializers.ModelSerializer):
Expand All @@ -268,9 +282,9 @@ class Meta:
"picture",
]

picture = PictureSerializer()
user = UserSerializer()
keg = KegSerializer()
picture = PictureSerializer(read_only=True)
user = UserSerializer(read_only=True)
keg = KegSerializer(read_only=True)


class AuthenticationTokenSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -302,7 +316,7 @@ class Meta:
"stats",
]

stats = serializers.JSONField(source="get_stats")
stats = serializers.JSONField(source="get_stats", read_only=True)


class ThermoSensorSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -338,10 +352,10 @@ class Meta:
"session",
]

drink = DrinkSerializer()
keg = KegSerializer()
user = UserSerializer()
session = DrinkingSessionSerializer()
drink = DrinkSerializer(read_only=True)
keg = KegSerializer(read_only=True)
user = UserSerializer(read_only=True)
session = DrinkingSessionSerializer(read_only=True)


class NotificationSettingsSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -373,13 +387,8 @@ class LoginSerializer(serializers.Serializer):
password = serializers.CharField()

def validate(self, data):
username = data["username"]
password = data["password"]
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
raise ValidationError("Incorrect username/password")
if not user.check_password(password):
user = authenticate(username=data["username"], password=data["password"])
if not user:
raise ValidationError("Incorrect username/password")
data["user"] = user
return data
Expand All @@ -403,7 +412,7 @@ class Meta:
"email",
]

picture = PictureSerializer(source="mugshot")
picture = PictureSerializer(source="mugshot", read_only=True)


class SystemStatusSerializer(serializers.Serializer):
Expand Down
Loading
Loading