qrcode: add get-qrcode endpoint (#82649)
gitea/passerelle/pipeline/head This commit looks good Details

This commit is contained in:
Corentin Sechet 2023-10-20 12:06:44 +02:00 committed by Benjamin Dauvergne
parent 1e12dae71b
commit a51c49a865
4 changed files with 112 additions and 1 deletions

View File

@ -1,11 +1,18 @@
import binascii
import os
import uuid
from io import BytesIO
from django.core.validators import RegexValidator
from django.db import models
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _
from nacl.signing import SigningKey
from qrcode import ERROR_CORRECT_Q, QRCode
from qrcode.image.pil import PilImage
from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint
@ -79,7 +86,12 @@ class QRCodeConnector(BaseResource):
certificate.data = data
certificate.save()
return {'data': {'uuid': certificate.uuid}}
return {
'data': {
'uuid': certificate.uuid,
'qrcode_url': certificate.get_qrcode_url(request),
}
}
@endpoint(
name='get-certificate',
@ -102,9 +114,53 @@ class QRCodeConnector(BaseResource):
'data': certificate.data,
'validity_start': certificate.validity_start.isoformat(),
'validity_end': certificate.validity_end.isoformat(),
'qrcode_url': certificate.get_qrcode_url(request),
},
}
@endpoint(
name='get-qrcode',
description=_('Get QR Code'),
pattern=f'^{UUID_PATTERN}$',
example_pattern='{uuid}',
parameters={
'uuid': {
'description': _('QRCode\'s certificate identifier'),
'example_value': '12345678-1234-1234-1234-123456789012',
}
},
)
def get_qrcode(self, request, uuid):
certificate = self.certificates.get(uuid=uuid)
qr_code = certificate.generate_qr_code()
return HttpResponse(qr_code, content_type='image/png')
def encode_mime_like(data):
msg = ''
for key, value in data.items():
msg += '%s: %s\n' % (key, value.replace('\n', '\n '))
return msg.encode()
BASE45_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
BASE45_DICT = {v: i for i, v in enumerate(BASE45_CHARSET)}
def b45encode(buf: bytes) -> bytes:
"""Convert bytes to base45-encoded string"""
res = ''
buflen = len(buf)
for i in range(0, buflen & ~1, 2):
x = (buf[i] << 8) + buf[i + 1]
e, x = divmod(x, 45 * 45)
d, c = divmod(x, 45)
res += BASE45_CHARSET[c] + BASE45_CHARSET[d] + BASE45_CHARSET[e]
if buflen & 1:
d, c = divmod(buf[-1], 45)
res += BASE45_CHARSET[c] + BASE45_CHARSET[d]
return res.encode()
class Certificate(models.Model):
uuid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4)
@ -112,3 +168,33 @@ class Certificate(models.Model):
validity_end = models.DateTimeField(verbose_name=_('Validity End Date'))
data = models.JSONField(null=True, verbose_name='Certificate Data')
resource = models.ForeignKey(QRCodeConnector, on_delete=models.CASCADE, related_name='certificates')
def generate_qr_code(self):
data = {
'uuid': str(self.uuid),
'validity_start': str(self.validity_start.timestamp()),
'validity_end': str(self.validity_end.timestamp()),
} | self.data
msg = encode_mime_like(data)
binary_key = binascii.unhexlify(self.resource.key)
signing_key = SigningKey(seed=binary_key)
signed = signing_key.sign(msg)
qr_code = QRCode(image_factory=PilImage, error_correction=ERROR_CORRECT_Q)
qr_code.add_data(b45encode(signed).decode())
qr_code.make(fit=True)
image = qr_code.make_image(fill_color='black', back_color='white')
fd = BytesIO()
image.save(fd)
return fd.getvalue()
def get_qrcode_url(self, request):
qrcode_relative_url = reverse(
'generic-endpoint',
kwargs={
'slug': self.resource.slug,
'connector': self.resource.get_connector_slug(),
'endpoint': 'get-qrcode',
'rest': str(self.uuid),
},
)
return request.build_absolute_uri(qrcode_relative_url)

View File

@ -175,6 +175,9 @@ setup(
'cryptography',
'xmltodict',
'phonenumbers',
'qrcode',
'pillow',
'pynacl',
],
cmdclass={
'build': build,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -14,6 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import uuid
from datetime import timezone
import pytest
@ -50,6 +51,7 @@ def test_save_certificate(app, connector):
assert result.json['err'] == 0
certificate_uuid = result.json['data']['uuid']
assert result.json['data']['qrcode_url'] == f'http://testserver/qrcode/test/get-qrcode/{certificate_uuid}'
certificate = connector.certificates.get(uuid=certificate_uuid)
assert certificate.data['first_name'] == 'Georges'
@ -96,5 +98,25 @@ def test_get_certificate(app, connector):
'data': {'first_name': 'Georges', 'last_name': 'Abitbol'},
'validity_start': '2022-01-01T10:00:00+00:00',
'validity_end': '2023-01-01T10:00:00+00:00',
'qrcode_url': f'http://testserver/qrcode/test/get-qrcode/{certificate.uuid}',
},
}
def test_get_qrcode(app, connector):
certificate = connector.certificates.create(
uuid=uuid.UUID('12345678-1234-5678-1234-567812345678'),
data={
'first_name': 'Georges',
'last_name': 'Abitbol',
},
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
)
endpoint = generic_endpoint_url('qrcode', 'get-qrcode', slug=connector.slug)
response = app.get(f'{endpoint}/{certificate.uuid}')
assert response.headers['Content-Type'] == 'image/png'
with open('tests/data/qrcode/test-qrcode.png', 'rb') as expected_qrcode:
# just check images are the same. Decoded content is tested javascript-side.
assert response.body == expected_qrcode.read()