Merge branch 'polishing'

This commit is contained in:
alonso.torres 2020-11-05 10:19:01 +01:00
commit a947a53aa2
485 changed files with 441456 additions and 37347 deletions

View File

@ -1,14 +1,14 @@
# Contributing Guide #
Thank you for your interest in contributing to UXBox. This is a
generic guide that details how to contribute to UXBox in a way that is
Thank you for your interest in contributing to Penpot. This is a
generic guide that details how to contribute to Penpot in a way that is
efficient for everyone. If you want a specific documentation for
different parts of the platform, please refer to `docs/` directory.
## Reporting Bugs ##
We are using [GitHub Issues](https://github.com/uxbox/uxbox/issues)
We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
for our public bugs. We keep a close eye on this and try to make it
clear when we have an internal fix in progress. Before filing a new
task, try to make sure your problem doesn't already exist.

View File

@ -6,14 +6,14 @@
[![Managed with Taiga.io](https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg)](https://tree.taiga.io/project/uxbox/ "Managed with Taiga.io")
# UXBOX #
# PENPOT #
![UXBOX](https://piweek.com/images/projects/uxbox.jpg)
![PENPOT](https://raw.githubusercontent.com/penpot/penpot/develop/docs/screenshot.png)
## Introduction ##
The open-source solution for design and prototyping. UXBOX is
The open-source solution for design and prototyping. PENPOT is
currently at an early development stage but we are working hard to
bring you the beta version as soon as possible. Follow the project
progress in Twitter or Github and stay tuned!
@ -23,8 +23,8 @@ progress in Twitter or Github and stay tuned!
## SVG based ##
UXBOX works with SVG, a standard format, for all your designs and
prototypes . This means that all your stuff in UXBOX is portable and
Penpot works with SVG, a standard format, for all your designs and
prototypes . This means that all your stuff in Penpot is portable and
editable in many other vector tools and easy to use on the web.
@ -34,7 +34,7 @@ editable in many other vector tools and easy to use on the web.
We love the open source software community. Contributing is our
passion and because of this, we'll be glad if you want to participate
and improve UXBOX. All your awesome ideas and code are welcome!
and improve Penpot. All your awesome ideas and code are welcome!
Please refer to the [Contributing Guide](./CONTRIBUTING.md)

View File

@ -0,0 +1,59 @@
<mjml>
<mj-head>
<mj-font name="Source Sans Pro" href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" />
<mj-attributes>
<mj-text font-family="Source Sans Pro, sans-serif" font-size="16px" color="#000000" line-height="150%" />
<mj-button background-color="#31EFB8" color="#1F1F1F" font-family="Source Sans Pro, sans-serif" font-size="16px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E5E5E5">
<mj-section padding="0">
<mj-column>
<mj-image src="{{ public-uri }}/images/email/uxbox-title.png"
width="97px" height="32px" align="left" padding="16px" />
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF">
<mj-column>
<mj-text font-size="24px" font-weight="600">Hello!</mj-text>
<mj-text>
{{invited-by}} has invited you to join the team “{{ team }}”.
</mj-text>
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
Accept invite
</mj-button>
<mj-text>Enjoy!</mj-text>
<mj-text>The UXBOX team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-social icon-size="24px" mode="horizontal">
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://uxbox.io/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpot" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/uxbox/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/uxbox/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
UXBOX © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>
</mg-body>
</mjml>

View File

@ -0,0 +1,468 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">{{invited-by}} has invited you to join the team “{{ team }}”.</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Accept invite </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The UXBOX team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
<![endif]-->
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://uxbox.io/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://twitter.com/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/uxbox/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://instagram.com/uxbox/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX © 2020 | Made with &lt;3 and Open Source</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div>
</body>
</html>

View File

@ -0,0 +1 @@
Inviation to join {{team}}

View File

@ -0,0 +1,10 @@
Hello!
{{invited-by}} has invited you to join the team “{{ team }}”.
Accept invitation using this link:
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The UXBOX team.

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
CLASSPATH=`(clojure -Spath)`
NEWCP="./resources:./app.jar"
NEWCP="./resources:./main:./common"
rm -rf ./target/dist
mkdir -p ./target/dist/deps
@ -15,10 +15,9 @@ for item in $(echo $CLASSPATH | tr ":" "\n"); do
done
cp ./resources/log4j2-bundle.xml ./target/dist/log4j2.xml
cp -r ./src ./target/dist/main
cp -r ../common ./target/dist/common
clojure -Ajar
cp ./target/app.jar ./target/dist/app.jar
echo $NEWCP > ./target/dist/classpath;
tee -a ./target/dist/run.sh >> /dev/null <<EOF
@ -47,10 +46,7 @@ if [ -f ./environ ]; then
fi
set -x
\$JAVA_CMD \$JVM_OPTS -classpath \$CP -Dlog4j.configurationFile=./log4j2.xml "\$@" clojure.main -m app.main
exec \$JAVA_CMD \$JVM_OPTS -classpath \$CP -Dlog4j.configurationFile=./log4j2.xml "\$@" clojure.main -m app.main
EOF
chmod +x ./target/dist/run.sh

View File

@ -24,7 +24,6 @@
[app.common.uuid :as uuid]
[app.services.mutations.projects :as projects]
[app.services.mutations.files :as files]
[app.services.mutations.colors :as colors]
[app.services.mutations.media :as media])
(:import
java.io.PushbackReader))

View File

@ -2,12 +2,22 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.db
(:require
[clojure.spec.alpha :as s]
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.config :as cfg]
[app.metrics :as mtx]
[app.util.data :as data]
[app.util.time :as dt]
[app.util.transit :as t]
[clojure.data.json :as json]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[clojure.tools.logging :as log]
[lambdaisland.uri :refer [uri]]
@ -17,19 +27,17 @@
[next.jdbc.optional :as jdbc-opt]
[next.jdbc.result-set :as jdbc-rs]
[next.jdbc.sql :as jdbc-sql]
[next.jdbc.sql.builder :as jdbc-bld]
[app.common.exceptions :as ex]
[app.config :as cfg]
[app.metrics :as mtx]
[app.util.time :as dt]
[app.util.transit :as t]
[app.util.data :as data])
[next.jdbc.sql.builder :as jdbc-bld])
(:import
org.postgresql.util.PGobject
org.postgresql.util.PGInterval
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
com.zaxxer.hikari.HikariConfig
com.zaxxer.hikari.HikariDataSource))
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
java.sql.Connection
java.sql.Savepoint
org.postgresql.jdbc.PgArray
org.postgresql.geometric.PGpoint
org.postgresql.util.PGInterval
org.postgresql.util.PGobject))
(def initsql
(str "SET statement_timeout = 10000;\n"
@ -158,10 +166,42 @@
[v]
(instance? PGInterval v))
(defn pgpoint?
[v]
(instance? PGpoint v))
(defn pgarray?
[v]
(instance? PgArray v))
(defn pgarray-of-uuid?
[v]
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
(defn pgpoint
[p]
(PGpoint. (:x p) (:y p)))
(defn decode-pgpoint
[^PGpoint v]
(gpt/point (.-x v) (.-y v)))
(defn pginterval
[data]
(org.postgresql.util.PGInterval. ^String data))
(defn savepoint
([^Connection conn]
(.setSavepoint conn))
([^Connection conn label]
(.setSavepoint conn (name label))))
(defn rollback!
([^Connection conn]
(.rollback conn))
([^Connection conn ^Savepoint sp]
(.rollback conn sp)))
(defn interval
[data]
(cond
@ -222,6 +262,14 @@
(.setType "jsonb")
(.setValue (json/write-str data))))
(defn pgarray->set
[v]
(set (.getArray ^PgArray v)))
(defn pgarray->vector
[v]
(vec (.getArray ^PgArray v)))
;; Instrumentation
(mtx/instrument-with-counter!

View File

@ -74,3 +74,16 @@
(def change-email
"Password change confirmation email"
(emails/build ::change-email default-context))
(s/def :internal.emails.invite-to-team/invited-by ::us/string)
(s/def :internal.emails.invite-to-team/team ::us/string)
(s/def :internal.emails.invite-to-team/token ::us/string)
(s/def ::invite-to-team
(s/keys :keys [:internal.emails.invite-to-team/invited-by
:internal.emails.invite-to-team/token
:internal.emails.invite-to-team/team]))
(def invite-to-team
"Teams member invitation email."
(emails/build ::invite-to-team default-context))

View File

@ -20,7 +20,7 @@
#{:create-demo-profile
:logout
:profile
:verify-profile-token
:verify-token
:recover-profile
:register-profile
:request-profile-recovery
@ -34,8 +34,7 @@
{::sq/type type})
data (cond-> data
(:profile-id req) (assoc :profile-id (:profile-id req)))]
(if (or (:profile-id req)
(contains? unauthorized-services type))
(if (or (:profile-id req) (contains? unauthorized-services type))
{:status 200
:body (sq/handle (with-meta data {:req req}))}
{:status 403
@ -51,18 +50,14 @@
{::sm/type type})
data (cond-> data
(:profile-id req) (assoc :profile-id (:profile-id req)))]
(if (or (:profile-id req)
(contains? unauthorized-services type))
(let [body (sm/handle (with-meta data {:req req}))]
(if (= type :delete-profile)
(do
(some-> (session/extract-auth-token req)
(session/delete))
{:status 204
:cookies (session/cookies "" {:max-age -1})
:body ""})
{:status 200
:body body}))
(if (or (:profile-id req) (contains? unauthorized-services type))
(let [result (sm/handle (with-meta data {:req req}))
mdata (meta result)
resp {:status (if (nil? (seq result)) 204 200)
:body result}]
(cond->> resp
(:transform-response mdata) ((:transform-response mdata) req)))
{:status 403
:body {:type :authentication
:code :unauthorized}})))

View File

@ -7,6 +7,8 @@
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; TODO: move to services.
(ns app.http.session
(:require
[app.db :as db]

View File

@ -10,19 +10,19 @@
(ns app.media
"Media postprocessing."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.spec :as us]
[app.config :as cfg]
[app.media-storage :as mst]
[app.util.http :as http]
[app.util.storage :as ust]
[clojure.core.async :as a]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[datoteka.core :as fs]
[mount.core :refer [defstate]]
[app.config :as cfg]
[app.common.data :as d]
[app.common.media :as cm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.media-storage :as mst]
[app.util.storage :as ust]
[app.util.http :as http])
[mount.core :refer [defstate]])
(:import
java.io.ByteArrayInputStream
java.io.InputStream
@ -34,6 +34,21 @@
(defstate semaphore
:start (Semaphore. (:image-process-max-threads cfg/config 1)))
;; --- Generic specs
(s/def :internal.http.upload/filename ::us/string)
(s/def :internal.http.upload/size ::us/integer)
(s/def :internal.http.upload/content-type cm/valid-media-types)
(s/def :internal.http.upload/tempfile any?)
(s/def ::upload
(s/keys :req-un [:internal.http.upload/filename
:internal.http.upload/size
:internal.http.upload/tempfile
:internal.http.upload/content-type]))
;; --- Thumbnails Generation
(s/def ::cmd keyword?)

View File

@ -98,6 +98,18 @@
{:name "0027-mod-file-table-ignore-sync"
:fn (mg/resource "app/migrations/sql/0027-mod-file-table-ignore-sync.sql")}
{:name "0028-add-team-project-profile-rel-table"
:fn (mg/resource "app/migrations/sql/0028-add-team-project-profile-rel-table.sql")}
{:name "0029-del-project-profile-rel-indexes"
:fn (mg/resource "app/migrations/sql/0029-del-project-profile-rel-indexes.sql")}
{:name "0030-mod-file-table-add-missing-index"
:fn (mg/resource "app/migrations/sql/0030-mod-file-table-add-missing-index.sql")}
{:name "0031-add-conversation-related-tables"
:fn (mg/resource "app/migrations/sql/0031-add-conversation-related-tables.sql")}
]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -78,7 +78,6 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
true);
CREATE TABLE team_profile_rel (
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE RESTRICT,

View File

@ -28,9 +28,6 @@ CREATE TABLE project_profile_rel (
PRIMARY KEY (profile_id, project_id)
);
COMMENT ON TABLE project_profile_rel
IS 'Relation between projects and profiles (NM)';
CREATE INDEX project_profile_rel__profile_id__idx
ON project_profile_rel(profile_id);

View File

@ -0,0 +1,12 @@
CREATE TABLE team_project_profile_rel (
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
is_pinned boolean NOT NULL DEFAULT false,
PRIMARY KEY (team_id, profile_id, project_id)
);

View File

@ -0,0 +1,4 @@
--- Drop duplicate indexes
DROP INDEX IF EXISTS project_profile_rel__project_id__idx;
DROP INDEX IF EXISTS project_profile_rel__profile_id__idx;

View File

@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS file__project_id__idx ON file (project_id);

View File

@ -0,0 +1,48 @@
CREATE TABLE comment_thread (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
owner_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
page_id uuid NOT NULL,
participants jsonb NOT NULL,
seqn integer NOT NULL DEFAULT 0,
position point NOT NULL,
is_resolved boolean NOT NULL DEFAULT false
);
CREATE INDEX comment_thread__owner_id__idx ON comment_thread(owner_id);
CREATE UNIQUE INDEX comment_thread__file_id__seqn__idx ON comment_thread(file_id, seqn);
CREATE TABLE comment_thread_status (
thread_id uuid NOT NULL REFERENCES comment_thread(id) ON DELETE CASCADE,
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (thread_id, profile_id)
);
CREATE TABLE comment (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
thread_id uuid NOT NULL REFERENCES comment_thread(id) ON DELETE CASCADE,
owner_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
content text NOT NULL
);
CREATE INDEX comment__thread_id__idx ON comment(thread_id);
CREATE INDEX comment__owner_id__idx ON comment(owner_id);
ALTER TABLE file ADD COLUMN comment_thread_seqn integer DEFAULT 0;

View File

@ -15,9 +15,9 @@
(defn- load-query-services
[]
(require 'app.services.queries.media)
(require 'app.services.queries.colors)
(require 'app.services.queries.projects)
(require 'app.services.queries.files)
(require 'app.services.queries.comments)
(require 'app.services.queries.profile)
(require 'app.services.queries.recent-files)
(require 'app.services.queries.viewer))
@ -26,11 +26,12 @@
[]
(require 'app.services.mutations.demo)
(require 'app.services.mutations.media)
(require 'app.services.mutations.colors)
(require 'app.services.mutations.projects)
(require 'app.services.mutations.files)
(require 'app.services.mutations.comments)
(require 'app.services.mutations.profile)
(require 'app.services.mutations.viewer))
(require 'app.services.mutations.viewer)
(require 'app.services.mutations.verify-token))
(defstate query-services
:start (load-query-services))

View File

@ -1,150 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.colors
(:require
[clojure.spec.alpha :as s]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.services.mutations :as sm]
[app.services.queries.teams :as teams]
[app.tasks :as tasks]
[app.util.time :as dt]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::library-id ::us/uuid)
(s/def ::content ::us/string)
;; --- Mutation: Create Color
(declare select-file-for-update)
(declare create-color)
(s/def ::create-color
(s/keys :req-un [::profile-id ::name ::content ::file-id]
:opt-un [::id]))
(sm/defmutation ::create-color
[{:keys [profile-id file-id] :as params}]
(db/with-atomic [conn db/pool]
(let [file (select-file-for-update conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(create-color conn params))))
(def ^:private sql:create-color
"insert into color (id, name, file_id, content)
values ($1, $2, $3, $4) returning *")
(defn create-color
[conn {:keys [id name file-id content]}]
(let [id (or id (uuid/next))]
(db/insert! conn :color {:id id
:name name
:file-id file-id
:content content})))
(def ^:private sql:select-file-for-update
"select file.*,
project.team_id as team_id
from file
inner join project on (project.id = file.project_id)
where file.id = ?
for update of file")
(defn- select-file-for-update
[conn id]
(let [row (db/exec-one! conn [sql:select-file-for-update id])]
(when-not row
(ex/raise :type :not-found))
row))
;; --- Mutation: Rename Color
(declare select-color-for-update)
(s/def ::rename-color
(s/keys :req-un [::id ::profile-id ::name]))
(sm/defmutation ::rename-color
[{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool]
(let [clr (select-color-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id clr))
(db/update! conn :color
{:name name}
{:id id}))))
(def ^:private sql:select-color-for-update
"select c.*,
p.team_id as team_id
from color as c
inner join file as f on f.id = c.file_id
inner join project as p on p.id = f.project_id
where c.id = ?
for update of c")
(defn- select-color-for-update
[conn id]
(let [row (db/exec-one! conn [sql:select-color-for-update id])]
(when-not row
(ex/raise :type :not-found))
row))
;; --- Mutation: Update Color
(s/def ::update-color
(s/keys :req-un [::profile-id ::id ::content]))
(sm/defmutation ::update-color
[{:keys [profile-id id content] :as params}]
(db/with-atomic [conn db/pool]
(let [clr (select-color-for-update conn id)
;; IMPORTANT: if the previous name was equal to the hex content,
;; we must rename it in addition to changing the value.
new-name (if (= (:name clr) (:content clr))
content
(:name clr))]
(teams/check-edition-permissions! conn profile-id (:team-id clr))
(db/update! conn :color
{:name new-name
:content content}
{:id id}))))
;; --- Delete Color
(declare delete-color)
(s/def ::delete-color
(s/keys :req-un [::id ::profile-id]))
(sm/defmutation ::delete-color
[{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool]
(let [clr (select-color-for-update conn id)]
(teams/check-edition-permissions! conn profile-id (:team-id clr))
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"
:delay cfg/default-deletion-delay
:props {:id id :type :color}})
(db/update! conn :color
{:deleted-at (dt/now)}
{:id id})
nil)))

View File

@ -0,0 +1,260 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.comments
(:require
[clojure.spec.alpha :as s]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.services.mutations :as sm]
[app.services.queries.projects :as proj]
[app.services.queries.files :as files]
[app.services.queries.comments :as comments]
[app.tasks :as tasks]
[app.util.blob :as blob]
[app.util.storage :as ust]
[app.util.transit :as t]
[app.util.time :as dt]))
;; --- Mutation: Create Comment Thread
(declare upsert-comment-thread-status!)
(declare create-comment-thread)
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::position ::us/point)
(s/def ::content ::us/string)
(s/def ::page-id ::us/uuid)
(s/def ::create-comment-thread
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
(sm/defmutation ::create-comment-thread
[{:keys [profile-id file-id] :as params}]
(db/with-atomic [conn db/pool]
(files/check-read-permissions! conn profile-id file-id)
(create-comment-thread conn params)))
(defn- retrieve-next-seqn
[conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
res (db/exec-one! conn [sql file-id])]
(:next-seqn res)))
(defn- create-comment-thread*
[conn {:keys [profile-id file-id page-id position content] :as params}]
(let [seqn (retrieve-next-seqn conn file-id)
now (dt/now)
thread (db/insert! conn :comment-thread
{:file-id file-id
:owner-id profile-id
:participants (db/tjson #{profile-id})
:page-id page-id
:created-at now
:modified-at now
:seqn seqn
:position (db/pgpoint position)})
;; Create a comment entry
comment (db/insert! conn :comment
{:thread-id (:id thread)
:owner-id profile-id
:created-at now
:modified-at now
:content content})]
;; Make the current thread as read.
(upsert-comment-thread-status! conn profile-id (:id thread))
;; Optimistic update of current seq number on file.
(db/update! conn :file
{:comment-thread-seqn seqn}
{:id file-id})
(-> (assoc thread
:content content
:comment comment)
(comments/decode-row))))
(defn- create-comment-thread
[conn params]
(loop [sp (db/savepoint conn)
rc 0]
(let [res (ex/try (create-comment-thread* conn params))]
(cond
(and (instance? Throwable res)
(< rc 3))
(do
(db/rollback! conn sp)
(recur (db/savepoint conn)
(inc rc)))
(instance? Throwable res)
(throw res)
:else res))))
;; --- Mutation: Update Comment Thread Status
(s/def ::id ::us/uuid)
(s/def ::update-comment-thread-status
(s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::update-comment-thread-status
[{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not cthr
(ex/raise :type :not-found))
(files/check-read-permissions! conn profile-id (:file-id cthr))
(upsert-comment-thread-status! conn profile-id (:id cthr)))))
(def sql:upsert-comment-thread-status
"insert into comment_thread_status (thread_id, profile_id)
values (?, ?)
on conflict (thread_id, profile_id)
do update set modified_at = clock_timestamp()
returning modified_at;")
(defn- upsert-comment-thread-status!
[conn profile-id thread-id]
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id]))
;; --- Mutation: Update Comment Thread
(s/def ::is-resolved ::us/boolean)
(s/def ::update-comment-thread
(s/keys :req-un [::profile-id ::id ::is-resolved]))
(sm/defmutation ::update-comment-thread
[{:keys [profile-id id is-resolved] :as params}]
(db/with-atomic [conn db/pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not thread
(ex/raise :type :not-found)
(files/check-read-permissions! conn profile-id (:file-id thread))
(db/update! conn :comment-thread
{:is-resolved is-resolved}
{:id id})
nil))))
;; --- Mutation: Add Comment
(s/def ::add-comment
(s/keys :req-un [::profile-id ::thread-id ::content]))
(sm/defmutation ::add-comment
[{:keys [profile-id thread-id content] :as params}]
(db/with-atomic [conn db/pool]
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
(comments/decode-row))]
;; Standard Checks
(when-not thread
(ex/raise :type :not-found))
(files/check-read-permissions! conn profile-id (:file-id thread))
;; NOTE: is important that all timestamptz related fields are
;; created or updated on the database level for avoid clock
;; inconsistencies (some user sees something read that is not
;; read, etc...)
(let [ppants (:participants thread #{})
comment (db/insert! conn :comment
{:thread-id thread-id
:owner-id profile-id
:content content})]
;; NOTE: this is done in SQL instead of using db/update!
;; helper bacause currently the helper does not allow pass raw
;; function call parameters to the underlying prepared
;; statement; in a future when we fix/improve it, this can be
;; changed to use the helper.
;; Update thread modified-at attribute and assoc the current
;; profile to the participant set.
(let [ppants (conj ppants profile-id)
sql "update comment_thread
set modified_at = clock_timestamp(),
participants = ?
where id = ?"]
(db/exec-one! conn [sql (db/tjson ppants) thread-id]))
;; Update the current profile status in relation to the
;; current thread.
(upsert-comment-thread-status! conn profile-id thread-id)
;; Return the created comment object.
comment))))
;; --- Mutation: Update Comment
(s/def ::update-comment
(s/keys :req-un [::profile-id ::id ::content]))
(sm/defmutation ::update-comment
[{:keys [profile-id id content] :as params}]
(db/with-atomic [conn db/pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})
_ (when-not comment (ex/raise :type :not-found))
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
_ (when-not thread (ex/raise :type :not-found))]
(files/check-read-permissions! conn profile-id (:file-id thread))
(db/update! conn :comment
{:content content
:modified-at (dt/now)}
{:id (:id comment)})
(db/update! conn :comment-thread
{:modified-at (dt/now)}
{:id (:id thread)})
nil)))
;; --- Mutation: Delete Comment Thread
(s/def ::delete-comment-thread
(s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::delete-comment-thread
[{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not (= (:owner-id cthr) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/delete! conn :comment-thread {:id id})
nil)))
;; --- Mutation: Delete comment
(s/def ::delete-comment
(s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::delete-comment
[{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})]
(when-not (= (:owner-id comment) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/delete! conn :comment {:id id}))))

View File

@ -21,7 +21,7 @@
[app.db :as db]
[app.redis :as redis]
[app.services.mutations :as sm]
[app.services.mutations.projects :as proj]
[app.services.queries.projects :as proj]
[app.services.queries.files :as files]
[app.tasks :as tasks]
[app.util.blob :as blob]
@ -49,6 +49,7 @@
(sm/defmutation ::create-file
[{:keys [profile-id project-id] :as params}]
(db/with-atomic [conn db/pool]
(proj/check-edition-permissions! conn profile-id project-id)
(create-file conn params)))
(defn- create-file-profile
@ -241,10 +242,14 @@
;; File changes that affect to the library, and must be notified
;; to all clients using it.
(def library-changes
#{:add-color :mod-color :del-color
:add-media :mod-media :del-media
:add-component :mod-component :del-component})
(defn library-change?
[change]
(or (#{:add-color :mod-color :del-color
:add-media :mod-media :del-media
:add-component :mod-component :del-component
:add-typography :mod-typography :del-typography} (:type change))
(and (= (:type change) :mod-obj)
(some? (:component-id change)))))
(declare update-file)
(declare retrieve-lagged-changes)
@ -285,12 +290,12 @@
:revn (:revn file)
:changes changes}
library-changes (filter #(library-changes (:type %)) changes)]
library-changes (filter library-change? changes)]
@(redis/run! :publish {:channel (str (:id file))
:message (t/encode-str msg)})
(if (and (:is-shared file) (seq library-changes))
(when (and (:is-shared file) (seq library-changes))
(let [{:keys [team-id] :as project}
(db/get-by-id conn :project (:project-id file))
@ -299,7 +304,7 @@
:file-id (:id file)
:session-id sid
:revn (:revn file)
:modified-at (:modified-at file)
:modified-at (dt/now)
:changes library-changes}]
@(redis/run! :publish {:channel (str team-id)

View File

@ -46,19 +46,7 @@
(declare persist-media-object-on-fs)
(declare persist-media-thumbnail-on-fs)
(s/def :app$upload/filename ::us/string)
(s/def :app$upload/size ::us/integer)
(s/def :app$upload/content-type cm/valid-media-types)
(s/def :app$upload/tempfile any?)
(s/def ::upload
(s/keys :req-un [:app$upload/filename
:app$upload/size
:app$upload/tempfile
:app$upload/content-type]))
(s/def ::content ::upload)
(s/def ::content ::media/upload)
(s/def ::is-local ::us/boolean)
(s/def ::add-media-object-from-url

View File

@ -18,32 +18,30 @@
[app.emails :as emails]
[app.media :as media]
[app.media-storage :as mst]
[app.http.session :as session]
[app.services.mutations :as sm]
[app.services.mutations.media :as media-mutations]
[app.services.mutations.projects :as projects]
[app.services.mutations.teams :as teams]
[app.services.queries.profile :as profile]
[app.services.tokens :as tokens]
[app.services.mutations.verify-token :refer [process-token]]
[app.tasks :as tasks]
[app.util.blob :as blob]
[app.util.storage :as ust]
[app.util.time :as dt]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs]))
[cuerdas.core :as str]))
;; --- Helpers & Specs
(s/def ::email ::us/email)
(s/def ::fullname ::us/string)
(s/def ::lang ::us/string)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/not-empty-string)
(s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/string)
(s/def ::old-password ::us/string)
(s/def ::password ::us/not-empty-string)
(s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string)
;; --- Mutation: Register Profile
@ -51,22 +49,15 @@
(declare check-profile-existence!)
(declare create-profile)
(declare create-profile-relations)
(declare email-domain-in-whitelist?)
(s/def ::token ::us/not-empty-string)
(s/def ::register-profile
(s/keys :req-un [::email ::password ::fullname]))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if given
whitelist is an empty string."
[whitelist email]
(if (str/blank? whitelist)
true
(let [domains (str/split whitelist #",\s*")
email-domain (second (str/split email #"@"))]
(contains? (set domains) email-domain))))
(s/keys :req-un [::email ::password ::fullname]
:opt-un [::token]))
(sm/defmutation ::register-profile
[params]
[{:keys [token] :as params}]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
:code :registration-disabled))
@ -79,25 +70,68 @@
(db/with-atomic [conn db/pool]
(check-profile-existence! conn params)
(let [profile (->> (create-profile conn params)
(create-profile-relations conn))
token (tokens/generate
{:iss :verify-email
:exp (dt/in-future "48h")
:profile-id (:id profile)
:email (:email profile)})]
(create-profile-relations conn))]
(emails/send! conn emails/register
{:to (:email profile)
:name (:fullname profile)
:token token})
profile)))
(if token
;; If token comes in params, this is because the user comes
;; from team-invitation process; in this case we revalidate
;; the token and process the token claims again with the new
;; profile data.
(let [claims (tokens/verify token {:iss :team-invitation})
claims (assoc claims :member-id (:id profile))
params (assoc params :profile-id (:id profile))]
(process-token conn params claims)
;; Automatically mark the created profile as active because
;; we already have the verification of email with the
;; team-invitation token.
(db/update! conn :profile
{:is-active true}
{:id (:id profile)})
;; Return profile data and create http session for
;; automatically login the profile.
(with-meta (assoc profile
:is-active true
:claims claims)
{:transform-response
(fn [request response]
(let [uagent (get-in request [:headers "user-agent"])
id (session/create (:id profile) uagent)]
(assoc response
:cookies (session/cookies id))))}))
;; If no token is provided, send a verification email
(let [token (tokens/generate
{:iss :verify-email
:exp (dt/in-future "48h")
:profile-id (:id profile)
:email (:email profile)})]
(emails/send! conn emails/register
{:to (:email profile)
:name (:fullname profile)
:token token})
profile)))))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if given
whitelist is an empty string."
[whitelist email]
(if (str/blank? whitelist)
true
(let [domains (str/split whitelist #",\s*")
email-domain (second (str/split email #"@"))]
(contains? (set domains) email-domain))))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = ?
and deleted_at is null) as val")
(defn- check-profile-existence!
(defn check-profile-existence!
[conn {:keys [email] :as params}]
(let [email (str/lower email)
result (db/exec-one! conn [sql:profile-existence email])]
@ -151,8 +185,6 @@
;; --- Mutation: Login
(declare retrieve-profile-by-email)
(s/def ::email ::us/email)
(s/def ::scope ::us/string)
@ -181,22 +213,12 @@
profile)]
(db/with-atomic [conn db/pool]
(let [prof (-> (retrieve-profile-by-email conn email)
(let [prof (-> (profile/retrieve-profile-data-by-email conn email)
(validate-profile)
(profile/strip-private-attrs))
addt (profile/retrieve-additional-data conn (:id prof))]
(merge prof addt)))))
(def sql:profile-by-email
"select * from profile
where email=?
and deleted_at is null")
(defn- retrieve-profile-by-email
[conn email]
(let [email (str/lower email)]
(db/exec-one! conn [sql:profile-by-email email])))
;; --- Mutation: Register if not exists
@ -221,7 +243,7 @@
(create-profile-relations conn)))]
(db/with-atomic [conn db/pool]
(let [profile (retrieve-profile-by-email conn email)
(let [profile (profile/retrieve-profile-data-by-email conn email)
profile (if profile
(populate-additional-data conn profile)
(register-profile conn params))]
@ -272,10 +294,9 @@
;; --- Mutation: Update Photo
(declare upload-photo)
(declare update-profile-photo)
(s/def ::file ::media-mutations/upload)
(s/def ::file ::media/upload)
(s/def ::update-profile-photo
(s/keys :req-un [::profile-id ::file]))
@ -286,7 +307,7 @@
(let [profile (profile/retrieve-profile conn profile-id)
_ (media/run {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
photo (upload-photo conn params)]
photo (teams/upload-photo conn params)]
;; Schedule deletion of old photo
(when (and (string? (:photo profile))
@ -296,22 +317,6 @@
;; Save new photo
(update-profile-photo conn profile-id photo))))
(defn- upload-photo
[conn {:keys [file profile-id]}]
(let [prefix (-> (bn/random-bytes 8)
(bc/bytes->b64u)
(bc/bytes->str))
thumb (media/run
{:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input {:path (fs/path (:tempfile file))
:mtype (:content-type file)}})
name (str prefix (cm/format->extension (:format thumb)))]
(ust/save! mst/media-storage name (:data thumb))))
(defn- update-profile-photo
[conn profile-id path]
(db/update! conn :profile
@ -345,63 +350,10 @@
:token token})
nil)))
(defn- select-profile-for-update
(defn select-profile-for-update
[conn id]
(db/get-by-id conn :profile id {:for-update true}))
;; --- Mutation: Verify Profile Token
;; Generic mutation for perform token based verification for auth
;; domain.
(defmulti process-token (fn [conn claims] (:iss claims)))
(s/def ::verify-profile-token
(s/keys :req-un [::token]))
(sm/defmutation ::verify-profile-token
[{:keys [token] :as params}]
(db/with-atomic [conn db/pool]
(let [claims (tokens/verify token)]
(process-token conn claims))))
(defmethod process-token :change-email
[conn {:keys [profile-id email] :as claims}]
(let [profile (select-profile-for-update conn profile-id)]
(check-profile-existence! conn {:email email})
(db/update! conn :profile
{:email email}
{:id profile-id})
claims))
(defmethod process-token :verify-email
[conn {:keys [profile-id] :as claims}]
(let [profile (select-profile-for-update conn profile-id)]
(when (:is-active profile)
(ex/raise :type :validation
:code :email-already-validated))
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)})
claims))
(defmethod process-token :auth
[conn {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)]
(assoc claims :profile profile)))
(defmethod process-token :default
[conn claims]
(ex/raise :type :validation
:code :invalid-token))
;; --- Mutation: Request Profile Recovery
(s/def ::request-profile-recovery
@ -424,7 +376,7 @@
(db/with-atomic [conn db/pool]
(some->> email
(retrieve-profile-by-email conn)
(profile/retrieve-profile-data-by-email conn)
(create-recovery-token conn)
(send-email-notification conn))
nil)))
@ -473,7 +425,14 @@
(db/update! conn :profile
{:deleted-at (dt/now)}
{:id profile-id})
nil))
(with-meta {}
{:transform-response
(fn [request response]
(some-> (session/extract-auth-token request)
(session/delete))
(assoc response
:cookies (session/cookies "" {:max-age -1})))})))
(def ^:private sql:teams-ownership-check
"with teams as (

View File

@ -16,6 +16,7 @@
[app.config :as cfg]
[app.db :as db]
[app.services.mutations :as sm]
[app.services.queries.projects :as proj]
[app.tasks :as tasks]
[app.util.blob :as blob]))
@ -25,42 +26,12 @@
(s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
;; --- Permissions Checks
(def ^:private sql:project-permissions
"select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
where p.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
where ppr.project_id = ?
and ppr.profile_id = ?")
(defn check-edition-permissions!
[conn profile-id project-id]
(let [rows (db/exec! conn [sql:project-permissions
project-id profile-id
project-id profile-id])]
(when (empty? rows)
(ex/raise :type :not-found))
(when-not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))
(ex/raise :type :validation
:code :not-authorized))))
;; --- Mutation: Create Project
(declare create-project)
(declare create-project-profile)
(declare create-team-project-profile)
(s/def ::team-id ::us/uuid)
(s/def ::create-project
@ -70,9 +41,11 @@
(sm/defmutation ::create-project
[params]
(db/with-atomic [conn db/pool]
(let [proj (create-project conn params)]
(create-project-profile conn (assoc params :project-id (:id proj)))
proj)))
(let [proj (create-project conn params)
params (assoc params :project-id (:id proj))]
(create-project-profile conn params)
(create-team-project-profile conn params)
(assoc proj :is-pinned true))))
(defn create-project
[conn {:keys [id profile-id team-id name default?] :as params}]
@ -93,6 +66,35 @@
:is-admin true
:can-edit true}))
(defn create-team-project-profile
[conn {:keys [team-id project-id profile-id] :as params}]
(db/insert! conn :team-project-profile-rel
{:project-id project-id
:profile-id profile-id
:team-id team-id
:is-pinned true}))
;; --- Mutation: Toggle Project Pin
(def ^:private
sql:update-project-pin
"insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned)
values (?, ?, ?, ?)
on conflict (team_id, project_id, profile_id)
do update set is_pinned=?")
(s/def ::is-pinned ::us/boolean)
(s/def ::project-id ::us/uuid)
(s/def ::update-project-pin
(s/keys :req-un [::profile-id ::id ::team-id ::is-pinned]))
(sm/defmutation ::update-project-pin
[{:keys [id profile-id team-id is-pinned] :as params}]
(db/with-atomic [conn db/pool]
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
nil))
;; --- Mutation: Rename Project
@ -106,7 +108,7 @@
[{:keys [id profile-id name] :as params}]
(db/with-atomic [conn db/pool]
(let [project (db/get-by-id conn :project id {:for-update true})]
(check-edition-permissions! conn profile-id id)
(proj/check-edition-permissions! conn profile-id id)
(db/update! conn :project
{:name name}
{:id id}))))
@ -121,7 +123,7 @@
(sm/defmutation ::delete-project
[{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool]
(check-edition-permissions! conn profile-id id)
(proj/check-edition-permissions! conn profile-id id)
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"

View File

@ -9,13 +9,28 @@
(ns app.services.mutations.teams
(:require
[clojure.spec.alpha :as s]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.emails :as emails]
[app.media :as media]
[app.media-storage :as mst]
[app.services.mutations :as sm]
[app.util.blob :as blob]))
[app.services.mutations.projects :as projects]
[app.services.queries.teams :as teams]
[app.services.tokens :as tokens]
[app.services.queries.profile :as profile]
[app.tasks :as tasks]
[app.util.storage :as ust]
[app.util.time :as dt]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs]))
;; --- Helpers & Specs
@ -27,6 +42,7 @@
(declare create-team)
(declare create-team-profile)
(declare create-team-default-project)
(s/def ::create-team
(s/keys :req-un [::profile-id ::name]
@ -35,8 +51,10 @@
(sm/defmutation ::create-team
[params]
(db/with-atomic [conn db/pool]
(let [team (create-team conn params)]
(create-team-profile conn (assoc params :team-id (:id team)))
(let [team (create-team conn params)
params (assoc params :team-id (:id team))]
(create-team-profile conn params)
(create-team-default-project conn params)
team)))
(defn create-team
@ -57,3 +75,244 @@
:is-owner true
:is-admin true
:can-edit true}))
(defn create-team-default-project
[conn {:keys [team-id profile-id] :as params}]
(let [proj (projects/create-project conn {:team-id team-id
:name "Drafts"
:default? true})]
(projects/create-project-profile conn {:project-id (:id proj)
:profile-id profile-id})))
;; --- Mutation: Update Team
(s/def ::update-team
(s/keys :req-un [::profile-id ::name ::id]))
(sm/defmutation ::update-team
[{:keys [id name profile-id] :as params}]
(db/with-atomic [conn db/pool]
(teams/check-edition-permissions! conn profile-id id)
(db/update! conn :team
{:name name}
{:id id})
nil))
;; --- Mutation: Leave Team
(s/def ::leave-team
(s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::leave-team
[{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool]
(let [perms (teams/check-read-permissions! conn profile-id id)
members (teams/retrieve-team-members conn id)]
(when (:is-owner perms)
(ex/raise :type :validation
:code :owner-cant-leave-team
:hint "reasing owner before leave"))
(when-not (> (count members) 1)
(ex/raise :type :validation
:code :cant-leave-team
:context {:members (count members)}))
(db/delete! conn :team-profile-rel
{:profile-id profile-id
:team-id id})
nil)))
;; --- Mutation: Delete Team
(s/def ::delete-team
(s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::delete-team
[{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool]
(let [perms (teams/check-edition-permissions! conn profile-id id)]
(when-not (:is-owner perms)
(ex/raise :type :validation
:code :only-owner-can-delete-team))
(db/delete! conn :team {:id id})
nil)))
;; --- Mutation: Tean Update Role
(declare retrieve-team-member)
(declare role->params)
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
(s/def ::role #{:owner :admin :editor :viewer})
(s/def ::update-team-member-role
(s/keys :req-un [::profile-id ::team-id ::member-id ::role]))
(sm/defmutation ::update-team-member-role
[{:keys [team-id profile-id member-id role] :as params}]
(db/with-atomic [conn db/pool]
(let [perms (teams/check-read-permissions! conn profile-id team-id)
;; We retrieve all team members instead of query the
;; database for a single member. This is just for
;; convenience, if this bocomes a bottleneck or problematic,
;; we will change it to more efficient fetch mechanims.
members (teams/retrieve-team-members conn team-id)
member (d/seek #(= member-id (:id %)) members)]
;; If no member is found, just 404
(when-not member
(ex/raise :type :not-found
:code :member-does-not-exist))
;; First check if we have permissions to change roles
(when-not (or (:is-owner perms)
(:is-admin perms))
(ex/raise :type :validation
:code :insufficient-permissions))
;; Don't allow change role of owner member
(when (:is-owner member)
(ex/raise :type :validation
:code :cant-change-role-to-owner))
;; Don't allow promote to owner to admin users.
(when (and (= role :owner)
(not (:is-owner perms)))
(ex/raise :type :validation
:code :cant-promote-to-owner))
(let [params (role->params role)]
;; Only allow single owner on team
(when (and (= role :owner)
(:is-owner perms))
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id team-id
:profile-id profile-id}))
(db/update! conn :team-profile-rel params
{:team-id team-id
:profile-id member-id})
nil))))
(defn role->params
[role]
(case role
:admin {:is-owner false :is-admin true :can-edit true}
:editor {:is-owner false :is-admin false :can-edit true}
:owner {:is-owner true :is-admin true :can-edit true}
:viewer {:is-owner false :is-admin false :can-edit false}))
;; --- Mutation: Team Update Role
(s/def ::delete-team-member
(s/keys :req-un [::profile-id ::team-id ::member-id]))
(sm/defmutation ::delete-team-member
[{:keys [team-id profile-id member-id] :as params}]
(db/with-atomic [conn db/pool]
(let [perms (teams/check-read-permissions! conn profile-id team-id)]
(when-not (or (:is-owner perms)
(:is-admin perms))
(ex/raise :type :validation
:code :insufficient-permissions))
(when (= member-id profile-id)
(ex/raise :type :validation
:code :cant-remove-yourself))
(db/delete! conn :team-profile-rel {:profile-id member-id
:team-id team-id})
nil)))
;; --- Mutation: Update Team Photo
(declare upload-photo)
(s/def ::file ::media/upload)
(s/def ::update-team-photo
(s/keys :req-un [::profile-id ::team-id ::file]))
(sm/defmutation ::update-team-photo
[{:keys [profile-id file team-id] :as params}]
(media/validate-media-type (:content-type file))
(db/with-atomic [conn db/pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [team (teams/retrieve-team conn profile-id team-id)
_ (media/run {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
photo (upload-photo conn params)]
;; Schedule deletion of old photo
(when (and (string? (:photo team))
(not (str/blank? (:photo team))))
(tasks/submit! conn {:name "remove-media"
:props {:path (:photo team)}}))
;; Save new photo
(db/update! conn :team
{:photo (str photo)}
{:id team-id})
(assoc team :photo (str photo)))))
(defn upload-photo
[conn {:keys [file profile-id]}]
(let [prefix (-> (bn/random-bytes 8)
(bc/bytes->b64u)
(bc/bytes->str))
thumb (media/run
{:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input {:path (fs/path (:tempfile file))
:mtype (:content-type file)}})
name (str prefix (cm/format->extension (:format thumb)))]
(ust/save! mst/media-storage name (:data thumb))))
;; --- Mutation: Invite Member
(s/def ::email ::us/email)
(s/def ::invite-team-member
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
(sm/defmutation ::invite-team-member
[{:keys [profile-id team-id email role] :as params}]
(db/with-atomic [conn db/pool]
(let [perms (teams/check-edition-permissions! conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
member (profile/retrieve-profile-data-by-email conn email)
team (db/get-by-id conn :team team-id)
token (tokens/generate
{:iss :team-invitation
:exp (dt/in-future "24h")
:profile-id (:id profile)
:role role
:team-id team-id
:member-email (:email member email)
:member-id (:id member)})]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(emails/send! conn emails/invite-to-team
{:to email
:invited-by (:fullname profile)
:team (:name team)
:token token})
nil)))

View File

@ -0,0 +1,143 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.verify-token
(:require
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.emails :as emails]
[app.http.session :as session]
[app.media :as media]
[app.media-storage :as mst]
[app.services.mutations :as sm]
[app.services.mutations.teams :as teams]
[app.services.queries.profile :as profile]
[app.services.tokens :as tokens]
[app.tasks :as tasks]
[app.util.blob :as blob]
[app.util.storage :as ust]
[app.util.time :as dt]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defmulti process-token (fn [conn params claims] (:iss claims)))
(s/def ::verify-token
(s/keys :req-un [::token]
:opt-un [::profile-id]))
(sm/defmutation ::verify-token
[{:keys [token] :as params}]
(db/with-atomic [conn db/pool]
(let [claims (tokens/verify token)]
(process-token conn params claims))))
(defmethod process-token :change-email
[conn params {:keys [profile-id email] :as claims}]
(let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
(when (profile/retrieve-profile-data-by-email conn email)
(ex/raise :type :validation
:code :email-already-exists))
(db/update! conn :profile
{:email email}
{:id profile-id})
claims))
(defmethod process-token :verify-email
[conn params {:keys [profile-id] :as claims}]
(let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
(when (:is-active profile)
(ex/raise :type :validation
:code :email-already-validated))
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)})
claims))
(defmethod process-token :auth
[conn params {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation
(s/def ::iss keyword?)
(s/def ::exp ::us/inst)
(s/def :internal.tokens.team-invitation/profile-id ::us/uuid)
(s/def :internal.tokens.team-invitation/role ::us/keyword)
(s/def :internal.tokens.team-invitation/team-id ::us/uuid)
(s/def :internal.tokens.team-invitation/member-email ::us/email)
(s/def :internal.tokens.team-invitation/member-id (s/nilable ::us/uuid))
(s/def ::team-invitation-claims
(s/keys :req-un [::iss ::exp
:internal.tokens.team-invitation/profile-id
:internal.tokens.team-invitation/role
:internal.tokens.team-invitation/team-id
:internal.tokens.team-invitation/member-email]
:opt-un [:internal.tokens.team-invitation/member-id]))
(defmethod process-token :team-invitation
[conn {:keys [profile-id token]} {:keys [member-id team-id role] :as claims}]
(us/assert ::team-invitation-claims claims)
(if (uuid? member-id)
(let [params (merge {:team-id team-id
:profile-id member-id}
(teams/role->params role))
claims (assoc claims :state :created)]
(db/insert! conn :team-profile-rel params)
(if (and (uuid? profile-id)
(= member-id profile-id))
;; If the current session is already matches the invited
;; member, then just return the token and leave the frontend
;; app redirect to correct team.
claims
;; If the session does not matches the invited member id,
;; replace the session with a new one matching the invited
;; member. This techinique should be considered secure because
;; the user clicking the link he already has access to the
;; email account.
(with-meta claims
{:transform-response
(fn [request response]
(let [uagent (get-in request [:headers "user-agent"])
id (session/create member-id uagent)]
(assoc response
:cookies (session/cookies id))))})))
;; In this case, we waint until frontend app redirect user to
;; registeration page, the user is correctly registered and the
;; register mutation call us again with the same token to finally
;; create the corresponding team-profile relation from the first
;; condition of this if.
(assoc claims
:token token
:state :pending)))
;; --- Default
(defmethod process-token :default
[conn params claims]
(ex/raise :type :validation
:code :invalid-token))

View File

@ -1,104 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns app.services.queries.colors
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.services.queries :as sq]
[app.services.queries.teams :as teams]
[app.util.blob :as blob]
[app.util.data :as data]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::file-id ::us/uuid)
;; --- Query: Colors (by file)
(declare retrieve-colors)
(declare retrieve-file)
(s/def ::colors
(s/keys :req-un [::profile-id ::file-id]))
(sq/defquery ::colors
[{:keys [profile-id file-id] :as params}]
(db/with-atomic [conn db/pool]
(let [file (retrieve-file conn file-id)]
(teams/check-read-permissions! conn profile-id (:team-id file))
(retrieve-colors conn file-id))))
(def ^:private sql:colors
"select *
from color
where color.deleted_at is null
and color.file_id = ?
order by created_at desc")
(defn- retrieve-colors
[conn file-id]
(db/exec! conn [sql:colors file-id]))
(def ^:private sql:retrieve-file
"select file.*,
project.team_id as team_id
from file
inner join project on (project.id = file.project_id)
where file.id = ?")
(defn- retrieve-file
[conn id]
(let [row (db/exec-one! conn [sql:retrieve-file id])]
(when-not row
(ex/raise :type :not-found))
row))
;; --- Query: Color (by ID)
(declare retrieve-color)
(s/def ::id ::us/uuid)
(s/def ::color
(s/keys :req-un [::profile-id ::id]))
(sq/defquery ::color
[{:keys [profile-id id] :as params}]
(db/with-atomic [conn db/pool]
(let [color (retrieve-color conn id)]
(teams/check-read-permissions! conn profile-id (:team-id color))
color)))
(def ^:private sql:single-color
"select color.*,
p.team_id as team_id
from color as color
inner join file as f on (color.file_id = f.id)
inner join project as p on (p.id = f.project_id)
where color.deleted_at is null
and color.id = ?
order by created_at desc")
(defn retrieve-color
[conn id]
(let [row (db/exec-one! conn [sql:single-color id])]
(when-not row
(ex/raise :type :not-found))
row))

View File

@ -0,0 +1,109 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.queries.comments
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.services.queries :as sq]
[app.services.queries.files :as files]
[app.util.time :as dt]
[app.util.transit :as t]
[clojure.spec.alpha :as s]
[datoteka.core :as fs]
[promesa.core :as p]))
(defn decode-row
[{:keys [participants position] :as row}]
(cond-> row
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
;; --- Query: Comment Threads
(declare retrieve-comment-threads)
(s/def ::file-id ::us/uuid)
(s/def ::comment-threads
(s/keys :req-un [::profile-id ::file-id]))
(sq/defquery ::comment-threads
[{:keys [profile-id file-id] :as params}]
(with-open [conn (db/open)]
(files/check-read-permissions! conn profile-id file-id)
(retrieve-comment-threads conn params)))
(def sql:comment-threads
"select distinct on (ct.id)
ct.*,
first_value(c.content) over w as content,
(select count(1)
from comment as c
where c.thread_id = ct.id) as count_comments,
(select count(1)
from comment as c
where c.thread_id = ct.id
and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
from comment_thread as ct
inner join comment as c on (c.thread_id = ct.id)
left join comment_thread_status as cts
on (cts.thread_id = ct.id and
cts.profile_id = ?)
where ct.file_id = ?
window w as (partition by c.thread_id order by c.created_at asc)")
(defn- retrieve-comment-threads
[conn {:keys [profile-id file-id]}]
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
(into [] (map decode-row))))
;; --- Query: Single Comment Thread
(s/def ::id ::us/uuid)
(s/def ::comment-thread
(s/keys :req-un [::profile-id ::file-id ::id]))
(sq/defquery ::comment-thread
[{:keys [profile-id file-id id] :as params}]
(with-open [conn (db/open)]
(files/check-read-permissions! conn profile-id file-id)
(let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")]
(-> (db/exec-one! conn [sql profile-id file-id id])
(decode-row)))))
;; --- Query: Comments
(declare retrieve-comments)
(s/def ::file-id ::us/uuid)
(s/def ::thread-id ::us/uuid)
(s/def ::comments
(s/keys :req-un [::profile-id ::thread-id]))
(sq/defquery ::comments
[{:keys [profile-id thread-id] :as params}]
(with-open [conn (db/open)]
(let [thread (db/get-by-id conn :comment-thread thread-id)]
(files/check-read-permissions! conn profile-id (:file-id thread))
(retrieve-comments conn thread-id))))
(def sql:comments
"select c.* from comment as c
where c.thread_id = ?
order by c.created_at asc")
(defn- retrieve-comments
[conn thread-id]
(->> (db/exec! conn [sql:comments thread-id])
(into [] (map decode-row))))

View File

@ -33,6 +33,61 @@
(s/def ::team-id ::us/uuid)
(s/def ::search-term ::us/string)
;; --- Query: File Permissions
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
where fpr.file_id = ?
and fpr.profile_id = ?
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?")
(defn check-edition-permissions!
[conn profile-id file-id]
(let [rows (db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])]
(when (empty? rows)
(ex/raise :type :not-found))
(when-not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))
(ex/raise :type :validation
:code :not-authorized))))
(defn check-read-permissions!
[conn profile-id file-id]
(let [rows (db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])]
(when-not (seq rows)
(ex/raise :type :validation
:code :not-authorized))))
;; --- Query: Files search
;; TODO: this query need to a good refactor
@ -99,52 +154,8 @@
(sq/defquery ::files
[{:keys [profile-id project-id] :as params}]
(with-open [conn (db/open)]
(let [project (db/get-by-id conn :project project-id)]
(projects/check-edition-permissions! conn profile-id project)
(into [] decode-row-xf (db/exec! conn [sql:files project-id])))))
;; --- Query: File Permissions
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
where fpr.file_id = ?
and fpr.profile_id = ?
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?")
(defn check-edition-permissions!
[conn profile-id file-id]
(let [rows (db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])]
(when (empty? rows)
(ex/raise :type :not-found))
(when-not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))
(ex/raise :type :validation
:code :not-authorized))))
(projects/check-read-permissions! conn profile-id project-id)
(into [] decode-row-xf (db/exec! conn [sql:files project-id]))))
;; --- Query: File (By ID)

View File

@ -2,11 +2,15 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.queries.profile
(:require
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
@ -53,16 +57,16 @@
(def ^:private sql:default-team-and-project
"select t.id
from team as t
inner join team_profile_rel as tpr on (tpr.team_id = t.id)
where tpr.profile_id = ?
and tpr.is_owner is true
inner join team_profile_rel as tp on (tp.team_id = t.id)
where tp.profile_id = ?
and tp.is_owner is true
and t.is_default is true
union all
select p.id
from project as p
inner join project_profile_rel as tpr on (tpr.project_id = p.id)
where tpr.profile_id = ?
and tpr.is_owner is true
inner join project_profile_rel as tp on (tp.project_id = p.id)
where tp.profile_id = ?
and tp.is_owner is true
and p.is_default is true")
(defn retrieve-additional-data
@ -87,6 +91,18 @@
profile))
(def sql:profile-by-email
"select * from profile
where email=?
and deleted_at is null")
(defn retrieve-profile-data-by-email
[conn email]
(let [email (str/lower email)]
(db/exec-one! conn [sql:profile-by-email email])))
;; --- Attrs Helpers
(defn strip-private-attrs

View File

@ -18,41 +18,46 @@
;; --- Check Project Permissions
;; This SQL checks if the: (1) project is part of the team where the
;; profile has edition permissions or (2) the profile has direct
;; edition access granted to this project.
(def sql:project-permissions
"select tp.can_edit,
tp.is_admin,
tp.is_owner
from team_profile_rel as tp
where tp.profile_id = ?
and tp.team_id = ?
union
select pp.can_edit,
pp.is_admin,
pp.is_owner
from project_profile_rel as pp
where pp.profile_id = ?
and pp.project_id = ?;")
(def ^:private sql:project-permissions
"select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
where p.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
where ppr.project_id = ?
and ppr.profile_id = ?")
(defn check-edition-permissions!
[conn profile-id project]
[conn profile-id project-id]
(let [rows (db/exec! conn [sql:project-permissions
profile-id
(:team-id project)
profile-id
(:id project)])]
project-id profile-id
project-id profile-id])]
(when (empty? rows)
(ex/raise :type :not-found))
(when-not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))
(ex/raise :type :validation
:code :not-authorized))))
(defn check-read-permissions!
[conn profile-id project-id]
(let [rows (db/exec! conn [sql:project-permissions
project-id profile-id
project-id profile-id])]
(when-not (seq rows)
(ex/raise :type :validation
:code :not-authorized))))
;; --- Query: Projects
@ -60,7 +65,6 @@
(s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::projects
(s/keys :req-un [::profile-id ::team-id]))
@ -68,31 +72,37 @@
[{:keys [profile-id team-id]}]
(with-open [conn (db/open)]
(teams/check-read-permissions! conn profile-id team-id)
(retrieve-projects conn team-id)))
(retrieve-projects conn profile-id team-id)))
(def sql:projects
"select p.*,
coalesce(tpp.is_pinned, false) as is_pinned,
(select count(*) from file as f
where f.project_id = p.id
and deleted_at is null)
where f.project_id = p.id
and deleted_at is null) as count
from project as p
left join team_project_profile_rel as tpp
on (tpp.project_id = p.id and
tpp.team_id = p.team_id and
tpp.profile_id = ?)
where p.team_id = ?
and p.deleted_at is null
order by p.modified_at desc")
(defn retrieve-projects
[conn team-id]
(db/exec! conn [sql:projects team-id]))
[conn profile-id team-id]
(db/exec! conn [sql:projects profile-id team-id]))
;; --- Query: Projec by ID
(s/def ::project-id ::us/uuid)
(s/def ::project-by-id
(s/keys :req-un [::profile-id ::project-id]))
;; --- Query: Project
(sq/defquery ::project-by-id
[{:keys [profile-id project-id]}]
(s/def ::id ::us/uuid)
(s/def ::project
(s/keys :req-un [::profile-id ::id]))
(sq/defquery ::project
[{:keys [profile-id id]}]
(with-open [conn (db/open)]
(let [project (db/get-by-id conn :project project-id)]
(check-edition-permissions! conn profile-id project)
(let [project (db/get-by-id conn :project id)]
(check-read-permissions! conn profile-id id)
project)))

View File

@ -18,19 +18,18 @@
[app.services.queries.projects :as projects :refer [retrieve-projects]]
[app.services.queries.files :refer [decode-row-xf]]))
(def sql:project-recent-files
"select f.*
from file as f
where f.project_id = ?
and f.deleted_at is null
order by f.modified_at desc
limit 5")
(defn recent-by-project
[conn profile-id project]
(let [project-id (:id project)]
(projects/check-edition-permissions! conn profile-id project)
(into [] decode-row-xf (db/exec! conn [sql:project-recent-files project-id]))))
(def sql:recent-files
"with recent_files as (
select f.*, row_number() over w as row_num
from file as f
join project as p on (p.id = f.project_id)
where p.team_id = ?
and p.deleted_at is null
and f.deleted_at is null
window w as (partition by f.project_id order by f.modified_at desc)
order by f.modified_at desc
)
select * from recent_files where row_num <= 6;")
(s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
@ -42,9 +41,7 @@
[{:keys [profile-id team-id]}]
(with-open [conn (db/open)]
(teams/check-read-permissions! conn profile-id team-id)
(->> (retrieve-projects conn team-id)
;; Retrieve for each proyect the 5 more recent files
(map (partial recent-by-project conn profile-id))
;; Change the structure so it's a map with project-id as keys
(flatten)
(group-by :project-id))))
(let [files (db/exec! conn [sql:recent-files team-id])]
(into [] decode-row-xf files))))

View File

@ -15,6 +15,7 @@
[app.common.uuid :as uuid]
[app.db :as db]
[app.services.queries :as sq]
[app.services.queries.profile :as profile]
[app.util.blob :as blob]))
;; --- Team Edition Permissions
@ -34,7 +35,8 @@
(:is-admin row)
(:is-owner row))
(ex/raise :type :validation
:code :not-authorized))))
:code :not-authorized))
row))
(defn check-read-permissions!
[conn profile-id team-id]
@ -42,4 +44,89 @@
;; when row is found this means that read permission is granted.
(when-not row
(ex/raise :type :validation
:code :not-authorized))))
:code :not-authorized))
row))
;; --- Query: Teams
(declare retrieve-teams)
(s/def ::profile-id ::us/uuid)
(s/def ::teams
(s/keys :req-un [::profile-id]))
(sq/defquery ::teams
[{:keys [profile-id]}]
(with-open [conn (db/open)]
(retrieve-teams conn profile-id)))
(def sql:teams
"select t.*,
tp.is_owner,
tp.is_admin,
tp.can_edit,
(t.id = ?) as is_default
from team_profile_rel as tp
join team as t on (t.id = tp.team_id)
where t.deleted_at is null
and tp.profile_id = ?
order by t.created_at asc")
(defn retrieve-teams
[conn profile-id]
(let [defaults (profile/retrieve-additional-data conn profile-id)]
(db/exec! conn [sql:teams (:default-team-id defaults) profile-id])))
;; --- Query: Team (by ID)
(declare retrieve-team)
(s/def ::id ::us/uuid)
(s/def ::team
(s/keys :req-un [::profile-id ::id]))
(sq/defquery ::team
[{:keys [profile-id id]}]
(with-open [conn (db/open)]
(retrieve-team conn profile-id id)))
(defn retrieve-team
[conn profile-id team-id]
(let [defaults (profile/retrieve-additional-data conn profile-id)
sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")
result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])]
(when-not result
(ex/raise :type :not-found
:code :object-does-not-exists))
result))
;; --- Query: Team Members
(declare retrieve-team-members)
(s/def ::team-id ::us/uuid)
(s/def ::team-members
(s/keys :req-un [::profile-id ::team-id]))
(sq/defquery ::team-members
[{:keys [profile-id team-id]}]
(with-open [conn (db/open)]
(check-edition-permissions! conn profile-id team-id)
(retrieve-team-members conn team-id)))
(def sql:team-members
"select tp.*,
p.id,
p.email,
p.fullname as name,
p.photo,
p.is_active
from team_profile_rel as tp
join profile as p on (p.id = tp.profile_id)
where tp.team_id = ?")
(defn retrieve-team-members
[conn team-id]
(db/exec! conn [sql:team-members team-id]))

View File

@ -22,7 +22,6 @@
[app.services.mutations.projects :as projects]
[app.services.mutations.teams :as teams]
[app.services.mutations.files :as files]
[app.services.mutations.colors :as colors]
[app.migrations]
[app.media]
[app.media-storage]

View File

@ -6,7 +6,7 @@
(ns app.common.data
"Data manipulation and query helper functions."
(:refer-clojure :exclude [concat read-string])
(:refer-clojure :exclude [concat read-string hash-map])
(:require [clojure.set :as set]
[linked.set :as lks]
#?(:cljs [cljs.reader :as r]
@ -109,7 +109,7 @@
([coll value]
(sequence (replace-by-id value) coll)))
(defn remove-nil-vals
(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."
[data]

View File

@ -25,9 +25,24 @@
[& {:keys [type code message hint cause] :as params}]
(s/assert ::error-params params)
(let [message (or message hint "")
payload (dissoc params :cause :message)]
payload (dissoc params :cause)]
(ex-info message payload cause)))
(defmacro raise
[& args]
`(throw (error ~@args)))
(defn try*
[f on-error]
(try (f) (catch #?(:clj Exception :cljs :default) e (on-error e))))
;; http://clj-me.cgrand.net/2013/09/11/macros-closures-and-unexpected-object-retention/
;; Explains the use of ^:once metadata
(defmacro ignoring
[& exprs]
`(try* (^:once fn* [] ~@exprs) (constantly nil)))
(defmacro try
[& exprs]
`(try* (^:once fn* [] ~@exprs) identity))

View File

@ -199,3 +199,27 @@
(defn center-points [points]
(let [k (point (count points))]
(reduce #(add %1 (divide %2 k)) (point) points)))
(defn normal-left
"Returns the normal unit vector on the left side"
[{:keys [x y]}]
(unit (point (- y) x)))
(defn normal-right
"Returns the normal unit vector on the right side"
[{:keys [x y]}]
(unit (point y (- x))))
(defn point-line-distance
"Returns the distance from a point to a line defined by two points"
[point line-point1 line-point2]
(let [{x0 :x y0 :y} point
{x1 :x y1 :y} line-point1
{x2 :x y2 :y} line-point2
num (mth/abs
(+ (* x0 (- y2 y1))
(- (* y0 (- x2 x1)))
(* x2 y1)
(- (* y2 x1))))
dist (distance line-point2 line-point1)]
(/ num dist)))

View File

@ -11,7 +11,6 @@
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
[app.common.pages-helpers :as cph]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.math :as mth]
@ -58,10 +57,17 @@
(update :points #(mapv inc-point %))
(update :segments #(mapv inc-point %)))))
;; Duplicated from pages-helpers to remove cyclic dependencies
(defn get-children [id objects]
(let [shapes (vec (get-in objects [id :shapes]))]
(if shapes
(d/concat shapes (mapcat #(get-children % objects) shapes))
[])))
(defn recursive-move
"Move the shape and all its recursive children."
[shape dpoint objects]
(let [children-ids (cph/get-children (:id shape) objects)
(let [children-ids (get-children (:id shape) objects)
children (map #(get objects %) children-ids)]
(map #(move % dpoint) (cons shape children))))
@ -406,6 +412,10 @@
:y miny
:width (- maxx minx)
:height (- maxy miny)
:points [(gpt/point minx miny)
(gpt/point maxx miny)
(gpt/point maxx maxy)
(gpt/point minx maxy)]
:type :rect}))
(defn translate-to-frame
@ -674,7 +684,7 @@
resize-transform (:resize-transform modifiers (gmt/matrix))
resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix))
rt-modif (:rotation modifiers 0)
rt-modif (or (:rotation modifiers) 0)
shape (-> shape
(transform ds-modifier))
@ -792,8 +802,8 @@
(assoc $ :points (shape->points $))
(assoc $ :selrect (points->selrect (:points $)))
(update $ :selrect fix-invalid-rect-values)
(update $ :rotation #(mod (+ (or % 0) (get-in $ [:modifiers :rotation] 0)) 360))
)]
(update $ :rotation #(mod (+ (or % 0)
(or (get-in $ [:modifiers :rotation]) 0)) 360)))]
new-shape))
(declare update-path-selrect)

View File

@ -12,6 +12,10 @@
#?(:cljs
(:require [goog.math :as math])))
(def PI
#?(:cljs (.-PI js/Math)
:clj Math/PI))
(defn nan?
[v]
#?(:cljs (js/isNaN v)

View File

@ -46,6 +46,7 @@
(<= % max-safe-int)))
(s/def ::component-id uuid?)
(s/def ::component-file uuid?)
(s/def ::component-root? boolean?)
(s/def ::shape-ref uuid?)
(s/def ::safe-number
@ -54,6 +55,93 @@
(>= % min-safe-int)
(<= % max-safe-int)))
;; GRADIENTS
(s/def :internal.gradient.stop/color ::string)
(s/def :internal.gradient.stop/opacity ::safe-number)
(s/def :internal.gradient.stop/offset ::safe-number)
(s/def :internal.gradient/type #{:linear :radial})
(s/def :internal.gradient/start-x ::safe-number)
(s/def :internal.gradient/start-y ::safe-number)
(s/def :internal.gradient/end-x ::safe-number)
(s/def :internal.gradient/end-y ::safe-number)
(s/def :internal.gradient/width ::safe-number)
(s/def :internal.gradient/stop
(s/keys :req-un [:internal.gradient.stop/color
:internal.gradient.stop/opacity
:internal.gradient.stop/offset]))
(s/def :internal.gradient/stops
(s/coll-of :internal.gradient/stop :kind vector?))
(s/def ::gradient
(s/keys :req-un [:internal.gradient/type
:internal.gradient/start-x
:internal.gradient/start-y
:internal.gradient/end-x
:internal.gradient/end-y
:internal.gradient/width
:internal.gradient/stops]))
;;; COLORS
(s/def :internal.color/name ::string)
(s/def :internal.color/value (s/nilable ::string))
(s/def :internal.color/color (s/nilable ::string))
(s/def :internal.color/opacity (s/nilable ::safe-number))
(s/def :internal.color/gradient (s/nilable ::gradient))
(s/def ::color
(s/keys :opt-un [::id
:internal.color/name
:internal.color/value
:internal.color/color
:internal.color/opacity
:internal.color/gradient]))
;;; SHADOW EFFECT
(s/def :internal.shadow/id uuid?)
(s/def :internal.shadow/style #{:drop-shadow :inner-shadow})
(s/def :internal.shadow/color ::color)
(s/def :internal.shadow/offset-x ::safe-number)
(s/def :internal.shadow/offset-y ::safe-number)
(s/def :internal.shadow/blur ::safe-number)
(s/def :internal.shadow/spread ::safe-number)
(s/def :internal.shadow/hidden boolean?)
(s/def :internal.shadow/shadow
(s/keys :req-un [:internal.shadow/id
:internal.shadow/style
:internal.shadow/color
:internal.shadow/offset-x
:internal.shadow/offset-y
:internal.shadow/blur
:internal.shadow/spread
:internal.shadow/hidden]))
(s/def ::shadow
(s/coll-of :internal.shadow/shadow :kind vector?))
;;; BLUR EFFECT
(s/def :internal.blur/id uuid?)
(s/def :internal.blur/type #{:layer-blur})
(s/def :internal.blur/value ::safe-number)
(s/def :internal.blur/hidden boolean?)
(s/def ::blur
(s/keys :req-un [:internal.blur/id
:internal.blur/type
:internal.blur/value
:internal.blur/hidden]))
;; Page Options
(s/def :internal.page.grid.color/value string?)
(s/def :internal.page.grid.color/opacity ::safe-number)
@ -109,10 +197,13 @@
(s/def :internal.shape/blocked boolean?)
(s/def :internal.shape/collapsed boolean?)
(s/def :internal.shape/content any?)
(s/def :internal.shape/fill-color string?)
(s/def :internal.shape/fill-opacity ::safe-number)
(s/def :internal.shape/fill-gradient (s/nilable ::gradient))
(s/def :internal.shape/fill-color-ref-file (s/nilable uuid?))
(s/def :internal.shape/fill-color-ref-id (s/nilable uuid?))
(s/def :internal.shape/fill-opacity ::safe-number)
(s/def :internal.shape/font-family string?)
(s/def :internal.shape/font-size ::safe-integer)
(s/def :internal.shape/font-style string?)
@ -141,6 +232,8 @@
(s/def :internal.shape/width ::safe-number)
(s/def :internal.shape/height ::safe-number)
(s/def :internal.shape/index integer?)
(s/def :internal.shape/shadow ::shadow)
(s/def :internal.shape/blur ::blur)
(s/def :internal.shape/x1 ::safe-number)
(s/def :internal.shape/y1 ::safe-number)
@ -211,7 +304,36 @@
:internal.shape/height
:internal.shape/interactions
:internal.shape/selrect
:internal.shape/points]))
:internal.shape/points
:internal.shape/masked-group?
:internal.shape/shadow
:internal.shape/blur]))
(def component-sync-attrs {:fill-color :fill-group
:fill-color-ref-file :fill-group
:fill-color-ref-id :fill-group
:fill-opacity :fill-group
:content :text-content-group
:font-family :text-font-group
:font-size :text-font-group
:font-style :text-font-group
:font-weight :text-font-group
:letter-spacing :text-display-group
:line-height :text-display-group
:text-align :text-display-group
:stroke-color :stroke-group
:stroke-color-ref-file :stroke-group
:stroke-color-ref-id :stroke-group
:stroke-opacity :stroke-group
:stroke-style :stroke-group
:stroke-width :stroke-group
:stroke-alignment :stroke-group
:width :size-group
:height :size-group
:proportion :size-group
:rx :radius-group
:ry :radius-group
:masked-group? :mask-group})
(s/def ::minimal-shape
(s/keys :req-un [::type ::name]
@ -222,6 +344,7 @@
(s/keys :opt-un [::id
::component-id
::component-file
::component-root?
::shape-ref])))
(s/def :internal.page/objects (s/map-of uuid? ::shape))
@ -232,13 +355,12 @@
:internal.page/options
:internal.page/objects]))
(s/def :internal.color/name ::string)
(s/def :internal.color/value ::string)
(s/def ::color
(s/keys :req-un [::id
:internal.color/name
:internal.color/value]))
(s/def ::recent-color
(s/keys :opt-un [:internal.color/value
:internal.color/color
:internal.color/opacity
:internal.color/gradient]))
(s/def :internal.media-object/name ::string)
(s/def :internal.media-object/path ::string)
@ -264,7 +386,32 @@
(s/map-of ::uuid ::color))
(s/def :internal.file/recent-colors
(s/coll-of ::string :kind vector?))
(s/coll-of ::recent-color :kind vector?))
(s/def :internal.typography/id ::id)
(s/def :internal.typography/name ::string)
(s/def :internal.typography/font-id ::string)
(s/def :internal.typography/font-family ::string)
(s/def :internal.typography/font-variant-id ::string)
(s/def :internal.typography/font-size ::string)
(s/def :internal.typography/font-weight ::string)
(s/def :internal.typography/font-style ::string)
(s/def :internal.typography/line-height ::string)
(s/def :internal.typography/letter-spacing ::string)
(s/def :internal.typography/text-transform ::string)
(s/def ::typography
(s/keys :req-un [:internal.typography/id
:internal.typography/name
:internal.typography/font-id
:internal.typography/font-family
:internal.typography/font-variant-id
:internal.typography/font-size
:internal.typography/font-weight
:internal.typography/font-style
:internal.typography/line-height
:internal.typography/letter-spacing
:internal.typography/text-transform]))
(s/def :internal.file/pages
(s/coll-of ::uuid :kind vector?))
@ -286,11 +433,16 @@
(s/def :internal.operations.set/attr keyword?)
(s/def :internal.operations.set/val any?)
(s/def :internal.operations.set/touched
(s/nilable (s/every keyword? :kind set?)))
(defmethod operation-spec :set [_]
(s/keys :req-un [:internal.operations.set/attr
:internal.operations.set/val]))
(defmethod operation-spec :set-touched [_]
(s/keys :req-un [:internal.operations.set/touched]))
(defmulti change-spec :type)
(s/def :internal.changes.set-option/option any?)
@ -311,7 +463,7 @@
(s/def ::operations (s/coll-of ::operation))
(defmethod change-spec :mod-obj [_]
(s/keys :req-un [::id ::page-id ::operations]))
(s/keys :req-un [::id (or ::page-id ::component-id) ::operations]))
(defmethod change-spec :del-obj [_]
(s/keys :req-un [::id ::page-id]))
@ -348,8 +500,10 @@
(defmethod change-spec :del-color [_]
(s/keys :req-un [::id]))
(s/def :internal.changes.add-recent-color/color ::recent-color)
(defmethod change-spec :add-recent-color [_]
(s/keys :req-un [:recent-color/color]))
(s/keys :req-un [:internal.changes.add-recent-color/color]))
(s/def :internal.changes.media/object ::media-object)
@ -374,6 +528,17 @@
(defmethod change-spec :del-component [_]
(s/keys :req-un [::id]))
(s/def :internal.changes.typography/typography ::typography)
(defmethod change-spec :add-typography [_]
(s/keys :req-un [:internal.changes.typography/typography]))
(defmethod change-spec :mod-typography [_]
(s/keys :req-un [:internal.changes.typography/typography]))
(defmethod change-spec :del-typography [_]
(s/keys :req-un [:internal.typography/id]))
(s/def ::change (s/multi-spec change-spec :type))
(s/def ::changes (s/coll-of ::change))
@ -559,12 +724,14 @@
:else (cph/insert-at-index shapes index [id]))))))))))))
(defmethod process-change :mod-obj
[data {:keys [id page-id operations] :as change}]
(d/update-in-when data [:pages-index page-id :objects]
(fn [objects]
(if-let [obj (get objects id)]
(assoc objects id (reduce process-operation obj operations))
objects))))
[data {:keys [id page-id component-id operations] :as change}]
(let [update-fn (fn [objects]
(if-let [obj (get objects id)]
(assoc objects id (reduce process-operation obj operations))
objects))]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] update-fn)
(d/update-in-when data [:components component-id :objects] update-fn))))
(defmethod process-change :del-obj
[data {:keys [page-id id] :as change}]
@ -616,7 +783,10 @@
(assoc :modifiers
(rotation-modifiers gcenter % (- (:rotation group 0))))
(geom/transform-shape))))
selrect (-> (into [] gxfm (:shapes group))
inner-shapes (if (:masked-group? group)
[(first (:shapes group))]
(:shapes group))
selrect (-> (into [] gxfm inner-shapes)
(geom/selection-rect))]
;; Rotate the group shape change the data and rotate back again
@ -629,7 +799,6 @@
(d/update-in-when data [:pages-index page-id :objects] reg-objects)))
(defmethod process-change :mov-objects
[data {:keys [parent-id shapes index page-id] :as change}]
(letfn [(is-valid-move? [objects shape-id]
@ -641,34 +810,40 @@
(let [prev-shapes (or prev-shapes [])]
(if index
(cph/insert-at-index prev-shapes index shapes)
(reduce (fn [acc id]
(if (some #{id} acc)
acc
(conj acc id)))
prev-shapes
shapes))))
(cph/append-at-the-end prev-shapes shapes))))
(check-insert-items [prev-shapes parent index shapes]
(if-not (:masked-group? parent)
(insert-items prev-shapes index shapes)
;; For masked groups, the first shape is the mask
;; and it cannot be moved.
(let [mask-id (first prev-shapes)
other-ids (rest prev-shapes)
not-mask-shapes (strip-id shapes mask-id)
new-index (if (nil? index) nil (max (dec index) 0))
new-shapes (insert-items other-ids new-index not-mask-shapes)]
(d/concat [mask-id] new-shapes))))
(strip-id [coll id]
(filterv #(not= % id) coll))
(remove-from-old-parent [cpindex objects shape-id]
(let [prev-parent-id (get cpindex shape-id)]
;; Do nothing if the parent id of the shape is the same as
;; the new destination target parent id.
(if (= prev-parent-id parent-id)
objects
(loop [sid shape-id
pid prev-parent-id
objects objects]
(let [obj (get objects pid)]
(if (and (= 1 (count (:shapes obj)))
(= sid (first (:shapes obj)))
(= :group (:type obj)))
(recur pid
(:parent-id obj)
(dissoc objects pid))
(update-in objects [pid :shapes] strip-id sid)))))))
(remove-from-old-parent [cpindex objects shape-id]
(let [prev-parent-id (get cpindex shape-id)]
;; Do nothing if the parent id of the shape is the same as
;; the new destination target parent id.
(if (= prev-parent-id parent-id)
objects
(loop [sid shape-id
pid prev-parent-id
objects objects]
(let [obj (get objects pid)]
(if (and (= 1 (count (:shapes obj)))
(= sid (first (:shapes obj)))
(= :group (:type obj)))
(recur pid
(:parent-id obj)
(dissoc objects pid))
(update-in objects [pid :shapes] strip-id sid)))))))
(update-parent-id [objects id]
(update objects id assoc :parent-id parent-id))
@ -700,7 +875,7 @@
(if valid?
(as-> objects $
(update-in $ [parent-id :shapes] insert-items index shapes)
(update-in $ [parent-id :shapes] check-insert-items parent index shapes)
(reduce update-parent-id $ shapes)
(reduce (partial remove-from-old-parent cpindex) $ shapes)
(reduce (partial update-frame-ids frm-id) $ (get-in $ [parent-id :shapes])))
@ -748,7 +923,7 @@
(defmethod process-change :mod-color
[data {:keys [color]}]
(d/update-in-when data [:colors (:id color)] merge color))
(d/assoc-in-when data [:colors (:id color)] color))
(defmethod process-change :del-color
[data {:keys [id]}]
@ -763,6 +938,8 @@
(subvec rc 1)
rc)))))
;; -- Media
(defmethod process-change :add-media
[data {:keys [object]}]
(update data :media assoc (:id object) object))
@ -775,6 +952,8 @@
[data {:keys [id]}]
(update data :media dissoc id))
;; -- Components
(defmethod process-change :add-component
[data {:keys [id name shapes]}]
(assoc-in data [:components id]
@ -793,16 +972,51 @@
[data {:keys [id]}]
(d/dissoc-in data [:components id]))
;; -- Typography
(defmethod process-change :add-typography
[data {:keys [typography]}]
(update data :typographies assoc (:id typography) typography))
(defmethod process-change :mod-typography
[data {:keys [typography]}]
(d/update-in-when data [:typographies (:id typography)] merge typography))
(defmethod process-change :del-typography
[data {:keys [id]}]
(update data :typographies dissoc id))
;; -- Operations
(defmethod process-operation :set
[shape op]
(let [attr (:attr op)
val (:val op)]
(if (nil? val)
(dissoc shape attr)
(assoc shape attr val))))
(let [attr (:attr op)
val (:val op)
ignore (:ignore-touched op)
shape-ref (:shape-ref shape)
group (get component-sync-attrs attr)]
(cond-> shape
(and shape-ref group (not ignore) (not= val (get shape attr)))
(update :touched #(conj (or % #{}) group))
(nil? val)
(dissoc attr)
(some? val)
(assoc attr val))))
(defmethod process-operation :set-touched
[shape op]
(let [touched (:touched op)
shape-ref (:shape-ref shape)]
(if (or (nil? shape-ref) (nil? touched) (empty? touched))
(dissoc shape :touched)
(assoc shape :touched touched))))
(defmethod process-operation :default
[shape op]
(ex/raise :type :not-implemented
:code :operation-not-implemented
:context {:type (:type op)}))

View File

@ -10,7 +10,8 @@
(ns app.common.pages-helpers
(:require
[app.common.data :as d]
[app.common.uuid :as uuid]))
[app.common.uuid :as uuid]
[app.common.geom.shapes :as gsh]))
(defn walk-pages
"Go through all pages of a file and apply a function to each one"
@ -20,9 +21,10 @@
(update data :pages-index #(d/mapm f %)))
(defn select-objects
"Get a list of all objects in a page that satisfy a condition"
[f page]
(filter f (vals (get page :objects))))
"Get a list of all objects in a container (a page or a component) that
satisfy a condition"
[f container]
(filter f (vals (get container :objects))))
(defn update-object-list
"Update multiple objects in a page at once"
@ -30,15 +32,15 @@
(update page :objects
#(into % (d/index-by :id objects-list))))
(defn get-root-component
"Get the root shape linked to the component for this shape, if any"
[id objects]
(let [obj (get objects id)]
(if-let [component-id (:component-id obj)]
id
(if-let [parent-id (:parent-id obj)]
(get-root-component parent-id obj)
nil))))
(defn get-root-shape
"Get the root shape linked to a component for this shape, if any"
[shape objects]
(if (:component-root? shape)
shape
(if-let [parent-id (:parent-id shape)]
(get-root-shape (get objects (:parent-id shape))
objects)
nil)))
(defn get-children
"Retrieve all children ids recursively for a given object"
@ -57,7 +59,7 @@
(defn get-object-with-children
"Retrieve a list with an object and all of its children"
[id objects]
(map #(get objects %) (concat [id] (get-children id objects))))
(map #(get objects %) (cons id (get-children id objects))))
(defn is-shape-grouped
"Checks if a shape is inside a group"
@ -115,6 +117,15 @@
ids
(remove p? after))))
(defn append-at-the-end
[prev-ids ids]
(reduce (fn [acc id]
(if (some #{id} acc)
acc
(conj acc id)))
prev-ids
ids))
(defn select-toplevel-shapes
([objects] (select-toplevel-shapes objects nil))
([objects {:keys [include-frames?] :or {include-frames? false}}]
@ -186,7 +197,7 @@
updated-object (update-original-object object new-object)
updated-objects (if (= object updated-object)
updated-objects (if (identical? object updated-object)
updated-children
(concat [updated-object] updated-children))]
@ -204,3 +215,44 @@
(concat new-children new-child-objects)
(concat updated-children updated-child-objects))))))))
(defn indexed-shapes
"Retrieves a list with the indexes for each element in the layer tree.
This will be used for shift+selection."
[objects]
(let [rec-index
(fn rec-index [cur-idx id]
(let [object (get objects id)
red-fn
(fn [cur-idx id]
(let [[prev-idx _] (first cur-idx)
prev-idx (or prev-idx 0)
cur-idx (conj cur-idx [(inc prev-idx) id])]
(rec-index cur-idx id)))]
(reduce red-fn cur-idx (reverse (:shapes object)))))]
(into {} (rec-index '() uuid/zero))))
(defn expand-region-selection
"Given a selection selects all the shapes between the first and last in
an indexed manner (shift selection)"
[objects selection]
(let [indexed-shapes (indexed-shapes objects)
filter-indexes (->> indexed-shapes
(filter (comp selection second))
(map first))
from (apply min filter-indexes)
to (apply max filter-indexes)]
(->> indexed-shapes
(filter (fn [[idx _]] (and (>= idx from) (<= idx to))))
(map second)
(into #{}))))
(defn frame-id-by-position [objects position]
(let [frames (select-frames objects)]
(or
(->> frames
(d/seek #(gsh/has-point? % position))
:id)
uuid/zero)))

View File

@ -12,11 +12,16 @@
(:refer-clojure :exclude [assert])
#?(:cljs (:require-macros [app.common.spec :refer [assert]]))
(:require
#?(:clj [clojure.spec.alpha :as s]
#?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])
#?(:clj [clojure.spec.test.alpha :as stest]
:cljs [cljs.spec.test.alpha :as stest])
[expound.alpha :as expound]
[app.common.uuid :as uuid]
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[cuerdas.core :as str]))
(s/check-asserts true)
@ -38,7 +43,7 @@
(if (string? v)
(if (re-matches uuid-rx v)
(uuid/uuid v)
(if (str/empty? v) nil ::s/invalid))
::s/invalid)
::s/invalid)))
(defn boolean-conformer
@ -87,9 +92,21 @@
v
::s/invalid))
(defn keyword-conformer
[v]
(cond
(keyword? v)
v
(string? v)
(keyword v)
:else
::s/invalid))
;; --- Default Specs
(s/def ::keyword keyword?)
(s/def ::keyword (s/conformer keyword-conformer name))
(s/def ::inst inst?)
(s/def ::string string?)
(s/def ::email (s/conformer email-conformer str))
@ -101,27 +118,54 @@
(s/def ::not-empty-string (s/and string? #(not (str/empty? %))))
(s/def ::url string?)
(s/def ::fn fn?)
(s/def ::point gpt/point?)
;; --- Macros
(defn spec-assert
[spec x]
[spec x message]
(if (s/valid? spec x)
x
(ex/raise :type :assertion
:data (s/explain-data spec x)
#?@(:cljs [:stack (.-stack (ex-info "assertion" {}))]))))
:message message
#?@(:cljs [:stack (.-stack (ex-info message {}))]))))
(defn spec-assert*
[spec x message context]
(if (s/valid? spec x)
x
(ex/raise :type :assertion
:data (s/explain-data spec x)
:context context
:message message
#?@(:cljs [:stack (.-stack (ex-info message {}))]))))
(defmacro assert
"Development only assertion macro."
[spec x]
(when *assert*
`(spec-assert ~spec ~x)))
(let [nsdata (:ns &env)
context (when nsdata
{:ns (str (:name nsdata))
:name (pr-str spec)
:line (:line &env)
:file (:file (:meta nsdata))})
message (str "Spec Assertion: '" (pr-str spec) "'")]
`(spec-assert* ~spec ~x ~message ~context))))
(defmacro verify
"Always active assertion macro (does not obey to :elide-asserts)"
[spec x]
`(spec-assert ~spec ~x))
(let [nsdata (:ns &env)
context (when nsdata
{:ns (str (:name nsdata))
:name (pr-str spec)
:line (:line &env)
:file (:file (:meta nsdata))})
message (str "Spec Assertion: '" (pr-str spec) "'")]
`(spec-assert* ~spec ~x ~message ~context)))
;; --- Public Api
@ -136,3 +180,14 @@
(expound/printer edata))
:data (::s/problems edata)))))
result))
(defmacro instrument!
[& {:keys [sym spec]}]
(when *assert*
(let [message (str "Spec failed on: " sym)]
`(let [origf# ~sym
mdata# (meta (var ~sym))]
(set! ~sym (fn [& params#]
(spec-assert* ~spec params# ~message mdata#)
(apply origf# params#)))))))

View File

@ -4,18 +4,16 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ARG EXTERNAL_UID=1000
ENV NODE_VERSION=v12.18.4 \
CLOJURE_VERSION=1.10.1.681 \
ENV NODE_VERSION=v12.19.0 \
CLOJURE_VERSION=1.10.1.727 \
LANG=en_US.UTF-8 \
LC_ALL=C.UTF-8
LC_ALL=en_US.UTF-8
RUN set -ex; \
mkdir -p /etc/resolvconf/resolv.conf.d; \
echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail;
RUN set -ex; \
apt-get update && \
apt-get install -yq \
echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail; \
apt-get -qq update; \
apt-get -qqy install --no-install-recommends \
locales \
gnupg2 \
ca-certificates \
@ -27,6 +25,19 @@ RUN set -ex; \
bash \
git \
rlwrap \
; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
useradd -m -g users -s /bin/bash -u $EXTERNAL_UID uxbox; \
passwd uxbox -d; \
echo "uxbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install --no-install-recommends \
python \
build-essential \
imagemagick \
@ -40,8 +51,8 @@ RUN set -ex; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
apt-get update && \
apt-get install -yq \
apt-get -qq update; \
apt-get -qqy install \
gconf-service \
libasound2 \
libatk1.0-0 \
@ -80,30 +91,25 @@ RUN set -ex; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 0xB1998361219BD9C9; \
echo "deb http://repos.azulsystems.com/debian stable main" >> /etc/apt/sources.list.d/zulu.list; \
mkdir -p /usr/share/man/man1; \
mkdir -p /usr/share/man/man7; \
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add -; \
echo "deb https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ buster main" >> /etc/apt/sources.list.d/adoptopenjdk.list; \
apt-get -qq update; \
apt-get -qqy install zulu-14; \
apt-get -qqy install adoptopenjdk-15-hotspot; \
rm -rf /var/lib/apt/lists/*; \
wget "https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh"; \
chmod +x "linux-install-$CLOJURE_VERSION.sh"; \
"./linux-install-$CLOJURE_VERSION.sh"; \
rm -rf "linux-install-$CLOJURE_VERSION.sh"
ENV JAVA_HOME=/usr/lib/jvm/zulu-14-amd64
RUN set -ex; \
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -; \
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \
apt-get -qq update; \
apt-get -qqy install postgresql-client-12; \
apt-get -qqy install postgresql-client-13; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
useradd -m -g users -s /bin/bash -u $EXTERNAL_UID uxbox; \
passwd uxbox -d; \
echo "uxbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN set -ex; \
wget https://github.com/RazrFalcon/svgcleaner/releases/download/v0.9.5/svgcleaner_linux_x86_64_0.9.5.tar.gz; \
tar xvf svgcleaner_linux_x86_64_0.9.5.tar.gz; \
@ -111,12 +117,12 @@ RUN set -ex; \
rm -rf svgcleaner_linux_x86_64_0.9.5.tar.gz;
COPY files/phantomjs-mock /usr/bin/phantomjs
COPY files/bashrc /root/.bashrc
COPY files/vimrc /root/.vimrc
COPY files/tmux.conf /root/.tmux.conf
COPY files/start-tmux.sh /home/start-tmux.sh
COPY files/entrypoint.sh /home/entrypoint.sh
COPY files/init.sh /home/init.sh
COPY files/bashrc /root/.bashrc
COPY files/vimrc /root/.vimrc
COPY files/tmux.conf /root/.tmux.conf
COPY files/start-tmux.sh /home/start-tmux.sh
COPY files/entrypoint.sh /home/entrypoint.sh
COPY files/init.sh /home/init.sh
USER uxbox
WORKDIR /home/uxbox
@ -134,5 +140,5 @@ EXPOSE 3449
EXPOSE 6060
EXPOSE 9090
ENTRYPOINT ["bash", "/home/entrypoint.sh"]
ENTRYPOINT ["/home/entrypoint.sh"]
CMD ["/home/init.sh"]

View File

@ -55,13 +55,11 @@ services:
- OPENDKIM_DOMAINS=smtp.uxbox.io
postgres:
image: postgres:12
image: postgres:13
command: postgres -c config_file=/etc/postgresql.conf
container_name: "uxbox-devenv-postgres"
restart: always
stop_signal: SIGINT
ports:
- 5432:5432
environment:
- POSTGRES_INITDB_ARGS=--data-checksums
- POSTGRES_DB=uxbox
@ -77,6 +75,3 @@ services:
hostname: "uxbox-devenv-redis"
container_name: "uxbox-devenv-redis"
restart: always
ports:
- 6379:6379

View File

@ -1,13 +1,12 @@
#!/usr/bin/env bash
source /home/uxbox/.bashrc
set -ex
set -e
sudo cp /root/.bashrc /home/uxbox/.bashrc
sudo cp /root/.vimrc /home/uxbox/.vimrc
sudo cp /root/.tmux.conf /home/uxbox/.tmux.conf
source /home/uxbox/.bashrc
sudo chown uxbox:users /home/uxbox
exec "$@"

View File

@ -1,753 +1,22 @@
# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
# This file consists of lines of the form:
#
# name = value
#
# (The "=" is optional.) Whitespace may be used. Comments are introduced with
# "#" anywhere on a line. The complete list of parameter names and allowed
# values can be found in the PostgreSQL documentation.
#
# The commented-out settings shown in this file represent the default values.
# Re-commenting a setting is NOT sufficient to revert it to the default value;
# you need to reload the server.
#
# This file is read on server startup and when the server receives a SIGHUP
# signal. If you edit the file on a running system, you have to SIGHUP the
# server for the changes to take effect, run "pg_ctl reload", or execute
# "SELECT pg_reload_conf()". Some parameters, which are marked below,
# require a server shutdown and restart to take effect.
#
# Any parameter can also be given as a command-line option to the server, e.g.,
# "postgres -c log_connections=on". Some parameters can be changed at run time
# with the "SET" SQL command.
#
# Memory units: kB = kilobytes Time units: ms = milliseconds
# MB = megabytes s = seconds
# GB = gigabytes min = minutes
# TB = terabytes h = hours
# d = days
#------------------------------------------------------------------------------
# FILE LOCATIONS
#------------------------------------------------------------------------------
# The default values of these variables are driven from the -D command-line
# option or PGDATA environment variable, represented here as ConfigDir.
#data_directory = 'ConfigDir' # use data in another directory
# (change requires restart)
#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
# (change requires restart)
#------------------------------------------------------------------------------
# CONNECTIONS AND AUTHENTICATION
#------------------------------------------------------------------------------
# - Connection Settings -
listen_addresses = '*'
# comma-separated list of addresses;
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories
# (change requires restart)
#unix_socket_group = '' # (change requires restart)
#unix_socket_permissions = 0777 # begin with 0 to use octal notation
# (change requires restart)
#bonjour = off # advertise server via Bonjour
# (change requires restart)
#bonjour_name = '' # defaults to the computer name
# (change requires restart)
max_connections = 100
shared_buffers = 128MB
temp_buffers = 8MB
work_mem = 8MB
# - TCP settings -
# see "man 7 tcp" for details
#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds;
# 0 selects the system default
#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds;
# 0 selects the system default
#tcp_keepalives_count = 0 # TCP_KEEPCNT;
# 0 selects the system default
#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds;
# 0 selects the system default
# - Authentication -
#authentication_timeout = 1min # 1s-600s
#password_encryption = md5 # md5 or scram-sha-256
#db_user_namespace = off
# GSSAPI using Kerberos
#krb_server_keyfile = ''
#krb_caseins_users = off
# - SSL -
#ssl = off
#ssl_ca_file = ''
#ssl_cert_file = 'server.crt'
#ssl_crl_file = ''
#ssl_key_file = 'server.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_min_protocol_version = 'TLSv1'
#ssl_max_protocol_version = ''
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
#------------------------------------------------------------------------------
# RESOURCE USAGE (except WAL)
#------------------------------------------------------------------------------
# - Memory -
shared_buffers = 256MB # min 128kB
# (change requires restart)
#huge_pages = try # on, off, or try
# (change requires restart)
temp_buffers = 8MB # min 800kB
#max_prepared_transactions = 0 # zero disables the feature
# (change requires restart)
# Caution: it is not advisable to set max_prepared_transactions nonzero unless
# you actively intend to use prepared transactions.
work_mem = 8MB # min 64kB
#maintenance_work_mem = 64MB # min 1MB
#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem
#max_stack_depth = 2MB # min 100kB
#shared_memory_type = mmap # the default is the first option
# supported by the operating system:
# mmap
# sysv
# windows
# (change requires restart)
dynamic_shared_memory_type = posix # the default is the first option
# supported by the operating system:
# posix
# sysv
# windows
# mmap
# (change requires restart)
# - Disk -
#temp_file_limit = -1 # limits per-process temp file space
# in kB, or -1 for no limit
# - Kernel Resources -
#max_files_per_process = 1000 # min 25
# (change requires restart)
# - Cost-Based Vacuum Delay -
#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables)
#vacuum_cost_page_hit = 1 # 0-10000 credits
#vacuum_cost_page_miss = 10 # 0-10000 credits
#vacuum_cost_page_dirty = 20 # 0-10000 credits
#vacuum_cost_limit = 200 # 1-10000 credits
# - Background Writer -
#bgwriter_delay = 200ms # 10-10000ms between rounds
#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables
#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round
#bgwriter_flush_after = 512kB # measured in pages, 0 disables
# - Asynchronous Behavior -
#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching
#max_worker_processes = 8 # (change requires restart)
#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers
#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers
#parallel_leader_participation = on
#max_parallel_workers = 8 # maximum number of max_worker_processes that
# can be used in parallel operations
#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate
# (change requires restart)
#backend_flush_after = 0 # measured in pages, 0 disables
#------------------------------------------------------------------------------
# WRITE-AHEAD LOG
#------------------------------------------------------------------------------
# - Settings -
#wal_level = replica # minimal, replica, or logical
# (change requires restart)
#fsync = on # flush data to disk for crash safety
# (turning this off can cause
# unrecoverable data corruption)
synchronous_commit = off # synchronization level;
# off, local, remote_write, remote_apply, or on
#wal_sync_method = fsync # the default is the first option
# supported by the operating system:
# open_datasync
# fdatasync (default on Linux)
# fsync
# fsync_writethrough
# open_sync
#full_page_writes = on # recover from partial page writes
#wal_compression = off # enable compression of full-page writes
#wal_log_hints = off # also do full page writes of non-critical updates
# (change requires restart)
#wal_init_zero = on # zero-fill new WAL files
#wal_recycle = on # recycle WAL files
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
# (change requires restart)
wal_writer_delay = 900ms # 1-10000 milliseconds
#wal_writer_flush_after = 1MB # measured in pages, 0 disables
#commit_delay = 0 # range 0-100000, in microseconds
#commit_siblings = 5 # range 1-1000
# - Checkpoints -
#checkpoint_timeout = 5min # range 30s-1d
dynamic_shared_memory_type = posix
synchronous_commit = off
wal_writer_delay = 900ms
max_wal_size = 1GB
min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_flush_after = 256kB # measured in pages, 0 disables
#checkpoint_warning = 30s # 0 disables
# - Archiving -
#archive_mode = off # enables archiving; off, on, or always
# (change requires restart)
#archive_command = '' # command to use to archive a logfile segment
# placeholders: %p = path of file to archive
# %f = file name only
# e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
#archive_timeout = 0 # force a logfile segment switch after this
# number of seconds; 0 disables
# - Archive Recovery -
# These are only used in recovery mode.
#restore_command = '' # command to use to restore an archived logfile segment
# placeholders: %p = path of file to restore
# %f = file name only
# e.g. 'cp /mnt/server/archivedir/%f %p'
# (change requires restart)
#archive_cleanup_command = '' # command to execute at every restartpoint
#recovery_end_command = '' # command to execute at completion of recovery
# - Recovery Target -
# Set these only when performing a targeted recovery.
#recovery_target = '' # 'immediate' to end recovery as soon as a
# consistent state is reached
# (change requires restart)
#recovery_target_name = '' # the named restore point to which recovery will proceed
# (change requires restart)
#recovery_target_time = '' # the time stamp up to which recovery will proceed
# (change requires restart)
#recovery_target_xid = '' # the transaction ID up to which recovery will proceed
# (change requires restart)
#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed
# (change requires restart)
#recovery_target_inclusive = on # Specifies whether to stop:
# just after the specified recovery target (on)
# just before the recovery target (off)
# (change requires restart)
#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID
# (change requires restart)
#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown'
# (change requires restart)
#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------
# - Sending Servers -
# Set these on the master and on any standby that will send replication data.
#max_wal_senders = 10 # max number of walsender processes
# (change requires restart)
#wal_keep_segments = 0 # in logfile segments; 0 disables
#wal_sender_timeout = 60s # in milliseconds; 0 disables
#max_replication_slots = 10 # max number of replication slots
# (change requires restart)
#track_commit_timestamp = off # collect timestamp of transaction commit
# (change requires restart)
# - Master Server -
# These settings are ignored on a standby server.
#synchronous_standby_names = '' # standby servers that provide sync rep
# method to choose sync standbys, number of sync standbys,
# and comma-separated list of application_name
# from standby(s); '*' = all
#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed
# - Standby Servers -
# These settings are ignored on a master server.
#primary_conninfo = '' # connection string to sending server
# (change requires restart)
#primary_slot_name = '' # replication slot on sending server
# (change requires restart)
#promote_trigger_file = '' # file name whose presence ends recovery
#hot_standby = on # "off" disallows queries during recovery
# (change requires restart)
#max_standby_archive_delay = 30s # max delay before canceling queries
# when reading WAL from archive;
# -1 allows indefinite delay
#max_standby_streaming_delay = 30s # max delay before canceling queries
# when reading streaming WAL;
# -1 allows indefinite delay
#wal_receiver_status_interval = 10s # send replies at least this often
# 0 disables
#hot_standby_feedback = off # send info from standby to prevent
# query conflicts
#wal_receiver_timeout = 60s # time that receiver waits for
# communication from master
# in milliseconds; 0 disables
#wal_retrieve_retry_interval = 5s # time to wait before retrying to
# retrieve WAL after a failed attempt
#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery
# - Subscribers -
# These settings are ignored on a publisher.
#max_logical_replication_workers = 4 # taken from max_worker_processes
# (change requires restart)
#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers
#------------------------------------------------------------------------------
# QUERY TUNING
#------------------------------------------------------------------------------
# - Planner Method Configuration -
#enable_bitmapscan = on
#enable_hashagg = on
#enable_hashjoin = on
#enable_indexscan = on
#enable_indexonlyscan = on
#enable_material = on
#enable_mergejoin = on
#enable_nestloop = on
#enable_parallel_append = on
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on
#enable_partitionwise_join = off
#enable_partitionwise_aggregate = off
#enable_parallel_hash = on
#enable_partition_pruning = on
# - Planner Cost Constants -
#seq_page_cost = 1.0 # measured on an arbitrary scale
#random_page_cost = 4.0 # same scale as above
#cpu_tuple_cost = 0.01 # same scale as above
#cpu_index_tuple_cost = 0.005 # same scale as above
#cpu_operator_cost = 0.0025 # same scale as above
#parallel_tuple_cost = 0.1 # same scale as above
#parallel_setup_cost = 1000.0 # same scale as above
#jit_above_cost = 100000 # perform JIT compilation if available
# and query more expensive than this;
# -1 disables
#jit_inline_above_cost = 500000 # inline small functions if query is
# more expensive than this; -1 disables
#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if
# query is more expensive than this;
# -1 disables
#min_parallel_table_scan_size = 8MB
#min_parallel_index_scan_size = 512kB
#effective_cache_size = 4GB
# - Genetic Query Optimizer -
#geqo = on
#geqo_threshold = 12
#geqo_effort = 5 # range 1-10
#geqo_pool_size = 0 # selects default based on effort
#geqo_generations = 0 # selects default based on effort
#geqo_selection_bias = 2.0 # range 1.5-2.0
#geqo_seed = 0.0 # range 0.0-1.0
# - Other Planner Options -
#default_statistics_target = 100 # range 1-10000
#constraint_exclusion = partition # on, off, or partition
#cursor_tuple_fraction = 0.1 # range 0.0-1.0
#from_collapse_limit = 8
#join_collapse_limit = 8 # 1 disables collapsing of explicit
# JOIN clauses
#force_parallel_mode = off
#jit = on # allow JIT compilation
#plan_cache_mode = auto # auto, force_generic_plan or
# force_custom_plan
#------------------------------------------------------------------------------
# REPORTING AND LOGGING
#------------------------------------------------------------------------------
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
# stderr, csvlog, syslog, and eventlog,
# depending on platform. csvlog
# requires logging_collector to be on.
# This is used when logging to stderr:
#logging_collector = off # Enable capturing of stderr and csvlog
# into log files. Required to be on for
# csvlogs.
# (change requires restart)
# These are only used if logging_collector is on:
#log_directory = 'log' # directory where log files are written,
# can be absolute or relative to PGDATA
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
# can include strftime() escapes
#log_file_mode = 0600 # creation mode for log files,
# begin with 0 to use octal notation
#log_truncate_on_rotation = off # If on, an existing log file with the
# same name as the new log file will be
# truncated rather than appended to.
# But such truncation only occurs on
# time-driven rotation, not on restarts
# or size-driven rotation. Default is
# off, meaning append to existing files
# in all cases.
#log_rotation_age = 1d # Automatic rotation of logfiles will
# happen after that time. 0 disables.
#log_rotation_size = 10MB # Automatic rotation of logfiles will
# happen after that much log output.
# 0 disables.
# These are relevant when logging to syslog:
#syslog_facility = 'LOCAL0'
#syslog_ident = 'postgres'
#syslog_sequence_numbers = on
#syslog_split_messages = on
# This is only relevant when logging to eventlog (win32):
# (change requires restart)
#event_source = 'PostgreSQL'
# - When to Log -
#log_min_messages = warning # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic
#log_min_error_statement = error # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic (effectively off)
#log_min_duration_statement = 0 # -1 is disabled, 0 logs all statements
# and their durations, > 0 logs only
# statements running at least this number
# of milliseconds
#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements
# are logged regardless of their duration. 1.0 logs all
# statements from all transactions, 0.0 never logs.
# - What to Log -
#debug_print_parse = off
#debug_print_rewritten = off
#debug_print_plan = off
#debug_pretty_print = on
#log_checkpoints = off
#log_connections = off
#log_disconnections = off
#log_duration = off
#log_error_verbosity = default # terse, default, or verbose messages
#log_hostname = off
#log_line_prefix = '%m [%p] ' # special values:
# %a = application name
# %u = user name
# %d = database name
# %r = remote host and port
# %h = remote host
# %p = process ID
# %t = timestamp without milliseconds
# %m = timestamp with milliseconds
# %n = timestamp with milliseconds (as a Unix epoch)
# %i = command tag
# %e = SQL state
# %c = session ID
# %l = session line number
# %s = session start timestamp
# %v = virtual transaction ID
# %x = transaction ID (0 if none)
# %q = stop here in non-session
# processes
# %% = '%'
# e.g. '<%u%%%d> '
#log_lock_waits = off # log lock waits >= deadlock_timeout
#log_statement = 'none' # none, ddl, mod, all
#log_replication_commands = off
#log_temp_files = -1 # log temporary files equal or larger
# than the specified size in kilobytes;
# -1 disables, 0 logs all temp files
log_timezone = 'Etc/UTC'
#------------------------------------------------------------------------------
# PROCESS TITLE
#------------------------------------------------------------------------------
#cluster_name = '' # added to process titles if nonempty
# (change requires restart)
#update_process_title = on
#------------------------------------------------------------------------------
# STATISTICS
#------------------------------------------------------------------------------
# - Query and Index Statistics Collector -
#track_activities = on
#track_counts = on
#track_io_timing = off
#track_functions = none # none, pl, all
#track_activity_query_size = 1024 # (change requires restart)
#stats_temp_directory = 'pg_stat_tmp'
# - Monitoring -
#log_parser_stats = off
#log_planner_stats = off
#log_executor_stats = off
#log_statement_stats = off
#------------------------------------------------------------------------------
# AUTOVACUUM
#------------------------------------------------------------------------------
#autovacuum = on # Enable autovacuum subprocess? 'on'
# requires track_counts to also be on.
#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and
# their durations, > 0 logs only
# actions running at least this number
# of milliseconds.
#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
# (change requires restart)
#autovacuum_naptime = 1min # time between autovacuum runs
#autovacuum_vacuum_threshold = 50 # min number of row updates before
# vacuum
#autovacuum_analyze_threshold = 50 # min number of row updates before
# analyze
#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum
# (change requires restart)
#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
# before forced vacuum
# (change requires restart)
#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for
# autovacuum, in milliseconds;
# -1 means use vacuum_cost_delay
#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
# autovacuum, -1 means use
# vacuum_cost_limit
#------------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
#------------------------------------------------------------------------------
# - Statement Behavior -
#client_min_messages = notice # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# log
# notice
# warning
# error
#search_path = '"$user", public' # schema names
#row_security = on
#default_tablespace = '' # a tablespace name, '' uses the default
#temp_tablespaces = '' # a list of tablespace names, '' uses
# only default tablespace
#default_table_access_method = 'heap'
#check_function_bodies = on
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
#vacuum_freeze_min_age = 50000000
#vacuum_freeze_table_age = 150000000
#vacuum_multixact_freeze_min_age = 5000000
#vacuum_multixact_freeze_table_age = 150000000
#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples
# before index cleanup, 0 always performs
# index cleanup
#bytea_output = 'hex' # hex, escape
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_fuzzy_search_limit = 0
#gin_pending_list_limit = 4MB
# - Locale and Formatting -
# log_min_duration_statement = 0
log_timezone = 'Europe/Madrid'
datestyle = 'iso, mdy'
#intervalstyle = 'postgres'
timezone = 'Etc/UTC'
#timezone_abbreviations = 'Default' # Select the set of available time zone
# abbreviations. Currently, there are
# Default
# Australia (historical usage)
# India
# You can create your own file in
# share/timezonesets/.
#extra_float_digits = 1 # min -15, max 3; any value >0 actually
# selects precise output mode
#client_encoding = sql_ascii # actually, defaults to database
# encoding
# These settings are initialized by initdb, but they can be changed.
lc_messages = 'en_US.utf8' # locale for system error message
# strings
lc_monetary = 'en_US.utf8' # locale for monetary formatting
lc_numeric = 'en_US.utf8' # locale for number formatting
lc_time = 'en_US.utf8' # locale for time formatting
# default configuration for text search
timezone = 'Europe/Madrid'
lc_messages = 'en_US.utf8'
lc_monetary = 'en_US.utf8'
lc_numeric = 'en_US.utf8'
lc_time = 'en_US.utf8'
default_text_search_config = 'pg_catalog.english'
# - Shared Library Preloading -
#shared_preload_libraries = '' # (change requires restart)
#local_preload_libraries = ''
#session_preload_libraries = ''
#jit_provider = 'llvmjit' # JIT library to use
# - Other Defaults -
#dynamic_library_path = '$libdir'
#------------------------------------------------------------------------------
# LOCK MANAGEMENT
#------------------------------------------------------------------------------
#deadlock_timeout = 1s
#max_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_relation = -2 # negative values mean
# (max_pred_locks_per_transaction
# / -max_pred_locks_per_relation) - 1
#max_pred_locks_per_page = 2 # min 0
#------------------------------------------------------------------------------
# VERSION AND PLATFORM COMPATIBILITY
#------------------------------------------------------------------------------
# - Previous PostgreSQL Versions -
#array_nulls = on
#backslash_quote = safe_encoding # on, off, or safe_encoding
#escape_string_warning = on
#lo_compat_privileges = off
#operator_precedence_warning = off
#quote_all_identifiers = off
#standard_conforming_strings = on
#synchronize_seqscans = on
# - Other Platforms and Clients -
#transform_null_equals = off
#------------------------------------------------------------------------------
# ERROR HANDLING
#------------------------------------------------------------------------------
#exit_on_error = off # terminate session on any error?
#restart_after_crash = on # reinitialize after backend crash?
#data_sync_retry = off # retry or panic on failure to fsync
# data?
# (change requires restart)
#------------------------------------------------------------------------------
# CONFIG FILE INCLUDES
#------------------------------------------------------------------------------
# These options allow settings to be loaded from files other than the
# default postgresql.conf. Note that these are directives, not variable
# assignments, so they can usefully be given more than once.
#include_dir = '...' # include files ending in '.conf' from
# a directory, e.g., 'conf.d'
#include_if_exists = '...' # include file only if it exists
#include = '...' # include file
#------------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
#------------------------------------------------------------------------------
# Add settings for extensions here

View File

@ -1,4 +1,4 @@
FROM azul/zulu-openjdk-debian:14
FROM adoptopenjdk/openjdk15:debianslim-jre
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ADD ./bundle/backend/ /opt/bundle/
WORKDIR /opt/bundle

View File

@ -1,24 +1,35 @@
FROM debian:buster
FROM debian:buster-slim
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ENV LANG=en_US.UTF-8 LC_ALL=C.UTF-8
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NODE_VERSION=v12.18.4
RUN set -ex; \
mkdir -p /etc/resolvconf/resolv.conf.d; \
echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail;
RUN set -ex; \
apt-get update && \
apt-get install -yq \
echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail; \
apt-get -qq update; \
apt-get -qqy install --no-install-recommends \
locales \
gnupg2 \
ca-certificates \
wget \
sudo \
vim \
curl \
bash \
xz-utils \
rlwrap \
; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install \
imagemagick \
netpbm \
potrace \
@ -69,11 +80,11 @@ RUN set -ex; \
RUN set -ex; \
mkdir -p /tmp/node; \
cd /tmp/node; \
export PATH="$PATH:/usr/local/node-v12.18.3/bin"; \
wget https://nodejs.org/dist/v12.18.3/node-v12.18.3-linux-x64.tar.xz; \
tar xvf node-v12.18.3-linux-x64.tar.xz; \
mv /tmp/node/node-v12.18.3-linux-x64 /usr/local/node-v12.18.3; \
/usr/local/node-v12.18.3/bin/npm install -g yarn; \
export PATH="$PATH:/usr/local/nodejs/bin"; \
wget https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.xz; \
tar xvf node-$NODE_VERSION-linux-x64.tar.xz; \
mv /tmp/node/node-$NODE_VERSION-linux-x64 /usr/local/nodejs; \
/usr/local/nodejs/bin/npm install -g yarn; \
rm -rf /tmp/node;
WORKDIR /opt/app
@ -81,7 +92,7 @@ WORKDIR /opt/app
ADD ./bundle/exporter/ /opt/app/
RUN set -ex; \
export PATH="$PATH:/usr/local/node-v12.18.3/bin"; \
export PATH="$PATH:/usr/local/nodejs/bin"; \
yarn install;
CMD ["/usr/local/node-v12.18.3/bin/node", "app.js"]
CMD ["/usr/local/nodejs/bin/node", "app.js"]

View File

@ -1,6 +1,6 @@
# Developer Guide #
This is a generic "getting started" guide for the uxbox platform. It
This is a generic "getting started" guide for the Penpot platform. It
intends to explain how to get the development environment up and
running with many additional tips.
@ -148,7 +148,7 @@ For more information, please refer to: `03-Backend-Guide.md`.
## Start the testenv ##
The purpose of the testenv (Test Environment) is provide an easy way
to get uxbox running in local pc without getting into the full
to get Penpot running in local pc without getting into the full
development environment.
As first step we still need to build devenv image because that image
@ -185,5 +185,5 @@ This will generate the necessary docker images ready to be executed.
And finally, start the docker-compose:
```bash
./manage.sh start-devenv
./manage.sh start-testenv
```

View File

@ -26,19 +26,22 @@ app.util.debug.debug_all()
app.util.debug.debug_none()
```
## Traces and step-by-step debugging
## Logging, Tracing & Debugging
There are some useful functions to trace program execution:
As a traditional way for debugging and tracing you have the followimg approach:
Print data to the devtool console using clojurescript helper:
**prn**. This helper automatically formats the clojure and js data
structures as plain EDN for easy visual inspection of the data and the
type of the data.
```clojure
(println "message" expression)
; Outputs data to the devtools console. Clojure variables are converted to string, as in (str) function.
(prn "message" expression)
```
```clojure
(js/console.log "message" (clj->js expression))
; Clojure values are converted to equivalent js objects, and displayed as a foldable widget in console.
```
An alternative is using the pprint function, usefull for pretty
printing a medium-big data sturcture for completly understand it.
```clojure
(:require [cljs.pprint :refer [pprint]])
@ -46,19 +49,82 @@ There are some useful functions to trace program execution:
; Outputs a clojure value as a string, nicely formatted and with data type information.
```
Use the js native functions for printing data. The clj->js converts
the clojure data sturcture to js data sturcture and it is
inspeccionable in the devtools console.
```clojure
(js/console.log "message" (clj->js expression))
```
Also we can insert breakpoints in the code with this function:
```clojure
(js-debugger)
```
You can also set a breakpoint from the sources tab in devtools. One way of locating a source file is to
output a trace with (js/console.log) and then clicking in the source link that shows in the console.
You can also set a breakpoint from the sources tab in devtools. One
way of locating a source file is to output a trace with
(js/console.log) and then clicking in the source link that shows in
the console.
### Logging framework
Additionally to the traditional way of putting traces in the code, we
have a logging framework with steroids. It is usefull for casual
debugging (as replacement for a `prn` and `js/console.log`) and as a
permanent traces in the code.
You have the ability to specify the logging level per namespace and
all logging is ellided in production build.
Lets start with a simple example:
```clojure
(ns some.ns
(:require [app.util.logging :as log]))
;; This function sets the level to the current namespace; messages
;; with level behind this will not be printed.
(log/set-level! :info)
;; Log some data; The `app.util.logging` has the following
;; functions/macros:
(log/error :msg "error message")
(log/warn :msg "warn message")
(log/info :msg "info message")
(log/debug :msg "debug message")
(log/trace :msg "trace message")
```
Each macro accept arbitrary number of key values pairs:
```clojure
(log/info :foo "bar" :msg "test" :value 1 :items #{1 2 3})
```
Some keys ara treated as special cases for helping in debugging:
```clojure
;; The special case for :js/whatever; if you namespace the key
;; with `js/`, the variable will be printed as javascript
;; inspectionable object.
(let [foobar {:a 1 :b 2}]
(log/info :msg "Some data" :js/data foobar))
;; The special case for `:err`; If you attach this key, the
;; exception stack trace is printed as additional log entry.
```
## Access to clojure from javascript console
The uxbox namespace of the main application is exported, so that is
The penpot namespace of the main application is exported, so that is
accessible from javascript console in Chrome developer tools. Object
names and data types are converted to javascript style. For example
you can emit the event to reset zoom level by typing this at the

View File

@ -6,7 +6,7 @@ and backend, such as: code style hints, architecture dicisions, etc...
## Assertions ##
UXBOX source code has this types of assertions:
Penpot source code has this types of assertions:
**assert**: just using the clojure builtin `assert` macro.
@ -55,7 +55,7 @@ This macro enables you have assetions on production code.
**Why don't use the `clojure.spec.alpha/assert` instead of the `app.common.spec/assert`?**
The uxbox variant does not peforms additional runtime checks for know
The Penpot variant does not peforms additional runtime checks for know
if asserts are disabled in "runtime". As a result it generates much
simplier code at development and production builds.

BIN
docs/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View File

@ -1,9 +1,9 @@
{:dependencies
[[funcool/promesa "5.1.0"]
[[funcool/promesa "6.0.0"]
[danlentz/clj-uuid "0.1.9"]
[funcool/cuerdas "2020.03.26-3"]
[lambdaisland/glogi "1.0.63"]
[metosin/reitit-core "0.5.2"]
[lambdaisland/glogi "1.0.74"]
[metosin/reitit-core "0.5.9"]
[com.cognitect/transit-cljs "0.8.264"]
[frankiesardo/linked "1.3.0"]]

View File

@ -5,22 +5,20 @@
com.cognitect/transit-cljs {:mvn/version "0.8.264"}
environ/environ {:mvn/version "1.2.0"}
metosin/reitit-core {:mvn/version "0.5.5"}
expound/expound {:mvn/version "0.8.5"}
metosin/reitit-core {:mvn/version "0.5.9"}
expound/expound {:mvn/version "0.8.6"}
danlentz/clj-uuid {:mvn/version "0.1.9"}
frankiesardo/linked {:mvn/version "1.3.0"}
funcool/lentes {:mvn/version "1.4.0-SNAPSHOT"}
funcool/beicon {:mvn/version "2020.05.08-2"}
funcool/cuerdas {:mvn/version "2020.03.26-3"}
funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/potok {:mvn/version "2020.08.10-2"}
funcool/promesa {:mvn/version "5.1.0"}
funcool/rumext {:mvn/version "2020.08.21-0"}
funcool/promesa {:mvn/version "6.0.0"}
funcool/rumext {:mvn/version "2020.10.14-1"}
lambdaisland/uri {:mvn/version "1.3.45"
lambdaisland/uri {:mvn/version "1.4.54"
:exclusions [org.clojure/data.json]}
}
@ -37,14 +35,14 @@
funcool/datoteka {:mvn/version "1.2.0"}
binaryage/devtools {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "2.11.4"}
thheller/shadow-cljs {:mvn/version "2.11.5"}
;; i18n parsing
carocad/parcera {:mvn/version "0.11.0"}
org.antlr/antlr4-runtime {:mvn/version "4.7"}}}
:outdated
{:extra-deps {olical/depot {:mvn/version "1.8.4"}}
{:extra-deps {olical/depot {:mvn/version "RELEASE"}}
:main-opts ["-m" "depot.outdated.main"]}
:repl

View File

@ -6,14 +6,14 @@
"license": "SEE LICENSE IN <LICENSE>",
"repository": {
"type": "git",
"url": "https://github.com/app/app"
"url": "https://github.com/penpot/penpot"
},
"browserslist": [
"defaults"
],
"scripts": {},
"devDependencies": {
"autoprefixer": "^9.8.6",
"autoprefixer": "^10.0.1",
"clean-css": "^4.2.3",
"gulp": "4.0.2",
"gulp-gzip": "^1.4.2",
@ -22,21 +22,21 @@
"gulp-rename": "^2.0.0",
"gulp-svg-sprite": "^1.5.0",
"mkdirp": "^1.0.4",
"postcss": "^7.0.32",
"postcss": "^8.1.2",
"rimraf": "^3.0.0",
"sass": "^1.26.10",
"shadow-cljs": "^2.11.0"
"shadow-cljs": "2.11.5"
},
"dependencies": {
"date-fns": "^2.15.0",
"map-stream": "0.0.7",
"mousetrap": "^1.6.5",
"randomcolor": "^0.6.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"rxjs": "^7.0.0-beta.4",
"slate": "^0.58.4",
"slate-react": "^0.58.4",
"react": "17.0.1",
"react-dom": "17.0.1",
"rxjs": "7.0.0-beta.4",
"slate": "^0.59.0",
"slate-react": "^0.59.0",
"source-map-support": "^0.5.16",
"tdigest": "^0.1.1",
"xregexp": "^4.3.0"

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.3 MiB

Some files were not shown because too many files have changed in this diff Show More