qrcode: add get-qrcode endpoint (#82649)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
This commit is contained in:
parent
1e12dae71b
commit
a51c49a865
|
@ -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)
|
||||
|
|
3
setup.py
3
setup.py
|
@ -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 |
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue