mirror of
https://github.com/LibreSign/libresign.git
synced 2025-12-18 05:20:45 +01:00
Merge branch 'signer' into 'master'
Signer See merge request lyseontech/assinador-digital!16
This commit is contained in:
commit
7d9cfcf76a
64 changed files with 15759 additions and 52 deletions
|
|
@ -1,3 +1,14 @@
|
|||
FROM golang:1.14 as cfssl
|
||||
WORKDIR /workdir
|
||||
RUN git clone https://github.com/cloudflare/cfssl.git /workdir && \
|
||||
git clone https://github.com/cloudflare/cfssl_trust.git /etc/cfssl && \
|
||||
make clean && \
|
||||
make bin/rice && ./bin/rice embed-go -i=./cli/serve && \
|
||||
make all && cp bin/* /usr/bin/
|
||||
RUN mkdir /home/cfssl/ && \
|
||||
chown 1000:1000 /home/cfssl/
|
||||
EXPOSE 8888
|
||||
|
||||
FROM nextcloud:stable-fpm as prod
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
RUN apt update && apt install -y git
|
||||
|
|
@ -7,6 +18,9 @@ RUN apt update && apt install -y cmake poppler-data libopenjp2-7-dev libfreetype
|
|||
&& cd poppler-20.08.0 && mkdir build && cd build \
|
||||
&& cmake .. -DBUILD_GTK_TESTS=OFF -DBUILD_QT5_TESTS=OFF -DBUILD_QT6_TESTS=OFF -DBUILD_CPP_TESTS=OFF -DENABLE_SPLASH=OFF -DENABLE_CPP=OFF -DENABLE_GLIB=OFF -DENABLE_GOBJECT_INTROSPECTION=OFF -DENABLE_QT5=OFF -DENABLE_QT6=OFF -DENABLE_CMS=lcms2 -DENABLE_LIBOPENJPEG=openjpeg2 \
|
||||
&& make
|
||||
RUN mkdir /tmp/cfssl/ && \
|
||||
chmod 777 /tmp/cfssl/
|
||||
COPY --from=cfssl /workdir/bin /usr/bin
|
||||
|
||||
FROM prod as dev
|
||||
RUN yes | pecl install xdebug-2.9.6
|
||||
|
|
|
|||
19
.docker/cfssl/entrypoint.sh
Executable file
19
.docker/cfssl/entrypoint.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#! /bin/bash
|
||||
|
||||
while [ ! -f /tmp/cfssl/csr_server.json ] || [ ! -f /tmp/cfssl/config_server.json ]; do
|
||||
echo "no /tmp/cfssl/csr_server.json or /tmp/cfssl/config_server.json detected!";
|
||||
sleep 10;
|
||||
done;
|
||||
|
||||
cd /home/cfssl/;
|
||||
if [ ! -f csr_server.json ]
|
||||
then
|
||||
cp /tmp/cfssl/csr_server.json /home/cfssl/csr_server.json;
|
||||
cfssl genkey -initca=true csr_server.json | cfssljson -bare ca;
|
||||
fi
|
||||
if [ ! -f config_server.json ]
|
||||
then
|
||||
cp /tmp/cfssl/config_server.json /home/cfssl/config_server.json;
|
||||
fi
|
||||
|
||||
cfssl serve -address=0.0.0.0 -ca-key ca-key.pem -ca ca.pem -config config_server.json
|
||||
41
Makefile
41
Makefile
|
|
@ -28,7 +28,7 @@ set-configs:
|
|||
init-cron:
|
||||
docker-compose up -d cron
|
||||
|
||||
install-dsv: fix-database build-dsv
|
||||
install-dsv: build-dsv
|
||||
|
||||
fix-database:
|
||||
docker-compose run --rm --user www-data app sh -c "php occ config:system:set dbname --value \$$POSTGRES_DB"
|
||||
|
|
@ -42,4 +42,41 @@ build-dsv:
|
|||
docker-compose exec app bash -c "cd /tmp/dsv/lib; composer install --no-interaction --no-dev"
|
||||
docker-compose exec app sh -c "cp -r /tmp/dsv /var/www/html/apps/"
|
||||
docker-compose exec --user www-data app php occ app:enable dsv
|
||||
|
||||
|
||||
install-signer:
|
||||
docker-compose build
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
docker-compose exec app sh -c "mkdir /var/www/html/apps/signer && \
|
||||
cp -r /tmp/signer/appinfo \
|
||||
/tmp/signer/img \
|
||||
/tmp/signer/js \
|
||||
/tmp/signer/lib \
|
||||
/tmp/signer/src \
|
||||
/tmp/signer/templates \
|
||||
/tmp/signer/composer.json \
|
||||
/tmp/signer/composer.lock /var/www/html/apps/signer"
|
||||
docker-compose exec -w /var/www/html/apps/signer app bash -c "composer install --no-interaction --no-dev"
|
||||
docker-compose exec -w /var/www/html/apps/signer app bash -c "chmod +x vendor/jeidison/jsignpdf-php/bin/jre1.8.0_241_linux/bin/java"
|
||||
docker-compose exec --user www-data app php occ app:enable signer
|
||||
|
||||
update-signer:
|
||||
docker-compose exec app sh -c "rsync -av /tmp/signer /var/www/html/apps/ --delete --exclude node_modules --exclude vendor"
|
||||
|
||||
test-signer: update-signer
|
||||
docker-compose exec -w /var/www/html/apps/signer app sh -c "vendor/phpunit/phpunit/phpunit -c phpunit.xml"
|
||||
|
||||
build-signer-frontend:
|
||||
cd ./signer; make
|
||||
|
||||
lint:
|
||||
cd ./signer; make lint-fix
|
||||
make update-signer
|
||||
|
||||
INSTALL=0
|
||||
serve-signer-frontend-dev: update-signer
|
||||
ifeq ($(INSTALL), 1)
|
||||
cd ./volumes/nextcloud/apps/signer; make serve
|
||||
else
|
||||
cd ./volumes/nextcloud/apps/signer; make watch
|
||||
endif
|
||||
|
|
|
|||
|
|
@ -15,4 +15,8 @@
|
|||
|
||||
## Documentação do app Digital Signature Validator
|
||||
|
||||
- [Instalação e uso do app de validação de assinaturas](./docs/instalacao-uso-dsv.md)
|
||||
- [Instalação e uso do app de validação de assinaturas](./docs/instalacao-uso-dsv.md)
|
||||
|
||||
## Documentação do app Signer
|
||||
|
||||
- [Instalação e uso do app assinador e gerador de assinaturas](./docs/instalacao-uso-signer.md)
|
||||
|
|
@ -1,57 +1,71 @@
|
|||
version: "3.7"
|
||||
|
||||
volumes:
|
||||
html:
|
||||
html:
|
||||
tmp:
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:12.3
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/postgres/data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
|
||||
db:
|
||||
image: postgres:12.3
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/postgres/data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ./.docker/app
|
||||
target: prod
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
|
||||
- ./volumes/nextcloud:/var/www/html
|
||||
- ./dsv:/tmp/dsv
|
||||
environment:
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
|
||||
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin}
|
||||
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin}
|
||||
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS:-mydomain.coop}
|
||||
depends_on:
|
||||
- db
|
||||
app:
|
||||
build:
|
||||
context: ./.docker/app
|
||||
target: prod
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
|
||||
- ./volumes/nextcloud:/var/www/html
|
||||
- ./dsv:/tmp/dsv
|
||||
- ./signer:/tmp/signer
|
||||
- tmp:/tmp/cfssl:rw
|
||||
environment:
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
|
||||
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin}
|
||||
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin}
|
||||
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS:-mydomain.coop}
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
web:
|
||||
image: nginx:1.18
|
||||
restart: always
|
||||
ports:
|
||||
- 443:443
|
||||
volumes:
|
||||
- ./.docker/web/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./.docker/web/conf.d/nextcloud.conf:/etc/nginx/conf.d/nextcloud.conf
|
||||
- ./volumes/nextcloud:/var/www/html:ro
|
||||
- ./certs/default.crt:/etc/nginx/certs/default.crt
|
||||
- ./certs/default.key:/etc/nginx/certs/default.key
|
||||
depends_on:
|
||||
- app
|
||||
web:
|
||||
image: nginx:1.18
|
||||
restart: always
|
||||
ports:
|
||||
- 443:443
|
||||
volumes:
|
||||
- ./.docker/web/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./.docker/web/conf.d/nextcloud.conf:/etc/nginx/conf.d/nextcloud.conf
|
||||
- ./volumes/nextcloud:/var/www/html:ro
|
||||
- ./certs/default.crt:/etc/nginx/certs/default.crt
|
||||
- ./certs/default.key:/etc/nginx/certs/default.key
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
cron:
|
||||
image: nextcloud:stable-fpm-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
|
||||
- ./volumes/nextcloud:/var/www/html
|
||||
entrypoint: /cron.sh
|
||||
cron:
|
||||
image: nextcloud:stable-fpm-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
|
||||
- ./volumes/nextcloud:/var/www/html
|
||||
entrypoint: /cron.sh
|
||||
|
||||
cfssl:
|
||||
build:
|
||||
context: ./.docker/app
|
||||
target: cfssl
|
||||
working_dir: /home/cfssl
|
||||
command: /home/cfssl/entrypoint.sh
|
||||
volumes:
|
||||
- ./.docker/cfssl/entrypoint.sh:/home/cfssl/entrypoint.sh
|
||||
- ./volumes/cfssl:/home/cfssl
|
||||
- tmp:/tmp/cfssl:ro
|
||||
|
|
|
|||
BIN
docs/assinar_documento_aba.png
Normal file
BIN
docs/assinar_documento_aba.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/certificado_raiz_painel_administrativo.png
Normal file
BIN
docs/certificado_raiz_painel_administrativo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
docs/formulario_nova_assinatura.png
Normal file
BIN
docs/formulario_nova_assinatura.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
37
docs/instalacao-uso-signer.md
Normal file
37
docs/instalacao-uso-signer.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Signer
|
||||
|
||||
Aplicativo NextCloud que permite assinar documentos PDF e gerar novas assinaturas de uso interno.
|
||||
|
||||
## Instalação
|
||||
|
||||
Para instalar o aplicativo, basta rodar na pasta raiz do projeto `make install-signer`.
|
||||
|
||||
## Configuração para Geração de Assinaturas
|
||||
|
||||
Para a geração de novos assinaturas de uso interno, é preciso primeiro configurar o certificado raiz.
|
||||
|
||||
Este certificado deve conter as informações da Entidade Certificadora Raiz que será responsável por todas as assinaturas geradas, e será por isso, será vinculado a todas as assinaturas geradas.
|
||||
|
||||
Para configurar, acesse o painel de configurações com um usuário administrador, e vá para a opção de "Segurança" na sessão "Administração".
|
||||
|
||||

|
||||
|
||||
**Importante:** no momento, não é possivel alterar as informações do certificado raiz.
|
||||
|
||||
## Geração de Assinaturas
|
||||
|
||||
Uma vez que o certificado raiz foi gerado pelo administrador do sistema, qualquer usuário pode gerar uma assinatura.
|
||||
|
||||
Para isso, basta acessar o icone "Signer" no topo da página, e informar os dados do formulário. Estes dados serão usados para compôr as ĩnformações do certificado, que irão ser adicionados aos arquivos assinados com a assinatura gerada.
|
||||
|
||||

|
||||
|
||||
A assinatura será salva no sistema de arquivos do NextCloud, conforme selecionado no formulário, e pode ser baixada clicando com o botão direito e selecionando a opção "Baixar".
|
||||
|
||||
## Assinatura de documentos
|
||||
|
||||
Ao abrir os detalhes de um arquivo PDF, haverá uma nova aba com o label `Assinar Documento`. Nesta aba, basta informar qual a assinatura a ser utilizada e a senha desta assinatura e clicar no botão de "Assinar Documento". Feito isso, será gerado um novo arquivo no mesmo local onde o arquivo original está, com o prefixo "signed_".
|
||||
|
||||

|
||||
|
||||
Os arquivos serão armazenados no sistema de arquivos do NextCloud, e podem ser baixados clicando com o botão direito e selecionando a opção "Baixar".
|
||||
5
signer/.eslintrc.js
Normal file
5
signer/.eslintrc.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
extends: [
|
||||
'@nextcloud',
|
||||
]
|
||||
}
|
||||
1
signer/.gitattributes
vendored
Normal file
1
signer/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/js/* binary
|
||||
6
signer/.gitignore
vendored
Normal file
6
signer/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.idea
|
||||
*.iml
|
||||
/vendor/
|
||||
/build/
|
||||
node_modules/
|
||||
/.php_cs.cache
|
||||
28
signer/Makefile
Normal file
28
signer/Makefile
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
all: dev-setup build-production
|
||||
serve: dev-setup watch
|
||||
|
||||
dev-setup: clean npm-init
|
||||
|
||||
npm-init:
|
||||
docker-compose run --rm node npm ci
|
||||
|
||||
npm-update:
|
||||
docker-compose run --rm node npm update
|
||||
|
||||
watch: npm-update
|
||||
docker-compose up
|
||||
|
||||
build-production: lint
|
||||
docker-compose run --rm node npm run build
|
||||
|
||||
lint-fix:
|
||||
docker-compose run --rm node npm run lint:fix
|
||||
docker-compose run --rm node npm run stylelint:fix
|
||||
|
||||
lint:
|
||||
docker-compose run --rm node npm run lint
|
||||
docker-compose run --rm node npm run stylelint
|
||||
|
||||
clean:
|
||||
rm -rf js/*
|
||||
rm -rf node_modules
|
||||
6
signer/appinfo/app.php
Normal file
6
signer/appinfo/app.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
if (false === (@include_once __DIR__.'/../vendor/autoload.php')) {
|
||||
throw new Exception('Cannot include autoload. Did you run install dependencies using composer?');
|
||||
}
|
||||
$app = \OC::$server->query(OCA\Signer\AppInfo\Application::class);
|
||||
23
signer/appinfo/info.xml
Normal file
23
signer/appinfo/info.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0"?>
|
||||
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||
<id>signer</id>
|
||||
<name>Signer</name>
|
||||
<summary>App for signing documents.</summary>
|
||||
<description><![CDATA[This is a app for signing documents]]></description>
|
||||
<version>1.0.1</version>
|
||||
<licence>agpl</licence>
|
||||
<namespace>Signer</namespace>
|
||||
<dependencies>
|
||||
<nextcloud min-version="17" max-version="20"/>
|
||||
</dependencies>
|
||||
<navigations>
|
||||
<navigation>
|
||||
<name>Signer</name>
|
||||
<route>signer.page.index</route>
|
||||
</navigation>
|
||||
</navigations>
|
||||
<settings>
|
||||
<admin>OCA\Signer\Settings\AdminSettings</admin>
|
||||
</settings>
|
||||
|
||||
</info>
|
||||
14
signer/appinfo/routes.php
Normal file
14
signer/appinfo/routes.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'api#preflighted_cors', 'url' => '/api/0.1/{path}',
|
||||
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+'], ],
|
||||
['name' => 'signer#sign', 'url' => '/api/0.1/sign', 'verb' => 'POST'],
|
||||
['name' => 'signature#generate', 'url' => '/api/0.1/signature/generate', 'verb' => 'POST'],
|
||||
['name' => 'signature#check', 'url' => '/api/0.1/signature/check', 'verb' => 'GET'],
|
||||
['name' => 'admin#generateCertificate', 'url' => '/api/0.1/admin/certificate', 'verb' => 'POST'],
|
||||
['name' => 'admin#loadCertificate', 'url' => '/api/0.1/admin/certificate', 'verb' => 'GET'],
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
],
|
||||
];
|
||||
6
signer/babel.config.js
Normal file
6
signer/babel.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
],
|
||||
presets: ['@babel/preset-env'],
|
||||
}
|
||||
18
signer/composer.json
Normal file
18
signer/composer.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "lt/signer",
|
||||
"description": "Signer",
|
||||
"type": "project",
|
||||
"license": "AGPL",
|
||||
"require": {
|
||||
"jeidison/jsignpdf-php": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5",
|
||||
"nextcloud/coding-standard": "^0.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
|
||||
"cs:check": "php-cs-fixer fix --dry-run --diff",
|
||||
"cs:fix": "php-cs-fixer fix"
|
||||
}
|
||||
}
|
||||
3219
signer/composer.lock
generated
Normal file
3219
signer/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
10
signer/docker-compose.yml
Normal file
10
signer/docker-compose.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
node:
|
||||
image: node
|
||||
user: node
|
||||
command: npm run watch
|
||||
working_dir: /home/node/app
|
||||
volumes:
|
||||
- ./:/home/node/app
|
||||
4
signer/img/app.svg
Normal file
4
signer/img/app.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0" viewBox="0 0 32 32" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<path fill="#FFF" d="m6 2c-2.216 0-4 1.784-4 4v20c0 2.216 1.784 4 4 4h20c2.216 0 4-1.784 4-4v-16.719l-0.906 0.9068-5.282-5.2818 2.907-2.9062h-20.719zm15.812 4.9062l5.282 5.2818-8.313 8.312-8.781 3.5 3.5-8.781 8.312-8.3128zm-7.406 9.1878l-2.656 4.406 1.75 1.75 4.406-2.656-3.5-3.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 629 B |
216
signer/js/signer-main.js
Normal file
216
signer/js/signer-main.js
Normal file
File diff suppressed because one or more lines are too long
1
signer/js/signer-main.js.map
Normal file
1
signer/js/signer-main.js.map
Normal file
File diff suppressed because one or more lines are too long
37
signer/js/signer-settings.js
Normal file
37
signer/js/signer-settings.js
Normal file
File diff suppressed because one or more lines are too long
1
signer/js/signer-settings.js.map
Normal file
1
signer/js/signer-settings.js.map
Normal file
File diff suppressed because one or more lines are too long
10
signer/js/signer-tab.js
Normal file
10
signer/js/signer-tab.js
Normal file
File diff suppressed because one or more lines are too long
1
signer/js/signer-tab.js.map
Normal file
1
signer/js/signer-tab.js.map
Normal file
File diff suppressed because one or more lines are too long
28
signer/lib/AppInfo/Application.php
Normal file
28
signer/lib/AppInfo/Application.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\AppInfo;
|
||||
|
||||
use OCA\Files\Event\LoadSidebar;
|
||||
use OCA\Signer\Listener\LoadSidebarListener;
|
||||
use OCA\Signer\Storage\ClientStorage;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
|
||||
class Application extends App
|
||||
{
|
||||
public const APP_ID = 'signer';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::APP_ID);
|
||||
$container = $this->getContainer();
|
||||
$dispatcher = $container->query(IEventDispatcher::class);
|
||||
$dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class);
|
||||
|
||||
$container->registerService(ClientStorage::class, function ($c) {
|
||||
return new ClientStorage(
|
||||
$c->query('ServerContainer')->getUserFolder()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
79
signer/lib/Controller/AdminController.php
Normal file
79
signer/lib/Controller/AdminController.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Controller;
|
||||
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCA\Signer\Service\AdminSignatureService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\IRequest;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
use HandleErrorsTrait;
|
||||
use HandleParamsTrait;
|
||||
|
||||
/** @var AdminSignatureService */
|
||||
private $service;
|
||||
|
||||
/** @var string */
|
||||
private $userId;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
AdminSignatureService $service,
|
||||
$userId
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->service = $service;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @todo remove NoCSRFRequired
|
||||
*/
|
||||
public function generateCertificate(
|
||||
string $commonName = null,
|
||||
string $country = null,
|
||||
string $organization = null,
|
||||
string $organizationUnit = null
|
||||
): DataResponse {
|
||||
try {
|
||||
$this->checkParams([
|
||||
'commonName' => $commonName,
|
||||
'country' => $country,
|
||||
'organization' => $organization,
|
||||
'organizationUnit' => $organizationUnit,
|
||||
]);
|
||||
|
||||
$this->service->generate(
|
||||
$commonName,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit
|
||||
);
|
||||
|
||||
return new DataResponse([]);
|
||||
} catch (\Exception $exception) {
|
||||
return $this->handleErrors($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @todo remove NoCSRFRequired
|
||||
*/
|
||||
public function loadCertificate(): DataResponse
|
||||
{
|
||||
try {
|
||||
$certificate = $this->service->loadKeys();
|
||||
|
||||
return new DataResponse($certificate);
|
||||
} catch (\Exception $exception) {
|
||||
return $this->handleErrors($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
signer/lib/Controller/HandleErrorsTrait.php
Normal file
22
signer/lib/Controller/HandleErrorsTrait.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Controller;
|
||||
|
||||
use OCA\Signer\Exception\SignerException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
|
||||
trait HandleErrorsTrait
|
||||
{
|
||||
protected function handleErrors(\Exception $exception): DataResponse
|
||||
{
|
||||
if ($exception instanceof SignerException) {
|
||||
return new DataResponse($exception->jsonSerialize(), $exception->getCode());
|
||||
}
|
||||
|
||||
return new DataResponse(
|
||||
['message' => $exception->getMessage()],
|
||||
Http::STATUS_INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
17
signer/lib/Controller/HandleParamsTrait.php
Normal file
17
signer/lib/Controller/HandleParamsTrait.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Controller;
|
||||
|
||||
use OCA\Signer\Exception\SignerException;
|
||||
|
||||
trait HandleParamsTrait
|
||||
{
|
||||
protected function checkParams(array $params): void
|
||||
{
|
||||
foreach ($params as $key => $param) {
|
||||
if (empty($param)) {
|
||||
throw new SignerException("parameter '{$key}' is required!", 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
signer/lib/Controller/PageController.php
Normal file
45
signer/lib/Controller/PageController.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Controller;
|
||||
|
||||
use OC\Config;
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\Util;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
public function __construct(IRequest $request, IConfig $config)
|
||||
{
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* Render default template
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
Util::addScript(Application::APP_ID, 'signer-main');
|
||||
|
||||
$response = new TemplateResponse(Application::APP_ID, 'main');
|
||||
|
||||
if ($this->config->getSystemValue('debug')) {
|
||||
$csp = new ContentSecurityPolicy();
|
||||
$csp->allowInlineScript(true);
|
||||
$response->setContentSecurityPolicy($csp);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
89
signer/lib/Controller/SignatureController.php
Normal file
89
signer/lib/Controller/SignatureController.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Controller;
|
||||
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCA\Signer\Service\SignatureService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\IRequest;
|
||||
|
||||
class SignatureController extends Controller
|
||||
{
|
||||
use HandleErrorsTrait;
|
||||
use HandleParamsTrait;
|
||||
|
||||
/** @var SignatureService */
|
||||
private $service;
|
||||
|
||||
/** @var string */
|
||||
private $userId;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
SignatureService $service,
|
||||
$userId
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->service = $service;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @todo remove NoCSRFRequired
|
||||
*/
|
||||
public function generate(
|
||||
string $commonName = null,
|
||||
string $hosts = null,
|
||||
string $country = null,
|
||||
string $organization = null,
|
||||
string $organizationUnit = null,
|
||||
string $path = null,
|
||||
string $password = null
|
||||
): DataResponse {
|
||||
try {
|
||||
$this->checkParams([
|
||||
'commonName' => $commonName,
|
||||
'hosts' => $hosts,
|
||||
'country' => $country,
|
||||
'organization' => $organization,
|
||||
'organizationUnit' => $organizationUnit,
|
||||
'path' => $path,
|
||||
'password' => $password,
|
||||
]);
|
||||
$hosts = explode(',', $hosts);
|
||||
$signaturePath = $this->service->generate(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$path,
|
||||
$password
|
||||
);
|
||||
|
||||
return new DataResponse(['signature' => $signaturePath]);
|
||||
} catch (\Exception $exception) {
|
||||
return $this->handleErrors($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @todo remove NoCSRFRequired
|
||||
*/
|
||||
public function check(){
|
||||
try{
|
||||
$checkData = $this->service->check();
|
||||
|
||||
return new DataResponse($checkData);
|
||||
} catch (\Exception $exception) {
|
||||
return $this->handleErrors($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
signer/lib/Controller/SignerController.php
Normal file
60
signer/lib/Controller/SignerController.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Controller;
|
||||
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCA\Signer\Exception\SignerException;
|
||||
use OCA\Signer\Service\SignerService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\IRequest;
|
||||
|
||||
class SignerController extends Controller
|
||||
{
|
||||
use HandleErrorsTrait;
|
||||
use HandleParamsTrait;
|
||||
|
||||
/** @var SignerService */
|
||||
private $service;
|
||||
|
||||
/** @var string */
|
||||
private $userId;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
SignerService $service,
|
||||
$userId
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->service = $service;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @todo remove NoCSRFRequired
|
||||
*/
|
||||
public function sign(
|
||||
string $inputFilePath = null,
|
||||
string $outputFolderPath = null,
|
||||
string $certificatePath = null,
|
||||
string $password = null
|
||||
): DataResponse {
|
||||
try {
|
||||
$this->checkParams([
|
||||
'inputFilePath' => $inputFilePath,
|
||||
'outputFolderPath' => $outputFolderPath,
|
||||
'certificatePath' => $certificatePath,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$fileSigned = $this->service->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
|
||||
|
||||
return new DataResponse(['fileSigned' => $fileSigned->getInternalPath()]);
|
||||
} catch (\Exception $exception) {
|
||||
return $this->handleErrors($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
signer/lib/Exception/SignerException.php
Normal file
13
signer/lib/Exception/SignerException.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Exception;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class SignerException extends \Exception implements JsonSerializable
|
||||
{
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return ['message' => $this->getMessage()];
|
||||
}
|
||||
}
|
||||
73
signer/lib/Handler/CfsslHandler.php
Normal file
73
signer/lib/Handler/CfsslHandler.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Handler;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use OCA\Signer\Exception\SignerException;
|
||||
|
||||
class CfsslHandler
|
||||
{
|
||||
public function generateCertificate(
|
||||
string $commonName,
|
||||
array $hosts,
|
||||
string $country,
|
||||
string $organization,
|
||||
string $organizationUnit,
|
||||
string $password
|
||||
) {
|
||||
$certKeys = $this->newCert(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit
|
||||
);
|
||||
$certContent = null;
|
||||
$isCertGenerated = openssl_pkcs12_export($certKeys['certificate'], $certContent, $certKeys['private_key'], $password);
|
||||
if (!$isCertGenerated) {
|
||||
throw new SignerException('Error while creating certificate file', 500);
|
||||
}
|
||||
|
||||
return $certContent;
|
||||
}
|
||||
|
||||
private function newCert(
|
||||
string $commonName,
|
||||
array $hosts,
|
||||
string $country,
|
||||
string $organization,
|
||||
string $organizationUnit
|
||||
) {
|
||||
$response = (new Client(['base_uri' => 'http://cfssl:8888/api/v1/cfssl/']))
|
||||
->request('POST', 'newcert', [
|
||||
'json' => [
|
||||
'profile' => 'CA',
|
||||
'request' => [
|
||||
'hosts' => $hosts,
|
||||
'CN' => $commonName,
|
||||
'key' => [
|
||||
'algo' => 'rsa',
|
||||
'size' => 2048,
|
||||
],
|
||||
'names' => [
|
||||
[
|
||||
'C' => $country,
|
||||
'O' => $organization,
|
||||
'OU' => $organizationUnit,
|
||||
'CN' => $commonName,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
)
|
||||
;
|
||||
|
||||
$responseDecoded = json_decode($response->getBody(), true);
|
||||
if (!$responseDecoded['success']) {
|
||||
throw new SignerException('Error while generating certificate keys!', 500);
|
||||
}
|
||||
|
||||
return $responseDecoded['result'];
|
||||
}
|
||||
}
|
||||
89
signer/lib/Handler/CfsslServerHandler.php
Normal file
89
signer/lib/Handler/CfsslServerHandler.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Handler;
|
||||
|
||||
use OCA\Signer\Exception\SignerException;
|
||||
|
||||
class CfsslServerHandler
|
||||
{
|
||||
const CSR_FILE = 'csr_server.json';
|
||||
const CONFIG_FILE = 'config_server.json';
|
||||
const TMP_DIR = '/tmp/cfssl/';
|
||||
|
||||
public function createConfigServer(
|
||||
$commonName,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$key
|
||||
) {
|
||||
$this->putCsrServer(
|
||||
$commonName,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit
|
||||
);
|
||||
$this->putConfigServer($key);
|
||||
}
|
||||
|
||||
private function putCsrServer(
|
||||
$commonName,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit
|
||||
)
|
||||
{
|
||||
$filename = self::TMP_DIR.self::CSR_FILE;
|
||||
$content = [
|
||||
'CN' => $commonName,
|
||||
'key' => [
|
||||
'algo' => 'rsa',
|
||||
'size' => 2048,
|
||||
],
|
||||
'names' => [
|
||||
[
|
||||
'C' => $country,
|
||||
'O' => $organization,
|
||||
'OU' => $organizationUnit,
|
||||
'CN' => $commonName,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = file_put_contents($filename, json_encode($content));
|
||||
if ($response === false) {
|
||||
throw new SignerException("Error while writing CSR server file!", 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function putConfigServer(string $key)
|
||||
{
|
||||
$filename = self::TMP_DIR.self::CONFIG_FILE;
|
||||
$content = [
|
||||
'signing' => [
|
||||
'profiles' => [
|
||||
'CA' => [
|
||||
'auth_key' => 'key1',
|
||||
'expiry' => '8760h',
|
||||
'usages' => [
|
||||
"signing",
|
||||
"digital signature",
|
||||
"cert sign"
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'auth_keys' => [
|
||||
'key1' => [
|
||||
'key' => $key,
|
||||
'type' => 'standard',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = file_put_contents($filename, json_encode($content));
|
||||
if ($response === false) {
|
||||
throw new SignerException("Error while writing config server file!", 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
signer/lib/Handler/JSignerHandler.php
Normal file
32
signer/lib/Handler/JSignerHandler.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Handler;
|
||||
|
||||
use Jeidison\JSignPDF\JSignPDF;
|
||||
use Jeidison\JSignPDF\Sign\JSignParam;
|
||||
use OC\Files\Node\File;
|
||||
use OCA\Signer\Storage\ClientStorage;
|
||||
|
||||
class JSignerHandler
|
||||
{
|
||||
public function signExistingFile(
|
||||
File $inputFile,
|
||||
File $certificate,
|
||||
string $password
|
||||
): array {
|
||||
$param = (new JSignParam())
|
||||
->setCertificate($certificate->getContent())
|
||||
->setPdf($inputFile->getContent())
|
||||
->setPassword($password)
|
||||
->setTempPath('/tmp/')
|
||||
;
|
||||
|
||||
$jSignPdf = new JSignPDF($param);
|
||||
$contentFileSigned = $jSignPdf->sign();
|
||||
|
||||
return [
|
||||
'signed_'.$inputFile->getName(),
|
||||
$contentFileSigned,
|
||||
];
|
||||
}
|
||||
}
|
||||
21
signer/lib/Listener/LoadSidebarListener.php
Normal file
21
signer/lib/Listener/LoadSidebarListener.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Listener;
|
||||
|
||||
use OCA\Files\Event\LoadSidebar;
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Util;
|
||||
|
||||
class LoadSidebarListener implements IEventListener
|
||||
{
|
||||
public function handle(Event $event): void
|
||||
{
|
||||
if (!($event instanceof LoadSidebar)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Util::addScript(Application::APP_ID, 'signer-tab');
|
||||
}
|
||||
}
|
||||
53
signer/lib/Service/AdminSignatureService.php
Normal file
53
signer/lib/Service/AdminSignatureService.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Service;
|
||||
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCA\Signer\Handler\CfsslServerHandler;
|
||||
use OCP\IAppConfig;
|
||||
|
||||
class AdminSignatureService
|
||||
{
|
||||
/** @var CfsslServerHandler */
|
||||
private $cfsslHandler;
|
||||
|
||||
/** @var IAppConfig */
|
||||
private $config;
|
||||
|
||||
public function __construct(CfsslServerHandler $cfsslHandler, IAppConfig $config)
|
||||
{
|
||||
$this->cfsslHandler = $cfsslHandler;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function generate(
|
||||
string $commonName,
|
||||
string $country,
|
||||
string $organization,
|
||||
string $organizationUnit
|
||||
) {
|
||||
$key = bin2hex(random_bytes(16));
|
||||
$this->config->setValue(Application::APP_ID, 'authkey', $key);
|
||||
$this->config->setValue(Application::APP_ID, 'commonName', $commonName);
|
||||
$this->config->setValue(Application::APP_ID, 'country', $country);
|
||||
$this->config->setValue(Application::APP_ID, 'organization', $organization);
|
||||
$this->config->setValue(Application::APP_ID, 'organizationUnit', $organizationUnit);
|
||||
|
||||
$this->cfsslHandler->createConfigServer(
|
||||
$commonName,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$key
|
||||
);
|
||||
}
|
||||
|
||||
public function loadKeys(){
|
||||
return [
|
||||
'commonName' => $this->config->getValue(Application::APP_ID, 'commonName'),
|
||||
'country' => $this->config->getValue(Application::APP_ID, 'country'),
|
||||
'organization' => $this->config->getValue(Application::APP_ID, 'organization'),
|
||||
'organizationUnit' => $this->config->getValue(Application::APP_ID, 'organizationUnit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
61
signer/lib/Service/SignatureService.php
Normal file
61
signer/lib/Service/SignatureService.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Service;
|
||||
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCA\Signer\Handler\CfsslHandler;
|
||||
use OCA\Signer\Storage\ClientStorage;
|
||||
use OCP\IAppConfig;
|
||||
|
||||
class SignatureService
|
||||
{
|
||||
/** @var CfsslHandler */
|
||||
private $cfsslHandler;
|
||||
|
||||
/** @var ClientStorage */
|
||||
private $clientStorage;
|
||||
|
||||
/** @var IAppConfig */
|
||||
private $config;
|
||||
|
||||
public function __construct(
|
||||
CfsslHandler $cfsslHandler,
|
||||
ClientStorage $clientStorage,
|
||||
IAppConfig $config
|
||||
) {
|
||||
$this->cfsslHandler = $cfsslHandler;
|
||||
$this->clientStorage = $clientStorage;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function generate(
|
||||
string $commonName,
|
||||
array $hosts,
|
||||
string $country,
|
||||
string $organization,
|
||||
string $organizationUnit,
|
||||
string $certificatePath,
|
||||
string $password
|
||||
) {
|
||||
$content = $this->cfsslHandler->generateCertificate(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$password
|
||||
);
|
||||
|
||||
$folder = $this->clientStorage->createFolder($certificatePath);
|
||||
$certificateFile = $this->clientStorage->saveFile($commonName.'.pfx', $content, $folder);
|
||||
|
||||
return $certificateFile->getInternalPath();
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
return [
|
||||
'hasRootCert' => null !== $this->config->getValue(Application::APP_ID, 'authkey'),
|
||||
];
|
||||
}
|
||||
}
|
||||
32
signer/lib/Service/SignerService.php
Normal file
32
signer/lib/Service/SignerService.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Service;
|
||||
|
||||
use OCA\Signer\Handler\JSignerHandler;
|
||||
use OCA\Signer\Storage\ClientStorage;
|
||||
|
||||
class SignerService
|
||||
{
|
||||
/** @var JSignerHandler */
|
||||
private $signerHandler;
|
||||
|
||||
/** @var ClientStorage */
|
||||
private $clientStorage;
|
||||
|
||||
public function __construct(JSignerHandler $signerHandler, ClientStorage $clientStorage)
|
||||
{
|
||||
$this->signerHandler = $signerHandler;
|
||||
$this->clientStorage = $clientStorage;
|
||||
}
|
||||
|
||||
public function sign(string $inputFilePath, string $outputFolderPath, string $certificatePath, string $password){
|
||||
$file = $this->clientStorage->getFile($inputFilePath);
|
||||
$certificate = $this->clientStorage->getFile($certificatePath);
|
||||
|
||||
list($filename, $content) = $this->signerHandler->signExistingFile($file, $certificate, $password);
|
||||
$folder = $this->clientStorage->createFolder($outputFolderPath);
|
||||
$certificateFile = $this->clientStorage->saveFile($filename, $content, $folder);
|
||||
|
||||
return $certificateFile;
|
||||
}
|
||||
}
|
||||
29
signer/lib/Settings/AdminSettings.php
Normal file
29
signer/lib/Settings/AdminSettings.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Settings;
|
||||
|
||||
use OCA\Signer\AppInfo\Application;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IDateTimeFormatter;
|
||||
use OCP\IL10N;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
class AdminSettings implements ISettings
|
||||
{
|
||||
public function getForm()
|
||||
{
|
||||
return new TemplateResponse(Application::APP_ID, 'settings');
|
||||
}
|
||||
|
||||
public function getSection()
|
||||
{
|
||||
return 'security';
|
||||
}
|
||||
|
||||
public function getPriority()
|
||||
{
|
||||
return 00;
|
||||
}
|
||||
}
|
||||
79
signer/lib/Storage/ClientStorage.php
Normal file
79
signer/lib/Storage/ClientStorage.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Storage;
|
||||
|
||||
use OC\Files\Node\File;
|
||||
use OC\Files\Node\Node;
|
||||
use OCA\Signer\Exception\SignerException;
|
||||
use OCP\Files\Folder;
|
||||
|
||||
class ClientStorage
|
||||
{
|
||||
/** @var Folder */
|
||||
private $storage;
|
||||
|
||||
public function __construct(Folder $userStorage)
|
||||
{
|
||||
$this->storage = $userStorage;
|
||||
}
|
||||
|
||||
public function getFile(string $path): ?File
|
||||
{
|
||||
$node = $this->getNode($path);
|
||||
if (!$node instanceof File) {
|
||||
throw new SignerException("path {$path} is not a valid file!", 400);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
public function createFolder(string $path): Folder
|
||||
{
|
||||
if (!$this->storage->nodeExists($path)) {
|
||||
return $this->storage->newFolder($path);
|
||||
}
|
||||
|
||||
$node = $this->storage->get($path);
|
||||
if (!$node instanceof Folder) {
|
||||
throw new SignerException("path {$path} already exists and is not a folder!", 400);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
public function saveFile(string $filePath, $content = null, Folder $folder = null): File
|
||||
{
|
||||
if (null === $folder) {
|
||||
$folder = $this->storage;
|
||||
}
|
||||
if (null !== $content && $folder->nodeExists($filePath)) {
|
||||
$node = $folder->get($filePath);
|
||||
|
||||
if (!$node instanceof File) {
|
||||
throw new SignerException("path {$filePath} already exists and is not a file!", 400);
|
||||
}
|
||||
|
||||
$node->putContent($content);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
if (null !== $content) {
|
||||
$file = $folder->newFile($filePath);
|
||||
$file->putContent($content);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
return $folder->newFile($filePath);
|
||||
}
|
||||
|
||||
private function getNode(string $path): Node
|
||||
{
|
||||
if (!$this->storage->nodeExists($path)) {
|
||||
throw new SignerException("path {$path} is not valid!", 400);
|
||||
}
|
||||
|
||||
return $this->storage->get($path);
|
||||
}
|
||||
}
|
||||
10020
signer/package-lock.json
generated
Normal file
10020
signer/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
66
signer/package.json
Normal file
66
signer/package.json
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "signer",
|
||||
"description": "A app for signing documents",
|
||||
"version": "1.0.0",
|
||||
"license": "agpl",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production webpack --progress --hide-modules --config webpack.js",
|
||||
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
|
||||
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint:fix": "eslint --ext .js,.vue src --fix",
|
||||
"stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue",
|
||||
"stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextcloud/axios": "^1.4.0",
|
||||
"@nextcloud/dialogs": "^2.0.1",
|
||||
"@nextcloud/l10n": "^1.4.1",
|
||||
"@nextcloud/logger": "^1.1.2",
|
||||
"@nextcloud/router": "^1.2.0",
|
||||
"@nextcloud/vue": "^2.6.5",
|
||||
"style-loader": "^1.2.1",
|
||||
"vue": "^2.6.12",
|
||||
"webpack-merge": "^5.2.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@nextcloud/browserslist-config": "^1.0.0",
|
||||
"@nextcloud/eslint-config": "^2.2.0",
|
||||
"@nextcloud/eslint-plugin": "^1.4.0",
|
||||
"@nextcloud/webpack-vue-config": "^1.4.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"css-loader": "^4.3.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-import-resolver-webpack": "^0.12.2",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"file-loader": "^6.1.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"stylelint": "^13.7.1",
|
||||
"stylelint-config-recommended-scss": "^4.2.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"stylelint-webpack-plugin": "^2.1.0",
|
||||
"url-loader": "^4.1.0",
|
||||
"vue-loader": "^15.9.3",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.12"
|
||||
}
|
||||
}
|
||||
7
signer/phpunit.xml
Normal file
7
signer/phpunit.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<phpunit bootstrap="tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="unit">
|
||||
<directory>./tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
152
signer/src/AdminFormSigner.vue
Normal file
152
signer/src/AdminFormSigner.vue
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<div id="formSigner" class="form-signer">
|
||||
<div class="form-group">
|
||||
<label for="commonName">{{ t('signer', 'Nome (CN)') }}</label>
|
||||
<input
|
||||
id="commonName"
|
||||
ref="commonName"
|
||||
v-model="certificate.commonName"
|
||||
type="text"
|
||||
:disabled="formDisabled">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="country">{{ t('signer', 'País (C)') }}</label>
|
||||
<input
|
||||
id="country"
|
||||
ref="country"
|
||||
v-model="certificate.country"
|
||||
type="text"
|
||||
:disabled="formDisabled">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="organization">{{ t('signer', 'Organização (O)') }}</label>
|
||||
<input
|
||||
id="organization"
|
||||
ref="organization"
|
||||
v-model="certificate.organization"
|
||||
type="text"
|
||||
:disabled="formDisabled">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="organizationUnit">{{ t('signer', 'Unidade da organização (OU)') }}</label>
|
||||
<input
|
||||
id="organizationUnit"
|
||||
ref="organizationUnit"
|
||||
v-model="certificate.organizationUnit"
|
||||
type="text"
|
||||
:disabled="formDisabled">
|
||||
</div>
|
||||
<input
|
||||
type="button"
|
||||
class="primary"
|
||||
:value="submitLabel"
|
||||
:disabled="formDisabled || !savePossible"
|
||||
@click="generateCertificate">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@nextcloud/dialogs/styles/toast.scss'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'AdminFormSigner',
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
certificate: {
|
||||
commonName: '',
|
||||
country: '',
|
||||
organization: '',
|
||||
organizationUnit: '',
|
||||
},
|
||||
submitLabel: t('signer', 'Gerar Certificado Raiz'),
|
||||
formDisabled: false,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
savePossible() {
|
||||
return (
|
||||
this.certificate
|
||||
&& this.certificate.commonName !== ''
|
||||
&& this.certificate.country !== ''
|
||||
&& this.certificate.organization !== ''
|
||||
&& this.certificate.organizationUnit !== ''
|
||||
)
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = false
|
||||
this.loadRootCertificate()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async generateCertificate() {
|
||||
this.formDisabled = true
|
||||
this.submitLabel = 'Gerando Certificado...'
|
||||
try {
|
||||
const response = await axios.post(
|
||||
generateUrl('/apps/signer/api/0.1/admin/certificate'),
|
||||
this.certificate
|
||||
)
|
||||
|
||||
if (!response.data || response.data.message) {
|
||||
throw new Error(response.data)
|
||||
}
|
||||
this.submitLabel = 'Certificado gerado!'
|
||||
|
||||
return
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showError(t('signer', 'Não foi possivel gerar certificado'))
|
||||
this.submitLabel = 'Gerar Certificado Raiz'
|
||||
|
||||
}
|
||||
this.formDisabled = false
|
||||
},
|
||||
|
||||
async loadRootCertificate() {
|
||||
this.formDisabled = true
|
||||
try {
|
||||
const response = await axios.get(
|
||||
generateUrl('/apps/signer/api/0.1/admin/certificate'),
|
||||
)
|
||||
if (!response.data || response.data.message) {
|
||||
throw new Error(response.data)
|
||||
}
|
||||
|
||||
if (response.data.commonName
|
||||
&& response.data.country
|
||||
&& response.data.organization
|
||||
&& response.data.organizationUnit
|
||||
) {
|
||||
this.certificate = response.data
|
||||
this.submitLabel = 'Certificado gerado!'
|
||||
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this.formDisabled = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#formSigner{
|
||||
width: 60%;
|
||||
text-align: left;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.form-group > input[type='text'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
68
signer/src/App.vue
Normal file
68
signer/src/App.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<AppContent>
|
||||
<div v-if="error" class="emptycontent">
|
||||
<div class="icon icon-error" />
|
||||
<h2>{{ error }}</h2>
|
||||
</div>
|
||||
<div v-else id="content" class="app-signer">
|
||||
<h2>{{ t('signer', 'Criar nova assinatura') }}</h2>
|
||||
<FormSigner />
|
||||
</div>
|
||||
</AppContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSigner from './FormSigner'
|
||||
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
import '@nextcloud/dialogs/styles/toast.scss'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
FormSigner,
|
||||
AppContent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
error: '',
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
async mounted() {
|
||||
await this.checkRootCertificate()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async checkRootCertificate() {
|
||||
this.error = ''
|
||||
try {
|
||||
const response = await axios.get(
|
||||
generateUrl('/apps/signer/api/0.1/signature/check'),
|
||||
)
|
||||
if (!response.data || !response.data.hasRootCert) {
|
||||
this.error = t('signer', 'Certificado raiz não foi configurado pelo Administrador!')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
#content {
|
||||
width: 100vw;
|
||||
padding: 20px;
|
||||
padding-top: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
220
signer/src/FormSigner.vue
Normal file
220
signer/src/FormSigner.vue
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<div id="formSigner" class="form-signer">
|
||||
<div class="form-group">
|
||||
<label for="hosts">{{ t('signer', 'Email') }}</label>
|
||||
<input
|
||||
id="hosts"
|
||||
ref="hosts"
|
||||
v-model="signature.hosts"
|
||||
type="email"
|
||||
:disabled="updating">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="commonName">{{ t('signer', 'Nome (CN)') }}</label>
|
||||
<input
|
||||
id="commonName"
|
||||
ref="commonName"
|
||||
v-model="signature.commonName"
|
||||
type="text"
|
||||
:disabled="updating">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="country">{{ t('signer', 'País (C)') }}</label>
|
||||
<input
|
||||
id="country"
|
||||
ref="country"
|
||||
v-model="signature.country"
|
||||
type="text"
|
||||
:disabled="updating">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="organization">{{ t('signer', 'Organização (O)') }}</label>
|
||||
<input
|
||||
id="organization"
|
||||
ref="organization"
|
||||
v-model="signature.organization"
|
||||
type="text"
|
||||
:disabled="updating">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="organizationUnit">{{ t('signer', 'Unidade da organização (OU)') }}</label>
|
||||
<input
|
||||
id="organizationUnit"
|
||||
ref="organizationUnit"
|
||||
v-model="signature.organizationUnit"
|
||||
type="text"
|
||||
:disabled="updating">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">{{ t('signer', 'Senha da assinatura') }}</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="signature.password"
|
||||
type="text"
|
||||
:disabled="updating">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="path">{{ t('signer', 'Armazenamento da assinatura') }}</label>
|
||||
<div>
|
||||
<input
|
||||
id="path"
|
||||
ref="path"
|
||||
v-model="signature.path"
|
||||
type="text"
|
||||
:disabled="1">
|
||||
<button
|
||||
id="pickFromCloud"
|
||||
:class="'icon-folder'"
|
||||
:title="t('signer', 'Selecionar pasta onde assinatura será salva')"
|
||||
:disabled="updating"
|
||||
@click.stop="pickFromCloud">
|
||||
{{ t('signer', 'Selecionar Pasta') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="button"
|
||||
class="primary"
|
||||
:value="t('signer', 'Gerar Assinatura')"
|
||||
:disabled="updating || !savePossible"
|
||||
@click="saveSignature">
|
||||
<Modal
|
||||
v-if="modal"
|
||||
dark=""
|
||||
@close="closeModal">
|
||||
<div class="modal_content">
|
||||
{{ t('signer','Assinatura gerada e disponivel em ') }} {{ signature.path }} !
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@nextcloud/dialogs/styles/toast.scss'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError, getFilePickerBuilder } from '@nextcloud/dialogs'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
export default {
|
||||
name: 'FormSigner',
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
signature: {
|
||||
commonName: '',
|
||||
hosts: '',
|
||||
country: '',
|
||||
organization: '',
|
||||
organizationUnit: '',
|
||||
path: '',
|
||||
password: '',
|
||||
},
|
||||
updating: false,
|
||||
loading: true,
|
||||
modal: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
savePossible() {
|
||||
return (
|
||||
this.signature
|
||||
&& this.signature.commonName !== ''
|
||||
&& this.signature.hosts !== ''
|
||||
&& this.signature.country !== ''
|
||||
&& this.signature.organization !== ''
|
||||
&& this.signature.organizationUnit !== ''
|
||||
&& this.signature.password !== ''
|
||||
&& this.signature.path !== ''
|
||||
)
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = false
|
||||
this.$refs.hosts.focus()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveSignature() {
|
||||
this.updating = true
|
||||
try {
|
||||
const response = await axios.post(
|
||||
generateUrl('/apps/signer/api/0.1/signature/generate'),
|
||||
this.signature
|
||||
)
|
||||
if (!response.data || !response.data.signature) {
|
||||
throw new Error(response.data)
|
||||
}
|
||||
this.signature.path = response.data.signature
|
||||
this.showModal()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showError(t('signer', 'Não foi possivel criar assinatura'))
|
||||
}
|
||||
this.updating = false
|
||||
},
|
||||
showModal() {
|
||||
this.modal = true
|
||||
},
|
||||
closeModal() {
|
||||
this.modal = false
|
||||
this.signature = {
|
||||
commonName: '',
|
||||
hosts: '',
|
||||
country: '',
|
||||
organization: '',
|
||||
organizationUnit: '',
|
||||
path: '',
|
||||
password: '',
|
||||
}
|
||||
},
|
||||
pickFromCloud() {
|
||||
const picker = getFilePickerBuilder(t('signer', 'Escolha uma pasta para armazenar a assinatura'))
|
||||
.setMultiSelect(false)
|
||||
.addMimeTypeFilter('httpd/unix-directory')
|
||||
.setModal(true)
|
||||
.setType(1)
|
||||
.allowDirectories(true)
|
||||
.build()
|
||||
|
||||
picker.pick().then((path) => {
|
||||
if (!path) {
|
||||
path = '/'
|
||||
}
|
||||
this.signature.path = path
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#formSigner{
|
||||
width: 60%;
|
||||
text-align: left;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.form-group > input[type='text'],
|
||||
.form-group > input[type='email'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#path {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#pickFromCloud{
|
||||
display: inline-block;
|
||||
margin: 16px;
|
||||
background-position: 16px center;
|
||||
padding: 12px;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.modal_content {
|
||||
text-align: center;
|
||||
margin: 40px;
|
||||
}
|
||||
</style>
|
||||
44
signer/src/Settings.vue
Normal file
44
signer/src/Settings.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<SettingsSection
|
||||
:title="title"
|
||||
:description="description">
|
||||
<AdminFormSigner />
|
||||
</SettingsSection>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminFormSigner from './AdminFormSigner'
|
||||
import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
components: {
|
||||
AdminFormSigner,
|
||||
SettingsSection,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
title: t('signer', 'Assinador Digital: Dados Certificado Raiz'),
|
||||
description: t('signer', 'Para gerar novas assinaturas, é preciso primeiro gerar o ceritificado raiz'),
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
async mounted() {},
|
||||
|
||||
methods: {},
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
#signer-admin-settings {
|
||||
width: 100vw;
|
||||
padding: 20px;
|
||||
padding-top: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
166
signer/src/SignerTab.vue
Normal file
166
signer/src/SignerTab.vue
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<template>
|
||||
<AppSidebarTab
|
||||
:id="id"
|
||||
:icon="icon"
|
||||
:name="name">
|
||||
<div v-if="error" class="emptycontent">
|
||||
<div class="icon icon-error" />
|
||||
<h2>{{ error }}</h2>
|
||||
</div>
|
||||
<div v-else-if="response" class="emptycontent">
|
||||
<div class="icon icon-checkmark" />
|
||||
<h2>{{ response }}</h2>
|
||||
</div>
|
||||
<div v-else id="signerTabContent">
|
||||
<label for="path">{{ t('signer', 'Local da assinatura') }}</label>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="path"
|
||||
ref="path"
|
||||
v-model="signaturePath"
|
||||
type="text"
|
||||
:disabled="1">
|
||||
<button
|
||||
id="pickFromCloud"
|
||||
:class="'icon-folder'"
|
||||
:title="t('signer', 'Selecionar local da assinatura')"
|
||||
:disabled="updating"
|
||||
@click.stop="pickFromCloud">
|
||||
{{ t('signer', 'Selecionar Assinatura') }}
|
||||
</button>
|
||||
</div>
|
||||
<label for="password">{{ t('signer', 'Senha da assinatura') }}</label>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
:disabled="updating">
|
||||
</div>
|
||||
<input
|
||||
type="button"
|
||||
class="primary"
|
||||
:value="t('signer', 'Assinar Documento')"
|
||||
:disabled="updating || !savePossible"
|
||||
@click="sign">
|
||||
</div>
|
||||
</AppSidebarTab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppSidebarTab from '@nextcloud/vue/dist/Components/AppSidebarTab'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
import { getFilePickerBuilder } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'SignerTab',
|
||||
|
||||
components: {
|
||||
AppSidebarTab,
|
||||
},
|
||||
mixins: [],
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
signaturePath: '',
|
||||
password: '',
|
||||
response: '',
|
||||
icon: 'icon-rename',
|
||||
updating: false,
|
||||
loading: true,
|
||||
name: t('signer', 'Assinar Documento'),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
id() {
|
||||
return 'signerTab'
|
||||
},
|
||||
activeTab() {
|
||||
return this.$parent.activeTab
|
||||
},
|
||||
savePossible() {
|
||||
return (
|
||||
this.password !== ''
|
||||
&& this.signaturePath !== ''
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async sign() {
|
||||
this.updating = true
|
||||
this.response = ''
|
||||
this.error = ''
|
||||
try {
|
||||
const response = await axios.post(
|
||||
generateUrl('/apps/signer/api/0.1/sign'),
|
||||
{
|
||||
inputFilePath: OC.joinPaths(this.fileInfo.get('path'), this.fileInfo.get('name')),
|
||||
outputFolderPath: this.fileInfo.get('path'),
|
||||
certificatePath: this.signaturePath,
|
||||
password: this.password,
|
||||
}
|
||||
)
|
||||
if (!response.data || !response.data.fileSigned) {
|
||||
throw new Error(response.data)
|
||||
}
|
||||
this.response = t('signer', 'Documento assinado disponivel em ') + response.data.fileSigned
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = t('signer', 'Não foi possivel assinar documento!')
|
||||
}
|
||||
this.updating = false
|
||||
},
|
||||
|
||||
pickFromCloud() {
|
||||
const picker = getFilePickerBuilder(t('signer', 'Escolha o local da assinatura'))
|
||||
.setMultiSelect(false)
|
||||
.addMimeTypeFilter('application/octet-stream')
|
||||
.setModal(true)
|
||||
.setType(1)
|
||||
.allowDirectories(false)
|
||||
.build()
|
||||
|
||||
picker.pick().then((path) => {
|
||||
this.signaturePath = path
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
#signerTabContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group > input {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.form-group > input[type='button'] {
|
||||
width: 80%;
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
#pickFromCloud{
|
||||
display: inline-block;
|
||||
background-position: 16px center;
|
||||
padding: 12px;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
</style>
|
||||
9
signer/src/main.js
Normal file
9
signer/src/main.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App'
|
||||
|
||||
Vue.mixin({ methods: { t, n } })
|
||||
|
||||
export default new Vue({
|
||||
el: '#content',
|
||||
render: h => h(App),
|
||||
})
|
||||
9
signer/src/settings.js
Normal file
9
signer/src/settings.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import Vue from 'vue'
|
||||
import Settings from './Settings'
|
||||
|
||||
Vue.mixin({ methods: { t, n } })
|
||||
|
||||
export default new Vue({
|
||||
el: '#signer-admin-settings',
|
||||
render: h => h(Settings),
|
||||
})
|
||||
14
signer/src/tab.js
Normal file
14
signer/src/tab.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import SignerTab from './SignerTab'
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (OCA.Files && OCA.Files.Sidebar) {
|
||||
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab('signer', SignerTab, (fileInfo) => {
|
||||
if (!fileInfo || fileInfo.isDirectory()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const mimetype = fileInfo.get('mimetype') || ''
|
||||
return mimetype === 'application/pdf'
|
||||
}))
|
||||
}
|
||||
})
|
||||
32
signer/stylelint.config.js
Normal file
32
signer/stylelint.config.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
module.exports = {
|
||||
extends: 'stylelint-config-recommended-scss',
|
||||
rules: {
|
||||
indentation: 'tab',
|
||||
'selector-type-no-unknown': null,
|
||||
'number-leading-zero': null,
|
||||
'rule-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
ignore: ['after-comment', 'inside-block'],
|
||||
},
|
||||
],
|
||||
'declaration-empty-line-before': [
|
||||
'never',
|
||||
{
|
||||
ignore: ['after-declaration'],
|
||||
},
|
||||
],
|
||||
'comment-empty-line-before': null,
|
||||
'selector-type-case': null,
|
||||
'selector-list-comma-newline-after': null,
|
||||
'no-descending-specificity': null,
|
||||
'string-quotes': 'single',
|
||||
'selector-pseudo-element-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignorePseudoElements: ['v-deep'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['stylelint-scss'],
|
||||
}
|
||||
1
signer/templates/main.php
Normal file
1
signer/templates/main.php
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div id="content"></div>
|
||||
5
signer/templates/settings.php
Normal file
5
signer/templates/settings.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
script('signer', 'signer-settings');
|
||||
?>
|
||||
<div id="signer-admin-settings" class="section">
|
||||
</div>
|
||||
121
signer/tests/Unit/Controller/SignatureControllerTest.php
Normal file
121
signer/tests/Unit/Controller/SignatureControllerTest.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Tests\Unit\Controller;
|
||||
|
||||
use OCA\Signer\Controller\SignatureController;
|
||||
use OCA\Signer\Service\SignatureService;
|
||||
use OCP\IRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class SignatureControllerTest extends TestCase
|
||||
{
|
||||
public function testGenerate()
|
||||
{
|
||||
$userId = 'john';
|
||||
$request = $this->prophesize(IRequest::class);
|
||||
$service = $this->prophesize(SignatureService::class);
|
||||
|
||||
$commonName = 'someCommonName';
|
||||
$hosts = 'someHosts';
|
||||
$country = 'someCountry';
|
||||
$organization = 'someOrganization';
|
||||
$organizationUnit = 'someOrganizationUnit';
|
||||
$path = '/path/to/somePath';
|
||||
$password = 'somePassword';
|
||||
|
||||
$service->generate(
|
||||
$commonName,
|
||||
[$hosts],
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$path,
|
||||
$password
|
||||
)
|
||||
->shouldBeCalled()
|
||||
->willReturn('/path/to/someSignature')
|
||||
;
|
||||
|
||||
$controller = new SignatureController(
|
||||
$request->reveal(),
|
||||
$service->reveal(),
|
||||
$userId
|
||||
);
|
||||
|
||||
$result = $controller->generate(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$path,
|
||||
$password
|
||||
);
|
||||
|
||||
static::assertSame(['signature' => '/path/to/someSignature'], $result->getData());
|
||||
}
|
||||
|
||||
public function failParameterMissingProvider()
|
||||
{
|
||||
$commonName = 'someCommonName';
|
||||
$hosts = 'someHosts';
|
||||
$country = 'someCountry';
|
||||
$organization = 'someOrganization';
|
||||
$organizationUnit = 'someOrganizationUnit';
|
||||
$path = '/path/to/somePath';
|
||||
$password = 'somePassword';
|
||||
|
||||
return [
|
||||
[null, $hosts, $country, $organization, $organizationUnit, $path, $password, 'commonName'],
|
||||
[$commonName, null, $country, $organization, $organizationUnit, $path, $password, 'hosts'],
|
||||
[$commonName, $hosts, null, $organization, $organizationUnit, $path, $password, 'country'],
|
||||
[$commonName, $hosts, $country, null, $organizationUnit, $path, $password, 'organization'],
|
||||
[$commonName, $hosts, $country, $organization, null, $path, $password, 'organizationUnit'],
|
||||
[$commonName, $hosts, $country, $organization, $organizationUnit, null, $password, 'path'],
|
||||
[$commonName, $hosts, $country, $organization, $organizationUnit, $path, null, 'password'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider failParameterMissingProvider */
|
||||
public function testGenerateFailParameterMissing(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$path,
|
||||
$password,
|
||||
$paramenterMissing
|
||||
) {
|
||||
$userId = 'john';
|
||||
$request = $this->prophesize(IRequest::class);
|
||||
$service = $this->prophesize(SignatureService::class);
|
||||
|
||||
$service->generate(\Prophecy\Argument::cetera())
|
||||
->shouldNotBeCalled()
|
||||
;
|
||||
|
||||
$controller = new SignatureController(
|
||||
$request->reveal(),
|
||||
$service->reveal(),
|
||||
$userId
|
||||
);
|
||||
|
||||
$result = $controller->generate(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$path,
|
||||
$password,
|
||||
);
|
||||
|
||||
static::assertSame(['message' => "parameter '{$paramenterMissing}' is required!"], $result->getData());
|
||||
static::assertSame(400, $result->getStatus());
|
||||
}
|
||||
}
|
||||
87
signer/tests/Unit/Controller/SignerControllerTest.php
Normal file
87
signer/tests/Unit/Controller/SignerControllerTest.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Tests\Unit\Controller;
|
||||
|
||||
use OC\Files\Node\File;
|
||||
use OCA\Signer\Controller\SignerController;
|
||||
use OCA\Signer\Service\SignerService;
|
||||
use OCP\IRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class SignerControllerTest extends TestCase
|
||||
{
|
||||
public function testSignFile()
|
||||
{
|
||||
$userId = 'john';
|
||||
$request = $this->prophesize(IRequest::class);
|
||||
$service = $this->prophesize(SignerService::class);
|
||||
$file = $this->prophesize(File::class);
|
||||
$file->getInternalPath()->willReturn("/path/to/someFileSigned");
|
||||
|
||||
$inputFilePath = '/path/to/someInputFilePath';
|
||||
$outputFolderPath = '/path/to/someOutputFolderPath';
|
||||
$certificatePath = '/path/to/someCertificatePath';
|
||||
$password = 'somePassword';
|
||||
|
||||
$service->sign($inputFilePath, $outputFolderPath, $certificatePath, $password)
|
||||
->shouldBeCalled()
|
||||
->willReturn($file->reveal())
|
||||
;
|
||||
|
||||
$controller = new SignerController(
|
||||
$request->reveal(),
|
||||
$service->reveal(),
|
||||
$userId
|
||||
);
|
||||
|
||||
$result = $controller->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
|
||||
|
||||
static::assertSame(['fileSigned' => '/path/to/someFileSigned'], $result->getData());
|
||||
}
|
||||
|
||||
public function failParameterMissingProvider()
|
||||
{
|
||||
$inputFilePath = '/path/to/someInputFilePath';
|
||||
$outputFolderPath = '/path/to/someOutputFolderPath';
|
||||
$certificatePath = '/path/to/someCertificatePath';
|
||||
$password = 'somePassword';
|
||||
|
||||
return [
|
||||
[null, $outputFolderPath, $certificatePath, $password, 'inputFilePath'],
|
||||
[$inputFilePath, null, $certificatePath, $password, 'outputFolderPath'],
|
||||
[$inputFilePath, $outputFolderPath, null, $password, 'certificatePath'],
|
||||
[$inputFilePath, $outputFolderPath, $certificatePath, null, 'password'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider failParameterMissingProvider */
|
||||
public function testSignFileFailParameterMissing(
|
||||
$inputFilePath,
|
||||
$outputFolderPath,
|
||||
$certificatePath,
|
||||
$password,
|
||||
$paramenterMissing
|
||||
){
|
||||
$userId = 'john';
|
||||
$request = $this->prophesize(IRequest::class);
|
||||
$service = $this->prophesize(SignerService::class);
|
||||
|
||||
$service->sign(\Prophecy\Argument::cetera())
|
||||
->shouldNotBeCalled();
|
||||
|
||||
$controller = new SignerController(
|
||||
$request->reveal(),
|
||||
$service->reveal(),
|
||||
$userId
|
||||
);
|
||||
|
||||
$result = $controller->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
|
||||
|
||||
static::assertSame(['message' => "parameter '{$paramenterMissing}' is required!"], $result->getData());
|
||||
static::assertSame(400, $result->getStatus());
|
||||
}
|
||||
}
|
||||
62
signer/tests/Unit/Service/SignatureServiceTest.php
Normal file
62
signer/tests/Unit/Service/SignatureServiceTest.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Tests\Unit\Service;
|
||||
|
||||
use OCA\Signer\Handler\CfsslHandler;
|
||||
use OCA\Signer\Service\SignatureService;
|
||||
use OCA\Signer\Storage\ClientStorage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class SignatureServiceTest extends TestCase
|
||||
{
|
||||
public function testGenerate()
|
||||
{
|
||||
$commonName = 'someCommonName';
|
||||
$hosts = ['someHosts'];
|
||||
$country = 'someCountry';
|
||||
$organization = 'someOrganization';
|
||||
$organizationUnit = 'someOrganizationUnit';
|
||||
$path = '/path/to/somePath';
|
||||
$password = 'somePassword';
|
||||
|
||||
$content = 'someContent';
|
||||
|
||||
$cfsslHandler = $this->prophesize(CfsslHandler::class);
|
||||
$clientStorage = $this->prophesize(ClientStorage::class);
|
||||
$cfsslHandler
|
||||
->generateCertificate(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$password
|
||||
)
|
||||
->shouldBeCalled()
|
||||
->willReturn($content)
|
||||
;
|
||||
|
||||
$clientStorage->createFolder($path)
|
||||
->shouldBeCalled()
|
||||
;
|
||||
$clientStorage->saveFile($commonName.'.pfx', $content, \Prophecy\Argument::any())
|
||||
->shouldBeCalled()
|
||||
;
|
||||
|
||||
$service = new SignatureService($cfsslHandler->reveal(), $clientStorage->reveal());
|
||||
|
||||
$service->generate(
|
||||
$commonName,
|
||||
$hosts,
|
||||
$country,
|
||||
$organization,
|
||||
$organizationUnit,
|
||||
$path,
|
||||
$password
|
||||
);
|
||||
}
|
||||
}
|
||||
52
signer/tests/Unit/Service/SignerServiceTest.php
Normal file
52
signer/tests/Unit/Service/SignerServiceTest.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Signer\Tests\Unit\Service;
|
||||
|
||||
use OCA\Signer\Handler\JSignerHandler;
|
||||
use OCA\Signer\Service\SignerService;
|
||||
use OCA\Signer\Storage\ClientStorage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class SignerServiceTest extends TestCase
|
||||
{
|
||||
public function testSignFile()
|
||||
{
|
||||
$inputFilePath = '/path/to/someInputFilePath';
|
||||
$outputFolderPath = '/path/to/someOutputFolderPath';
|
||||
$certificatePath = '/path/to/someCertificatePath';
|
||||
$password = 'somePassword';
|
||||
$signerHandler = $this->prophesize(JSignerHandler::class);
|
||||
$clientStorage = $this->prophesize(ClientStorage::class);
|
||||
|
||||
$filename = 'someFilename';
|
||||
$content = 'someContent';
|
||||
|
||||
$clientStorage->getFile($inputFilePath)
|
||||
->shouldBeCalled()
|
||||
;
|
||||
|
||||
$clientStorage->getFile($certificatePath)
|
||||
->shouldBeCalled()
|
||||
;
|
||||
|
||||
$signerHandler->signExistingFile(\Prophecy\Argument::any(), \Prophecy\Argument::any(), $password)
|
||||
->shouldBeCalled()
|
||||
->willReturn([$filename, $content])
|
||||
;
|
||||
|
||||
$clientStorage->createFolder($outputFolderPath)
|
||||
->shouldBeCalled()
|
||||
;
|
||||
|
||||
$clientStorage->saveFile($filename, $content, \Prophecy\Argument::any())
|
||||
->shouldBeCalled()
|
||||
;
|
||||
|
||||
$service = new SignerService($signerHandler->reveal(), $clientStorage->reveal());
|
||||
$service->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
|
||||
}
|
||||
}
|
||||
7
signer/tests/bootstrap.php
Normal file
7
signer/tests/bootstrap.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__.'/../../../lib/base.php';
|
||||
require_once __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
\OC::$loader->addValidRoot(OC::$SERVERROOT . '/tests');
|
||||
\OC_App::loadApp('signer');
|
||||
12
signer/webpack.js
Normal file
12
signer/webpack.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const { merge } = require('webpack-merge')
|
||||
const path = require('path')
|
||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
tab: path.resolve(path.join('src', 'tab.js')),
|
||||
settings: path.resolve(path.join('src', 'settings.js')),
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = merge(config, webpackConfig)
|
||||
Loading…
Add table
Reference in a new issue