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 os
import uuid import uuid
from io import BytesIO
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _ 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.base.models import BaseResource
from passerelle.utils.api import endpoint from passerelle.utils.api import endpoint
@ -79,7 +86,12 @@ class QRCodeConnector(BaseResource):
certificate.data = data certificate.data = data
certificate.save() certificate.save()
return {'data': {'uuid': certificate.uuid}} return {
'data': {
'uuid': certificate.uuid,
'qrcode_url': certificate.get_qrcode_url(request),
}
}
@endpoint( @endpoint(
name='get-certificate', name='get-certificate',
@ -102,9 +114,53 @@ class QRCodeConnector(BaseResource):
'data': certificate.data, 'data': certificate.data,
'validity_start': certificate.validity_start.isoformat(), 'validity_start': certificate.validity_start.isoformat(),
'validity_end': certificate.validity_end.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): class Certificate(models.Model):
uuid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4) 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')) validity_end = models.DateTimeField(verbose_name=_('Validity End Date'))
data = models.JSONField(null=True, verbose_name='Certificate Data') data = models.JSONField(null=True, verbose_name='Certificate Data')
resource = models.ForeignKey(QRCodeConnector, on_delete=models.CASCADE, related_name='certificates') 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', 'cryptography',
'xmltodict', 'xmltodict',
'phonenumbers', 'phonenumbers',
'qrcode',
'pillow',
'pynacl',
], ],
cmdclass={ cmdclass={
'build': build, '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 # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime import datetime
import uuid
from datetime import timezone from datetime import timezone
import pytest import pytest
@ -50,6 +51,7 @@ def test_save_certificate(app, connector):
assert result.json['err'] == 0 assert result.json['err'] == 0
certificate_uuid = result.json['data']['uuid'] 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) certificate = connector.certificates.get(uuid=certificate_uuid)
assert certificate.data['first_name'] == 'Georges' assert certificate.data['first_name'] == 'Georges'
@ -96,5 +98,25 @@ def test_get_certificate(app, connector):
'data': {'first_name': 'Georges', 'last_name': 'Abitbol'}, 'data': {'first_name': 'Georges', 'last_name': 'Abitbol'},
'validity_start': '2022-01-01T10:00:00+00:00', 'validity_start': '2022-01-01T10:00:00+00:00',
'validity_end': '2023-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()