diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e9d613a0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +svglib diff --git a/setup/sign_biometric_oca/odoo/addons/sign_biometric_oca b/setup/sign_biometric_oca/odoo/addons/sign_biometric_oca new file mode 120000 index 00000000..070f6831 --- /dev/null +++ b/setup/sign_biometric_oca/odoo/addons/sign_biometric_oca @@ -0,0 +1 @@ +../../../../sign_biometric_oca \ No newline at end of file diff --git a/setup/sign_biometric_oca/setup.py b/setup/sign_biometric_oca/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/sign_biometric_oca/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/sign_biometric_oca/README.rst b/sign_biometric_oca/README.rst new file mode 100644 index 00000000..f59cfab5 --- /dev/null +++ b/sign_biometric_oca/README.rst @@ -0,0 +1,76 @@ +================== +Sign Biometric Oca +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1a1329c3d712379ddbc489e3c1499da3721b4b6ffae51187a8fd5afeb42d5bc8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsign-lightgray.png?logo=github + :target: https://github.com/OCA/sign/tree/16.0/sign_biometric_oca + :alt: OCA/sign +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sign-16-0/sign-16-0-sign_biometric_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/sign&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allow to generate biometric signatures with OCA Sign Application. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- Enric Tobella + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/sign `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sign_biometric_oca/__init__.py b/sign_biometric_oca/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/sign_biometric_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sign_biometric_oca/__manifest__.py b/sign_biometric_oca/__manifest__.py new file mode 100644 index 00000000..774e6355 --- /dev/null +++ b/sign_biometric_oca/__manifest__.py @@ -0,0 +1,36 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sign Biometric Oca", + "summary": """ + Add a new widget in order to store biometric information""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sign", + "depends": [ + "sign_oca", + ], + "data": [ + "data/data.xml", + ], + "demo": [], + "external_dependencies": {"python": ["svglib"]}, + "assets": { + "web.assets_backend": [ + "sign_biometric_oca/static/src/lib/perfect-freehand.esm.js", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.xml", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js", + "sign_biometric_oca/static/src/components/biometric.esm.js", + "sign_biometric_oca/static/src/components/biometric.scss", + ], + "web.assets_frontend": [ + "sign_biometric_oca/static/src/lib/perfect-freehand.esm.js", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.xml", + "sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js", + "sign_biometric_oca/static/src/components/biometric.esm.js", + "sign_biometric_oca/static/src/components/biometric.scss", + ], + }, +} diff --git a/sign_biometric_oca/data/data.xml b/sign_biometric_oca/data/data.xml new file mode 100644 index 00000000..96b27f0b --- /dev/null +++ b/sign_biometric_oca/data/data.xml @@ -0,0 +1,7 @@ + + + + Biometric Signature + biometric + + diff --git a/sign_biometric_oca/models/__init__.py b/sign_biometric_oca/models/__init__.py new file mode 100644 index 00000000..d634bb75 --- /dev/null +++ b/sign_biometric_oca/models/__init__.py @@ -0,0 +1,2 @@ +from . import sign_oca_field +from . import sign_oca_request diff --git a/sign_biometric_oca/models/sign_oca_field.py b/sign_biometric_oca/models/sign_oca_field.py new file mode 100644 index 00000000..dbeaa3b1 --- /dev/null +++ b/sign_biometric_oca/models/sign_oca_field.py @@ -0,0 +1,13 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SignOcaField(models.Model): + _inherit = "sign.oca.field" + + field_type = fields.Selection( + selection_add=[("biometric", "Biometric")], + ondelete={"biometric": "set default"}, + ) diff --git a/sign_biometric_oca/models/sign_oca_request.py b/sign_biometric_oca/models/sign_oca_request.py new file mode 100644 index 00000000..5abc282c --- /dev/null +++ b/sign_biometric_oca/models/sign_oca_request.py @@ -0,0 +1,39 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from base64 import b64decode +from io import BytesIO + +from PyPDF2 import PdfFileReader +from reportlab.pdfgen import canvas +from svglib.svglib import svg2rlg + +from odoo import models + + +class SignOcaRequestSigner(models.Model): + + _inherit = "sign.oca.request.signer" + + def _get_pdf_page_biometric(self, item, box): + packet = BytesIO() + can = canvas.Canvas(packet, pagesize=(box.getWidth(), box.getHeight())) + if not item.get("value"): + return False + drawing = svg2rlg(BytesIO(b64decode(item["value"]))) + scaling_x = item["width"] / 100 * float(box.getWidth()) / drawing.width + scaling_y = item["height"] / 100 * float(box.getHeight()) / drawing.height + + drawing.width = item["width"] / 100 * float(box.getWidth()) + drawing.height = item["height"] / 100 * float(box.getHeight()) + drawing.scale(scaling_x, scaling_y) + + drawing.drawOn( + can, + item["position_x"] / 100 * float(box.getWidth()), + (100 - item["position_y"] - item["height"]) / 100 * float(box.getHeight()), + ) + can.save() + packet.seek(0) + new_pdf = PdfFileReader(packet) + return new_pdf.getPage(0) diff --git a/sign_biometric_oca/readme/CONTRIBUTORS.md b/sign_biometric_oca/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..6295fedf --- /dev/null +++ b/sign_biometric_oca/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ + - Enric Tobella diff --git a/sign_biometric_oca/readme/DESCRIPTION.md b/sign_biometric_oca/readme/DESCRIPTION.md new file mode 100644 index 00000000..4304d29d --- /dev/null +++ b/sign_biometric_oca/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Allow to generate biometric signatures with OCA Sign Application. diff --git a/sign_biometric_oca/static/description/icon.png b/sign_biometric_oca/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/sign_biometric_oca/static/description/icon.png differ diff --git a/sign_biometric_oca/static/description/index.html b/sign_biometric_oca/static/description/index.html new file mode 100644 index 00000000..6dd7025c --- /dev/null +++ b/sign_biometric_oca/static/description/index.html @@ -0,0 +1,423 @@ + + + + + +Sign Biometric Oca + + + +
+

Sign Biometric Oca

+ + +

Beta License: AGPL-3 OCA/sign Translate me on Weblate Try me on Runboat

+

Allow to generate biometric signatures with OCA Sign Application.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+
    +
  • Enric Tobella
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/sign project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sign_biometric_oca/static/src/components/biometric.esm.js b/sign_biometric_oca/static/src/components/biometric.esm.js new file mode 100644 index 00000000..c97d2ad6 --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric.esm.js @@ -0,0 +1,77 @@ +/** @odoo-module **/ + +import {BiometricSignatureDialog} from "./biometric_signature_dialog.esm"; +import core from "web.core"; +import {registry} from "@web/core/registry"; + +const signatureSignOca = { + uploadSignature: function (parent, item, signatureItem, data) { + item.value = data.svg; + // TODO: Transform this in something more standard.... + parent.sensitiveData[item.id] = data.paths; + parent.postIframeField(item); + parent.checkFilledAll(); + var next_items = _.filter( + parent.info.items, + (i) => i.tabindex > item.tabindex + ).sort((a, b) => a.tabindex - b.tabindex); + if (next_items.length > 0) { + parent.items[next_items[0].id].dispatchEvent(new Event("focus_signature")); + } + }, + generate: function (parent, item, signatureItem) { + var input = $( + core.qweb.render( + "sign_biometric_oca.sign_iframe_field_biometric_signature", + {item: item} + ) + )[0]; + if (item.role === parent.info.role) { + signatureItem[0].addEventListener("focus_signature", () => { + var signatureOptions = { + displaySignatureRatio: + signatureItem[0].clientWidth / signatureItem[0].clientHeight, + }; + parent.env.services.dialog.add(BiometricSignatureDialog, { + ...signatureOptions, + uploadSignature: (data) => + this.uploadSignature(parent, item, signatureItem, data), + }); + }); + input.addEventListener("click", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + var signatureOptions = { + displaySignatureRatio: + signatureItem[0].clientWidth / signatureItem[0].clientHeight, + }; + parent.env.services.dialog.add(BiometricSignatureDialog, { + ...signatureOptions, + uploadSignature: (data) => + this.uploadSignature(parent, item, signatureItem, data), + }); + }); + input.addEventListener("keydown", (ev) => { + if ((ev.keyCode || ev.which) !== 9) { + return true; + } + ev.preventDefault(); + var next_items = _.filter( + parent.info.items, + (i) => i.tabindex > item.tabindex && i.role === parent.info.role + ); + if (next_items.length > 0) { + ev.currentTarget.blur(); + parent.items[next_items[0].id].dispatchEvent( + new Event("focus_signature") + ); + } + }); + } + return input; + }, + check: function (item) { + return Boolean(item.value); + }, +}; +registry.category("sign_oca").add("biometric", signatureSignOca); diff --git a/sign_biometric_oca/static/src/components/biometric.scss b/sign_biometric_oca/static/src/components/biometric.scss new file mode 100644 index 00000000..4b0c601d --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric.scss @@ -0,0 +1,6 @@ +.o_sign_biometric_oca { + touch-action: none; + width: 100%; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} diff --git a/sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js b/sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js new file mode 100644 index 00000000..0d099484 --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric_signature_dialog.esm.js @@ -0,0 +1,131 @@ +/** @odoo-module **/ + +import {Component, onMounted, useRef, useState} from "@odoo/owl"; + +import {Dialog} from "@web/core/dialog/dialog"; +import {getStroke} from "@sign_biometric_oca/lib/perfect-freehand.esm"; + +const average = (a, b) => (a + b) / 2; + +function getSvgPathFromStroke(points, closed = true) { + const len = points.length; + + if (len < 4) { + return ``; + } + + let a = points[0]; + let b = points[1]; + const c = points[2]; + + let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed( + 2 + )},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average( + b[1], + c[1] + ).toFixed(2)} T`; + + for (let i = 2, max = len - 1; i < max; i++) { + a = points[i]; + b = points[i + 1]; + result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed( + 2 + )} `; + } + + if (closed) { + result += "Z"; + } + + return result; +} + +export class BiometricSignatureDialog extends Component { + setup() { + this.title = this.env._t("Adopt Your Signature"); + this.svg = useRef("BiometricSignatureBox"); + this.state = useState({current_key: 0, paths: {}, recording: false}); + onMounted(() => { + this.svg.el.style.height = + this.svg.el.clientWidth / this.props.displaySignatureRatio; + this.svg.el.setAttribute( + "viewBox", + "0 0 " + + this.svg.el.clientWidth + + " " + + parseFloat(this.svg.el.style.height) + ); + }); + } + addPoint(e, current_key) { + this.state.paths[current_key] = [ + ...this.state.paths[current_key], + [e.offsetX, e.offsetY, e.pressure, new Date().getTime()], + ]; + } + handlePointerUp(e) { + this.addPoint(e, this.state.current_key); + this.state.recording = false; + } + handlePointerDown(e) { + e.target.setPointerCapture(e.pointerId); + var current_key = this.state.current_key + 1; + this.state.current_key = current_key; + this.state.recording = true; + this.state.paths[current_key] = []; + this.addPoint(e, current_key); + } + handlePointerMove(e) { + if (!this.state.recording) { + return; + } + this.addPoint(e, this.state.current_key); + } + onClickClear() { + this.state.current_key = 0; + this.state.paths = []; + } + /** + * Upload the signature image when confirm. + * + * @private + */ + onClickConfirm() { + var result = { + paths: JSON.parse(JSON.stringify(this.state.paths)), + data: this.pathData, + width: this.svg.el.clientWidth, + height: this.svg.el.clientHeight, + }; + this.svg.el.style = {}; + this.svg.el.class = ""; + result.svg = btoa(new XMLSerializer().serializeToString(this.svg.el)); + this.props.uploadSignature(result); + this.props.close(); + } + get pathData() { + var result = []; + for (const key in this.state.paths) { + result.push( + getSvgPathFromStroke( + getStroke(this.state.paths[key], { + size: 8, + thinning: 0.5, + smoothing: 0.5, + streamline: 0.5, + }) + ) + ); + } + return result; + } + get isSignatureEmpty() { + return this.state.current_key === 0; + } +} + +BiometricSignatureDialog.template = "sign_biometric_oca.BiometricSignatureDialog"; +BiometricSignatureDialog.components = {Dialog}; +BiometricSignatureDialog.defaultProps = { + displaySignatureRatio: 3.0, +}; diff --git a/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml b/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml new file mode 100644 index 00000000..a71209dc --- /dev/null +++ b/sign_biometric_oca/static/src/components/biometric_signature_dialog.xml @@ -0,0 +1,50 @@ + + + + + +
+ + + +
By clicking Adopt & Sign, I agree that the chosen signature/initials will be a valid electronic representation of my hand-written signature/initials for all purposes when it is used on documents, including legally binding contracts.
+
+ + + + + +
+
+ + + +
+ + diff --git a/sign_biometric_oca/static/src/lib/perfect-freehand.esm.js b/sign_biometric_oca/static/src/lib/perfect-freehand.esm.js new file mode 100644 index 00000000..e808fc0b --- /dev/null +++ b/sign_biometric_oca/static/src/lib/perfect-freehand.esm.js @@ -0,0 +1,227 @@ +/** @odoo-module **/ + +function $(e, t, u, x = (h) => h) { + return e * x(0.5 - t * (0.5 - u)); +} +function se(e) { + return [-e[0], -e[1]]; +} +function l(e, t) { + return [e[0] + t[0], e[1] + t[1]]; +} +function a(e, t) { + return [e[0] - t[0], e[1] - t[1]]; +} +function b(e, t) { + return [e[0] * t, e[1] * t]; +} +function he(e, t) { + return [e[0] / t, e[1] / t]; +} +function R(e) { + return [e[1], -e[0]]; +} +function B(e, t) { + return e[0] * t[0] + e[1] * t[1]; +} +function ue(e, t) { + return e[0] === t[0] && e[1] === t[1]; +} +function ge(e) { + return Math.hypot(e[0], e[1]); +} +function de(e) { + return e[0] * e[0] + e[1] * e[1]; +} +function A(e, t) { + return de(a(e, t)); +} +function G(e) { + return he(e, ge(e)); +} +function ie(e, t) { + return Math.hypot(e[1] - t[1], e[0] - t[0]); +} +function L(e, t, u) { + const x = Math.sin(u), + h = Math.cos(u), + y = e[0] - t[0], + n = e[1] - t[1], + f = y * h - n * x, + d = y * x + n * h; + return [f + t[0], d + t[1]]; +} +function K(e, t, u) { + return l(e, b(a(t, e), u)); +} +function ee(e, t, u) { + return l(e, b(t, u)); +} +var {min: C, PI: xe} = Math, + pe = 0.275, + V = xe + 1e-4; +function ce(e, t = {}) { + let { + size: u = 16, + smoothing: x = 0.5, + thinning: h = 0.5, + simulatePressure: y = !0, + easing: n = (r) => r, + start: f = {}, + end: d = {}, + last: D = !1, + } = t, + {cap: S = !0, easing: j = (r) => r * (2 - r)} = f, + {cap: q = !0, easing: c = (r) => --r * r * r + 1} = d; + if (e.length === 0 || u <= 0) return []; + let p = e[e.length - 1].runningLength, + g = f.taper === !1 ? 0 : f.taper === !0 ? Math.max(u, p) : f.taper, + T = d.taper === !1 ? 0 : d.taper === !0 ? Math.max(u, p) : d.taper, + te = Math.pow(u * x, 2), + _ = [], + M = [], + H = e.slice(0, 10).reduce((r, i) => { + let o = i.pressure; + if (y) { + const s = C(1, i.distance / u), + W = C(1, 1 - s); + o = C(1, r + (W - r) * (s * pe)); + } + return (r + o) / 2; + }, e[0].pressure), + m = $(u, h, e[e.length - 1].pressure, n), + U, + X = e[0].vector, + z = e[0].point, + F = z, + O = z, + E = F, + J = !1; + for (let r = 0; r < e.length; r++) { + let {pressure: i} = e[r], + {point: o, vector: s, distance: W, runningLength: I} = e[r]; + if (r < e.length - 1 && p - I < 3) continue; + if (h) { + if (y) { + const v = C(1, W / u), + Z = C(1, 1 - v); + i = C(1, H + (Z - H) * (v * pe)); + } + m = $(u, h, i, n); + } else m = u / 2; + U === void 0 && (U = m); + const le = I < g ? j(I / g) : 1, + fe = p - I < T ? c((p - I) / T) : 1; + m = Math.max(0.01, m * Math.min(le, fe)); + const re = (r < e.length - 1 ? e[r + 1] : e[r]).vector, + Y = r < e.length - 1 ? B(s, re) : 1, + be = B(s, X) < 0 && !J, + ne = Y !== null && Y < 0; + if (be || ne) { + const v = b(R(X), m); + for (let Z = 1 / 13, w = 0; w <= 1; w += Z) + (O = L(a(o, v), o, V * w)), _.push(O), (E = L(l(o, v), o, V * -w)), M.push(E); + (z = O), (F = E), ne && (J = !0); + continue; + } + if (((J = !1), r === e.length - 1)) { + const v = b(R(s), m); + _.push(a(o, v)), M.push(l(o, v)); + continue; + } + const oe = b(R(K(re, s, Y)), m); + (O = a(o, oe)), + (r <= 1 || A(z, O) > te) && (_.push(O), (z = O)), + (E = l(o, oe)), + (r <= 1 || A(F, E) > te) && (M.push(E), (F = E)), + (H = i), + (X = s); + } + const P = e[0].point.slice(0, 2), + k = e.length > 1 ? e[e.length - 1].point.slice(0, 2) : l(e[0].point, [1, 1]), + Q = [], + N = []; + if (e.length === 1) { + if (!(g || T) || D) { + const r = ee(P, G(R(a(P, k))), -(U || m)), + i = []; + for (let o = 1 / 13, s = o; s <= 1; s += o) i.push(L(r, P, V * 2 * s)); + return i; + } + } else { + if (!(g || (T && e.length === 1))) + if (S) + for (let i = 1 / 13, o = i; o <= 1; o += i) { + const s = L(M[0], P, V * o); + Q.push(s); + } + else { + const i = a(_[0], M[0]), + o = b(i, 0.5), + s = b(i, 0.51); + Q.push(a(P, o), a(P, s), l(P, s), l(P, o)); + } + const r = R(se(e[e.length - 1].vector)); + if (T || (g && e.length === 1)) N.push(k); + else if (q) { + const i = ee(k, r, m); + for (let o = 1 / 29, s = o; s < 1; s += o) N.push(L(i, k, V * 3 * s)); + } else + N.push(l(k, b(r, m)), l(k, b(r, m * 0.99)), a(k, b(r, m * 0.99)), a(k, b(r, m))); + } + return _.concat(N, M.reverse(), Q); +} +function me(e, t = {}) { + var q; + const {streamline: u = 0.5, size: x = 16, last: h = !1} = t; + if (e.length === 0) return []; + let y = 0.15 + (1 - u) * 0.85, + n = Array.isArray(e[0]) ? e : e.map(({x: c, y: p, pressure: g = 0.5}) => [c, p, g]); + if (n.length === 2) { + const c = n[1]; + n = n.slice(0, -1); + for (let p = 1; p < 5; p++) n.push(K(n[0], c, p / 4)); + } + n.length === 1 && (n = [...n, [...l(n[0], [1, 1]), ...n[0].slice(2)]]); + let f = [ + { + point: [n[0][0], n[0][1]], + pressure: n[0][2] >= 0 ? n[0][2] : 0.25, + vector: [1, 1], + distance: 0, + runningLength: 0, + }, + ], + d = !1, + D = 0, + S = f[0], + j = n.length - 1; + for (let c = 1; c < n.length; c++) { + const p = h && c === j ? n[c].slice(0, 2) : K(S.point, n[c], y); + if (ue(S.point, p)) continue; + const g = ie(p, S.point); + if (((D += g), c < j && !d)) { + if (D < x) continue; + d = !0; + } + (S = { + point: p, + pressure: n[c][2] >= 0 ? n[c][2] : 0.5, + vector: G(a(S.point, p)), + distance: g, + runningLength: D, + }), + f.push(S); + } + return (f[0].vector = ((q = f[1]) == null ? void 0 : q.vector) || [0, 0]), f; +} +function ae(e, t = {}) { + return ce(me(e, t), t); +} +var _e = ae; +export { + _e as default, + ae as getStroke, + ce as getStrokeOutlinePoints, + me as getStrokePoints, +}; diff --git a/sign_oca/__manifest__.py b/sign_oca/__manifest__.py index 8378e5be..f1d8b0a5 100644 --- a/sign_oca/__manifest__.py +++ b/sign_oca/__manifest__.py @@ -25,6 +25,7 @@ "views/sign_oca_field.xml", "views/sign_oca_role.xml", "views/sign_oca_template.xml", + "views/sign_oca_certificate.xml", "templates/assets.xml", ], "demo": [ diff --git a/sign_oca/controllers/main.py b/sign_oca/controllers/main.py index cbfd94de..140b2b0e 100644 --- a/sign_oca/controllers/main.py +++ b/sign_oca/controllers/main.py @@ -71,6 +71,21 @@ def get_sign_oca_content_access(self, signer_id, access_token): signer_sudo.request_id, "data" ).get_response(mimetype="application/pdf") + @http.route( + ["/sign_oca/certificate//"], + type="json", + auth="public", + website=True, + ) + def get_sign_oca_certificate(self, signer_id, access_token): + try: + signer_sudo = self._document_check_access( + "sign.oca.request.signer", signer_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + return signer_sudo.sign_certificate_id.data + @http.route( ["/sign_oca/info//"], type="json", @@ -92,11 +107,13 @@ def get_sign_oca_info_access(self, signer_id, access_token): auth="public", website=True, ) - def get_sign_oca_sign_access(self, signer_id, access_token, items): + def get_sign_oca_sign_access(self, signer_id, access_token, items, encrypted_data): try: signer_sudo = self._document_check_access( "sign.oca.request.signer", signer_id, access_token ) except (AccessError, MissingError): return request.redirect("/my") - return signer_sudo.action_sign(items, access_token=access_token) + return signer_sudo.action_sign( + items, encrypted_data=encrypted_data, access_token=access_token + ) diff --git a/sign_oca/models/__init__.py b/sign_oca/models/__init__.py index 1cbaaba0..e8d1c2b1 100644 --- a/sign_oca/models/__init__.py +++ b/sign_oca/models/__init__.py @@ -5,3 +5,4 @@ from . import sign_oca_role from . import sign_oca_field from . import sign_oca_request +from . import sign_oca_certificate diff --git a/sign_oca/models/sign_oca_certificate.py b/sign_oca/models/sign_oca_certificate.py new file mode 100644 index 00000000..9dae88d2 --- /dev/null +++ b/sign_oca/models/sign_oca_certificate.py @@ -0,0 +1,19 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SignOcaCertificate(models.Model): + """ + This certificate will allow us to encrypt sensitive data + We will not be able to decrypt it without the private key + """ + + _name = "sign.oca.certificate" + _description = "Sign Public Certificate" + _order = "id desc" + + name = fields.Char(required=True) + data = fields.Char() + active = fields.Boolean(default=True) diff --git a/sign_oca/models/sign_oca_request.py b/sign_oca/models/sign_oca_request.py index 910b28b7..8adeda5d 100644 --- a/sign_oca/models/sign_oca_request.py +++ b/sign_oca/models/sign_oca_request.py @@ -345,13 +345,21 @@ class SignOcaRequestSigner(models.Model): _inherit = "portal.mixin" _description = "Sign Request Value" - data = fields.Binary(related="request_id.data") + data = fields.Binary(related="request_id.data", copy=False) request_id = fields.Many2one("sign.oca.request", required=True, ondelete="cascade") partner_name = fields.Char(related="partner_id.name") partner_id = fields.Many2one("res.partner", required=True, ondelete="restrict") role_id = fields.Many2one("sign.oca.role", required=True, ondelete="restrict") - signed_on = fields.Datetime(readonly=True) - signature_hash = fields.Char(readonly=True) + signed_on = fields.Datetime(readonly=True, copy=False) + signature_hash = fields.Char(readonly=True, copy=False) + sign_certificate_id = fields.Many2one( + "sign.oca.certificate", + default=lambda r: r._get_sign_certificate(), + readonly=True, + copy=False, + ) + sensitive_data = fields.Binary(readonly=True, copy=False) + encrypted_data = fields.Json() model = fields.Char(compute="_compute_model", store=True) res_id = fields.Integer(compute="_compute_res_id", store=True) is_allow_signature = fields.Boolean(compute="_compute_is_allow_signature") @@ -388,6 +396,10 @@ def _compute_is_allow_signature(self): not item.signed_on and item.partner_id == user.partner_id ) + @api.model + def _get_sign_certificate(self): + return self.env["sign.oca.certificate"].search([], limit=1) + def _compute_access_url(self): result = super()._compute_access_url() for record in self: @@ -412,6 +424,7 @@ def get_info(self, access_token=False): "name": self.request_id.template_id.name, "items": self.request_id.signatory_data, "to_sign": self.request_id.to_sign, + "certificate_id": self.sign_certificate_id.id, "partner": { "id": self.partner_id.id, "name": self.partner_id.name, @@ -430,7 +443,7 @@ def sign(self): "url": self.access_url, } - def action_sign(self, items, access_token=False): + def action_sign(self, items, encrypted_data=False, access_token=False): self.ensure_one() if self.signed_on: raise ValidationError( @@ -439,6 +452,7 @@ def action_sign(self, items, access_token=False): if self.request_id.state != "sent": raise ValidationError(_("Request cannot be signed")) self.signed_on = fields.Datetime.now() + self.encrypted_data = encrypted_data # current_hash = self.request_id.current_hash signatory_data = self.request_id.signatory_data diff --git a/sign_oca/security/ir.model.access.csv b/sign_oca/security/ir.model.access.csv index a7530d69..6b4914f5 100644 --- a/sign_oca/security/ir.model.access.csv +++ b/sign_oca/security/ir.model.access.csv @@ -17,3 +17,5 @@ edit_sign_generate_signer,edit_sign_generate_signer,model_sign_oca_template_gene edit_sign_generate_multi,edit_sign_generate_multi,model_sign_oca_template_generate_multi,sign_oca_group_user,1,1,1,1 access_sign_request_log,access_sign_request_log,model_sign_oca_request_log,sign_oca_group_user,1,0,0,0 access_sign_request_log_admin,access_sign_request_log_admin,model_sign_oca_request_log,sign_oca_group_admin,1,1,1,1 +access_sign_certificate,access_sign_certificate,model_sign_oca_certificate,sign_oca_group_user,1,0,0,0 +edit_sign_certificate,edit_sign_certificate,model_sign_oca_certificate,base.group_system,1,1,1,0 diff --git a/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js index 1a0f1de4..e0dc1962 100644 --- a/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js +++ b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.esm.js @@ -8,6 +8,7 @@ export default class SignOcaPdf extends SignOcaPdfCommon { setup() { super.setup(...arguments); this.to_sign = false; + this.sensitiveData = {}; } async willStart() { await super.willStart(...arguments); @@ -21,6 +22,54 @@ export default class SignOcaPdf extends SignOcaPdfCommon { }); this.to_sign = this.to_sign_update; } + async _encryptSensitiveData(publicKeyBase64) { + const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), (c) => + c.charCodeAt(0) + ); + var importedPublicKey = await crypto.subtle.importKey( + "spki", + publicKeyBytes.buffer, + {name: "ECDH", namedCurve: "P-256"}, + true, + [] + ); + const privateKey = await crypto.subtle.generateKey( + { + name: "ECDH", + namedCurve: "P-256", + }, + true, + ["deriveKey"] + ); + const sharedKey = await crypto.subtle.deriveKey( + { + name: "ECDH", + public: importedPublicKey, + }, + privateKey.privateKey, + { + name: "AES-CBC", + length: 256, + }, + true, + ["encrypt", "decrypt"] + ); + const iv = crypto.getRandomValues(new Uint8Array(16)); + const encryptedBuffer = await crypto.subtle.encrypt( + { + name: "AES-CBC", + iv: iv, + }, + sharedKey, + new TextEncoder().encode(JSON.stringify(this.sensitiveData)) + ); + const publicKey = await crypto.subtle.exportKey("spki", privateKey.publicKey); + this.encryptedData = { + iv: btoa(String.fromCharCode(...iv)), + data: btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer))), + public: btoa(String.fromCharCode(...new Uint8Array(publicKey))), + }; + } renderButtons(to_sign) { var $buttons = $( core.qweb.render("oca_sign_oca.SignatureButtons", { @@ -28,15 +77,46 @@ export default class SignOcaPdf extends SignOcaPdfCommon { }) ); $buttons.on("click.o_sign_oca_button_sign", () => { - this.env.services - .rpc({ - model: this.props.model, - method: "action_sign", - args: [[this.props.res_id], this.info.items], - }) - .then(() => { - this.props.trigger("history_back"); - }); + // TODO: Add encryption here + var todoFirst = []; + this.encryptedData = {}; + if ( + Object.keys(this.sensitiveData).length > 0 && + this.info.certificate_id + ) { + todoFirst.push( + new Promise((resolve) => { + this.env.services + .rpc({ + model: "sign.oca.certificate", + method: "read", + args: [[this.info.certificate_id], ["data"]], + }) + .then((public_certificate_info) => { + this._encryptSensitiveData( + public_certificate_info[0].data + ).then(() => { + resolve(); + }); + }); + }) + ); + } + Promise.all(todoFirst).then(() => { + this.env.services + .rpc({ + model: this.props.model, + method: "action_sign", + args: [ + [this.props.res_id], + this.info.items, + this.encryptedData, + ], + }) + .then(() => { + this.props.trigger("history_back"); + }); + }); }); return $buttons; } diff --git a/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js b/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js index 3b0c2cf3..1516f0f5 100644 --- a/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js +++ b/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.esm.js @@ -12,6 +12,7 @@ export class SignOcaPdfPortal extends SignOcaPdf { setup() { super.setup(...arguments); this.signOcaFooter = useRef("sign_oca_footer"); + this.sensitiveData = {}; } async willStart() { this.info = await this.env.services.rpc({ @@ -40,7 +41,18 @@ export class SignOcaPdfPortal extends SignOcaPdf { super.postIframeFields(...arguments); this.checkFilledAll(); } - _onClickSign() { + async _onClickSign() { + this.encryptedData = false; + if (Object.keys(this.sensitiveData).length > 0 && this.info.certificate_id) { + const public_certificate_info = await this.env.services.rpc({ + route: + "/sign_oca/certificate/" + + this.props.signer_id + + "/" + + this.props.access_token, + }); + await this._encryptSensitiveData(public_certificate_info); + } this.env.services .rpc({ route: @@ -48,7 +60,7 @@ export class SignOcaPdfPortal extends SignOcaPdf { this.props.signer_id + "/" + this.props.access_token, - params: {items: this.info.items}, + params: {items: this.info.items, encrypted_data: this.encryptedData}, }) .then((action) => { // As we are on frontend env, it is not possible to use do_action(), so we diff --git a/sign_oca/views/sign_oca_certificate.xml b/sign_oca/views/sign_oca_certificate.xml new file mode 100644 index 00000000..fdf3fb5e --- /dev/null +++ b/sign_oca/views/sign_oca_certificate.xml @@ -0,0 +1,55 @@ + + + + + + sign.oca.certificate + +
+
+
+ + + + + + +
+
+
+ + + sign.oca.certificate + + + + + + + + + sign.oca.certificate + + + + + + + + + Sign Oca Certificate + sign.oca.certificate + tree,form + [] + {} + + + + Sign Oca Certificate + + + + + +