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 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)
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -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 |
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue