Merge branch 'polishing'
@ -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.
|
||||
|
||||
12
README.md
@ -6,14 +6,14 @@
|
||||
[](https://tree.taiga.io/project/uxbox/ "Managed with Taiga.io")
|
||||
|
||||
|
||||
# UXBOX #
|
||||
# PENPOT #
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
59
backend/resources/emails-mjml/invite-to-team/en.mjml
Normal 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 <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mg-body>
|
||||
</mjml>
|
||||
468
backend/resources/emails/invite-to-team/en.html
Normal 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 <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>
|
||||
1
backend/resources/emails/invite-to-team/en.subj
Normal file
@ -0,0 +1 @@
|
||||
Inviation to join {{team}}
|
||||
10
backend/resources/emails/invite-to-team/en.txt
Normal 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.
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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!
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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}})))
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
;; TODO: move to services.
|
||||
|
||||
(ns app.http.session
|
||||
(:require
|
||||
[app.db :as db]
|
||||
|
||||
@ -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?)
|
||||
|
||||
@ -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")}
|
||||
]})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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)
|
||||
);
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
CREATE INDEX IF NOT EXISTS file__project_id__idx ON file (project_id);
|
||||
@ -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;
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)))
|
||||
260
backend/src/app/services/mutations/comments.clj
Normal 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}))))
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)))
|
||||
|
||||
143
backend/src/app/services/mutations/verify_token.clj
Normal 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))
|
||||
|
||||
@ -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))
|
||||
|
||||
109
backend/src/app/services/queries/comments.clj
Normal 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))))
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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))))
|
||||
|
||||
|
||||
|
||||
@ -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]))
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)}))
|
||||
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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#)))))))
|
||||
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 "$@"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
After Width: | Height: | Size: 303 KiB |
@ -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"]]
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
BIN
frontend/resources/fonts/WorkSans-Black.eot
Normal file
16248
frontend/resources/fonts/WorkSans-Black.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/resources/fonts/WorkSans-Black.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-Black.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-Black.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-BlackItalic.eot
Normal file
16277
frontend/resources/fonts/WorkSans-BlackItalic.svg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
frontend/resources/fonts/WorkSans-BlackItalic.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-BlackItalic.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-BlackItalic.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-Bold.eot
Normal file
25069
frontend/resources/fonts/WorkSans-Bold.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/resources/fonts/WorkSans-Bold.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-Bold.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-Bold.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-BoldItalic.eot
Normal file
24489
frontend/resources/fonts/WorkSans-BoldItalic.svg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/resources/fonts/WorkSans-BoldItalic.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-BoldItalic.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-BoldItalic.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraBold.eot
Normal file
25065
frontend/resources/fonts/WorkSans-ExtraBold.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/resources/fonts/WorkSans-ExtraBold.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraBold.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraBold.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraBoldItalic.eot
Normal file
24462
frontend/resources/fonts/WorkSans-ExtraBoldItalic.svg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/resources/fonts/WorkSans-ExtraBoldItalic.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraBoldItalic.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraLight.eot
Normal file
25774
frontend/resources/fonts/WorkSans-ExtraLight.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/resources/fonts/WorkSans-ExtraLight.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraLight.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraLight.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraLightItalic.eot
Normal file
25162
frontend/resources/fonts/WorkSans-ExtraLightItalic.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/resources/fonts/WorkSans-ExtraLightItalic.ttf
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraLightItalic.woff
Normal file
BIN
frontend/resources/fonts/WorkSans-ExtraLightItalic.woff2
Normal file
BIN
frontend/resources/fonts/WorkSans-Italic.eot
Normal file
21510
frontend/resources/fonts/WorkSans-Italic.svg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |