mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 13:09:22 +00:00
Merge remote-tracking branch 'origin/staging'
This commit is contained in:
commit
4926c826af
82
CHANGES.md
82
CHANGES.md
@ -1,5 +1,87 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 1.18.0
|
||||||
|
|
||||||
|
### :sparkles: New features
|
||||||
|
- Adds more accessibility improvements in dashboard [Taiga #4577](https://tree.taiga.io/project/penpot/us/4577)
|
||||||
|
- Adds paddings and gaps prediction on layout creation [Taiga #4838](https://tree.taiga.io/project/penpot/task/4838)
|
||||||
|
- Add visual feedback when proportionally scaling text elements with **K** [Taiga #3415](https://tree.taiga.io/project/penpot/us/3415)
|
||||||
|
- Add visualization and mouse control to paddings, margins and gaps in frames with layout [Taiga #4839](https://tree.taiga.io/project/penpot/task/4839)
|
||||||
|
- Allow for absolute positioned elements inside layout [Taiga #4834](https://tree.taiga.io/project/penpot/us/4834)
|
||||||
|
- Add z-index option for flex layout items [Taiga #2980](https://tree.taiga.io/project/penpot/us/2980)
|
||||||
|
- Scale content proportionally affects strokes, shadows, blurs and corners [Taiga #1951](https://tree.taiga.io/project/penpot/us/1951)
|
||||||
|
- Use tabulators to navigate layers [Taiga #5010](https://tree.taiga.io/project/penpot/issue/5010)
|
||||||
|
|
||||||
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
- Fix problem with rules position on changing pages [Taiga #4847](https://tree.taiga.io/project/penpot/issue/4847)
|
||||||
|
- Fix error streen when uploading wrong SVG [#2995](https://github.com/penpot/penpot/issues/2995)
|
||||||
|
- Fix selecting children from hidden parent layers [Taiga #4934](https://tree.taiga.io/project/penpot/issue/4934)
|
||||||
|
- Fix problem when undoing multiple selected colors [Taiga #4920](https://tree.taiga.io/project/penpot/issue/4920)
|
||||||
|
- Allow selection of empty board by partial rect [Taiga #4806](https://tree.taiga.io/project/penpot/issue/4806)
|
||||||
|
- Improve behavior for undo on text edition [Taiga #4693](https://tree.taiga.io/project/penpot/issue/4693)
|
||||||
|
- Improve deeps selection of nested arboards [Taiga #4913](https://tree.taiga.io/project/penpot/issue/4913)
|
||||||
|
- Fix problem on selection numeric inputs on Firefox [#2991](https://github.com/penpot/penpot/issues/2991)
|
||||||
|
- Changed the text dominant-baseline to use ideographic [Taiga #4791](https://tree.taiga.io/project/penpot/issue/4791)
|
||||||
|
- Viewer wrong translations [Github #3035](https://github.com/penpot/penpot/issues/3035)
|
||||||
|
- Fix problem with text editor in Safari
|
||||||
|
- Fix unlink library color when blur color picker input [#3026](https://github.com/penpot/penpot/issues/3026)
|
||||||
|
- Fix snap pixel when moving path points on high zoom [#2930](https://github.com/penpot/penpot/issues/2930)
|
||||||
|
- Fix shortcuts for zoom now take into account the mouse position [#2924](https://github.com/penpot/penpot/issues/2924)
|
||||||
|
- Fix close colorpicker on Firefox when mouse-up is outside the picker [#2911](https://github.com/penpot/penpot/issues/2911)
|
||||||
|
- Fix problems with touch devices and Wacom tablets [#2216](https://github.com/penpot/penpot/issues/2216)
|
||||||
|
- Fix problem with board titles misplaced [Taiga #4738](https://tree.taiga.io/project/penpot/issue/4738)
|
||||||
|
- Fix problem with alt getting stuck when alt+tab [Taiga #5013](https://tree.taiga.io/project/penpot/issue/5013)
|
||||||
|
- Fix problem with z positioning of elements [Taiga #5014](https://tree.taiga.io/project/penpot/issue/5014)
|
||||||
|
- Fix problem in Firefox with scroll jumping when changin pages [#3052](https://github.com/penpot/penpot/issues/3052)
|
||||||
|
- Fix nested frame interaction created flow in wrong frame [Taiga #5043](https://tree.taiga.io/project/penpot/issue/5043)
|
||||||
|
- Font-Kerning does not work on Artboard Export to PNG/JPG/PDF [#3029](https://github.com/penpot/penpot/issues/3029)
|
||||||
|
- Fix manipulate duplicated project (delete, duplicate, rename, pin/unpin...) [Taiga #5027](https://tree.taiga.io/project/penpot/issue/5027)
|
||||||
|
- Fix deleted files appear in search results [Taiga #5002](https://tree.taiga.io/project/penpot/issue/5002)
|
||||||
|
- Fix problem with selected colors and texts [Taiga #5051](https://tree.taiga.io/project/penpot/issue/5051)
|
||||||
|
- Fix problem when assigning color from palette or assets [Taiga #5050](https://tree.taiga.io/project/penpot/issue/5050)
|
||||||
|
- Fix shortcuts for alignment [Taiga #5030](https://tree.taiga.io/project/penpot/issue/5030)
|
||||||
|
- Fix path options not showing when editing rects or ellipses [Taiga #5053](https://tree.taiga.io/project/penpot/issue/5053)
|
||||||
|
- Fix tooltips for some alignment options are truncated on design tab [Taiga #5040](https://tree.taiga.io/project/penpot/issue/5040)
|
||||||
|
- Fix horizontal margins drag don't always start from place [Taiga #5020](https://tree.taiga.io/project/penpot/issue/5020)
|
||||||
|
- Fix multiplayer username sometimes is not displayed correctly [Taiga #4400](https://tree.taiga.io/project/penpot/issue/4400)
|
||||||
|
- Show warning when trying to invite a user that is already in members [Taiga #4147](https://tree.taiga.io/project/penpot/issue/4147)
|
||||||
|
- Fix problem with text out of borders when changing from auto-width to fixed [Taiga #4308](https://tree.taiga.io/project/penpot/issue/4308)
|
||||||
|
- Fix header not showing when exiting fullscreen mode in viewer [Taiga #4244](https://tree.taiga.io/project/penpot/issue/4244)
|
||||||
|
- Fix visual problem in select options [Taiga #5028](https://tree.taiga.io/project/penpot/issue/5028)
|
||||||
|
- Forbid empty names for assets [Taiga #5056](https://tree.taiga.io/project/penpot/issue/5056)
|
||||||
|
- Select children after ungroup action [Taiga #4917](https://tree.taiga.io/project/penpot/issue/4917)
|
||||||
|
- Fix problem with guides not showing when moving over nested frames [Taiga #4905](https://tree.taiga.io/project/penpot/issue/4905)
|
||||||
|
- Fix change email and password for users signed in via social login [Taiga #4273](https://tree.taiga.io/project/penpot/issue/4273)
|
||||||
|
- Fix drag and drop files from browser or file explorer under circumstances [Taiga #5054](https://tree.taiga.io/project/penpot/issue/5054)
|
||||||
|
- Fix problem when copy/pasting shapes [Taiga #4931](https://tree.taiga.io/project/penpot/issue/4931)
|
||||||
|
- Fix problem with color picker not able to change hue [Taiga #5065](https://tree.taiga.io/project/penpot/issue/5065)
|
||||||
|
- Fix problem with outer stroke in texts [Taiga #5078](https://tree.taiga.io/project/penpot/issue/5078)
|
||||||
|
- Fix problem with text carring over next line when changing to fixed [Taiga #5067](https://tree.taiga.io/project/penpot/issue/5067)
|
||||||
|
- Fix don't show invite user hero to users with editor role [Taiga #5086](https://tree.taiga.io/project/penpot/issue/5086)
|
||||||
|
- Fix enter emails on onboarding new user creating team [Taiga #5089](https://tree.taiga.io/project/penpot/issue/5089)
|
||||||
|
- Fix invalid files amount after moving on dashboard [Taiga #5080](https://tree.taiga.io/project/penpot/issue/5080)
|
||||||
|
- Fix dashboard left sidebar, the [x] overlaps the field [Taiga #5064](https://tree.taiga.io/project/penpot/issue/5064)
|
||||||
|
- Fix expanded typography on assets sidebar is moving [Taiga #5063](https://tree.taiga.io/project/penpot/issue/5063)
|
||||||
|
- Fix spelling mistake in confirmation after importing only 1 file [Taiga #5095](https://tree.taiga.io/project/penpot/issue/5095)
|
||||||
|
- Fix problem with selection colors and texts [Taiga #5079](https://tree.taiga.io/project/penpot/issue/5079)
|
||||||
|
- Remove "show in view mode" flag when moving frame to frame [Taiga #5091](https://tree.taiga.io/project/penpot/issue/5091)
|
||||||
|
- Fix problem creating files in project page [Taiga #5060](https://tree.taiga.io/project/penpot/issue/5060)
|
||||||
|
- Disable empty names on rename files [Taiga #5088](https://tree.taiga.io/project/penpot/issue/5088)
|
||||||
|
- Fix problem with SVG and flex layout [Taiga #](https://tree.taiga.io/project/penpot/issue/5099)
|
||||||
|
- Fix unpublish and delete shared library warning messages [Taiga #5090](https://tree.taiga.io/project/penpot/issue/5090)
|
||||||
|
- Fix last update project timer update after creating new file [Taiga #5096](https://tree.taiga.io/project/penpot/issue/5096)
|
||||||
|
- Fix dashboard scrolling using 'Page Up' and 'Page Down' [Taiga #5081](https://tree.taiga.io/project/penpot/issue/5081)
|
||||||
|
- Fix view mode header buttons overlapping in small resolutions [Taiga #5058](https://tree.taiga.io/project/penpot/issue/5058)
|
||||||
|
- Fix precision for wrap in flex [Taiga #5072](https://tree.taiga.io/project/penpot/issue/5072)
|
||||||
|
- Fix relative position overlay positioning [Taiga #5092](https://tree.taiga.io/project/penpot/issue/5092)
|
||||||
|
- Fix hide grid keyboard shortcut [Github #3071](https://github.com/penpot/penpot/pull/3071)
|
||||||
|
- Fix problem with opacity in imported SVG's [Taiga #4923](https://tree.taiga.io/project/penpot/issue/4923)
|
||||||
|
|
||||||
|
### :heart: Community contributions by (Thank you!)
|
||||||
|
- To @ondrejkonec: for contributing to the code with:
|
||||||
|
- Refactor CSS variables [Github #2948](https://github.com/penpot/penpot/pull/2948)
|
||||||
|
|
||||||
## 1.17.3
|
## 1.17.3
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|||||||
@ -101,14 +101,14 @@ Each commit should have:
|
|||||||
|
|
||||||
Examples of good commit messages:
|
Examples of good commit messages:
|
||||||
|
|
||||||
- :bug: Fix unexpected error on launching modal
|
- `:bug: Fix unexpected error on launching modal`
|
||||||
- :bug: Set proper error message on generic error
|
- `:bug: Set proper error message on generic error`
|
||||||
- :sparkles: Enable new modal for profile
|
- `:sparkles: Enable new modal for profile`
|
||||||
- :zap: Improve performance of dashboard navigation
|
- `:zap: Improve performance of dashboard navigation`
|
||||||
- :wrench: Update default backend configuration
|
- `:wrench: Update default backend configuration`
|
||||||
- :books: Add more documentation for authentication process
|
- `:books: Add more documentation for authentication process`
|
||||||
- :ambulance: Fix critical bug on user registration process
|
- `:ambulance: Fix critical bug on user registration process`
|
||||||
- :tada: Add new approach for user registration
|
- `:tada: Add new approach for user registration`
|
||||||
|
|
||||||
|
|
||||||
## Code of conduct ##
|
## Code of conduct ##
|
||||||
|
|||||||
10
README.md
10
README.md
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<br>
|
<br>
|
||||||
<img src="https://penpot.app/images/readme/readme-logo.jpg" alt="PENPOT">
|
<img src="https://penpot.app/images/readme/git-readme-header.png" alt="PENPOT">
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||||
@ -50,7 +50,7 @@ Being web based, Penpot is not dependent on operating systems or local installat
|
|||||||
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
|
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://penpot.app/images/readme/open-source.png" alt="Open Source">
|
<img src="https://penpot.app/images/readme/git-open.png" alt="Open Source" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ Here’s a step-by-step guide on [getting started with Docker.](https://help.pen
|
|||||||
If you prefer not to install Penpot in a local environment, [login or register on our Penpot cloud app](https://design.penpot.app). Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** on your own.
|
If you prefer not to install Penpot in a local environment, [login or register on our Penpot cloud app](https://design.penpot.app). Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** on your own.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://help.penpot.app/img/home-techguide.png" alt="Getting started">
|
<img src="https://penpot.app/images/readme/git-self-host.png" alt="Getting started" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Community ##
|
## Community ##
|
||||||
@ -93,7 +93,7 @@ You will find the following categories:
|
|||||||
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://penpot.app/images/readme/cross-teams.webp" alt="Community">
|
<img src="https://penpot.app/images/readme/git-collaborate.png" alt="Communnity" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Contributing ##
|
## Contributing ##
|
||||||
@ -111,7 +111,7 @@ Every sort of contribution will be very helpful to enhance Penpot. How you’ll
|
|||||||
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/).
|
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://help.penpot.app/img/home-contributing.png" alt="Contributing">
|
<img src="https://penpot.app/images/readme/git-community.png" alt="Contributing" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Resources ##
|
## Resources ##
|
||||||
|
|||||||
@ -16,16 +16,11 @@
|
|||||||
{:src-dirs ["src" "resources"]
|
{:src-dirs ["src" "resources"]
|
||||||
:target-dir class-dir})
|
:target-dir class-dir})
|
||||||
|
|
||||||
(b/compile-clj
|
|
||||||
{:basis basis
|
|
||||||
:src-dirs ["src"]
|
|
||||||
:class-dir class-dir})
|
|
||||||
|
|
||||||
(b/uber
|
(b/uber
|
||||||
{:class-dir class-dir
|
{:class-dir class-dir
|
||||||
:uber-file jar-file
|
:uber-file jar-file
|
||||||
:main 'clojure.main
|
:main 'clojure.main
|
||||||
:exclude [#"goog.*" #"^javasist.*"]
|
:exclude [#".*Log4j2Plugins\.dat$"]
|
||||||
:basis basis}))
|
:basis basis}))
|
||||||
|
|
||||||
(defn compile [_]
|
(defn compile [_]
|
||||||
|
|||||||
@ -3,9 +3,6 @@
|
|||||||
org.clojure/clojure {:mvn/version "1.11.1"}
|
org.clojure/clojure {:mvn/version "1.11.1"}
|
||||||
org.clojure/core.async {:mvn/version "1.6.673"}
|
org.clojure/core.async {:mvn/version "1.6.673"}
|
||||||
|
|
||||||
;; Logging
|
|
||||||
org.zeromq/jeromq {:mvn/version "0.5.3"}
|
|
||||||
|
|
||||||
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
|
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
|
||||||
org.clojure/data.fressian {:mvn/version "1.0.0"}
|
org.clojure/data.fressian {:mvn/version "1.0.0"}
|
||||||
|
|
||||||
@ -29,7 +26,7 @@
|
|||||||
|
|
||||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
|
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
|
||||||
metosin/reitit-core {:mvn/version "0.5.18"}
|
metosin/reitit-core {:mvn/version "0.5.18"}
|
||||||
org.postgresql/postgresql {:mvn/version "42.5.1"}
|
org.postgresql/postgresql {:mvn/version "42.5.2"}
|
||||||
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
||||||
|
|
||||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||||
@ -55,7 +52,7 @@
|
|||||||
|
|
||||||
;; Pretty Print specs
|
;; Pretty Print specs
|
||||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||||
software.amazon.awssdk/s3 {:mvn/version "2.19.8"}
|
software.amazon.awssdk/s3 {:mvn/version "2.19.29"}
|
||||||
}
|
}
|
||||||
|
|
||||||
:paths ["src" "resources" "target/classes"]
|
:paths ["src" "resources" "target/classes"]
|
||||||
@ -70,10 +67,9 @@
|
|||||||
mockery/mockery {:mvn/version "RELEASE"}}
|
mockery/mockery {:mvn/version "RELEASE"}}
|
||||||
:extra-paths ["test" "dev"]}
|
:extra-paths ["test" "dev"]}
|
||||||
|
|
||||||
|
|
||||||
:build
|
:build
|
||||||
{:extra-deps
|
{:extra-deps
|
||||||
{io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}}
|
{io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"}}
|
||||||
:ns-default build}
|
:ns-default build}
|
||||||
|
|
||||||
:test
|
:test
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
<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 {{name}}!</mj-text>
|
|
||||||
<mj-text>We received a request to change your current email to {{ pending-email }}.</mj-text>
|
|
||||||
<mj-text>Click to the link below to confirm the change:</mj-text>
|
|
||||||
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
|
|
||||||
Confirm email change
|
|
||||||
</mj-button>
|
|
||||||
<mj-text>
|
|
||||||
If you received this email by mistake, please consider changing your password
|
|
||||||
for security reasons.
|
|
||||||
</mj-text>
|
|
||||||
<mj-text>Enjoy!</mj-text>
|
|
||||||
<mj-text>The Penpot 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">
|
|
||||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain 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://penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" 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%">
|
|
||||||
Penpot © 2020 | Made with <3 and Open Source
|
|
||||||
</mj-text>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
|
|
||||||
</mg-body>
|
|
||||||
</mjml>
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
<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 Penpot 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">
|
|
||||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain 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://penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" 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%">
|
|
||||||
Penpot © 2020 | Made with <3 and Open Source
|
|
||||||
</mj-text>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
|
|
||||||
</mg-body>
|
|
||||||
</mjml>
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
<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 {{name}}!</mj-text>
|
|
||||||
<mj-text>
|
|
||||||
We have received a request to reset your password. Click the link
|
|
||||||
below to choose a new one:
|
|
||||||
</mj-text>
|
|
||||||
<mj-button href="{{ public-uri }}/#/auth/recovery?token={{token}}">
|
|
||||||
Reset password
|
|
||||||
</mj-button>
|
|
||||||
<mj-text>
|
|
||||||
If you received this email by mistake, you can safely ignore
|
|
||||||
it. Your password won't be changed.
|
|
||||||
</mj-text>
|
|
||||||
<mj-text>Enjoy!</mj-text>
|
|
||||||
<mj-text>The Penpot 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">
|
|
||||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain 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://penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" 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%">
|
|
||||||
Penpot © 2020 | Made with <3 and Open Source
|
|
||||||
</mj-text>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
|
|
||||||
</mg-body>
|
|
||||||
</mjml>
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
<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 {{name}}!</mj-text>
|
|
||||||
<mj-text>
|
|
||||||
Thanks for signing up for your Penpot account! Please verify your
|
|
||||||
email using the link below and get started building mockups and
|
|
||||||
prototypes today!
|
|
||||||
</mj-text>
|
|
||||||
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
|
|
||||||
Verify email
|
|
||||||
</mj-button>
|
|
||||||
<mj-text>Enjoy!</mj-text>
|
|
||||||
<mj-text>The Penpot 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">
|
|
||||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain 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://penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
|
||||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" 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%">
|
|
||||||
Penpot © 2020 | Made with <3 and Open Source
|
|
||||||
</mj-text>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
|
|
||||||
</mg-body>
|
|
||||||
</mjml>
|
|
||||||
112
backend/resources/app/templates/error-report.v2.tmpl
Normal file
112
backend/resources/app/templates/error-report.v2.tmpl
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{% extends "app/templates/base.tmpl" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
penpot - error report v2 {{id}}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<nav>
|
||||||
|
<div>[<a href="/dbg/error">⮜</a>]</div>
|
||||||
|
<div>[<a href="#message">message</a>]</div>
|
||||||
|
<div>[<a href="#props">props</a>]</div>
|
||||||
|
<div>[<a href="#context">context</a>]</div>
|
||||||
|
{% if params %}
|
||||||
|
<div>[<a href="#params">request params</a>]</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if data %}
|
||||||
|
<div>[<a href="#edata">error data</a>]</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if spec-explain %}
|
||||||
|
<div>[<a href="#spec-explain">spec explain</a>]</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if spec-problems %}
|
||||||
|
<div>[<a href="#spec-problems">spec problems</a>]</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if spec-value %}
|
||||||
|
<div>[<a href="#spec-value">spec value</a>]</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if trace %}
|
||||||
|
<div>[<a href="#trace">error trace</a>]</div>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
<div class="table">
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="message" class="table-key">MESSAGE: </div>
|
||||||
|
|
||||||
|
<div class="table-val">
|
||||||
|
<h1>{{hint}}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="props" class="table-key">LOG PROPS: </div>
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{props}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="context" class="table-key">CONTEXT: </div>
|
||||||
|
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{context}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if params %}
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="params" class="table-key">REQUEST PARAMS: </div>
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{params}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if data %}
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="edata" class="table-key">ERROR DATA: </div>
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{data}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if spec-explain %}
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="spec-explain" class="table-key">SPEC EXPLAIN: </div>
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{spec-explain}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if spec-problems %}
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="spec-problems" class="table-key">SPEC PROBLEMS: </div>
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{spec-problems}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if spec-value %}
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="spec-value" class="table-key">SPEC VALUE: </div>
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{spec-value}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if trace %}
|
||||||
|
<div class="table-row multiline">
|
||||||
|
<div id="trace" class="table-key">TRACE:</div>
|
||||||
|
<div class="table-val">
|
||||||
|
<pre>{{trace}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@ -23,6 +23,10 @@ input[type=text], input[type=submit] {
|
|||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,11 +14,6 @@
|
|||||||
</Policies>
|
</Policies>
|
||||||
<DefaultRolloverStrategy max="9"/>
|
<DefaultRolloverStrategy max="9"/>
|
||||||
</RollingFile>
|
</RollingFile>
|
||||||
|
|
||||||
<JeroMQ name="zmq">
|
|
||||||
<Property name="endpoint">tcp://localhost:45556</Property>
|
|
||||||
<JsonLayout complete="false" compact="true" includeTimeMillis="true" stacktraceAsString="true" properties="true" />
|
|
||||||
</JeroMQ>
|
|
||||||
</Appenders>
|
</Appenders>
|
||||||
|
|
||||||
<Loggers>
|
<Loggers>
|
||||||
@ -37,17 +32,12 @@
|
|||||||
<Logger name="app.rpc.climit" level="info" />
|
<Logger name="app.rpc.climit" level="info" />
|
||||||
<Logger name="app.rpc.mutations.files" level="info" />
|
<Logger name="app.rpc.mutations.files" level="info" />
|
||||||
|
|
||||||
<Logger name="app.cli" level="debug" additivity="false">
|
|
||||||
<AppenderRef ref="console"/>
|
|
||||||
</Logger>
|
|
||||||
|
|
||||||
<Logger name="app.loggers" level="debug" additivity="false">
|
<Logger name="app.loggers" level="debug" additivity="false">
|
||||||
<AppenderRef ref="main" level="debug" />
|
<AppenderRef ref="main" level="debug" />
|
||||||
</Logger>
|
</Logger>
|
||||||
|
|
||||||
<Logger name="app" level="all" additivity="false">
|
<Logger name="app" level="all" additivity="false">
|
||||||
<AppenderRef ref="main" level="trace" />
|
<AppenderRef ref="main" level="trace" />
|
||||||
<AppenderRef ref="zmq" level="debug" />
|
|
||||||
</Logger>
|
</Logger>
|
||||||
|
|
||||||
<Logger name="user" level="trace" additivity="false">
|
<Logger name="user" level="trace" additivity="false">
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
<Logger name="com.zaxxer.hikari" level="error" />
|
<Logger name="com.zaxxer.hikari" level="error" />
|
||||||
<Logger name="org.postgresql" level="error" />
|
<Logger name="org.postgresql" level="error" />
|
||||||
|
|
||||||
|
<Logger name="app.util" level="info" />
|
||||||
<Logger name="app" level="info" additivity="false">
|
<Logger name="app" level="info" additivity="false">
|
||||||
<AppenderRef ref="console" />
|
<AppenderRef ref="console" />
|
||||||
</Logger>
|
</Logger>
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
{:default
|
{:default
|
||||||
[[:default :window "200000/h"]]
|
[[:default :window "200000/h"]]
|
||||||
|
|
||||||
#{:query/teams}
|
#{:command/get-teams}
|
||||||
[[:burst :bucket "5/1/5s"]]
|
[[:burst :bucket "5/1/5s"]]
|
||||||
|
|
||||||
#{:query/profile}
|
#{:command/get-profile}
|
||||||
[[:burst :bucket "100/60/1m"]]}
|
[[:burst :bucket "60/60/1m"]]}
|
||||||
|
|||||||
@ -45,11 +45,12 @@ export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
|
|||||||
export OPTIONS="
|
export OPTIONS="
|
||||||
-A:jmx-remote -A:dev \
|
-A:jmx-remote -A:dev \
|
||||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||||
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
|
||||||
-J-XX:+UseG1GC \
|
|
||||||
-J-XX:-OmitStackTraceInFastThrow \
|
|
||||||
-J-Xms50m -J-Xmx1024m \
|
|
||||||
-J-Djdk.attach.allowAttachSelf \
|
-J-Djdk.attach.allowAttachSelf \
|
||||||
|
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||||
|
-J-Xms50m \
|
||||||
|
-J-Xmx1024m \
|
||||||
|
-J-XX:+UseZGC \
|
||||||
|
-J-XX:-OmitStackTraceInFastThrow \
|
||||||
-J-XX:+UnlockDiagnosticVMOptions \
|
-J-XX:+UnlockDiagnosticVMOptions \
|
||||||
-J-XX:+DebugNonSafepoints";
|
-J-XX:+DebugNonSafepoints";
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc.queries.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -375,7 +375,7 @@
|
|||||||
::fullname
|
::fullname
|
||||||
::props]))
|
::props]))
|
||||||
|
|
||||||
(defn retrieve-info
|
(defn get-info
|
||||||
[{:keys [provider] :as cfg} {:keys [params] :as request}]
|
[{:keys [provider] :as cfg} {:keys [params] :as request}]
|
||||||
(letfn [(validate-oidc [info]
|
(letfn [(validate-oidc [info]
|
||||||
;; If the provider is OIDC, we can proceed to check
|
;; If the provider is OIDC, we can proceed to check
|
||||||
@ -422,14 +422,12 @@
|
|||||||
(p/then' validate-oidc)
|
(p/then' validate-oidc)
|
||||||
(p/then' (partial post-process state))))))
|
(p/then' (partial post-process state))))))
|
||||||
|
|
||||||
(defn- retrieve-profile
|
(defn- get-profile
|
||||||
[{:keys [::db/pool ::wrk/executor] :as cfg} info]
|
[{:keys [::db/pool ::wrk/executor] :as cfg} info]
|
||||||
(px/with-dispatch executor
|
(px/with-dispatch executor
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(some->> (:email info)
|
(some->> (:email info)
|
||||||
(profile/retrieve-profile-data-by-email conn)
|
(profile/get-profile-by-email conn)))))
|
||||||
(profile/populate-additional-data conn)
|
|
||||||
(profile/decode-profile-row)))))
|
|
||||||
|
|
||||||
(defn- redirect-response
|
(defn- redirect-response
|
||||||
[uri]
|
[uri]
|
||||||
@ -443,9 +441,9 @@
|
|||||||
(redirect-response uri)))
|
(redirect-response uri)))
|
||||||
|
|
||||||
(defn- generate-redirect
|
(defn- generate-redirect
|
||||||
[{:keys [::session/session] :as cfg} request info profile]
|
[cfg request info profile]
|
||||||
(if profile
|
(if profile
|
||||||
(let [sxf (session/create-fn session (:id profile))
|
(let [sxf (session/create-fn cfg (:id profile))
|
||||||
token (or (:invitation-token info)
|
token (or (:invitation-token info)
|
||||||
(tokens/generate (::main/props cfg)
|
(tokens/generate (::main/props cfg)
|
||||||
{:iss :auth
|
{:iss :auth
|
||||||
@ -496,8 +494,8 @@
|
|||||||
(defn- callback-handler
|
(defn- callback-handler
|
||||||
[cfg request]
|
[cfg request]
|
||||||
(letfn [(process-request []
|
(letfn [(process-request []
|
||||||
(p/let [info (retrieve-info cfg request)
|
(p/let [info (get-info cfg request)
|
||||||
profile (retrieve-profile cfg info)]
|
profile (get-profile cfg info)]
|
||||||
(generate-redirect cfg request info profile)))
|
(generate-redirect cfg request info profile)))
|
||||||
|
|
||||||
(handle-error [cause]
|
(handle-error [cause]
|
||||||
@ -549,23 +547,24 @@
|
|||||||
|
|
||||||
(s/def ::providers (s/map-of ::us/keyword (s/nilable ::provider)))
|
(s/def ::providers (s/map-of ::us/keyword (s/nilable ::provider)))
|
||||||
|
|
||||||
|
(s/def ::routes vector?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::routes
|
(defmethod ig/pre-init-spec ::routes
|
||||||
[_]
|
[_]
|
||||||
(s/keys :req [::http/client
|
(s/keys :req [::session/manager
|
||||||
|
::http/client
|
||||||
::wrk/executor
|
::wrk/executor
|
||||||
::main/props
|
::main/props
|
||||||
::db/pool
|
::db/pool
|
||||||
::providers
|
::providers]))
|
||||||
::session/session]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [::wrk/executor ::session/session] :as cfg}]
|
[_ {:keys [::wrk/executor] :as cfg}]
|
||||||
(let [cfg (update cfg :provider d/without-nils)]
|
(let [cfg (update cfg :provider d/without-nils)]
|
||||||
["" {:middleware [[(:middleware session)]
|
["" {:middleware [[session/authz cfg]
|
||||||
[hmw/with-dispatch executor]
|
[hmw/with-dispatch executor]
|
||||||
[hmw/with-config cfg]
|
[hmw/with-config cfg]
|
||||||
[provider-lookup]
|
[provider-lookup]]}
|
||||||
]}
|
|
||||||
["/auth/oauth"
|
["/auth/oauth"
|
||||||
["/:provider"
|
["/:provider"
|
||||||
{:handler auth-handler
|
{:handler auth-handler
|
||||||
@ -573,4 +572,3 @@
|
|||||||
["/:provider/callback"
|
["/:provider/callback"
|
||||||
{:handler callback-handler
|
{:handler callback-handler
|
||||||
:allowed-methods #{:get}}]]]))
|
:allowed-methods #{:get}}]]]))
|
||||||
|
|
||||||
|
|||||||
@ -10,9 +10,8 @@
|
|||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.main :as main]
|
[app.main :as main]
|
||||||
[app.rpc.commands.auth :as cmd.auth]
|
[app.rpc.commands.auth :as auth]
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.queries.profile :refer [retrieve-profile-data-by-email]]
|
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[clojure.tools.cli :refer [parse-opts]]
|
[clojure.tools.cli :refer [parse-opts]]
|
||||||
[integrant.core :as ig])
|
[integrant.core :as ig])
|
||||||
@ -55,16 +54,17 @@
|
|||||||
:type :password}))]
|
:type :password}))]
|
||||||
(try
|
(try
|
||||||
(db/with-atomic [conn (:app.db/pool system)]
|
(db/with-atomic [conn (:app.db/pool system)]
|
||||||
(->> (cmd.auth/create-profile conn
|
(->> (auth/create-profile! conn
|
||||||
{:fullname fullname
|
{:fullname fullname
|
||||||
:email email
|
:email email
|
||||||
:password password
|
:password password
|
||||||
:is-active true
|
:is-active true
|
||||||
:is-demo false})
|
:is-demo false})
|
||||||
(cmd.auth/create-profile-relations conn)))
|
(auth/create-profile-rels! conn)))
|
||||||
|
|
||||||
(when (pos? (:verbosity options))
|
(when (pos? (:verbosity options))
|
||||||
(println "User created successfully."))
|
(println "User created successfully."))
|
||||||
|
|
||||||
(System/exit 0)
|
(System/exit 0)
|
||||||
|
|
||||||
(catch Exception _e
|
(catch Exception _e
|
||||||
@ -79,7 +79,7 @@
|
|||||||
(db/with-atomic [conn (:app.db/pool system)]
|
(db/with-atomic [conn (:app.db/pool system)]
|
||||||
(let [email (or (:email options)
|
(let [email (or (:email options)
|
||||||
(read-from-console {:label "Email:"}))
|
(read-from-console {:label "Email:"}))
|
||||||
profile (retrieve-profile-data-by-email conn email)]
|
profile (profile/get-profile-by-email conn email)]
|
||||||
(when-not profile
|
(when-not profile
|
||||||
(when (pos? (:verbosity options))
|
(when (pos? (:verbosity options))
|
||||||
(println "Profile does not exists."))
|
(println "Profile does not exists."))
|
||||||
|
|||||||
@ -51,7 +51,6 @@
|
|||||||
:database-password "penpot"
|
:database-password "penpot"
|
||||||
|
|
||||||
:default-blob-version 5
|
:default-blob-version 5
|
||||||
:loggers-zmq-uri "tcp://localhost:45556"
|
|
||||||
|
|
||||||
:rpc-rlimit-config (fs/path "resources/rlimit.edn")
|
:rpc-rlimit-config (fs/path "resources/rlimit.edn")
|
||||||
:rpc-climit-config (fs/path "resources/climit.edn")
|
:rpc-climit-config (fs/path "resources/climit.edn")
|
||||||
@ -126,6 +125,7 @@
|
|||||||
(s/def ::database-max-pool-size ::us/integer)
|
(s/def ::database-max-pool-size ::us/integer)
|
||||||
|
|
||||||
(s/def ::quotes-teams-per-profile ::us/integer)
|
(s/def ::quotes-teams-per-profile ::us/integer)
|
||||||
|
(s/def ::quotes-access-tokens-per-profile ::us/integer)
|
||||||
(s/def ::quotes-projects-per-team ::us/integer)
|
(s/def ::quotes-projects-per-team ::us/integer)
|
||||||
(s/def ::quotes-invitations-per-team ::us/integer)
|
(s/def ::quotes-invitations-per-team ::us/integer)
|
||||||
(s/def ::quotes-profiles-per-team ::us/integer)
|
(s/def ::quotes-profiles-per-team ::us/integer)
|
||||||
@ -174,8 +174,6 @@
|
|||||||
(s/def ::ldap-ssl ::us/boolean)
|
(s/def ::ldap-ssl ::us/boolean)
|
||||||
(s/def ::ldap-starttls ::us/boolean)
|
(s/def ::ldap-starttls ::us/boolean)
|
||||||
(s/def ::ldap-user-query ::us/string)
|
(s/def ::ldap-user-query ::us/string)
|
||||||
(s/def ::loggers-loki-uri ::us/string)
|
|
||||||
(s/def ::loggers-zmq-uri ::us/string)
|
|
||||||
(s/def ::media-directory ::us/string)
|
(s/def ::media-directory ::us/string)
|
||||||
(s/def ::media-uri ::us/string)
|
(s/def ::media-uri ::us/string)
|
||||||
(s/def ::profile-bounce-max-age ::dt/duration)
|
(s/def ::profile-bounce-max-age ::dt/duration)
|
||||||
@ -271,8 +269,6 @@
|
|||||||
::ldap-starttls
|
::ldap-starttls
|
||||||
::ldap-user-query
|
::ldap-user-query
|
||||||
::local-assets-uri
|
::local-assets-uri
|
||||||
::loggers-loki-uri
|
|
||||||
::loggers-zmq-uri
|
|
||||||
::media-max-file-size
|
::media-max-file-size
|
||||||
::profile-bounce-max-age
|
::profile-bounce-max-age
|
||||||
::profile-bounce-threshold
|
::profile-bounce-threshold
|
||||||
@ -281,6 +277,7 @@
|
|||||||
::public-uri
|
::public-uri
|
||||||
|
|
||||||
::quotes-teams-per-profile
|
::quotes-teams-per-profile
|
||||||
|
::quotes-access-tokens-per-profile
|
||||||
::quotes-projects-per-team
|
::quotes-projects-per-team
|
||||||
::quotes-invitations-per-team
|
::quotes-invitations-per-team
|
||||||
::quotes-profiles-per-team
|
::quotes-profiles-per-team
|
||||||
@ -355,7 +352,7 @@
|
|||||||
(merge defaults)
|
(merge defaults)
|
||||||
(us/conform ::config))
|
(us/conform ::config))
|
||||||
(catch Throwable e
|
(catch Throwable e
|
||||||
(when (ex/ex-info? e)
|
(when (ex/error? e)
|
||||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
|
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
|
||||||
(println "Error on validating configuration:")
|
(println "Error on validating configuration:")
|
||||||
(println (some-> e ex-data ex/explain))
|
(println (some-> e ex-data ex/explain))
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.migrations :as mg]
|
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
@ -32,7 +31,6 @@
|
|||||||
io.whitfin.siphash.SipHasherContainer
|
io.whitfin.siphash.SipHasherContainer
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
java.io.OutputStream
|
java.io.OutputStream
|
||||||
java.lang.AutoCloseable
|
|
||||||
java.sql.Connection
|
java.sql.Connection
|
||||||
java.sql.Savepoint
|
java.sql.Savepoint
|
||||||
org.postgresql.PGConnection
|
org.postgresql.PGConnection
|
||||||
@ -50,12 +48,9 @@
|
|||||||
;; Initialization
|
;; Initialization
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(declare apply-migrations!)
|
|
||||||
|
|
||||||
(s/def ::connection-timeout ::us/integer)
|
(s/def ::connection-timeout ::us/integer)
|
||||||
(s/def ::max-size ::us/integer)
|
(s/def ::max-size ::us/integer)
|
||||||
(s/def ::min-size ::us/integer)
|
(s/def ::min-size ::us/integer)
|
||||||
(s/def ::migrations map?)
|
|
||||||
(s/def ::name keyword?)
|
(s/def ::name keyword?)
|
||||||
(s/def ::password ::us/string)
|
(s/def ::password ::us/string)
|
||||||
(s/def ::uri ::us/not-empty-string)
|
(s/def ::uri ::us/not-empty-string)
|
||||||
@ -64,26 +59,26 @@
|
|||||||
(s/def ::read-only? ::us/boolean)
|
(s/def ::read-only? ::us/boolean)
|
||||||
|
|
||||||
(s/def ::pool-options
|
(s/def ::pool-options
|
||||||
(s/keys :opt-un [::uri ::name
|
(s/keys :opt [::uri
|
||||||
::min-size
|
::name
|
||||||
::max-size
|
::min-size
|
||||||
::connection-timeout
|
::max-size
|
||||||
::validation-timeout
|
::connection-timeout
|
||||||
::migrations
|
::validation-timeout
|
||||||
::username
|
::username
|
||||||
::password
|
::password
|
||||||
::mtx/metrics
|
::mtx/metrics
|
||||||
::read-only?]))
|
::read-only?]))
|
||||||
|
|
||||||
(def defaults
|
(def defaults
|
||||||
{:name :main
|
{::name :main
|
||||||
:min-size 0
|
::min-size 0
|
||||||
:max-size 60
|
::max-size 60
|
||||||
:connection-timeout 10000
|
::connection-timeout 10000
|
||||||
:validation-timeout 10000
|
::validation-timeout 10000
|
||||||
:idle-timeout 120000 ; 2min
|
::idle-timeout 120000 ; 2min
|
||||||
:max-lifetime 1800000 ; 30m
|
::max-lifetime 1800000 ; 30m
|
||||||
:read-only? false})
|
::read-only? false})
|
||||||
|
|
||||||
(defmethod ig/prep-key ::pool
|
(defmethod ig/prep-key ::pool
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
@ -93,39 +88,23 @@
|
|||||||
(defmethod ig/pre-init-spec ::pool [_] ::pool-options)
|
(defmethod ig/pre-init-spec ::pool [_] ::pool-options)
|
||||||
|
|
||||||
(defmethod ig/init-key ::pool
|
(defmethod ig/init-key ::pool
|
||||||
[_ {:keys [migrations read-only? uri] :as cfg}]
|
[_ {:keys [::uri ::read-only?] :as cfg}]
|
||||||
(if uri
|
(when uri
|
||||||
(let [pool (create-pool cfg)]
|
(l/info :hint "initialize connection pool"
|
||||||
(l/info :hint "initialize connection pool"
|
:name (d/name (::name cfg))
|
||||||
:name (d/name (:name cfg))
|
:uri uri
|
||||||
:uri uri
|
:read-only read-only?
|
||||||
:read-only read-only?
|
:with-credentials (and (contains? cfg ::username)
|
||||||
:with-credentials (and (contains? cfg :username)
|
(contains? cfg ::password))
|
||||||
(contains? cfg :password))
|
:min-size (::min-size cfg)
|
||||||
:min-size (:min-size cfg)
|
:max-size (::max-size cfg))
|
||||||
:max-size (:max-size cfg))
|
(create-pool cfg)))
|
||||||
(when-not read-only?
|
|
||||||
(some->> (seq migrations) (apply-migrations! pool)))
|
|
||||||
pool)
|
|
||||||
|
|
||||||
(do
|
|
||||||
(l/warn :hint "unable to initialize pool, missing url"
|
|
||||||
:name (d/name (:name cfg))
|
|
||||||
:read-only read-only?)
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::pool
|
(defmethod ig/halt-key! ::pool
|
||||||
[_ pool]
|
[_ pool]
|
||||||
(when pool
|
(when pool
|
||||||
(.close ^HikariDataSource pool)))
|
(.close ^HikariDataSource pool)))
|
||||||
|
|
||||||
(defn- apply-migrations!
|
|
||||||
[pool migrations]
|
|
||||||
(with-open [conn ^AutoCloseable (open pool)]
|
|
||||||
(mg/setup! conn)
|
|
||||||
(doseq [[name steps] migrations]
|
|
||||||
(mg/migrate! conn {:name (d/name name) :steps steps}))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; API & Impl
|
;; API & Impl
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -135,19 +114,19 @@
|
|||||||
"SET idle_in_transaction_session_timeout = 300000;"))
|
"SET idle_in_transaction_session_timeout = 300000;"))
|
||||||
|
|
||||||
(defn- create-datasource-config
|
(defn- create-datasource-config
|
||||||
[{:keys [metrics uri] :as cfg}]
|
[{:keys [::mtx/metrics ::uri] :as cfg}]
|
||||||
(let [config (HikariConfig.)]
|
(let [config (HikariConfig.)]
|
||||||
(doto config
|
(doto config
|
||||||
(.setJdbcUrl (str "jdbc:" uri))
|
(.setJdbcUrl (str "jdbc:" uri))
|
||||||
(.setPoolName (d/name (:name cfg)))
|
(.setPoolName (d/name (::name cfg)))
|
||||||
(.setAutoCommit true)
|
(.setAutoCommit true)
|
||||||
(.setReadOnly (:read-only? cfg))
|
(.setReadOnly (::read-only? cfg))
|
||||||
(.setConnectionTimeout (:connection-timeout cfg))
|
(.setConnectionTimeout (::connection-timeout cfg))
|
||||||
(.setValidationTimeout (:validation-timeout cfg))
|
(.setValidationTimeout (::validation-timeout cfg))
|
||||||
(.setIdleTimeout (:idle-timeout cfg))
|
(.setIdleTimeout (::idle-timeout cfg))
|
||||||
(.setMaxLifetime (:max-lifetime cfg))
|
(.setMaxLifetime (::max-lifetime cfg))
|
||||||
(.setMinimumIdle (:min-size cfg))
|
(.setMinimumIdle (::min-size cfg))
|
||||||
(.setMaximumPoolSize (:max-size cfg))
|
(.setMaximumPoolSize (::max-size cfg))
|
||||||
(.setConnectionInitSql initsql)
|
(.setConnectionInitSql initsql)
|
||||||
(.setInitializationFailTimeout -1))
|
(.setInitializationFailTimeout -1))
|
||||||
|
|
||||||
@ -157,8 +136,8 @@
|
|||||||
(PrometheusMetricsTrackerFactory.)
|
(PrometheusMetricsTrackerFactory.)
|
||||||
(.setMetricsTrackerFactory config)))
|
(.setMetricsTrackerFactory config)))
|
||||||
|
|
||||||
(some->> ^String (:username cfg) (.setUsername config))
|
(some->> ^String (::username cfg) (.setUsername config))
|
||||||
(some->> ^String (:password cfg) (.setPassword config))
|
(some->> ^String (::password cfg) (.setPassword config))
|
||||||
|
|
||||||
config))
|
config))
|
||||||
|
|
||||||
@ -166,11 +145,9 @@
|
|||||||
[v]
|
[v]
|
||||||
(instance? javax.sql.DataSource v))
|
(instance? javax.sql.DataSource v))
|
||||||
|
|
||||||
(s/def ::pool pool?)
|
|
||||||
(s/def ::conn some?)
|
(s/def ::conn some?)
|
||||||
|
(s/def ::nilable-pool (s/nilable ::pool))
|
||||||
;; DEPRECATED: to be removed in 1.18
|
(s/def ::pool pool?)
|
||||||
(s/def ::conn-or-pool some?)
|
|
||||||
(s/def ::pool-or-conn some?)
|
(s/def ::pool-or-conn some?)
|
||||||
|
|
||||||
(defn closed?
|
(defn closed?
|
||||||
@ -178,8 +155,18 @@
|
|||||||
(.isClosed ^HikariDataSource pool))
|
(.isClosed ^HikariDataSource pool))
|
||||||
|
|
||||||
(defn read-only?
|
(defn read-only?
|
||||||
[pool]
|
[pool-or-conn]
|
||||||
(.isReadOnly ^HikariDataSource pool))
|
(cond
|
||||||
|
(instance? HikariDataSource pool-or-conn)
|
||||||
|
(.isReadOnly ^HikariDataSource pool-or-conn)
|
||||||
|
|
||||||
|
(instance? Connection pool-or-conn)
|
||||||
|
(.isReadOnly ^Connection pool-or-conn)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :invalid-connection
|
||||||
|
:hint "invalid connection provided")))
|
||||||
|
|
||||||
(defn create-pool
|
(defn create-pool
|
||||||
[cfg]
|
[cfg]
|
||||||
@ -237,44 +224,46 @@
|
|||||||
[pool]
|
[pool]
|
||||||
(jdbc/get-connection pool))
|
(jdbc/get-connection pool))
|
||||||
|
|
||||||
|
(def ^:private default-opts
|
||||||
|
{:builder-fn sql/as-kebab-maps})
|
||||||
|
|
||||||
(defn exec!
|
(defn exec!
|
||||||
([ds sv]
|
([ds sv]
|
||||||
(exec! ds sv {}))
|
(jdbc/execute! ds sv default-opts))
|
||||||
([ds sv opts]
|
([ds sv opts]
|
||||||
(jdbc/execute! ds sv (assoc opts :builder-fn sql/as-kebab-maps))))
|
(jdbc/execute! ds sv (merge default-opts opts))))
|
||||||
|
|
||||||
(defn exec-one!
|
(defn exec-one!
|
||||||
([ds sv] (exec-one! ds sv {}))
|
([ds sv]
|
||||||
|
(jdbc/execute-one! ds sv default-opts))
|
||||||
([ds sv opts]
|
([ds sv opts]
|
||||||
(jdbc/execute-one! ds sv (assoc opts :builder-fn sql/as-kebab-maps))))
|
(jdbc/execute-one! ds sv
|
||||||
|
(-> (merge default-opts opts)
|
||||||
|
(assoc :return-keys (::return-keys? opts false))))))
|
||||||
|
|
||||||
(defn insert!
|
(defn insert!
|
||||||
([ds table params] (insert! ds table params nil))
|
[ds table params & {:as opts}]
|
||||||
([ds table params opts]
|
(exec-one! ds
|
||||||
(exec-one! ds
|
(sql/insert table params opts)
|
||||||
(sql/insert table params opts)
|
(merge {::return-keys? true} opts)))
|
||||||
(merge {:return-keys true} opts))))
|
|
||||||
|
|
||||||
(defn insert-multi!
|
(defn insert-multi!
|
||||||
([ds table cols rows] (insert-multi! ds table cols rows nil))
|
[ds table cols rows & {:as opts}]
|
||||||
([ds table cols rows opts]
|
(exec! ds
|
||||||
(exec! ds
|
(sql/insert-multi table cols rows opts)
|
||||||
(sql/insert-multi table cols rows opts)
|
(merge {::return-keys? true} opts)))
|
||||||
(merge {:return-keys true} opts))))
|
|
||||||
|
|
||||||
(defn update!
|
(defn update!
|
||||||
([ds table params where] (update! ds table params where nil))
|
[ds table params where & {:as opts}]
|
||||||
([ds table params where opts]
|
(exec-one! ds
|
||||||
(exec-one! ds
|
(sql/update table params where opts)
|
||||||
(sql/update table params where opts)
|
(merge {::return-keys? true} opts)))
|
||||||
(merge {:return-keys true} opts))))
|
|
||||||
|
|
||||||
(defn delete!
|
(defn delete!
|
||||||
([ds table params] (delete! ds table params nil))
|
[ds table params & {:as opts}]
|
||||||
([ds table params opts]
|
(exec-one! ds
|
||||||
(exec-one! ds
|
(sql/delete table params opts)
|
||||||
(sql/delete table params opts)
|
(merge {::return-keys? true} opts)))
|
||||||
(assoc opts :return-keys true))))
|
|
||||||
|
|
||||||
(defn is-row-deleted?
|
(defn is-row-deleted?
|
||||||
[{:keys [deleted-at]}]
|
[{:keys [deleted-at]}]
|
||||||
@ -283,56 +272,34 @@
|
|||||||
(inst-ms (dt/now)))))
|
(inst-ms (dt/now)))))
|
||||||
|
|
||||||
(defn get*
|
(defn get*
|
||||||
"Internal function for retrieve a single row from database that
|
"Retrieve a single row from database that matches a simple filters. Do
|
||||||
matches a simple filters."
|
not raises exceptions."
|
||||||
([ds table params]
|
[ds table params & {:as opts}]
|
||||||
(get* ds table params nil))
|
(let [rows (exec! ds (sql/select table params opts))
|
||||||
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
|
rows (cond->> rows
|
||||||
(let [rows (exec! ds (sql/select table params opts))
|
(::remove-deleted? opts true)
|
||||||
rows (cond->> rows
|
(remove is-row-deleted?))]
|
||||||
check-deleted?
|
(first rows)))
|
||||||
(remove is-row-deleted?))]
|
|
||||||
(first rows))))
|
|
||||||
|
|
||||||
(defn get
|
(defn get
|
||||||
([ds table params]
|
"Retrieve a single row from database that matches a simple
|
||||||
(get ds table params nil))
|
filters. Raises :not-found exception if no object is found."
|
||||||
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
|
[ds table params & {:as opts}]
|
||||||
(let [row (get* ds table params opts)]
|
(let [row (get* ds table params opts)]
|
||||||
(when (and (not row) check-deleted?)
|
(when (and (not row) (::check-deleted? opts true))
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
:code :object-not-found
|
:code :object-not-found
|
||||||
:table table
|
:table table
|
||||||
:hint "database object not found"))
|
:hint "database object not found"))
|
||||||
row)))
|
row))
|
||||||
|
|
||||||
(defn get-by-params
|
|
||||||
"DEPRECATED"
|
|
||||||
([ds table params]
|
|
||||||
(get-by-params ds table params nil))
|
|
||||||
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
|
|
||||||
(let [row (get* ds table params (assoc opts :check-deleted? check-not-found))]
|
|
||||||
(when (and (not row) check-not-found)
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :object-not-found
|
|
||||||
:table table
|
|
||||||
:hint "database object not found"))
|
|
||||||
row)))
|
|
||||||
|
|
||||||
(defn get-by-id
|
(defn get-by-id
|
||||||
([ds table id]
|
[ds table id & {:as opts}]
|
||||||
(get ds table {:id id} nil))
|
(get ds table {:id id} opts))
|
||||||
([ds table id opts]
|
|
||||||
(let [opts (cond-> opts
|
|
||||||
(contains? opts :check-not-found)
|
|
||||||
(assoc :check-deleted? (:check-not-found opts)))]
|
|
||||||
(get ds table {:id id} opts))))
|
|
||||||
|
|
||||||
(defn query
|
(defn query
|
||||||
([ds table params]
|
[ds table params & {:as opts}]
|
||||||
(query ds table params nil))
|
(exec! ds (sql/select table params opts)))
|
||||||
([ds table params opts]
|
|
||||||
(exec! ds (sql/select table params opts))))
|
|
||||||
|
|
||||||
(defn pgobject?
|
(defn pgobject?
|
||||||
([v]
|
([v]
|
||||||
@ -475,6 +442,11 @@
|
|||||||
(.setType "jsonb")
|
(.setType "jsonb")
|
||||||
(.setValue (json/encode-str data)))))
|
(.setValue (json/encode-str data)))))
|
||||||
|
|
||||||
|
(defn get-update-count
|
||||||
|
[result]
|
||||||
|
(:next.jdbc/update-count result))
|
||||||
|
|
||||||
|
|
||||||
;; --- Locks
|
;; --- Locks
|
||||||
|
|
||||||
(def ^:private siphash-state
|
(def ^:private siphash-state
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
(ns app.db.sql
|
(ns app.db.sql
|
||||||
(:refer-clojure :exclude [update])
|
(:refer-clojure :exclude [update])
|
||||||
(:require
|
(:require
|
||||||
|
[app.db :as-alias db]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[next.jdbc.optional :as jdbc-opt]
|
[next.jdbc.optional :as jdbc-opt]
|
||||||
[next.jdbc.sql.builder :as sql]))
|
[next.jdbc.sql.builder :as sql]))
|
||||||
@ -43,8 +44,10 @@
|
|||||||
([table where-params opts]
|
([table where-params opts]
|
||||||
(let [opts (merge default-opts opts)
|
(let [opts (merge default-opts opts)
|
||||||
opts (cond-> opts
|
opts (cond-> opts
|
||||||
(:for-update opts) (assoc :suffix "FOR UPDATE")
|
(::db/for-update? opts) (assoc :suffix "FOR UPDATE")
|
||||||
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
|
(::db/for-share? opts) (assoc :suffix "FOR KEY SHARE")
|
||||||
|
(:for-update opts) (assoc :suffix "FOR UPDATE")
|
||||||
|
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
|
||||||
(sql/for-query table where-params opts))))
|
(sql/for-query table where-params opts))))
|
||||||
|
|
||||||
(defn update
|
(defn update
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.emails
|
(ns app.email
|
||||||
"Main api for send emails."
|
"Main api for send emails."
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
@ -14,7 +14,7 @@
|
|||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
[app.emails.invite-to-team :as-alias emails.invite-to-team]
|
[app.email.invite-to-team :as-alias email.invite-to-team]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.util.template :as tmpl]
|
[app.util.template :as tmpl]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
@ -71,7 +71,7 @@
|
|||||||
(.addFrom ^MimeMessage mmsg from)))))
|
(.addFrom ^MimeMessage mmsg from)))))
|
||||||
|
|
||||||
(defn- assign-reply-to
|
(defn- assign-reply-to
|
||||||
[mmsg {:keys [default-reply-to] :as cfg} {:keys [reply-to] :as params}]
|
[mmsg {:keys [::default-reply-to] :as cfg} {:keys [reply-to] :as params}]
|
||||||
(let [reply-to (or reply-to default-reply-to)]
|
(let [reply-to (or reply-to default-reply-to)]
|
||||||
(when reply-to
|
(when reply-to
|
||||||
(let [reply-to (parse-address reply-to)]
|
(let [reply-to (parse-address reply-to)]
|
||||||
@ -127,9 +127,8 @@
|
|||||||
mmsg))
|
mmsg))
|
||||||
|
|
||||||
(defn- opts->props
|
(defn- opts->props
|
||||||
[{:keys [username tls host port timeout default-from]
|
[{:keys [::username ::tls ::host ::port ::timeout ::default-from]
|
||||||
:or {timeout 30000}
|
:or {timeout 30000}}]
|
||||||
:as opts}]
|
|
||||||
(reduce-kv
|
(reduce-kv
|
||||||
(fn [^Properties props k v]
|
(fn [^Properties props k v]
|
||||||
(if (nil? v)
|
(if (nil? v)
|
||||||
@ -150,8 +149,8 @@
|
|||||||
"mail.smtp.connectiontimeout" timeout}))
|
"mail.smtp.connectiontimeout" timeout}))
|
||||||
|
|
||||||
(defn- create-smtp-session
|
(defn- create-smtp-session
|
||||||
[opts]
|
[cfg]
|
||||||
(let [props (opts->props opts)]
|
(let [props (opts->props cfg)]
|
||||||
(Session/getInstance props)))
|
(Session/getInstance props)))
|
||||||
|
|
||||||
(defn- create-smtp-message
|
(defn- create-smtp-message
|
||||||
@ -171,7 +170,7 @@
|
|||||||
;; TEMPLATE EMAIL IMPL
|
;; TEMPLATE EMAIL IMPL
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private email-path "app/emails/%(id)s/%(lang)s.%(type)s")
|
(def ^:private email-path "app/email/%(id)s/%(lang)s.%(type)s")
|
||||||
|
|
||||||
(defn- render-email-template-part
|
(defn- render-email-template-part
|
||||||
[type id context]
|
[type id context]
|
||||||
@ -283,14 +282,14 @@
|
|||||||
(s/def ::default-from ::cf/smtp-default-from)
|
(s/def ::default-from ::cf/smtp-default-from)
|
||||||
|
|
||||||
(s/def ::smtp-config
|
(s/def ::smtp-config
|
||||||
(s/keys :opt-un [::username
|
(s/keys :opt [::username
|
||||||
::password
|
::password
|
||||||
::tls
|
::tls
|
||||||
::ssl
|
::ssl
|
||||||
::host
|
::host
|
||||||
::port
|
::port
|
||||||
::default-from
|
::default-from
|
||||||
::default-reply-to]))
|
::default-reply-to]))
|
||||||
|
|
||||||
(declare send-to-logger!)
|
(declare send-to-logger!)
|
||||||
|
|
||||||
@ -306,8 +305,8 @@
|
|||||||
(let [session (create-smtp-session cfg)]
|
(let [session (create-smtp-session cfg)]
|
||||||
(with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))]
|
(with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))]
|
||||||
(.connect ^Transport transport
|
(.connect ^Transport transport
|
||||||
^String (:username cfg)
|
^String (::username cfg)
|
||||||
^String (:password cfg))
|
^String (::password cfg))
|
||||||
|
|
||||||
(let [^MimeMessage message (create-smtp-message cfg session params)]
|
(let [^MimeMessage message (create-smtp-message cfg session params)]
|
||||||
(.sendMessage ^Transport transport
|
(.sendMessage ^Transport transport
|
||||||
@ -319,10 +318,10 @@
|
|||||||
(send-to-logger! cfg params))))
|
(send-to-logger! cfg params))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
(s/keys :req-un [::sendmail ::mtx/metrics]))
|
(s/keys :req [::sendmail ::mtx/metrics]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
(defmethod ig/init-key ::handler
|
||||||
[_ {:keys [sendmail]}]
|
[_ {:keys [::sendmail]}]
|
||||||
(fn [{:keys [props] :as task}]
|
(fn [{:keys [props] :as task}]
|
||||||
(sendmail props)))
|
(sendmail props)))
|
||||||
|
|
||||||
@ -380,14 +379,14 @@
|
|||||||
"Password change confirmation email"
|
"Password change confirmation email"
|
||||||
(template-factory ::change-email))
|
(template-factory ::change-email))
|
||||||
|
|
||||||
(s/def ::emails.invite-to-team/invited-by ::us/string)
|
(s/def ::email.invite-to-team/invited-by ::us/string)
|
||||||
(s/def ::emails.invite-to-team/team ::us/string)
|
(s/def ::email.invite-to-team/team ::us/string)
|
||||||
(s/def ::emails.invite-to-team/token ::us/string)
|
(s/def ::email.invite-to-team/token ::us/string)
|
||||||
|
|
||||||
(s/def ::invite-to-team
|
(s/def ::invite-to-team
|
||||||
(s/keys :req-un [::emails.invite-to-team/invited-by
|
(s/keys :req-un [::email.invite-to-team/invited-by
|
||||||
::emails.invite-to-team/token
|
::email.invite-to-team/token
|
||||||
::emails.invite-to-team/team]))
|
::email.invite-to-team/team]))
|
||||||
|
|
||||||
(def invite-to-team
|
(def invite-to-team
|
||||||
"Teams member invitation email."
|
"Teams member invitation email."
|
||||||
@ -6,13 +6,22 @@
|
|||||||
|
|
||||||
(ns app.http
|
(ns app.http
|
||||||
(:require
|
(:require
|
||||||
|
[app.auth.oidc :as-alias oidc]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
|
[app.db :as-alias db]
|
||||||
|
[app.http.access-token :as actoken]
|
||||||
|
[app.http.assets :as-alias assets]
|
||||||
|
[app.http.awsns :as-alias awsns]
|
||||||
|
[app.http.debug :as-alias debug]
|
||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.http.middleware :as mw]
|
[app.http.middleware :as mw]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
|
[app.http.websocket :as-alias ws]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.doc :as-alias rpc.doc]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
@ -37,47 +46,53 @@
|
|||||||
(s/def ::max-body-size integer?)
|
(s/def ::max-body-size integer?)
|
||||||
(s/def ::max-multipart-body-size integer?)
|
(s/def ::max-multipart-body-size integer?)
|
||||||
(s/def ::io-threads integer?)
|
(s/def ::io-threads integer?)
|
||||||
(s/def ::worker-threads integer?)
|
|
||||||
|
|
||||||
(defmethod ig/prep-key ::server
|
(defmethod ig/prep-key ::server
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(merge {:name "http"
|
(merge {::port 6060
|
||||||
:port 6060
|
::host "0.0.0.0"
|
||||||
:host "0.0.0.0"
|
::max-body-size (* 1024 1024 30) ; 30 MiB
|
||||||
:max-body-size (* 1024 1024 30) ; 30 MiB
|
::max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
|
||||||
:max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
|
|
||||||
(d/without-nils cfg)))
|
(d/without-nils cfg)))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::server [_]
|
(defmethod ig/pre-init-spec ::server [_]
|
||||||
(s/and
|
(s/keys :req [::port ::host]
|
||||||
(s/keys :req-un [::port ::host ::name ::max-body-size ::max-multipart-body-size]
|
:opt [::max-body-size
|
||||||
:opt-un [::router ::handler ::io-threads ::worker-threads ::wrk/executor])
|
::max-multipart-body-size
|
||||||
(fn [cfg]
|
::router
|
||||||
(or (contains? cfg :router)
|
::handler
|
||||||
(contains? cfg :handler)))))
|
::io-threads
|
||||||
|
::wrk/executor]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::server
|
(defmethod ig/init-key ::server
|
||||||
[_ {:keys [handler router port name host] :as cfg}]
|
[_ {:keys [::handler ::router ::host ::port] :as cfg}]
|
||||||
(l/info :hint "starting http server" :port port :host host :name name)
|
(l/info :hint "starting http server" :port port :host host)
|
||||||
(let [options {:http/port port
|
(let [options {:http/port port
|
||||||
:http/host host
|
:http/host host
|
||||||
:http/max-body-size (:max-body-size cfg)
|
:http/max-body-size (::max-body-size cfg)
|
||||||
:http/max-multipart-body-size (:max-multipart-body-size cfg)
|
:http/max-multipart-body-size (::max-multipart-body-size cfg)
|
||||||
:xnio/io-threads (:io-threads cfg)
|
:xnio/io-threads (::io-threads cfg)
|
||||||
:xnio/worker-threads (:worker-threads cfg)
|
:xnio/dispatch (::wrk/executor cfg)
|
||||||
:xnio/dispatch (:executor cfg)
|
|
||||||
:ring/async true}
|
:ring/async true}
|
||||||
|
|
||||||
handler (if (some? router)
|
handler (cond
|
||||||
|
(some? router)
|
||||||
(wrap-router router)
|
(wrap-router router)
|
||||||
|
|
||||||
handler)
|
(some? handler)
|
||||||
server (yt/server handler (d/without-nils options))]
|
handler
|
||||||
(assoc cfg :server (yt/start! server))))
|
|
||||||
|
:else
|
||||||
|
(throw (UnsupportedOperationException. "handler or router are required")))
|
||||||
|
|
||||||
|
options (d/without-nils options)
|
||||||
|
server (yt/server handler options)]
|
||||||
|
|
||||||
|
(assoc cfg ::server (yt/start! server))))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::server
|
(defmethod ig/halt-key! ::server
|
||||||
[_ {:keys [server name port] :as cfg}]
|
[_ {:keys [::server ::port] :as cfg}]
|
||||||
(l/info :msg "stopping http server" :name name :port port)
|
(l/info :msg "stopping http server" :port port)
|
||||||
(yt/stop! server))
|
(yt/stop! server))
|
||||||
|
|
||||||
(defn- not-found-handler
|
(defn- not-found-handler
|
||||||
@ -113,64 +128,41 @@
|
|||||||
;; HTTP ROUTER
|
;; HTTP ROUTER
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(s/def ::assets map?)
|
|
||||||
(s/def ::awsns-handler fn?)
|
|
||||||
(s/def ::debug-routes (s/nilable vector?))
|
|
||||||
(s/def ::doc-routes (s/nilable vector?))
|
|
||||||
(s/def ::feedback fn?)
|
|
||||||
(s/def ::oauth map?)
|
|
||||||
(s/def ::oidc-routes (s/nilable vector?))
|
|
||||||
(s/def ::rpc-routes (s/nilable vector?))
|
|
||||||
(s/def ::session ::session/session)
|
|
||||||
(s/def ::storage map?)
|
|
||||||
(s/def ::ws fn?)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::router [_]
|
(defmethod ig/pre-init-spec ::router [_]
|
||||||
(s/keys :req-un [::mtx/metrics
|
(s/keys :req [::session/manager
|
||||||
::ws
|
::actoken/manager
|
||||||
::storage
|
::ws/routes
|
||||||
::assets
|
::rpc/routes
|
||||||
::session
|
::rpc.doc/routes
|
||||||
::feedback
|
::oidc/routes
|
||||||
::awsns-handler
|
::assets/routes
|
||||||
::debug-routes
|
::debug/routes
|
||||||
::oidc-routes
|
::db/pool
|
||||||
::rpc-routes
|
::mtx/routes
|
||||||
::doc-routes]))
|
::awsns/routes]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::router
|
(defmethod ig/init-key ::router
|
||||||
[_ {:keys [ws session metrics assets feedback] :as cfg}]
|
[_ cfg]
|
||||||
(rr/router
|
(rr/router
|
||||||
[["" {:middleware [[mw/server-timing]
|
[["" {:middleware [[mw/server-timing]
|
||||||
[mw/format-response]
|
[mw/format-response]
|
||||||
[mw/params]
|
[mw/params]
|
||||||
[mw/parse-request]
|
[mw/parse-request]
|
||||||
[session/middleware-1 session]
|
[session/soft-auth cfg]
|
||||||
|
[actoken/soft-auth cfg]
|
||||||
[mw/errors errors/handle]
|
[mw/errors errors/handle]
|
||||||
[mw/restrict-methods]]}
|
[mw/restrict-methods]]}
|
||||||
|
|
||||||
["/metrics" {:handler (::mtx/handler metrics)
|
(::mtx/routes cfg)
|
||||||
:allowed-methods #{:get}}]
|
(::assets/routes cfg)
|
||||||
|
(::debug/routes cfg)
|
||||||
["/assets" {:middleware [[session/middleware-2 session]]}
|
|
||||||
["/by-id/:id" {:handler (:objects-handler assets)}]
|
|
||||||
["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}]
|
|
||||||
["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]]
|
|
||||||
|
|
||||||
(:debug-routes cfg)
|
|
||||||
|
|
||||||
["/webhooks"
|
["/webhooks"
|
||||||
["/sns" {:handler (:awsns-handler cfg)
|
(::awsns/routes cfg)]
|
||||||
:allowed-methods #{:post}}]]
|
|
||||||
|
|
||||||
["/ws/notifications" {:middleware [[session/middleware-2 session]]
|
(::ws/routes cfg)
|
||||||
:handler ws
|
|
||||||
:allowed-methods #{:get}}]
|
|
||||||
|
|
||||||
["/api" {:middleware [[mw/cors]
|
["/api" {:middleware [[mw/cors]]}
|
||||||
[session/middleware-2 session]]}
|
(::oidc/routes cfg)
|
||||||
["/feedback" {:handler feedback
|
(::rpc.doc/routes cfg)
|
||||||
:allowed-methods #{:post}}]
|
(::rpc/routes cfg)]]]))
|
||||||
(:doc-routes cfg)
|
|
||||||
(:oidc-routes cfg)
|
|
||||||
(:rpc-routes cfg)]]]))
|
|
||||||
|
|||||||
96
backend/src/app/http/access_token.clj
Normal file
96
backend/src/app/http/access_token.clj
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.http.access-token
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.main :as-alias main]
|
||||||
|
[app.tokens :as tokens]
|
||||||
|
[app.worker :as-alias wrk]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[promesa.core :as p]
|
||||||
|
[promesa.exec :as px]
|
||||||
|
[yetti.request :as yrq]))
|
||||||
|
|
||||||
|
|
||||||
|
(s/def ::manager
|
||||||
|
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::manager [_] ::manager)
|
||||||
|
(defmethod ig/init-key ::manager [_ cfg] cfg)
|
||||||
|
(defmethod ig/halt-key! ::manager [_ _])
|
||||||
|
|
||||||
|
(def header-re #"^Token\s+(.*)")
|
||||||
|
|
||||||
|
(defn- get-token
|
||||||
|
[request]
|
||||||
|
(some->> (yrq/get-header request "authorization")
|
||||||
|
(re-matches header-re)
|
||||||
|
(second)))
|
||||||
|
|
||||||
|
(defn- decode-token
|
||||||
|
[props token]
|
||||||
|
(when token
|
||||||
|
(tokens/verify props {:token token :iss "access-token"})))
|
||||||
|
|
||||||
|
(defn- get-token-perms
|
||||||
|
[pool token-id]
|
||||||
|
(when-not (db/read-only? pool)
|
||||||
|
(when-let [token (db/get* pool :access-token {:id token-id} {:columns [:perms]})]
|
||||||
|
(some-> (:perms token)
|
||||||
|
(db/decode-pgarray #{})))))
|
||||||
|
|
||||||
|
(defn- wrap-soft-auth
|
||||||
|
[handler {:keys [::manager]}]
|
||||||
|
(us/assert! ::manager manager)
|
||||||
|
|
||||||
|
(let [{:keys [::wrk/executor ::main/props]} manager]
|
||||||
|
(fn [request respond raise]
|
||||||
|
(let [token (get-token request)]
|
||||||
|
(->> (px/submit! executor (partial decode-token props token))
|
||||||
|
(p/fnly (fn [claims cause]
|
||||||
|
(when cause
|
||||||
|
(l/trace :hint "exception on decoding malformed token" :cause cause))
|
||||||
|
(let [request (cond-> request
|
||||||
|
(map? claims)
|
||||||
|
(assoc ::id (:tid claims)))]
|
||||||
|
(handler request respond raise)))))))))
|
||||||
|
|
||||||
|
(defn- wrap-authz
|
||||||
|
[handler {:keys [::manager]}]
|
||||||
|
(us/assert! ::manager manager)
|
||||||
|
(let [{:keys [::wrk/executor ::db/pool]} manager]
|
||||||
|
(fn [request respond raise]
|
||||||
|
(if-let [token-id (::id request)]
|
||||||
|
(->> (px/submit! executor (partial get-token-perms pool token-id))
|
||||||
|
(p/fnly (fn [perms cause]
|
||||||
|
(cond
|
||||||
|
(some? cause)
|
||||||
|
(raise cause)
|
||||||
|
|
||||||
|
(nil? perms)
|
||||||
|
(handler request respond raise)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(let [request (assoc request ::perms perms)]
|
||||||
|
(handler request respond raise))))))
|
||||||
|
(handler request respond raise)))))
|
||||||
|
|
||||||
|
(def soft-auth
|
||||||
|
{:name ::soft-auth
|
||||||
|
:compile (fn [& _]
|
||||||
|
(when (contains? cf/flags :access-tokens)
|
||||||
|
wrap-soft-auth))})
|
||||||
|
|
||||||
|
(def authz
|
||||||
|
{:name ::authz
|
||||||
|
:compile (fn [& _]
|
||||||
|
(when (contains? cf/flags :access-tokens)
|
||||||
|
wrap-authz))})
|
||||||
@ -7,18 +7,17 @@
|
|||||||
(ns app.http.assets
|
(ns app.http.assets
|
||||||
"Assets related handlers."
|
"Assets related handlers."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.metrics :as mtx]
|
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[promesa.exec :as px]
|
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as yrs]))
|
||||||
|
|
||||||
(def ^:private cache-max-age
|
(def ^:private cache-max-age
|
||||||
@ -27,105 +26,100 @@
|
|||||||
(def ^:private signature-max-age
|
(def ^:private signature-max-age
|
||||||
(dt/duration {:hours 24 :minutes 15}))
|
(dt/duration {:hours 24 :minutes 15}))
|
||||||
|
|
||||||
(defn coerce-id
|
(defn get-id
|
||||||
[id]
|
[{:keys [path-params]}]
|
||||||
(let [res (parse-uuid id)]
|
(if-let [id (some-> path-params :id d/parse-uuid)]
|
||||||
(when-not (uuid? res)
|
(p/resolved id)
|
||||||
(ex/raise :type :not-found
|
(p/rejected (ex/error :type :not-found
|
||||||
:hint "object not found"))
|
:hunt "object not found"))))
|
||||||
res))
|
|
||||||
|
|
||||||
(defn- get-file-media-object
|
(defn- get-file-media-object
|
||||||
[{:keys [pool executor] :as storage} id]
|
[pool id]
|
||||||
(px/with-dispatch executor
|
(db/get pool :file-media-object {:id id}))
|
||||||
(let [id (coerce-id id)
|
|
||||||
mobj (db/exec-one! pool ["select * from file_media_object where id=?" id])]
|
(defn- serve-object-from-s3
|
||||||
(when-not mobj
|
[{:keys [::sto/storage] :as cfg} obj]
|
||||||
(ex/raise :type :not-found
|
(let [mdata (meta obj)]
|
||||||
:hint "object does not found"))
|
(->> (sto/get-object-url storage obj {:max-age signature-max-age})
|
||||||
mobj)))
|
(p/fmap (fn [{:keys [host port] :as url}]
|
||||||
|
(let [headers {"location" (str url)
|
||||||
|
"x-host" (cond-> host port (str ":" port))
|
||||||
|
"x-mtype" (:content-type mdata)
|
||||||
|
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
|
||||||
|
(yrs/response
|
||||||
|
:status 307
|
||||||
|
:headers headers)))))))
|
||||||
|
|
||||||
|
(defn- serve-object-from-fs
|
||||||
|
[{:keys [::path]} obj]
|
||||||
|
(let [purl (u/join (u/uri path)
|
||||||
|
(sto/object->relative-path obj))
|
||||||
|
mdata (meta obj)
|
||||||
|
headers {"x-accel-redirect" (:path purl)
|
||||||
|
"content-type" (:content-type mdata)
|
||||||
|
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
|
||||||
|
(p/resolved
|
||||||
|
(yrs/response :status 204 :headers headers))))
|
||||||
|
|
||||||
(defn- serve-object
|
(defn- serve-object
|
||||||
"Helper function that returns the appropriate response depending on
|
"Helper function that returns the appropriate response depending on
|
||||||
the storage object backend type."
|
the storage object backend type."
|
||||||
[{:keys [storage] :as cfg} obj]
|
[{:keys [::sto/storage] :as cfg} {:keys [backend] :as obj}]
|
||||||
(let [mdata (meta obj)
|
(let [backend (sto/resolve-backend storage backend)]
|
||||||
backend (sto/resolve-backend storage (:backend obj))]
|
(case (::sto/type backend)
|
||||||
(case (:type backend)
|
:s3 (serve-object-from-s3 cfg obj)
|
||||||
:s3
|
:fs (serve-object-from-fs cfg obj))))
|
||||||
(p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
|
|
||||||
(yrs/response :status 307
|
|
||||||
:headers {"location" (str url)
|
|
||||||
"x-host" (cond-> host port (str ":" port))
|
|
||||||
"x-mtype" (:content-type mdata)
|
|
||||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}))
|
|
||||||
|
|
||||||
:fs
|
|
||||||
(p/let [purl (u/uri (:assets-path cfg))
|
|
||||||
purl (u/join purl (sto/object->relative-path obj))]
|
|
||||||
(yrs/response :status 204
|
|
||||||
:headers {"x-accel-redirect" (:path purl)
|
|
||||||
"content-type" (:content-type mdata)
|
|
||||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))})))))
|
|
||||||
|
|
||||||
(defn objects-handler
|
(defn objects-handler
|
||||||
"Handler that servers storage objects by id."
|
"Handler that servers storage objects by id."
|
||||||
[{:keys [storage executor] :as cfg} request respond raise]
|
[{:keys [::sto/storage ::wrk/executor] :as cfg} request respond raise]
|
||||||
(-> (px/with-dispatch executor
|
(->> (get-id request)
|
||||||
(p/let [id (get-in request [:path-params :id])
|
(p/mcat executor (fn [id] (sto/get-object storage id)))
|
||||||
id (coerce-id id)
|
(p/mcat executor (fn [obj]
|
||||||
obj (sto/get-object storage id)]
|
(if (some? obj)
|
||||||
(if obj
|
(serve-object cfg obj)
|
||||||
(serve-object cfg obj)
|
(p/resolved (yrs/response 404)))))
|
||||||
(yrs/response 404))))
|
(p/fnly executor (fn [result cause]
|
||||||
|
(if cause (raise cause) (respond result))))))
|
||||||
(p/bind p/wrap)
|
|
||||||
(p/then' respond)
|
|
||||||
(p/catch raise)))
|
|
||||||
|
|
||||||
(defn- generic-handler
|
(defn- generic-handler
|
||||||
"A generic handler helper/common code for file-media based handlers."
|
"A generic handler helper/common code for file-media based handlers."
|
||||||
[{:keys [storage] :as cfg} request kf]
|
[{:keys [::sto/storage ::wrk/executor] :as cfg} request kf]
|
||||||
(p/let [id (get-in request [:path-params :id])
|
(let [pool (::db/pool storage)]
|
||||||
mobj (get-file-media-object storage id)
|
(->> (get-id request)
|
||||||
obj (sto/get-object storage (kf mobj))]
|
(p/fmap executor (fn [id] (get-file-media-object pool id)))
|
||||||
(if obj
|
(p/mcat executor (fn [mobj] (sto/get-object storage (kf mobj))))
|
||||||
(serve-object cfg obj)
|
(p/mcat executor (fn [sobj]
|
||||||
(yrs/response 404))))
|
(if sobj
|
||||||
|
(serve-object cfg sobj)
|
||||||
|
(p/resolved (yrs/response 404))))))))
|
||||||
|
|
||||||
(defn file-objects-handler
|
(defn file-objects-handler
|
||||||
"Handler that serves storage objects by file media id."
|
"Handler that serves storage objects by file media id."
|
||||||
[cfg request respond raise]
|
[cfg request respond raise]
|
||||||
(-> (generic-handler cfg request :media-id)
|
(->> (generic-handler cfg request :media-id)
|
||||||
(p/then respond)
|
(p/fnly (fn [result cause]
|
||||||
(p/catch raise)))
|
(if cause (raise cause) (respond result))))))
|
||||||
|
|
||||||
(defn file-thumbnails-handler
|
(defn file-thumbnails-handler
|
||||||
"Handler that serves storage objects by thumbnail-id and quick
|
"Handler that serves storage objects by thumbnail-id and quick
|
||||||
fallback to file-media-id if no thumbnail is available."
|
fallback to file-media-id if no thumbnail is available."
|
||||||
[cfg request respond raise]
|
[cfg request respond raise]
|
||||||
(-> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
|
(->> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
|
||||||
(p/then respond)
|
(p/fnly (fn [result cause]
|
||||||
(p/catch raise)))
|
(if cause (raise cause) (respond result))))))
|
||||||
|
|
||||||
;; --- Initialization
|
;; --- Initialization
|
||||||
|
|
||||||
(s/def ::storage some?)
|
(s/def ::path ::us/string)
|
||||||
(s/def ::assets-path ::us/string)
|
(s/def ::routes vector?)
|
||||||
(s/def ::cache-max-age ::dt/duration)
|
|
||||||
(s/def ::signature-max-age ::dt/duration)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handlers [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req-un [::storage
|
(s/keys :req [::sto/storage ::wrk/executor ::path]))
|
||||||
::wrk/executor
|
|
||||||
::mtx/metrics
|
|
||||||
::assets-path
|
|
||||||
::cache-max-age
|
|
||||||
::signature-max-age]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::handlers
|
(defmethod ig/init-key ::routes
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
{:objects-handler (partial objects-handler cfg)
|
["/assets"
|
||||||
:file-objects-handler (partial file-objects-handler cfg)
|
["/by-id/:id" {:handler (partial objects-handler cfg)}]
|
||||||
:file-thumbnails-handler (partial file-thumbnails-handler cfg)})
|
["/by-file-media-id/:id" {:handler (partial file-objects-handler cfg)}]
|
||||||
|
["/by-file-media-id/:id/thumbnail" {:handler (partial file-thumbnails-handler cfg)}]])
|
||||||
|
|||||||
@ -28,18 +28,20 @@
|
|||||||
(declare parse-notification)
|
(declare parse-notification)
|
||||||
(declare process-report)
|
(declare process-report)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req [::http/client
|
(s/keys :req [::http/client
|
||||||
::main/props
|
::main/props
|
||||||
::db/pool
|
::db/pool
|
||||||
::wrk/executor]))
|
::wrk/executor]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [::wrk/executor] :as cfg}]
|
[_ {:keys [::wrk/executor] :as cfg}]
|
||||||
(fn [request respond _]
|
(letfn [(handler [request respond _]
|
||||||
(let [data (-> request yrq/body slurp)]
|
(let [data (-> request yrq/body slurp)]
|
||||||
(px/run! executor #(handle-request cfg data)))
|
(px/run! executor #(handle-request cfg data)))
|
||||||
(respond (yrs/response 200))))
|
(respond (yrs/response 200)))]
|
||||||
|
["/sns" {:handler handler
|
||||||
|
:allowed-methods #{:post}}]))
|
||||||
|
|
||||||
(defn handle-request
|
(defn handle-request
|
||||||
[cfg data]
|
[cfg data]
|
||||||
@ -105,8 +107,7 @@
|
|||||||
[cfg headers]
|
[cfg headers]
|
||||||
(let [tdata (get headers "x-penpot-data")]
|
(let [tdata (get headers "x-penpot-data")]
|
||||||
(when-not (str/empty? tdata)
|
(when-not (str/empty? tdata)
|
||||||
(let [sprops (::main/props cfg)
|
(let [result (tokens/verify (::main/props cfg) {:token tdata :iss :profile-identity})]
|
||||||
result (tokens/verify sprops {:token tdata :iss :profile-identity})]
|
|
||||||
(:profile-id result)))))
|
(:profile-id result)))))
|
||||||
|
|
||||||
(defn- parse-notification
|
(defn- parse-notification
|
||||||
|
|||||||
@ -43,9 +43,9 @@
|
|||||||
(defn req!
|
(defn req!
|
||||||
"A convencience toplevel function for gradual migration to a new API
|
"A convencience toplevel function for gradual migration to a new API
|
||||||
convention."
|
convention."
|
||||||
([{:keys [::client] :as holder} request]
|
([{:keys [::client]} request]
|
||||||
(us/assert! ::client-holder holder)
|
(us/assert! ::client client)
|
||||||
(send! client request {}))
|
(send! client request {}))
|
||||||
([{:keys [::client] :as holder} request options]
|
([{:keys [::client]} request options]
|
||||||
(us/assert! ::client-holder holder)
|
(us/assert! ::client client)
|
||||||
(send! client request options)))
|
(send! client request options)))
|
||||||
|
|||||||
@ -16,8 +16,8 @@
|
|||||||
[app.http.middleware :as mw]
|
[app.http.middleware :as mw]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.rpc.commands.binfile :as binf]
|
[app.rpc.commands.binfile :as binf]
|
||||||
[app.rpc.commands.files.create :refer [create-file]]
|
[app.rpc.commands.files-create :refer [create-file]]
|
||||||
[app.rpc.queries.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.template :as tmpl]
|
[app.util.template :as tmpl]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -39,9 +39,9 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn authorized?
|
(defn authorized?
|
||||||
[pool {:keys [profile-id]}]
|
[pool {:keys [::session/profile-id]}]
|
||||||
(or (= "devenv" (cf/get :host))
|
(or (= "devenv" (cf/get :host))
|
||||||
(let [profile (ex/ignoring (profile/retrieve-profile-data pool profile-id))
|
(let [profile (ex/ignoring (profile/get-profile pool profile-id))
|
||||||
admins (or (cf/get :admins) #{})]
|
admins (or (cf/get :admins) #{})]
|
||||||
(contains? admins (:email profile)))))
|
(contains? admins (:email profile)))))
|
||||||
|
|
||||||
@ -61,7 +61,7 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn index-handler
|
(defn index-handler
|
||||||
[{:keys [pool]} request]
|
[{:keys [::db/pool]} request]
|
||||||
(when-not (authorized? pool request)
|
(when-not (authorized? pool request)
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :only-admins-allowed))
|
:code :only-admins-allowed))
|
||||||
@ -81,7 +81,7 @@
|
|||||||
"select revn, changes, data from file_change where file_id=? and revn = ?")
|
"select revn, changes, data from file_change where file_id=? and revn = ?")
|
||||||
|
|
||||||
(defn- retrieve-file-data
|
(defn- retrieve-file-data
|
||||||
[{:keys [pool]} {:keys [params profile-id] :as request}]
|
[{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}]
|
||||||
(when-not (authorized? pool request)
|
(when-not (authorized? pool request)
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :only-admins-allowed))
|
:code :only-admins-allowed))
|
||||||
@ -107,8 +107,9 @@
|
|||||||
(prepare-download-response data filename)
|
(prepare-download-response data filename)
|
||||||
|
|
||||||
(contains? params :clone)
|
(contains? params :clone)
|
||||||
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
|
(let [profile (profile/get-profile pool profile-id)
|
||||||
data (some-> data blob/decode)]
|
project-id (:default-project-id profile)
|
||||||
|
data (blob/decode data)]
|
||||||
(create-file pool {:id (uuid/next)
|
(create-file pool {:id (uuid/next)
|
||||||
:name (str "Cloned file: " filename)
|
:name (str "Cloned file: " filename)
|
||||||
:project-id project-id
|
:project-id project-id
|
||||||
@ -117,7 +118,7 @@
|
|||||||
(yrs/response 201 "OK CREATED"))
|
(yrs/response 201 "OK CREATED"))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(prepare-response (some-> data blob/decode))))))
|
(prepare-response (blob/decode data))))))
|
||||||
|
|
||||||
(defn- is-file-exists?
|
(defn- is-file-exists?
|
||||||
[pool id]
|
[pool id]
|
||||||
@ -125,8 +126,9 @@
|
|||||||
(-> (db/exec-one! pool [sql id]) :exists)))
|
(-> (db/exec-one! pool [sql id]) :exists)))
|
||||||
|
|
||||||
(defn- upload-file-data
|
(defn- upload-file-data
|
||||||
[{:keys [pool]} {:keys [profile-id params] :as request}]
|
[{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}]
|
||||||
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
|
(let [profile (profile/get-profile pool profile-id)
|
||||||
|
project-id (:default-project-id profile)
|
||||||
data (some-> params :file :path io/read-as-bytes blob/decode)]
|
data (some-> params :file :path io/read-as-bytes blob/decode)]
|
||||||
|
|
||||||
(if (and data project-id)
|
(if (and data project-id)
|
||||||
@ -162,7 +164,7 @@
|
|||||||
:code :method-not-found)))
|
:code :method-not-found)))
|
||||||
|
|
||||||
(defn file-changes-handler
|
(defn file-changes-handler
|
||||||
[{:keys [pool]} {:keys [params] :as request}]
|
[{:keys [::db/pool]} {:keys [params] :as request}]
|
||||||
(when-not (authorized? pool request)
|
(when-not (authorized? pool request)
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :only-admins-allowed))
|
:code :only-admins-allowed))
|
||||||
@ -202,46 +204,48 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn error-handler
|
(defn error-handler
|
||||||
[{:keys [pool]} request]
|
[{:keys [::db/pool]} request]
|
||||||
(letfn [(parse-id [request]
|
(letfn [(get-report [{:keys [path-params]}]
|
||||||
(let [id (get-in request [:path-params :id])
|
|
||||||
id (parse-uuid id)]
|
|
||||||
(when (uuid? id)
|
|
||||||
id)))
|
|
||||||
|
|
||||||
(retrieve-report [id]
|
|
||||||
(ex/ignoring
|
(ex/ignoring
|
||||||
(some-> (db/get-by-id pool :server-error-report id) :content db/decode-transit-pgobject)))
|
(let [report-id (some-> path-params :id parse-uuid)]
|
||||||
|
(some-> (db/get-by-id pool :server-error-report report-id)
|
||||||
|
(update :content db/decode-transit-pgobject)))))
|
||||||
|
|
||||||
(render-template [report]
|
(render-template-v1 [{:keys [content]}]
|
||||||
(let [context (dissoc report
|
(let [context (dissoc content
|
||||||
:trace :cause :params :data :spec-problems :message
|
:trace :cause :params :data :spec-problems :message
|
||||||
:spec-explain :spec-value :error :explain :hint)
|
:spec-explain :spec-value :error :explain :hint)
|
||||||
params {:context (pp/pprint-str context :width 200)
|
params {:context (pp/pprint-str context :width 200)
|
||||||
:hint (:hint report)
|
:hint (:hint content)
|
||||||
:spec-explain (:spec-explain report)
|
:spec-explain (:spec-explain content)
|
||||||
:spec-problems (:spec-problems report)
|
:spec-problems (:spec-problems content)
|
||||||
:spec-value (:spec-value report)
|
:spec-value (:spec-value content)
|
||||||
:data (:data report)
|
:data (:data content)
|
||||||
:trace (or (:trace report)
|
:trace (or (:trace content)
|
||||||
(some-> report :error :trace))
|
(some-> content :error :trace))
|
||||||
:params (:params report)}]
|
:params (:params content)}]
|
||||||
(-> (io/resource "app/templates/error-report.tmpl")
|
(-> (io/resource "app/templates/error-report.tmpl")
|
||||||
(tmpl/render params))))]
|
(tmpl/render params))))
|
||||||
|
|
||||||
|
(render-template-v2 [{report :content}]
|
||||||
|
(-> (io/resource "app/templates/error-report.v2.tmpl")
|
||||||
|
(tmpl/render report)))
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
(when-not (authorized? pool request)
|
(when-not (authorized? pool request)
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :only-admins-allowed))
|
:code :only-admins-allowed))
|
||||||
|
|
||||||
(let [result (some-> (parse-id request)
|
(if-let [report (get-report request)]
|
||||||
(retrieve-report)
|
(let [result (if (= 1 (:version report))
|
||||||
(render-template))]
|
(render-template-v1 report)
|
||||||
(if result
|
(render-template-v2 report))]
|
||||||
(yrs/response :status 200
|
(yrs/response :status 200
|
||||||
:body result
|
:body result
|
||||||
:headers {"content-type" "text/html; charset=utf-8"
|
:headers {"content-type" "text/html; charset=utf-8"
|
||||||
"x-robots-tag" "noindex"})
|
"x-robots-tag" "noindex"}))
|
||||||
(yrs/response 404 "not found")))))
|
(yrs/response 404 "not found"))))
|
||||||
|
|
||||||
(def sql:error-reports
|
(def sql:error-reports
|
||||||
"SELECT id, created_at,
|
"SELECT id, created_at,
|
||||||
@ -251,7 +255,7 @@
|
|||||||
LIMIT 100")
|
LIMIT 100")
|
||||||
|
|
||||||
(defn error-list-handler
|
(defn error-list-handler
|
||||||
[{:keys [pool]} request]
|
[{:keys [::db/pool]} request]
|
||||||
(when-not (authorized? pool request)
|
(when-not (authorized? pool request)
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :only-admins-allowed))
|
:code :only-admins-allowed))
|
||||||
@ -268,7 +272,7 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn export-handler
|
(defn export-handler
|
||||||
[{:keys [pool] :as cfg} {:keys [params profile-id] :as request}]
|
[{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}]
|
||||||
|
|
||||||
(let [file-ids (->> (:file-ids params)
|
(let [file-ids (->> (:file-ids params)
|
||||||
(remove empty?)
|
(remove empty?)
|
||||||
@ -287,7 +291,8 @@
|
|||||||
(assoc ::binf/include-libraries? libs?)
|
(assoc ::binf/include-libraries? libs?)
|
||||||
(binf/export-to-tmpfile!))]
|
(binf/export-to-tmpfile!))]
|
||||||
(if clone?
|
(if clone?
|
||||||
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)]
|
(let [profile (profile/get-profile pool profile-id)
|
||||||
|
project-id (:default-project-id profile)]
|
||||||
(binf/import!
|
(binf/import!
|
||||||
(assoc cfg
|
(assoc cfg
|
||||||
::binf/input path
|
::binf/input path
|
||||||
@ -309,15 +314,16 @@
|
|||||||
|
|
||||||
|
|
||||||
(defn import-handler
|
(defn import-handler
|
||||||
[{:keys [pool] :as cfg} {:keys [params profile-id] :as request}]
|
[{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}]
|
||||||
(when-not (contains? params :file)
|
(when-not (contains? params :file)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :missing-upload-file
|
:code :missing-upload-file
|
||||||
:hint "missing upload file"))
|
:hint "missing upload file"))
|
||||||
|
|
||||||
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
|
(let [profile (profile/get-profile pool profile-id)
|
||||||
|
project-id (:default-project-id profile)
|
||||||
overwrite? (contains? params :overwrite)
|
overwrite? (contains? params :overwrite)
|
||||||
migrate? (contains? params :migrate)
|
migrate? (contains? params :migrate)
|
||||||
ignore-index-errors? (contains? params :ignore-index-errors)]
|
ignore-index-errors? (contains? params :ignore-index-errors)]
|
||||||
|
|
||||||
(when-not project-id
|
(when-not project-id
|
||||||
@ -345,15 +351,14 @@
|
|||||||
|
|
||||||
(defn health-handler
|
(defn health-handler
|
||||||
"Mainly a task that performs a health check."
|
"Mainly a task that performs a health check."
|
||||||
[{:keys [pool]} _]
|
[{:keys [::db/pool]} _]
|
||||||
(db/with-atomic [conn pool]
|
(try
|
||||||
(try
|
(db/exec-one! pool ["select count(*) as count from server_prop;"])
|
||||||
(db/exec-one! conn ["select count(*) as count from server_prop;"])
|
(yrs/response 200 "OK")
|
||||||
(yrs/response 200 "OK")
|
(catch Throwable cause
|
||||||
(catch Throwable cause
|
(l/warn :hint "unable to execute query on health handler"
|
||||||
(l/warn :hint "unable to execute query on health handler"
|
:cause cause)
|
||||||
:cause cause)
|
(yrs/response 503 "KO"))))
|
||||||
(yrs/response 503 "KO")))))
|
|
||||||
|
|
||||||
(defn changelog-handler
|
(defn changelog-handler
|
||||||
[_ _]
|
[_ _]
|
||||||
@ -381,16 +386,17 @@
|
|||||||
(raise (ex/error :type :authentication
|
(raise (ex/error :type :authentication
|
||||||
:code :only-admins-allowed))))))})
|
:code :only-admins-allowed))))))})
|
||||||
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::routes [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req-un [::db/pool ::wrk/executor ::session/session]))
|
(s/keys :req [::db/pool
|
||||||
|
::wrk/executor
|
||||||
|
::session/manager]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [session pool executor] :as cfg}]
|
[_ {:keys [::db/pool ::wrk/executor] :as cfg}]
|
||||||
[["/readyz" {:middleware [[mw/with-dispatch executor]
|
[["/readyz" {:middleware [[mw/with-dispatch executor]
|
||||||
[mw/with-config cfg]]
|
[mw/with-config cfg]]
|
||||||
:handler health-handler}]
|
:handler health-handler}]
|
||||||
["/dbg" {:middleware [[session/middleware-2 session]
|
["/dbg" {:middleware [[session/authz cfg]
|
||||||
[with-authorization pool]
|
[with-authorization pool]
|
||||||
[mw/with-dispatch executor]
|
[mw/with-dispatch executor]
|
||||||
[mw/with-config cfg]]}
|
[mw/with-config cfg]]}
|
||||||
|
|||||||
@ -7,37 +7,36 @@
|
|||||||
(ns app.http.errors
|
(ns app.http.errors
|
||||||
"A errors handling for the http server."
|
"A errors handling for the http server."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
|
[app.http.access-token :as-alias actoken]
|
||||||
|
[app.http.session :as-alias session]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[yetti.request :as yrq]
|
[yetti.request :as yrq]
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as yrs]))
|
||||||
|
|
||||||
(def ^:dynamic *context* {})
|
|
||||||
|
|
||||||
(defn- parse-client-ip
|
(defn- parse-client-ip
|
||||||
[request]
|
[request]
|
||||||
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
|
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
|
||||||
(yrq/get-header request "x-real-ip")
|
(yrq/get-header request "x-real-ip")
|
||||||
(yrq/remote-addr request)))
|
(yrq/remote-addr request)))
|
||||||
|
|
||||||
(defn get-context
|
(defn request->context
|
||||||
|
"Extracts error report relevant context data from request."
|
||||||
[request]
|
[request]
|
||||||
(let [claims (:session-token-claims request)]
|
(let [claims (-> {}
|
||||||
(merge
|
(into (::session/token-claims request))
|
||||||
*context*
|
(into (::actoken/token-claims request)))]
|
||||||
{:path (:path request)
|
{:path (:path request)
|
||||||
:method (:method request)
|
:method (:method request)
|
||||||
:params (:params request)
|
:params (:params request)
|
||||||
:ip-addr (parse-client-ip request)}
|
:ip-addr (parse-client-ip request)
|
||||||
(d/without-nils
|
:user-agent (yrq/get-header request "user-agent")
|
||||||
{:user-agent (yrq/get-header request "user-agent")
|
:profile-id (:uid claims)
|
||||||
:frontend-version (or (yrq/get-header request "x-frontend-version")
|
:version (or (yrq/get-header request "x-frontend-version")
|
||||||
"unknown")
|
"unknown")}))
|
||||||
:profile-id (:uid claims)}))))
|
|
||||||
|
|
||||||
(defmulti handle-exception
|
(defmulti handle-exception
|
||||||
(fn [err & _rest]
|
(fn [err & _rest]
|
||||||
@ -49,6 +48,10 @@
|
|||||||
[err _]
|
[err _]
|
||||||
(yrs/response 401 (ex-data err)))
|
(yrs/response 401 (ex-data err)))
|
||||||
|
|
||||||
|
(defmethod handle-exception :authorization
|
||||||
|
[err _]
|
||||||
|
(yrs/response 403 (ex-data err)))
|
||||||
|
|
||||||
(defmethod handle-exception :restriction
|
(defmethod handle-exception :restriction
|
||||||
[err _]
|
[err _]
|
||||||
(yrs/response 400 (ex-data err)))
|
(yrs/response 400 (ex-data err)))
|
||||||
@ -79,15 +82,14 @@
|
|||||||
[error request]
|
[error request]
|
||||||
(let [edata (ex-data error)
|
(let [edata (ex-data error)
|
||||||
explain (ex/explain edata)]
|
explain (ex/explain edata)]
|
||||||
(l/error :hint (ex-message error)
|
(binding [l/*context* (request->context request)]
|
||||||
:cause error
|
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
|
||||||
::l/context (get-context request))
|
(yrs/response :status 500
|
||||||
(yrs/response :status 500
|
:body {:type :server-error
|
||||||
:body {:type :server-error
|
:code :assertion
|
||||||
:code :assertion
|
:data (-> edata
|
||||||
:data (-> edata
|
(dissoc ::s/problems ::s/value ::s/spec)
|
||||||
(dissoc ::s/problems ::s/value ::s/spec)
|
(cond-> explain (assoc :explain explain)))}))))
|
||||||
(cond-> explain (assoc :explain explain)))})))
|
|
||||||
|
|
||||||
(defmethod handle-exception :not-found
|
(defmethod handle-exception :not-found
|
||||||
[err _]
|
[err _]
|
||||||
@ -101,10 +103,8 @@
|
|||||||
(yrs/response 429)
|
(yrs/response 429)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do
|
(binding [l/*context* (request->context request)]
|
||||||
(l/error :hint (ex-message error)
|
(l/error :hint "Internal error" :message (ex-message error) :cause error)
|
||||||
:cause error
|
|
||||||
::l/context (get-context request))
|
|
||||||
(yrs/response 500 {:type :server-error
|
(yrs/response 500 {:type :server-error
|
||||||
:code :unhandled
|
:code :unhandled
|
||||||
:hint (ex-message error)
|
:hint (ex-message error)
|
||||||
@ -113,25 +113,24 @@
|
|||||||
(defmethod handle-exception org.postgresql.util.PSQLException
|
(defmethod handle-exception org.postgresql.util.PSQLException
|
||||||
[error request]
|
[error request]
|
||||||
(let [state (.getSQLState ^java.sql.SQLException error)]
|
(let [state (.getSQLState ^java.sql.SQLException error)]
|
||||||
(l/error :hint (ex-message error)
|
(binding [l/*context* (request->context request)]
|
||||||
:cause error
|
(l/error :hint "PSQL error" :message (ex-message error) :cause error)
|
||||||
::l/context (get-context request))
|
(cond
|
||||||
(cond
|
(= state "57014")
|
||||||
(= state "57014")
|
(yrs/response 504 {:type :server-error
|
||||||
(yrs/response 504 {:type :server-error
|
:code :statement-timeout
|
||||||
:code :statement-timeout
|
:hint (ex-message error)})
|
||||||
:hint (ex-message error)})
|
|
||||||
|
|
||||||
(= state "25P03")
|
(= state "25P03")
|
||||||
(yrs/response 504 {:type :server-error
|
(yrs/response 504 {:type :server-error
|
||||||
:code :idle-in-transaction-timeout
|
:code :idle-in-transaction-timeout
|
||||||
:hint (ex-message error)})
|
:hint (ex-message error)})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(yrs/response 500 {:type :server-error
|
(yrs/response 500 {:type :server-error
|
||||||
:code :unexpected
|
:code :unexpected
|
||||||
:hint (ex-message error)
|
:hint (ex-message error)
|
||||||
:state state}))))
|
:state state})))))
|
||||||
|
|
||||||
(defmethod handle-exception :default
|
(defmethod handle-exception :default
|
||||||
[error request]
|
[error request]
|
||||||
@ -139,10 +138,8 @@
|
|||||||
(cond
|
(cond
|
||||||
;; This means that exception is not a controlled exception.
|
;; This means that exception is not a controlled exception.
|
||||||
(nil? edata)
|
(nil? edata)
|
||||||
(do
|
(binding [l/*context* (request->context request)]
|
||||||
(l/error :hint (ex-message error)
|
(l/error :hint "Unexpected error" :message (ex-message error) :cause error)
|
||||||
:cause error
|
|
||||||
::l/context (get-context request))
|
|
||||||
(yrs/response 500 {:type :server-error
|
(yrs/response 500 {:type :server-error
|
||||||
:code :unexpected
|
:code :unexpected
|
||||||
:hint (ex-message error)}))
|
:hint (ex-message error)}))
|
||||||
@ -157,10 +154,8 @@
|
|||||||
(handle-exception (:handling edata) request)
|
(handle-exception (:handling edata) request)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do
|
(binding [l/*context* (request->context request)]
|
||||||
(l/error :hint (ex-message error)
|
(l/error :hint "Unhandled error" :message (ex-message error) :cause error)
|
||||||
:cause error
|
|
||||||
::l/context (get-context request))
|
|
||||||
(yrs/response 500 {:type :server-error
|
(yrs/response 500 {:type :server-error
|
||||||
:code :unhandled
|
:code :unhandled
|
||||||
:hint (ex-message error)
|
:hint (ex-message error)
|
||||||
@ -168,16 +163,7 @@
|
|||||||
|
|
||||||
(defn handle
|
(defn handle
|
||||||
[cause request]
|
[cause request]
|
||||||
(cond
|
(if (or (instance? java.util.concurrent.CompletionException cause)
|
||||||
(or (instance? java.util.concurrent.CompletionException cause)
|
(instance? java.util.concurrent.ExecutionException cause))
|
||||||
(instance? java.util.concurrent.ExecutionException cause))
|
(handle-exception (ex-cause cause) request)
|
||||||
(handle-exception (.getCause ^Throwable cause) request)
|
|
||||||
|
|
||||||
(ex/wrapped? cause)
|
|
||||||
(let [context (meta cause)
|
|
||||||
cause (deref cause)]
|
|
||||||
(binding [*context* context]
|
|
||||||
(handle-exception cause request)))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(handle-exception cause request)))
|
(handle-exception cause request)))
|
||||||
|
|||||||
@ -1,80 +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/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.http.feedback
|
|
||||||
"A general purpose feedback module."
|
|
||||||
(:require
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.db :as db]
|
|
||||||
[app.emails :as eml]
|
|
||||||
[app.rpc.queries.profile :as profile]
|
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[integrant.core :as ig]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]
|
|
||||||
[yetti.request :as yrq]
|
|
||||||
[yetti.response :as yrs]))
|
|
||||||
|
|
||||||
(declare ^:private send-feedback)
|
|
||||||
(declare ^:private handler)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
|
||||||
(s/keys :req-un [::db/pool ::wrk/executor]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
|
||||||
[_ {:keys [executor] :as cfg}]
|
|
||||||
(let [enabled? (contains? cf/flags :user-feedback)]
|
|
||||||
(if enabled?
|
|
||||||
(fn [request respond raise]
|
|
||||||
(-> (px/submit! executor #(handler cfg request))
|
|
||||||
(p/then' respond)
|
|
||||||
(p/catch raise)))
|
|
||||||
(fn [_ _ raise]
|
|
||||||
(raise (ex/error :type :validation
|
|
||||||
:code :feedback-disabled
|
|
||||||
:hint "feedback module is disabled"))))))
|
|
||||||
|
|
||||||
(defn- handler
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id] :as request}]
|
|
||||||
(let [ftoken (cf/get :feedback-token ::no-token)
|
|
||||||
token (yrq/get-header request "x-feedback-token")
|
|
||||||
params (d/merge (:params request)
|
|
||||||
(:body-params request))]
|
|
||||||
(cond
|
|
||||||
(uuid? profile-id)
|
|
||||||
(let [profile (profile/retrieve-profile-data pool profile-id)
|
|
||||||
params (assoc params :from (:email profile))]
|
|
||||||
(send-feedback pool profile params))
|
|
||||||
|
|
||||||
(= token ftoken)
|
|
||||||
(send-feedback cfg nil params))
|
|
||||||
|
|
||||||
(yrs/response 204)))
|
|
||||||
|
|
||||||
(s/def ::content ::us/string)
|
|
||||||
(s/def ::from ::us/email)
|
|
||||||
(s/def ::subject ::us/string)
|
|
||||||
(s/def ::feedback
|
|
||||||
(s/keys :req-un [::from ::subject ::content]))
|
|
||||||
|
|
||||||
(defn- send-feedback
|
|
||||||
[pool profile params]
|
|
||||||
(let [params (us/conform ::feedback params)
|
|
||||||
destination (cf/get :feedback-destination)]
|
|
||||||
(eml/send! {::eml/conn pool
|
|
||||||
::eml/factory eml/feedback
|
|
||||||
:from destination
|
|
||||||
:to destination
|
|
||||||
:profile profile
|
|
||||||
:reply-to (:from params)
|
|
||||||
:email (:from params)
|
|
||||||
:subject (:subject params)
|
|
||||||
:content (:content params)})
|
|
||||||
nil))
|
|
||||||
@ -80,8 +80,8 @@
|
|||||||
(fn [request respond raise]
|
(fn [request respond raise]
|
||||||
(let [request (ex/try! (process-request request))]
|
(let [request (ex/try! (process-request request))]
|
||||||
(if (ex/exception? request)
|
(if (ex/exception? request)
|
||||||
(if (instance? RuntimeException request)
|
(if (ex/runtime-exception? request)
|
||||||
(handle-error raise (or (ex/cause request) request))
|
(handle-error raise (or (ex-cause request) request))
|
||||||
(handle-error raise request))
|
(handle-error raise request))
|
||||||
(handler request respond raise))))))
|
(handler request respond raise))))))
|
||||||
|
|
||||||
|
|||||||
@ -8,15 +8,19 @@
|
|||||||
(:refer-clojure :exclude [read])
|
(:refer-clojure :exclude [read])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
|
[app.http.session.tasks :as-alias tasks]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[promesa.exec :as px]
|
[promesa.exec :as px]
|
||||||
@ -45,55 +49,55 @@
|
|||||||
|
|
||||||
(defprotocol ISessionManager
|
(defprotocol ISessionManager
|
||||||
(read [_ key])
|
(read [_ key])
|
||||||
(decode [_ key])
|
|
||||||
(write! [_ key data])
|
(write! [_ key data])
|
||||||
(update! [_ data])
|
(update! [_ data])
|
||||||
(delete! [_ key]))
|
(delete! [_ key]))
|
||||||
|
|
||||||
(s/def ::session #(satisfies? ISessionManager %))
|
(s/def ::manager #(satisfies? ISessionManager %))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; STORAGE IMPL
|
;; STORAGE IMPL
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(s/def ::session-params
|
||||||
|
(s/keys :req-un [::user-agent
|
||||||
|
::profile-id
|
||||||
|
::created-at]))
|
||||||
|
|
||||||
(defn- prepare-session-params
|
(defn- prepare-session-params
|
||||||
[props data]
|
[key params]
|
||||||
(let [profile-id (:profile-id data)
|
(us/assert! ::us/not-empty-string key)
|
||||||
user-agent (:user-agent data)
|
(us/assert! ::session-params params)
|
||||||
created-at (or (:created-at data) (dt/now))
|
|
||||||
token (tokens/generate props {:iss "authentication"
|
{:user-agent (:user-agent params)
|
||||||
:iat created-at
|
:profile-id (:profile-id params)
|
||||||
:uid profile-id})]
|
:created-at (:created-at params)
|
||||||
{:user-agent user-agent
|
:updated-at (:created-at params)
|
||||||
:profile-id profile-id
|
:id key})
|
||||||
:created-at created-at
|
|
||||||
:updated-at created-at
|
|
||||||
:id token}))
|
|
||||||
|
|
||||||
(defn- database-manager
|
(defn- database-manager
|
||||||
[{:keys [::db/pool ::wrk/executor ::main/props]}]
|
[{:keys [::db/pool ::wrk/executor ::main/props]}]
|
||||||
|
^{::wrk/executor executor
|
||||||
|
::db/pool pool
|
||||||
|
::main/props props}
|
||||||
(reify ISessionManager
|
(reify ISessionManager
|
||||||
(read [_ token]
|
(read [_ token]
|
||||||
(px/with-dispatch executor
|
(px/with-dispatch executor
|
||||||
(db/exec-one! pool (sql/select :http-session {:id token}))))
|
(db/exec-one! pool (sql/select :http-session {:id token}))))
|
||||||
|
|
||||||
(decode [_ token]
|
(write! [_ key params]
|
||||||
(px/with-dispatch executor
|
(px/with-dispatch executor
|
||||||
(tokens/verify props {:token token :iss "authentication"})))
|
(let [params (prepare-session-params key params)]
|
||||||
|
|
||||||
(write! [_ _ data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(let [params (prepare-session-params props data)]
|
|
||||||
(db/insert! pool :http-session params)
|
(db/insert! pool :http-session params)
|
||||||
params)))
|
params)))
|
||||||
|
|
||||||
(update! [_ data]
|
(update! [_ params]
|
||||||
(let [updated-at (dt/now)]
|
(let [updated-at (dt/now)]
|
||||||
(px/with-dispatch executor
|
(px/with-dispatch executor
|
||||||
(db/update! pool :http-session
|
(db/update! pool :http-session
|
||||||
{:updated-at updated-at}
|
{:updated-at updated-at}
|
||||||
{:id (:id data)})
|
{:id (:id params)})
|
||||||
(assoc data :updated-at updated-at))))
|
(assoc params :updated-at updated-at))))
|
||||||
|
|
||||||
(delete! [_ token]
|
(delete! [_ token]
|
||||||
(px/with-dispatch executor
|
(px/with-dispatch executor
|
||||||
@ -101,27 +105,26 @@
|
|||||||
nil))))
|
nil))))
|
||||||
|
|
||||||
(defn inmemory-manager
|
(defn inmemory-manager
|
||||||
[{:keys [::wrk/executor ::main/props]}]
|
[{:keys [::db/pool ::wrk/executor ::main/props]}]
|
||||||
(let [cache (atom {})]
|
(let [cache (atom {})]
|
||||||
|
^{::main/props props
|
||||||
|
::wrk/executor executor
|
||||||
|
::db/pool pool}
|
||||||
(reify ISessionManager
|
(reify ISessionManager
|
||||||
(read [_ token]
|
(read [_ token]
|
||||||
(p/do (get @cache token)))
|
(p/do (get @cache token)))
|
||||||
|
|
||||||
(decode [_ token]
|
(write! [_ key params]
|
||||||
(px/with-dispatch executor
|
|
||||||
(tokens/verify props {:token token :iss "authentication"})))
|
|
||||||
|
|
||||||
(write! [_ _ data]
|
|
||||||
(p/do
|
(p/do
|
||||||
(let [{:keys [token] :as params} (prepare-session-params props data)]
|
(let [params (prepare-session-params key params)]
|
||||||
(swap! cache assoc token params)
|
(swap! cache assoc key params)
|
||||||
params)))
|
params)))
|
||||||
|
|
||||||
(update! [_ data]
|
(update! [_ params]
|
||||||
(p/do
|
(p/do
|
||||||
(let [updated-at (dt/now)]
|
(let [updated-at (dt/now)]
|
||||||
(swap! cache update (:id data) assoc :updated-at updated-at)
|
(swap! cache update (:id params) assoc :updated-at updated-at)
|
||||||
(assoc data :updated-at updated-at))))
|
(assoc params :updated-at updated-at))))
|
||||||
|
|
||||||
(delete! [_ token]
|
(delete! [_ token]
|
||||||
(p/do
|
(p/do
|
||||||
@ -144,25 +147,34 @@
|
|||||||
;; MANAGER IMPL
|
;; MANAGER IMPL
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(declare assign-auth-token-cookie)
|
(declare ^:private assign-auth-token-cookie)
|
||||||
(declare assign-authenticated-cookie)
|
(declare ^:private assign-authenticated-cookie)
|
||||||
(declare clear-auth-token-cookie)
|
(declare ^:private clear-auth-token-cookie)
|
||||||
(declare clear-authenticated-cookie)
|
(declare ^:private clear-authenticated-cookie)
|
||||||
|
(declare ^:private gen-token)
|
||||||
|
|
||||||
(defn create-fn
|
(defn create-fn
|
||||||
[manager profile-id]
|
[{:keys [::manager]} profile-id]
|
||||||
(fn [request response]
|
(us/assert! ::manager manager)
|
||||||
(let [uagent (yrq/get-header request "user-agent")
|
(us/assert! ::us/uuid profile-id)
|
||||||
params {:profile-id profile-id
|
|
||||||
:user-agent uagent}]
|
(let [props (-> manager meta ::main/props)]
|
||||||
(-> (write! manager nil params)
|
(fn [request response]
|
||||||
(p/then (fn [session]
|
(let [uagent (yrq/get-header request "user-agent")
|
||||||
(l/trace :hint "create" :profile-id profile-id)
|
params {:profile-id profile-id
|
||||||
(-> response
|
:user-agent uagent
|
||||||
(assign-auth-token-cookie session)
|
:created-at (dt/now)}
|
||||||
(assign-authenticated-cookie session))))))))
|
token (gen-token props params)]
|
||||||
|
|
||||||
|
(->> (write! manager token params)
|
||||||
|
(p/fmap (fn [session]
|
||||||
|
(l/trace :hint "create" :profile-id (str profile-id))
|
||||||
|
(-> response
|
||||||
|
(assign-auth-token-cookie session)
|
||||||
|
(assign-authenticated-cookie session)))))))))
|
||||||
(defn delete-fn
|
(defn delete-fn
|
||||||
[manager]
|
[{:keys [::manager]}]
|
||||||
|
(us/assert! ::manager manager)
|
||||||
(letfn [(delete [{:keys [profile-id] :as request}]
|
(letfn [(delete [{:keys [profile-id] :as request}]
|
||||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||||
cookie (yrq/get-cookie request cname)]
|
cookie (yrq/get-cookie request cname)]
|
||||||
@ -177,68 +189,93 @@
|
|||||||
(clear-auth-token-cookie)
|
(clear-auth-token-cookie)
|
||||||
(clear-authenticated-cookie))))))
|
(clear-authenticated-cookie))))))
|
||||||
|
|
||||||
(def middleware-1
|
(defn- gen-token
|
||||||
(letfn [(decode-cookie [manager cookie]
|
[props {:keys [profile-id created-at]}]
|
||||||
(if-let [value (:value cookie)]
|
(tokens/generate props {:iss "authentication"
|
||||||
(decode manager value)
|
:iat created-at
|
||||||
(p/resolved nil)))
|
:uid profile-id}))
|
||||||
|
(defn- decode-token
|
||||||
|
[props token]
|
||||||
|
(when token
|
||||||
|
(tokens/verify props {:token token :iss "authentication"})))
|
||||||
|
|
||||||
(wrap-handler [manager handler request respond raise]
|
(defn- get-token
|
||||||
(let [cookie (some->> (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
[request]
|
||||||
(yrq/get-cookie request))]
|
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||||
(->> (decode-cookie manager cookie)
|
cookie (some-> (yrq/get-cookie request cname) :value)]
|
||||||
(p/fnly (fn [claims _]
|
(when-not (str/empty? cookie)
|
||||||
(cond-> request
|
cookie)))
|
||||||
(some? claims) (assoc :session-token-claims claims)
|
|
||||||
:always (handler respond raise)))))))]
|
|
||||||
{:name :session-1
|
|
||||||
:compile (fn [& _]
|
|
||||||
(fn [handler manager]
|
|
||||||
(partial wrap-handler manager handler)))}))
|
|
||||||
|
|
||||||
(def middleware-2
|
(defn- get-session
|
||||||
(letfn [(wrap-handler [manager handler request respond raise]
|
[manager token]
|
||||||
(-> (retrieve-session manager request)
|
(some->> token (read manager)))
|
||||||
(p/finally (fn [session cause]
|
|
||||||
(cond
|
|
||||||
(some? cause)
|
|
||||||
(raise cause)
|
|
||||||
|
|
||||||
(nil? session)
|
(defn- renew-session?
|
||||||
(handler request respond raise)
|
[{:keys [updated-at] :as session}]
|
||||||
|
(and (dt/instant? updated-at)
|
||||||
|
(let [elapsed (dt/diff updated-at (dt/now))]
|
||||||
|
(neg? (compare default-renewal-max-age elapsed)))))
|
||||||
|
|
||||||
:else
|
(defn- wrap-reneval
|
||||||
(let [request (-> request
|
[respond manager session]
|
||||||
(assoc :profile-id (:profile-id session))
|
(fn [response]
|
||||||
(assoc :session-id (:id session)))
|
(p/let [session (update! manager session)]
|
||||||
respond (cond-> respond
|
(-> response
|
||||||
(renew-session? session)
|
(assign-auth-token-cookie session)
|
||||||
(wrap-respond manager session))]
|
(assign-authenticated-cookie session)
|
||||||
(handler request respond raise)))))))
|
(respond)))))
|
||||||
|
|
||||||
(retrieve-session [manager request]
|
(defn- wrap-soft-auth
|
||||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
[handler {:keys [::manager]}]
|
||||||
cookie (yrq/get-cookie request cname)]
|
(us/assert! ::manager manager)
|
||||||
(some->> (:value cookie) (read manager))))
|
|
||||||
|
|
||||||
(renew-session? [{:keys [updated-at] :as session}]
|
(let [{:keys [::wrk/executor ::main/props]} (meta manager)]
|
||||||
(and (dt/instant? updated-at)
|
(fn [request respond raise]
|
||||||
(let [elapsed (dt/diff updated-at (dt/now))]
|
(let [token (ex/try! (get-token request))]
|
||||||
(neg? (compare default-renewal-max-age elapsed)))))
|
(if (ex/exception? token)
|
||||||
|
(raise token)
|
||||||
|
(->> (px/submit! executor (partial decode-token props token))
|
||||||
|
(p/fnly (fn [claims cause]
|
||||||
|
(when cause
|
||||||
|
(l/trace :hint "exception on decoding malformed token" :cause cause))
|
||||||
|
(let [request (cond-> request
|
||||||
|
(map? claims)
|
||||||
|
(-> (assoc ::token-claims claims)
|
||||||
|
(assoc ::token token)))]
|
||||||
|
(handler request respond raise))))))))))
|
||||||
|
|
||||||
;; Wrap respond with session renewal code
|
(defn- wrap-authz
|
||||||
(wrap-respond [respond manager session]
|
[handler {:keys [::manager]}]
|
||||||
(fn [response]
|
(us/assert! ::manager manager)
|
||||||
(p/let [session (update! manager session)]
|
(fn [request respond raise]
|
||||||
(-> response
|
(if-let [token (::token request)]
|
||||||
(assign-auth-token-cookie session)
|
(->> (get-session manager token)
|
||||||
(assign-authenticated-cookie session)
|
(p/fnly (fn [session cause]
|
||||||
(respond)))))]
|
(cond
|
||||||
|
(some? cause)
|
||||||
|
(raise cause)
|
||||||
|
|
||||||
{:name :session-2
|
(nil? session)
|
||||||
:compile (fn [& _]
|
(handler request respond raise)
|
||||||
(fn [handler manager]
|
|
||||||
(partial wrap-handler manager handler)))}))
|
:else
|
||||||
|
(let [request (-> request
|
||||||
|
(assoc ::profile-id (:profile-id session))
|
||||||
|
(assoc ::id (:id session)))
|
||||||
|
respond (cond-> respond
|
||||||
|
(renew-session? session)
|
||||||
|
(wrap-reneval manager session))]
|
||||||
|
(handler request respond raise))))))
|
||||||
|
|
||||||
|
(handler request respond raise))))
|
||||||
|
|
||||||
|
(def soft-auth
|
||||||
|
{:name ::soft-auth
|
||||||
|
:compile (constantly wrap-soft-auth)})
|
||||||
|
|
||||||
|
(def authz
|
||||||
|
{:name ::authz
|
||||||
|
:compile (constantly wrap-authz)})
|
||||||
|
|
||||||
;; --- IMPL
|
;; --- IMPL
|
||||||
|
|
||||||
@ -264,13 +301,16 @@
|
|||||||
(defn- assign-authenticated-cookie
|
(defn- assign-authenticated-cookie
|
||||||
[response {updated-at :updated-at}]
|
[response {updated-at :updated-at}]
|
||||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
||||||
|
domain (cf/get :authenticated-cookie-domain)
|
||||||
|
cname (cf/get :authenticated-cookie-name "authenticated")
|
||||||
|
|
||||||
created-at (or updated-at (dt/now))
|
created-at (or updated-at (dt/now))
|
||||||
renewal (dt/plus created-at default-renewal-max-age)
|
renewal (dt/plus created-at default-renewal-max-age)
|
||||||
expires (dt/plus created-at max-age)
|
expires (dt/plus created-at max-age)
|
||||||
|
|
||||||
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
||||||
secure? (contains? cf/flags :secure-session-cookies)
|
secure? (contains? cf/flags :secure-session-cookies)
|
||||||
domain (cf/get :authenticated-cookie-domain)
|
|
||||||
name (cf/get :authenticated-cookie-name "authenticated")
|
|
||||||
cookie {:domain domain
|
cookie {:domain domain
|
||||||
:expires expires
|
:expires expires
|
||||||
:path "/"
|
:path "/"
|
||||||
@ -280,41 +320,46 @@
|
|||||||
:secure secure?}]
|
:secure secure?}]
|
||||||
(cond-> response
|
(cond-> response
|
||||||
(string? domain)
|
(string? domain)
|
||||||
(update :cookies assoc name cookie))))
|
(update :cookies assoc cname cookie))))
|
||||||
|
|
||||||
(defn- clear-auth-token-cookie
|
(defn- clear-auth-token-cookie
|
||||||
[response]
|
[response]
|
||||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
||||||
(update response :cookies assoc cname {:path "/" :value "" :max-age -1})))
|
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||||
|
|
||||||
(defn- clear-authenticated-cookie
|
(defn- clear-authenticated-cookie
|
||||||
[response]
|
[response]
|
||||||
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
|
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
|
||||||
domain (cf/get :authenticated-cookie-domain)]
|
domain (cf/get :authenticated-cookie-domain)]
|
||||||
(cond-> response
|
(cond-> response
|
||||||
(string? domain)
|
(string? domain)
|
||||||
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age -1}))))
|
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; TASK: SESSION GC
|
;; TASK: SESSION GC
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(declare sql:delete-expired)
|
(s/def ::tasks/max-age ::dt/duration)
|
||||||
|
|
||||||
(s/def ::max-age ::dt/duration)
|
(defmethod ig/pre-init-spec ::tasks/gc [_]
|
||||||
|
(s/keys :req [::db/pool]
|
||||||
|
:opt [::tasks/max-age]))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::gc-task [_]
|
(defmethod ig/prep-key ::tasks/gc
|
||||||
(s/keys :req-un [::db/pool]
|
|
||||||
:opt-un [::max-age]))
|
|
||||||
|
|
||||||
(defmethod ig/prep-key ::gc-task
|
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(merge {:max-age default-cookie-max-age}
|
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)]
|
||||||
(d/without-nils cfg)))
|
(merge {::tasks/max-age max-age} (d/without-nils cfg))))
|
||||||
|
|
||||||
(defmethod ig/init-key ::gc-task
|
(def ^:private
|
||||||
[_ {:keys [pool max-age] :as cfg}]
|
sql:delete-expired
|
||||||
|
"delete from http_session
|
||||||
|
where updated_at < now() - ?::interval
|
||||||
|
or (updated_at is null and
|
||||||
|
created_at < now() - ?::interval)")
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::tasks/gc
|
||||||
|
[_ {:keys [::db/pool ::tasks/max-age] :as cfg}]
|
||||||
(l/debug :hint "initializing session gc task" :max-age max-age)
|
(l/debug :hint "initializing session gc task" :max-age max-age)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
@ -326,9 +371,3 @@
|
|||||||
:deleted result)
|
:deleted result)
|
||||||
result))))
|
result))))
|
||||||
|
|
||||||
(def ^:private
|
|
||||||
sql:delete-expired
|
|
||||||
"delete from http_session
|
|
||||||
where updated_at < now() - ?::interval
|
|
||||||
or (updated_at is null and
|
|
||||||
created_at < now() - ?::interval)")
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.http.session :as session]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.msgbus :as mbus]
|
[app.msgbus :as mbus]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -34,7 +35,7 @@
|
|||||||
(def state (atom {}))
|
(def state (atom {}))
|
||||||
|
|
||||||
(defn- on-connect
|
(defn- on-connect
|
||||||
[{:keys [metrics]} wsp]
|
[{:keys [::mtx/metrics]} wsp]
|
||||||
(let [created-at (dt/now)]
|
(let [created-at (dt/now)]
|
||||||
(swap! state assoc (::ws/id @wsp) wsp)
|
(swap! state assoc (::ws/id @wsp) wsp)
|
||||||
(mtx/run! metrics
|
(mtx/run! metrics
|
||||||
@ -48,7 +49,7 @@
|
|||||||
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)))))
|
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)))))
|
||||||
|
|
||||||
(defn- on-rcv-message
|
(defn- on-rcv-message
|
||||||
[{:keys [metrics]} _ message]
|
[{:keys [::mtx/metrics]} _ message]
|
||||||
(mtx/run! metrics
|
(mtx/run! metrics
|
||||||
:id :websocket-messages-total
|
:id :websocket-messages-total
|
||||||
:labels recv-labels
|
:labels recv-labels
|
||||||
@ -56,7 +57,7 @@
|
|||||||
message)
|
message)
|
||||||
|
|
||||||
(defn- on-snd-message
|
(defn- on-snd-message
|
||||||
[{:keys [metrics]} _ message]
|
[{:keys [::mtx/metrics]} _ message]
|
||||||
(mtx/run! metrics
|
(mtx/run! metrics
|
||||||
:id :websocket-messages-total
|
:id :websocket-messages-total
|
||||||
:labels send-labels
|
:labels send-labels
|
||||||
@ -95,7 +96,6 @@
|
|||||||
:user-agent (::ws/user-agent @wsp)
|
:user-agent (::ws/user-agent @wsp)
|
||||||
:ip-addr (::ws/remote-addr @wsp)
|
:ip-addr (::ws/remote-addr @wsp)
|
||||||
:last-activity-at (::ws/last-activity-at @wsp)
|
:last-activity-at (::ws/last-activity-at @wsp)
|
||||||
:http-session-id (::ws/http-session-id @wsp)
|
|
||||||
:subscribed-file (-> wsp deref ::file-subscription :file-id)
|
:subscribed-file (-> wsp deref ::file-subscription :file-id)
|
||||||
:subscribed-team (-> wsp deref ::team-subscription :team-id)}))
|
:subscribed-team (-> wsp deref ::team-subscription :team-id)}))
|
||||||
|
|
||||||
@ -120,7 +120,7 @@
|
|||||||
(defmethod handle-message :connect
|
(defmethod handle-message :connect
|
||||||
[cfg wsp _]
|
[cfg wsp _]
|
||||||
|
|
||||||
(let [msgbus (:msgbus cfg)
|
(let [msgbus (::mbus/msgbus cfg)
|
||||||
conn-id (::ws/id @wsp)
|
conn-id (::ws/id @wsp)
|
||||||
profile-id (::profile-id @wsp)
|
profile-id (::profile-id @wsp)
|
||||||
session-id (::session-id @wsp)
|
session-id (::session-id @wsp)
|
||||||
@ -139,7 +139,7 @@
|
|||||||
|
|
||||||
(defmethod handle-message :disconnect
|
(defmethod handle-message :disconnect
|
||||||
[cfg wsp _]
|
[cfg wsp _]
|
||||||
(let [msgbus (:msgbus cfg)
|
(let [msgbus (::mbus/msgbus cfg)
|
||||||
conn-id (::ws/id @wsp)
|
conn-id (::ws/id @wsp)
|
||||||
profile-id (::profile-id @wsp)
|
profile-id (::profile-id @wsp)
|
||||||
session-id (::session-id @wsp)
|
session-id (::session-id @wsp)
|
||||||
@ -173,7 +173,7 @@
|
|||||||
|
|
||||||
(defmethod handle-message :subscribe-team
|
(defmethod handle-message :subscribe-team
|
||||||
[cfg wsp {:keys [team-id] :as params}]
|
[cfg wsp {:keys [team-id] :as params}]
|
||||||
(let [msgbus (:msgbus cfg)
|
(let [msgbus (::mbus/msgbus cfg)
|
||||||
conn-id (::ws/id @wsp)
|
conn-id (::ws/id @wsp)
|
||||||
session-id (::session-id @wsp)
|
session-id (::session-id @wsp)
|
||||||
output-ch (::ws/output-ch @wsp)
|
output-ch (::ws/output-ch @wsp)
|
||||||
@ -204,8 +204,8 @@
|
|||||||
(a/<! (mbus/sub! msgbus :topic team-id :chan channel)))))
|
(a/<! (mbus/sub! msgbus :topic team-id :chan channel)))))
|
||||||
|
|
||||||
(defmethod handle-message :subscribe-file
|
(defmethod handle-message :subscribe-file
|
||||||
[cfg wsp {:keys [file-id] :as params}]
|
[cfg wsp {:keys [file-id version] :as params}]
|
||||||
(let [msgbus (:msgbus cfg)
|
(let [msgbus (::mbus/msgbus cfg)
|
||||||
conn-id (::ws/id @wsp)
|
conn-id (::ws/id @wsp)
|
||||||
profile-id (::profile-id @wsp)
|
profile-id (::profile-id @wsp)
|
||||||
session-id (::session-id @wsp)
|
session-id (::session-id @wsp)
|
||||||
@ -239,7 +239,8 @@
|
|||||||
(let [message {:type :presence
|
(let [message {:type :presence
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
:session-id session-id
|
:session-id session-id
|
||||||
:profile-id profile-id}]
|
:profile-id profile-id
|
||||||
|
:version version}]
|
||||||
(a/<! (mbus/pub! msgbus :topic file-id :message message))))
|
(a/<! (mbus/pub! msgbus :topic file-id :message message))))
|
||||||
(a/>! output-ch message)
|
(a/>! output-ch message)
|
||||||
(recur))))
|
(recur))))
|
||||||
@ -258,7 +259,7 @@
|
|||||||
|
|
||||||
(defmethod handle-message :unsubscribe-file
|
(defmethod handle-message :unsubscribe-file
|
||||||
[cfg wsp {:keys [file-id] :as params}]
|
[cfg wsp {:keys [file-id] :as params}]
|
||||||
(let [msgbus (:msgbus cfg)
|
(let [msgbus (::mbus/msgbus cfg)
|
||||||
conn-id (::ws/id @wsp)
|
conn-id (::ws/id @wsp)
|
||||||
session-id (::session-id @wsp)
|
session-id (::session-id @wsp)
|
||||||
profile-id (::profile-id @wsp)
|
profile-id (::profile-id @wsp)
|
||||||
@ -288,7 +289,7 @@
|
|||||||
|
|
||||||
(defmethod handle-message :pointer-update
|
(defmethod handle-message :pointer-update
|
||||||
[cfg wsp {:keys [file-id] :as message}]
|
[cfg wsp {:keys [file-id] :as message}]
|
||||||
(let [msgbus (:msgbus cfg)
|
(let [msgbus (::mbus/msgbus cfg)
|
||||||
profile-id (::profile-id @wsp)
|
profile-id (::profile-id @wsp)
|
||||||
session-id (::session-id @wsp)
|
session-id (::session-id @wsp)
|
||||||
subs (::file-subscription @wsp)
|
subs (::file-subscription @wsp)
|
||||||
@ -313,39 +314,47 @@
|
|||||||
;; HTTP HANDLER
|
;; HTTP HANDLER
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(s/def ::msgbus ::mbus/msgbus)
|
|
||||||
(s/def ::session-id ::us/uuid)
|
(s/def ::session-id ::us/uuid)
|
||||||
|
|
||||||
(s/def ::handler-params
|
(s/def ::handler-params
|
||||||
(s/keys :req-un [::session-id]))
|
(s/keys :req-un [::session-id]))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
(defn- http-handler
|
||||||
(s/keys :req-un [::msgbus ::db/pool ::mtx/metrics]))
|
[cfg {:keys [params ::session/profile-id] :as request} respond raise]
|
||||||
|
(let [{:keys [session-id]} (us/conform ::handler-params params)]
|
||||||
|
(cond
|
||||||
|
(not profile-id)
|
||||||
|
(raise (ex/error :type :authentication
|
||||||
|
:hint "Authentication required."))
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
(not (yws/upgrade-request? request))
|
||||||
|
(raise (ex/error :type :validation
|
||||||
|
:code :websocket-request-expected
|
||||||
|
:hint "this endpoint only accepts websocket connections"))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(do
|
||||||
|
(l/trace :hint "websocket request" :profile-id profile-id :session-id session-id)
|
||||||
|
|
||||||
|
(->> (ws/handler
|
||||||
|
::ws/on-rcv-message (partial on-rcv-message cfg)
|
||||||
|
::ws/on-snd-message (partial on-snd-message cfg)
|
||||||
|
::ws/on-connect (partial on-connect cfg)
|
||||||
|
::ws/handler (partial handle-message cfg)
|
||||||
|
::profile-id profile-id
|
||||||
|
::session-id session-id)
|
||||||
|
(yws/upgrade request)
|
||||||
|
(respond))))))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
|
(s/keys :req [::mbus/msgbus
|
||||||
|
::mtx/metrics
|
||||||
|
::db/pool
|
||||||
|
::session/manager]))
|
||||||
|
|
||||||
|
(s/def ::routes vector?)
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::routes
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(fn [{:keys [profile-id params] :as req} respond raise]
|
["/ws/notifications" {:middleware [[session/authz cfg]]
|
||||||
(let [{:keys [session-id]} (us/conform ::handler-params params)]
|
:handler (partial http-handler cfg)
|
||||||
(cond
|
:allowed-methods #{:get}}])
|
||||||
(not profile-id)
|
|
||||||
(raise (ex/error :type :authentication
|
|
||||||
:hint "Authentication required."))
|
|
||||||
|
|
||||||
(not (yws/upgrade-request? req))
|
|
||||||
(raise (ex/error :type :validation
|
|
||||||
:code :websocket-request-expected
|
|
||||||
:hint "this endpoint only accepts websocket connections"))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(do
|
|
||||||
(l/trace :hint "websocket request" :profile-id profile-id :session-id session-id)
|
|
||||||
|
|
||||||
(->> (ws/handler
|
|
||||||
::ws/on-rcv-message (partial on-rcv-message cfg)
|
|
||||||
::ws/on-snd-message (partial on-snd-message cfg)
|
|
||||||
::ws/on-connect (partial on-connect cfg)
|
|
||||||
::ws/handler (partial handle-message cfg)
|
|
||||||
::profile-id profile-id
|
|
||||||
::session-id session-id)
|
|
||||||
(yws/upgrade req)
|
|
||||||
(respond)))))))
|
|
||||||
|
|||||||
@ -149,7 +149,7 @@
|
|||||||
;; this case we just retry the operation.
|
;; this case we just retry the operation.
|
||||||
(rtry/with-retry {::rtry/when rtry/conflict-exception?
|
(rtry/with-retry {::rtry/when rtry/conflict-exception?
|
||||||
::rtry/max-retries 6
|
::rtry/max-retries 6
|
||||||
::rtry/label "persist-audit-log-event"}
|
::rtry/label "persist-audit-log"}
|
||||||
(let [now (dt/now)]
|
(let [now (dt/now)]
|
||||||
(db/insert! conn-or-pool :audit-log
|
(db/insert! conn-or-pool :audit-log
|
||||||
(-> params
|
(-> params
|
||||||
|
|||||||
@ -7,16 +7,17 @@
|
|||||||
(ns app.loggers.database
|
(ns app.loggers.database
|
||||||
"A specific logger impl that persists errors on the database."
|
"A specific logger impl that persists errors on the database."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.pprint :as pp]
|
||||||
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.zmq :as lzmq]
|
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]
|
||||||
|
[promesa.exec.csp :as sp]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Error Listener
|
;; Error Listener
|
||||||
@ -27,73 +28,79 @@
|
|||||||
(defonce enabled (atom true))
|
(defonce enabled (atom true))
|
||||||
|
|
||||||
(defn- persist-on-database!
|
(defn- persist-on-database!
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [id] :as event}]
|
[pool id report]
|
||||||
(when-not (db/read-only? pool)
|
(when-not (db/read-only? pool)
|
||||||
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
|
(db/insert! pool :server-error-report
|
||||||
|
{:id id
|
||||||
|
:version 2
|
||||||
|
:content (db/tjson report)})))
|
||||||
|
|
||||||
(defn- parse-event-data
|
(defn record->report
|
||||||
[event]
|
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
|
||||||
(reduce-kv
|
(us/assert! ::l/record record)
|
||||||
(fn [acc k v]
|
|
||||||
(cond
|
|
||||||
(= k :id) (assoc acc k (uuid/uuid v))
|
|
||||||
(= k :profile-id) (assoc acc k (uuid/uuid v))
|
|
||||||
(str/blank? v) acc
|
|
||||||
:else (assoc acc k v)))
|
|
||||||
{}
|
|
||||||
event))
|
|
||||||
|
|
||||||
(defn parse-event
|
(merge
|
||||||
[event]
|
{:context (-> context
|
||||||
(-> (parse-event-data event)
|
(assoc :tenant (cf/get :tenant))
|
||||||
(assoc :hint (or (:hint event) (:message event)))
|
(assoc :host (cf/get :host))
|
||||||
(assoc :tenant (cf/get :tenant))
|
(assoc :public-uri (cf/get :public-uri))
|
||||||
(assoc :host (cf/get :host))
|
(assoc :version (:full cf/version))
|
||||||
(assoc :public-uri (cf/get :public-uri))
|
(assoc :logger-name logger)
|
||||||
(assoc :version (:full cf/version))
|
(assoc :logger-level level)
|
||||||
(update :id #(or % (uuid/next)))))
|
(dissoc :params)
|
||||||
|
(pp/pprint-str :width 200))
|
||||||
|
:params (some-> (:params context)
|
||||||
|
(pp/pprint-str :width 200))
|
||||||
|
:props (pp/pprint-str props :width 200)
|
||||||
|
:hint (or (ex-message cause) @message)
|
||||||
|
:trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)}
|
||||||
|
|
||||||
|
(when-let [data (ex-data cause)]
|
||||||
|
{:spec-value (some-> (::s/value data) (pp/pprint-str :width 200))
|
||||||
|
:spec-explain (ex/explain data)
|
||||||
|
:data (-> data
|
||||||
|
(dissoc ::s/problems ::s/value ::s/spec :hint)
|
||||||
|
(pp/pprint-str :width 200))})))
|
||||||
|
|
||||||
(defn- handle-event
|
(defn- handle-event
|
||||||
[cfg event]
|
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
|
||||||
(try
|
(try
|
||||||
(let [event (parse-event event)
|
(let [uri (cf/get :public-uri)
|
||||||
uri (cf/get :public-uri)]
|
report (-> record record->report d/without-nils)]
|
||||||
|
(l/debug :hint "registering error on database" :id id
|
||||||
|
:uri (str uri "/dbg/error/" id))
|
||||||
|
|
||||||
(l/debug :hint "registering error on database" :id (:id event)
|
(persist-on-database! pool id report))
|
||||||
:uri (str uri "/dbg/error/" (:id event)))
|
|
||||||
|
|
||||||
(persist-on-database! cfg event))
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||||
|
|
||||||
(defn- error-event?
|
(defn error-record?
|
||||||
[event]
|
[{:keys [::l/level ::l/cause]}]
|
||||||
(= "error" (:logger/level event)))
|
(and (= :error level)
|
||||||
|
(ex/exception? cause)))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::reporter [_]
|
(defmethod ig/pre-init-spec ::reporter [_]
|
||||||
(s/keys :req [::db/pool ::lzmq/receiver]))
|
(s/keys :req [::db/pool]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::reporter
|
(defmethod ig/init-key ::reporter
|
||||||
[_ {:keys [::lzmq/receiver] :as cfg}]
|
[_ cfg]
|
||||||
(px/thread
|
(let [input (sp/chan (sp/sliding-buffer 32) (filter error-record?))]
|
||||||
{:name "penpot/database-reporter"}
|
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||||
(l/info :hint "initializing database error persistence")
|
(px/thread
|
||||||
|
{:name "penpot/database-reporter" :virtual true}
|
||||||
(let [input (a/chan (a/sliding-buffer 5)
|
(l/info :hint "initializing database error persistence")
|
||||||
(filter error-event?))]
|
|
||||||
(try
|
(try
|
||||||
(lzmq/sub! receiver input)
|
|
||||||
(loop []
|
(loop []
|
||||||
(when-let [msg (a/<!! input)]
|
(when-let [record (sp/take! input)]
|
||||||
(handle-event cfg msg))
|
(handle-event cfg record)
|
||||||
(recur))
|
(recur)))
|
||||||
|
|
||||||
(catch InterruptedException _
|
(catch InterruptedException _
|
||||||
(l/debug :hint "reporter interrupted"))
|
(l/debug :hint "reporter interrupted"))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/error :hint "unexpected error" :cause cause))
|
(l/error :hint "unexpected error" :cause cause))
|
||||||
(finally
|
(finally
|
||||||
(a/close! input)
|
(sp/close! input)
|
||||||
|
(remove-watch l/log-record ::reporter)
|
||||||
(l/info :hint "reporter terminated"))))))
|
(l/info :hint "reporter terminated"))))))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::reporter
|
(defmethod ig/halt-key! ::reporter
|
||||||
|
|||||||
@ -1,89 +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/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.loggers.loki
|
|
||||||
"A Loki integration."
|
|
||||||
(:require
|
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.http.client :as http]
|
|
||||||
[app.loggers.zmq :as lzmq]
|
|
||||||
[app.util.json :as json]
|
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[integrant.core :as ig]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
(declare ^:private handle-event)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::reporter [_]
|
|
||||||
(s/keys :req [::http/client
|
|
||||||
::lzmq/receiver]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::reporter
|
|
||||||
[_ cfg]
|
|
||||||
(when-let [uri (cf/get :loggers-loki-uri)]
|
|
||||||
(px/thread
|
|
||||||
{:name "penpot/loki-reporter"}
|
|
||||||
(l/info :hint "reporter started" :uri uri)
|
|
||||||
(let [input (a/chan (a/dropping-buffer 2048))
|
|
||||||
cfg (assoc cfg ::uri uri)]
|
|
||||||
|
|
||||||
(try
|
|
||||||
(lzmq/sub! (::lzmq/receiver cfg) input)
|
|
||||||
(loop []
|
|
||||||
(when-let [msg (a/<!! input)]
|
|
||||||
(handle-event cfg msg)
|
|
||||||
(recur)))
|
|
||||||
|
|
||||||
(catch InterruptedException _
|
|
||||||
(l/debug :hint "reporter interrupted"))
|
|
||||||
(catch Throwable cause
|
|
||||||
(l/error :hint "unexpected exception"
|
|
||||||
:cause cause))
|
|
||||||
(finally
|
|
||||||
(a/close! input)
|
|
||||||
(l/info :hint "reporter terminated")))))))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::reporter
|
|
||||||
[_ thread]
|
|
||||||
(some-> thread px/interrupt!))
|
|
||||||
|
|
||||||
(defn- prepare-payload
|
|
||||||
[event]
|
|
||||||
(let [labels {:host (cf/get :host)
|
|
||||||
:tenant (cf/get :tenant)
|
|
||||||
:version (:full cf/version)
|
|
||||||
:logger (:logger/name event)
|
|
||||||
:level (:logger/level event)}]
|
|
||||||
{:streams
|
|
||||||
[{:stream labels
|
|
||||||
:values [[(str (* (inst-ms (:created-at event)) 1000000))
|
|
||||||
(str (:message event)
|
|
||||||
(when-let [error (:trace event)]
|
|
||||||
(str "\n" error)))]]}]}))
|
|
||||||
|
|
||||||
(defn- make-request
|
|
||||||
[{:keys [::uri] :as cfg} payload]
|
|
||||||
(http/req! cfg
|
|
||||||
{:uri uri
|
|
||||||
:timeout 3000
|
|
||||||
:method :post
|
|
||||||
:headers {"content-type" "application/json"}
|
|
||||||
:body (json/encode payload)}
|
|
||||||
{:sync? true}))
|
|
||||||
|
|
||||||
(defn- handle-event
|
|
||||||
[cfg event]
|
|
||||||
(try
|
|
||||||
(let [payload (prepare-payload event)
|
|
||||||
response (make-request cfg payload)]
|
|
||||||
(when-not (= 204 (:status response))
|
|
||||||
(l/error :hint "error on sending log to loki (unexpected response)"
|
|
||||||
:response (pr-str response))))
|
|
||||||
(catch Throwable cause
|
|
||||||
(l/error :hint "error on sending log to loki (unexpected exception)"
|
|
||||||
:cause cause))))
|
|
||||||
@ -7,24 +7,35 @@
|
|||||||
(ns app.loggers.mattermost
|
(ns app.loggers.mattermost
|
||||||
"A mattermost integration for error reporting."
|
"A mattermost integration for error reporting."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.http.client :as http]
|
[app.http.client :as http]
|
||||||
[app.loggers.database :as ldb]
|
[app.loggers.database :as ldb]
|
||||||
[app.loggers.zmq :as lzmq]
|
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]
|
||||||
|
[promesa.exec.csp :as sp]))
|
||||||
|
|
||||||
(defonce enabled (atom true))
|
(defonce enabled (atom false))
|
||||||
|
|
||||||
(defn- send-mattermost-notification!
|
(defn- send-mattermost-notification!
|
||||||
[cfg {:keys [host id public-uri] :as event}]
|
[cfg {:keys [id public-uri] :as report}]
|
||||||
(let [text (str "Exception on (host: " host ", url: " public-uri "/dbg/error/" id ")\n"
|
(let [text (str "Exception: " public-uri "/dbg/error/" id " "
|
||||||
(when-let [pid (:profile-id event)]
|
(when-let [pid (:profile-id report)]
|
||||||
(str "- profile-id: #uuid-" pid "\n")))
|
(str "(pid: #uuid-" pid ")"))
|
||||||
|
"\n"
|
||||||
|
"```\n"
|
||||||
|
"- host: `" (:host report) "`\n"
|
||||||
|
"- tenant: `" (:tenant report) "`\n"
|
||||||
|
"- version: `" (:version report) "`\n"
|
||||||
|
"\n"
|
||||||
|
"Trace:\n"
|
||||||
|
(:trace report)
|
||||||
|
"```")
|
||||||
|
|
||||||
resp (http/req! cfg
|
resp (http/req! cfg
|
||||||
{:uri (cf/get :error-report-webhook)
|
{:uri (cf/get :error-report-webhook)
|
||||||
:method :post
|
:method :post
|
||||||
@ -36,32 +47,41 @@
|
|||||||
(l/warn :hint "error on sending data"
|
(l/warn :hint "error on sending data"
|
||||||
:response (pr-str resp)))))
|
:response (pr-str resp)))))
|
||||||
|
|
||||||
|
(defn record->report
|
||||||
|
[{:keys [::l/context ::l/id ::l/cause] :as record}]
|
||||||
|
(us/assert! ::l/record record)
|
||||||
|
{:id id
|
||||||
|
:tenant (cf/get :tenant)
|
||||||
|
:host (cf/get :host)
|
||||||
|
:public-uri (cf/get :public-uri)
|
||||||
|
:version (:full cf/version)
|
||||||
|
:profile-id (:profile-id context)
|
||||||
|
:trace (ex/format-throwable cause :detail? false :header? false)})
|
||||||
|
|
||||||
(defn handle-event
|
(defn handle-event
|
||||||
[cfg event]
|
[cfg record]
|
||||||
(when @enabled
|
(when @enabled
|
||||||
(try
|
(try
|
||||||
(let [event (ldb/parse-event event)]
|
(let [report (record->report record)]
|
||||||
(send-mattermost-notification! cfg event))
|
(send-mattermost-notification! cfg report))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unhandled error"
|
(l/warn :hint "unhandled error" :cause cause)))))
|
||||||
:cause cause)))))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::reporter [_]
|
(defmethod ig/pre-init-spec ::reporter [_]
|
||||||
(s/keys :req [::http/client
|
(s/keys :req [::http/client]))
|
||||||
::lzmq/receiver]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::reporter
|
(defmethod ig/init-key ::reporter
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(when-let [uri (cf/get :error-report-webhook)]
|
(when-let [uri (cf/get :error-report-webhook)]
|
||||||
(px/thread
|
(px/thread
|
||||||
{:name "penpot/mattermost-reporter"}
|
{:name "penpot/mattermost-reporter"
|
||||||
(l/info :msg "initializing error reporter" :uri uri)
|
:virtual true}
|
||||||
(let [input (a/chan (a/sliding-buffer 128)
|
(l/info :hint "initializing error reporter" :uri uri)
|
||||||
(filter #(= (:logger/level %) "error")))]
|
(let [input (sp/chan (sp/sliding-buffer 128) (filter ldb/error-record?))]
|
||||||
|
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||||
(try
|
(try
|
||||||
(lzmq/sub! (::lzmq/receiver cfg) input)
|
|
||||||
(loop []
|
(loop []
|
||||||
(when-let [msg (a/<!! input)]
|
(when-let [msg (sp/take! input)]
|
||||||
(handle-event cfg msg)
|
(handle-event cfg msg)
|
||||||
(recur)))
|
(recur)))
|
||||||
(catch InterruptedException _
|
(catch InterruptedException _
|
||||||
@ -69,7 +89,8 @@
|
|||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/error :hint "unexpected error" :cause cause))
|
(l/error :hint "unexpected error" :cause cause))
|
||||||
(finally
|
(finally
|
||||||
(a/close! input)
|
(sp/close! input)
|
||||||
|
(remove-watch l/log-record ::reporter)
|
||||||
(l/info :hint "reporter terminated")))))))
|
(l/info :hint "reporter terminated")))))))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::reporter
|
(defmethod ig/halt-key! ::reporter
|
||||||
|
|||||||
@ -111,7 +111,7 @@
|
|||||||
" where id=?")
|
" where id=?")
|
||||||
err
|
err
|
||||||
(:id whook)]
|
(:id whook)]
|
||||||
res (db/exec-one! pool sql {:return-keys true})]
|
res (db/exec-one! pool sql {::db/return-keys? true})]
|
||||||
(when (>= (:error-count res) max-errors)
|
(when (>= (:error-count res) max-errors)
|
||||||
(db/update! pool :webhook {:is-active false} {:id (:id whook)})))
|
(db/update! pool :webhook {:is-active false} {:id (:id whook)})))
|
||||||
|
|
||||||
|
|||||||
@ -1,130 +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/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.loggers.zmq
|
|
||||||
"A generic ZMQ listener."
|
|
||||||
(:require
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.loggers.zmq.receiver :as-alias receiver]
|
|
||||||
[app.util.json :as json]
|
|
||||||
[app.util.time :as dt]
|
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[integrant.core :as ig]
|
|
||||||
[promesa.exec :as px])
|
|
||||||
(:import
|
|
||||||
org.zeromq.SocketType
|
|
||||||
org.zeromq.ZMQ$Socket
|
|
||||||
org.zeromq.ZContext))
|
|
||||||
|
|
||||||
(declare prepare)
|
|
||||||
(declare start-rcv-loop)
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::receiver
|
|
||||||
[_ cfg]
|
|
||||||
(let [uri (cf/get :loggers-zmq-uri)
|
|
||||||
buffer (a/chan 1)
|
|
||||||
output (a/chan 1 (comp (filter map?)
|
|
||||||
(keep prepare)))
|
|
||||||
mult (a/mult output)
|
|
||||||
thread (when uri
|
|
||||||
(px/thread
|
|
||||||
{:name "penpot/zmq-receiver"
|
|
||||||
:daemon false}
|
|
||||||
(l/info :hint "receiver started")
|
|
||||||
(try
|
|
||||||
(start-rcv-loop buffer uri)
|
|
||||||
(catch InterruptedException _
|
|
||||||
(l/debug :hint "receiver interrupted"))
|
|
||||||
(catch java.lang.IllegalStateException cause
|
|
||||||
(if (= "errno 4" (ex-message cause))
|
|
||||||
(l/debug :hint "receiver interrupted")
|
|
||||||
(l/error :hint "unhandled error" :cause cause)))
|
|
||||||
(catch Throwable cause
|
|
||||||
(l/error :hint "unhandled error" :cause cause))
|
|
||||||
(finally
|
|
||||||
(l/info :hint "receiver terminated")))))]
|
|
||||||
|
|
||||||
(a/pipe buffer output)
|
|
||||||
(-> cfg
|
|
||||||
(assoc ::receiver/mult mult)
|
|
||||||
(assoc ::receiver/thread thread)
|
|
||||||
(assoc ::receiver/output output)
|
|
||||||
(assoc ::receiver/buffer buffer))))
|
|
||||||
|
|
||||||
(s/def ::receiver/mult some?)
|
|
||||||
(s/def ::receiver/thread #(instance? Thread %))
|
|
||||||
(s/def ::receiver/output some?)
|
|
||||||
(s/def ::receiver/buffer some?)
|
|
||||||
(s/def ::receiver
|
|
||||||
(s/keys :req [::receiver/mult
|
|
||||||
::receiver/thread
|
|
||||||
::receiver/output
|
|
||||||
::receiver/buffer]))
|
|
||||||
|
|
||||||
(defn sub!
|
|
||||||
[{:keys [::receiver/mult]} ch]
|
|
||||||
(a/tap mult ch))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::receiver
|
|
||||||
[_ {:keys [::receiver/buffer ::receiver/thread]}]
|
|
||||||
(some-> thread px/interrupt!)
|
|
||||||
(some-> buffer a/close!))
|
|
||||||
|
|
||||||
(def ^:private json-mapper
|
|
||||||
(json/mapper
|
|
||||||
{:encode-key-fn str/camel
|
|
||||||
:decode-key-fn (comp keyword str/kebab)}))
|
|
||||||
|
|
||||||
(defn- start-rcv-loop
|
|
||||||
[output endpoint]
|
|
||||||
(let [zctx (ZContext. 1)
|
|
||||||
socket (.. zctx (createSocket SocketType/SUB))]
|
|
||||||
(try
|
|
||||||
(.. socket (connect ^String endpoint))
|
|
||||||
(.. socket (subscribe ""))
|
|
||||||
(.. socket (setReceiveTimeOut 5000))
|
|
||||||
(loop []
|
|
||||||
(let [msg (.recv ^ZMQ$Socket socket)
|
|
||||||
msg (ex/ignoring (json/decode msg json-mapper))
|
|
||||||
msg (if (nil? msg) :empty msg)]
|
|
||||||
(when (a/>!! output msg)
|
|
||||||
(recur))))
|
|
||||||
|
|
||||||
(finally
|
|
||||||
(.close ^java.lang.AutoCloseable socket)
|
|
||||||
(.destroy ^ZContext zctx)))))
|
|
||||||
|
|
||||||
(s/def ::logger-name string?)
|
|
||||||
(s/def ::level string?)
|
|
||||||
(s/def ::thread string?)
|
|
||||||
(s/def ::time-millis integer?)
|
|
||||||
(s/def ::message string?)
|
|
||||||
(s/def ::context-map map?)
|
|
||||||
(s/def ::thrown map?)
|
|
||||||
|
|
||||||
(s/def ::log4j-event
|
|
||||||
(s/keys :req-un [::logger-name ::level ::thread ::time-millis ::message]
|
|
||||||
:opt-un [::context-map ::thrown]))
|
|
||||||
|
|
||||||
(defn- prepare
|
|
||||||
[event]
|
|
||||||
(if (s/valid? ::log4j-event event)
|
|
||||||
(merge {:message (:message event)
|
|
||||||
:created-at (dt/instant (:time-millis event))
|
|
||||||
:logger/name (:logger-name event)
|
|
||||||
:logger/level (str/lower (:level event))}
|
|
||||||
|
|
||||||
(when-let [trace (-> event :thrown :extended-stack-trace)]
|
|
||||||
{:trace trace})
|
|
||||||
|
|
||||||
(:context-map event))
|
|
||||||
(do
|
|
||||||
(l/warn :hint "invalid event" :event event)
|
|
||||||
nil)))
|
|
||||||
@ -12,16 +12,28 @@
|
|||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as-alias db]
|
[app.db :as-alias db]
|
||||||
|
[app.email :as-alias email]
|
||||||
|
[app.http :as-alias http]
|
||||||
|
[app.http.access-token :as-alias actoken]
|
||||||
|
[app.http.assets :as-alias http.assets]
|
||||||
|
[app.http.awsns :as http.awsns]
|
||||||
[app.http.client :as-alias http.client]
|
[app.http.client :as-alias http.client]
|
||||||
[app.http.session :as-alias http.session]
|
[app.http.debug :as-alias http.debug]
|
||||||
|
[app.http.session :as-alias session]
|
||||||
|
[app.http.session.tasks :as-alias session.tasks]
|
||||||
|
[app.http.websocket :as http.ws]
|
||||||
[app.loggers.audit.tasks :as-alias audit.tasks]
|
[app.loggers.audit.tasks :as-alias audit.tasks]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.loggers.zmq :as-alias lzmq]
|
|
||||||
[app.metrics :as-alias mtx]
|
[app.metrics :as-alias mtx]
|
||||||
[app.metrics.definition :as-alias mdef]
|
[app.metrics.definition :as-alias mdef]
|
||||||
|
[app.msgbus :as-alias mbus]
|
||||||
[app.redis :as-alias rds]
|
[app.redis :as-alias rds]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.doc :as-alias rpc.doc]
|
||||||
[app.srepl :as-alias srepl]
|
[app.srepl :as-alias srepl]
|
||||||
[app.storage :as-alias sto]
|
[app.storage :as-alias sto]
|
||||||
|
[app.storage.fs :as-alias sto.fs]
|
||||||
|
[app.storage.s3 :as-alias sto.s3]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as-alias wrk]
|
[app.worker :as-alias wrk]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
@ -152,15 +164,13 @@
|
|||||||
|
|
||||||
(def system-config
|
(def system-config
|
||||||
{::db/pool
|
{::db/pool
|
||||||
{:uri (cf/get :database-uri)
|
{::db/uri (cf/get :database-uri)
|
||||||
:username (cf/get :database-username)
|
::db/username (cf/get :database-username)
|
||||||
:password (cf/get :database-password)
|
::db/password (cf/get :database-password)
|
||||||
:read-only (cf/get :database-readonly false)
|
::db/read-only? (cf/get :database-readonly false)
|
||||||
:metrics (ig/ref ::mtx/metrics)
|
::db/min-size (cf/get :database-min-pool-size 0)
|
||||||
:migrations (ig/ref :app.migrations/all)
|
::db/max-size (cf/get :database-max-pool-size 60)
|
||||||
:name :main
|
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||||
:min-size (cf/get :database-min-pool-size 0)
|
|
||||||
:max-size (cf/get :database-max-pool-size 60)}
|
|
||||||
|
|
||||||
;; Default thread pool for IO operations
|
;; Default thread pool for IO operations
|
||||||
::wrk/executor
|
::wrk/executor
|
||||||
@ -175,19 +185,19 @@
|
|||||||
::wrk/executor (ig/ref ::wrk/executor)}
|
::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
:app.migrations/migrations
|
:app.migrations/migrations
|
||||||
{}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
::mtx/metrics
|
::mtx/metrics
|
||||||
{:default default-metrics}
|
{:default default-metrics}
|
||||||
|
|
||||||
:app.migrations/all
|
::mtx/routes
|
||||||
{:main (ig/ref :app.migrations/migrations)}
|
{::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||||
|
|
||||||
::rds/redis
|
::rds/redis
|
||||||
{::rds/uri (cf/get :redis-uri)
|
{::rds/uri (cf/get :redis-uri)
|
||||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||||
|
|
||||||
:app.msgbus/msgbus
|
::mbus/msgbus
|
||||||
{:backend (cf/get :msgbus-backend :redis)
|
{:backend (cf/get :msgbus-backend :redis)
|
||||||
:executor (ig/ref ::wrk/executor)
|
:executor (ig/ref ::wrk/executor)
|
||||||
:redis (ig/ref ::rds/redis)}
|
:redis (ig/ref ::rds/redis)}
|
||||||
@ -197,40 +207,43 @@
|
|||||||
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
||||||
|
|
||||||
::sto/gc-deleted-task
|
::sto/gc-deleted-task
|
||||||
{:pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
:storage (ig/ref ::sto/storage)
|
::sto/storage (ig/ref ::sto/storage)}
|
||||||
:executor (ig/ref ::wrk/executor)}
|
|
||||||
|
|
||||||
::sto/gc-touched-task
|
::sto/gc-touched-task
|
||||||
{:pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
::http.client/client
|
::http.client/client
|
||||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
:app.http.session/manager
|
::session/manager
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
::props (ig/ref :app.setup/props)}
|
::props (ig/ref :app.setup/props)}
|
||||||
|
|
||||||
:app.http.session/gc-task
|
::actoken/manager
|
||||||
{:pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
:max-age (cf/get :auth-token-cookie-max-age)}
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
|
::props (ig/ref :app.setup/props)}
|
||||||
|
|
||||||
:app.http.awsns/handler
|
::session.tasks/gc
|
||||||
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
|
::http.awsns/routes
|
||||||
{::props (ig/ref :app.setup/props)
|
{::props (ig/ref :app.setup/props)
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::http.client/client (ig/ref ::http.client/client)
|
::http.client/client (ig/ref ::http.client/client)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)}
|
::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
:app.http/server
|
::http/server
|
||||||
{:port (cf/get :http-server-port)
|
{::http/port (cf/get :http-server-port)
|
||||||
:host (cf/get :http-server-host)
|
::http/host (cf/get :http-server-host)
|
||||||
:router (ig/ref :app.http/router)
|
::http/router (ig/ref ::http/router)
|
||||||
:metrics (ig/ref ::mtx/metrics)
|
::http/metrics (ig/ref ::mtx/metrics)
|
||||||
:executor (ig/ref ::wrk/executor)
|
::http/executor (ig/ref ::wrk/executor)
|
||||||
:io-threads (cf/get :http-server-io-threads)
|
::http/io-threads (cf/get :http-server-io-threads)
|
||||||
:max-body-size (cf/get :http-server-max-body-size)
|
::http/max-body-size (cf/get :http-server-max-body-size)
|
||||||
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||||
|
|
||||||
::ldap/provider
|
::ldap/provider
|
||||||
{:host (cf/get :ldap-host)
|
{:host (cf/get :ldap-host)
|
||||||
@ -259,85 +272,74 @@
|
|||||||
{::http.client/client (ig/ref ::http.client/client)}
|
{::http.client/client (ig/ref ::http.client/client)}
|
||||||
|
|
||||||
::oidc/routes
|
::oidc/routes
|
||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::props (ig/ref :app.setup/props)
|
::props (ig/ref :app.setup/props)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
::oidc/providers {:google (ig/ref ::oidc.providers/google)
|
::oidc/providers {:google (ig/ref ::oidc.providers/google)
|
||||||
:github (ig/ref ::oidc.providers/github)
|
:github (ig/ref ::oidc.providers/github)
|
||||||
:gitlab (ig/ref ::oidc.providers/gitlab)
|
:gitlab (ig/ref ::oidc.providers/gitlab)
|
||||||
:oidc (ig/ref ::oidc.providers/generic)}
|
:oidc (ig/ref ::oidc.providers/generic)}
|
||||||
::http.session/session (ig/ref :app.http.session/manager)}
|
::session/manager (ig/ref ::session/manager)}
|
||||||
|
|
||||||
;; TODO: revisit the dependencies of this service, looks they are too much unused of them
|
|
||||||
:app.http/router
|
:app.http/router
|
||||||
{:assets (ig/ref :app.http.assets/handlers)
|
{::session/manager (ig/ref ::session/manager)
|
||||||
:feedback (ig/ref :app.http.feedback/handler)
|
::actoken/manager (ig/ref ::actoken/manager)
|
||||||
:session (ig/ref :app.http.session/manager)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
:awsns-handler (ig/ref :app.http.awsns/handler)
|
::db/pool (ig/ref ::db/pool)
|
||||||
:debug-routes (ig/ref :app.http.debug/routes)
|
::rpc/routes (ig/ref ::rpc/routes)
|
||||||
:oidc-routes (ig/ref ::oidc/routes)
|
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
|
||||||
:ws (ig/ref :app.http.websocket/handler)
|
::props (ig/ref :app.setup/props)
|
||||||
:metrics (ig/ref ::mtx/metrics)
|
::mtx/routes (ig/ref ::mtx/routes)
|
||||||
:public-uri (cf/get :public-uri)
|
::oidc/routes (ig/ref ::oidc/routes)
|
||||||
:storage (ig/ref ::sto/storage)
|
::http.debug/routes (ig/ref ::http.debug/routes)
|
||||||
:rpc-routes (ig/ref :app.rpc/routes)
|
::http.assets/routes (ig/ref ::http.assets/routes)
|
||||||
:doc-routes (ig/ref :app.rpc.doc/routes)
|
::http.ws/routes (ig/ref ::http.ws/routes)
|
||||||
:executor (ig/ref ::wrk/executor)}
|
::http.awsns/routes (ig/ref ::http.awsns/routes)}
|
||||||
|
|
||||||
:app.http.debug/routes
|
:app.http.debug/routes
|
||||||
{:pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
:executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
:storage (ig/ref ::sto/storage)
|
::session/manager (ig/ref ::session/manager)}
|
||||||
:session (ig/ref :app.http.session/manager)
|
|
||||||
|
|
||||||
::db/pool (ig/ref ::db/pool)
|
:app.http.websocket/routes
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::sto/storage (ig/ref ::sto/storage)}
|
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
|
::mbus/msgbus (ig/ref :app.msgbus/msgbus)
|
||||||
|
::session/manager (ig/ref ::session/manager)}
|
||||||
|
|
||||||
:app.http.websocket/handler
|
:app.http.assets/routes
|
||||||
{:pool (ig/ref ::db/pool)
|
{::http.assets/path (cf/get :assets-path)
|
||||||
:metrics (ig/ref ::mtx/metrics)
|
::http.assets/cache-max-age (dt/duration {:hours 24})
|
||||||
:msgbus (ig/ref :app.msgbus/msgbus)}
|
::http.assets/cache-max-agesignature-max-age (dt/duration {:hours 24 :minutes 5})
|
||||||
|
::sto/storage (ig/ref ::sto/storage)
|
||||||
:app.http.assets/handlers
|
::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
{:metrics (ig/ref ::mtx/metrics)
|
|
||||||
:assets-path (cf/get :assets-path)
|
|
||||||
:storage (ig/ref ::sto/storage)
|
|
||||||
:executor (ig/ref ::wrk/executor)
|
|
||||||
:cache-max-age (dt/duration {:hours 24})
|
|
||||||
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
|
|
||||||
|
|
||||||
:app.http.feedback/handler
|
|
||||||
{:pool (ig/ref ::db/pool)
|
|
||||||
:executor (ig/ref ::wrk/executor)}
|
|
||||||
|
|
||||||
:app.rpc/climit
|
:app.rpc/climit
|
||||||
{:metrics (ig/ref ::mtx/metrics)
|
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
:executor (ig/ref ::wrk/executor)}
|
::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
:app.rpc/rlimit
|
:app.rpc/rlimit
|
||||||
{:executor (ig/ref ::wrk/executor)
|
{::wrk/executor (ig/ref ::wrk/executor)
|
||||||
:scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
||||||
|
|
||||||
:app.rpc/methods
|
:app.rpc/methods
|
||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
::props (ig/ref :app.setup/props)
|
::session/manager (ig/ref ::session/manager)
|
||||||
::ldap/provider (ig/ref ::ldap/provider)
|
::ldap/provider (ig/ref ::ldap/provider)
|
||||||
|
::sto/storage (ig/ref ::sto/storage)
|
||||||
|
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
|
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||||
|
::rds/redis (ig/ref ::rds/redis)
|
||||||
|
|
||||||
|
::rpc/climit (ig/ref ::rpc/climit)
|
||||||
|
::rpc/rlimit (ig/ref ::rpc/rlimit)
|
||||||
|
|
||||||
|
::props (ig/ref :app.setup/props)
|
||||||
|
|
||||||
:pool (ig/ref ::db/pool)
|
:pool (ig/ref ::db/pool)
|
||||||
:session (ig/ref :app.http.session/manager)
|
|
||||||
:sprops (ig/ref :app.setup/props)
|
|
||||||
:metrics (ig/ref ::mtx/metrics)
|
|
||||||
:storage (ig/ref ::sto/storage)
|
|
||||||
:msgbus (ig/ref :app.msgbus/msgbus)
|
|
||||||
:public-uri (cf/get :public-uri)
|
|
||||||
:redis (ig/ref ::rds/redis)
|
|
||||||
:http-client (ig/ref ::http.client/client)
|
|
||||||
:climit (ig/ref :app.rpc/climit)
|
|
||||||
:rlimit (ig/ref :app.rpc/rlimit)
|
|
||||||
:executor (ig/ref ::wrk/executor)
|
|
||||||
:templates (ig/ref :app.setup/builtin-templates)
|
:templates (ig/ref :app.setup/builtin-templates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,12 +347,17 @@
|
|||||||
{:methods (ig/ref :app.rpc/methods)}
|
{:methods (ig/ref :app.rpc/methods)}
|
||||||
|
|
||||||
:app.rpc/routes
|
:app.rpc/routes
|
||||||
{:methods (ig/ref :app.rpc/methods)}
|
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||||
|
::db/pool (ig/ref ::db/pool)
|
||||||
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
|
::session/manager (ig/ref ::session/manager)
|
||||||
|
::actoken/manager (ig/ref ::actoken/manager)
|
||||||
|
::props (ig/ref :app.setup/props)}
|
||||||
|
|
||||||
::wrk/registry
|
::wrk/registry
|
||||||
{:metrics (ig/ref ::mtx/metrics)
|
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
:tasks
|
::wrk/tasks
|
||||||
{:sendmail (ig/ref :app.emails/handler)
|
{:sendmail (ig/ref ::email/handler)
|
||||||
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
|
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
|
||||||
:file-gc (ig/ref :app.tasks.file-gc/handler)
|
:file-gc (ig/ref :app.tasks.file-gc/handler)
|
||||||
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
|
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
|
||||||
@ -358,7 +365,7 @@
|
|||||||
:storage-gc-touched (ig/ref ::sto/gc-touched-task)
|
:storage-gc-touched (ig/ref ::sto/gc-touched-task)
|
||||||
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
||||||
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
||||||
:session-gc (ig/ref :app.http.session/gc-task)
|
:session-gc (ig/ref ::session.tasks/gc)
|
||||||
:audit-log-archive (ig/ref ::audit.tasks/archive)
|
:audit-log-archive (ig/ref ::audit.tasks/archive)
|
||||||
:audit-log-gc (ig/ref ::audit.tasks/gc)
|
:audit-log-gc (ig/ref ::audit.tasks/gc)
|
||||||
|
|
||||||
@ -367,34 +374,32 @@
|
|||||||
:run-webhook
|
:run-webhook
|
||||||
(ig/ref ::webhooks/run-webhook-handler)}}
|
(ig/ref ::webhooks/run-webhook-handler)}}
|
||||||
|
|
||||||
|
::email/sendmail
|
||||||
|
{::email/host (cf/get :smtp-host)
|
||||||
|
::email/port (cf/get :smtp-port)
|
||||||
|
::email/ssl (cf/get :smtp-ssl)
|
||||||
|
::email/tls (cf/get :smtp-tls)
|
||||||
|
::email/username (cf/get :smtp-username)
|
||||||
|
::email/password (cf/get :smtp-password)
|
||||||
|
::email/default-reply-to (cf/get :smtp-default-reply-to)
|
||||||
|
::email/default-from (cf/get :smtp-default-from)}
|
||||||
|
|
||||||
:app.emails/sendmail
|
::email/handler
|
||||||
{:host (cf/get :smtp-host)
|
{::email/sendmail (ig/ref ::email/sendmail)
|
||||||
:port (cf/get :smtp-port)
|
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||||
:ssl (cf/get :smtp-ssl)
|
|
||||||
:tls (cf/get :smtp-tls)
|
|
||||||
:username (cf/get :smtp-username)
|
|
||||||
:password (cf/get :smtp-password)
|
|
||||||
:default-reply-to (cf/get :smtp-default-reply-to)
|
|
||||||
:default-from (cf/get :smtp-default-from)}
|
|
||||||
|
|
||||||
:app.emails/handler
|
|
||||||
{:sendmail (ig/ref :app.emails/sendmail)
|
|
||||||
:metrics (ig/ref ::mtx/metrics)}
|
|
||||||
|
|
||||||
:app.tasks.tasks-gc/handler
|
:app.tasks.tasks-gc/handler
|
||||||
{:pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
:max-age cf/deletion-delay}
|
|
||||||
|
|
||||||
:app.tasks.objects-gc/handler
|
:app.tasks.objects-gc/handler
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::sto/storage (ig/ref ::sto/storage)}
|
::sto/storage (ig/ref ::sto/storage)}
|
||||||
|
|
||||||
:app.tasks.file-gc/handler
|
:app.tasks.file-gc/handler
|
||||||
{:pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
:app.tasks.file-xlog-gc/handler
|
:app.tasks.file-xlog-gc/handler
|
||||||
{:pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
:app.tasks.telemetry/handler
|
:app.tasks.telemetry/handler
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
@ -402,22 +407,23 @@
|
|||||||
::props (ig/ref :app.setup/props)}
|
::props (ig/ref :app.setup/props)}
|
||||||
|
|
||||||
[::srepl/urepl ::srepl/server]
|
[::srepl/urepl ::srepl/server]
|
||||||
{:port (cf/get :urepl-port 6062)
|
{::srepl/port (cf/get :urepl-port 6062)
|
||||||
:host (cf/get :urepl-host "localhost")}
|
::srepl/host (cf/get :urepl-host "localhost")}
|
||||||
|
|
||||||
[::srepl/prepl ::srepl/server]
|
[::srepl/prepl ::srepl/server]
|
||||||
{:port (cf/get :prepl-port 6063)
|
{::srepl/port (cf/get :prepl-port 6063)
|
||||||
:host (cf/get :prepl-host "localhost")}
|
::srepl/host (cf/get :prepl-host "localhost")}
|
||||||
|
|
||||||
:app.setup/builtin-templates
|
:app.setup/builtin-templates
|
||||||
{::http.client/client (ig/ref ::http.client/client)}
|
{::http.client/client (ig/ref ::http.client/client)}
|
||||||
|
|
||||||
:app.setup/props
|
:app.setup/props
|
||||||
{:pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
:key (cf/get :secret-key)}
|
::key (cf/get :secret-key)
|
||||||
|
|
||||||
::lzmq/receiver
|
;; NOTE: this dependency is only necessary for proper initialization ordering, props
|
||||||
{}
|
;; module requires the migrations to run before initialize.
|
||||||
|
::migrations (ig/ref :app.migrations/migrations)}
|
||||||
|
|
||||||
::audit.tasks/archive
|
::audit.tasks/archive
|
||||||
{::props (ig/ref :app.setup/props)
|
{::props (ig/ref :app.setup/props)
|
||||||
@ -435,38 +441,27 @@
|
|||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::http.client/client (ig/ref ::http.client/client)}
|
::http.client/client (ig/ref ::http.client/client)}
|
||||||
|
|
||||||
:app.loggers.loki/reporter
|
|
||||||
{::lzmq/receiver (ig/ref ::lzmq/receiver)
|
|
||||||
::http.client/client (ig/ref ::http.client/client)}
|
|
||||||
|
|
||||||
:app.loggers.mattermost/reporter
|
:app.loggers.mattermost/reporter
|
||||||
{::lzmq/receiver (ig/ref ::lzmq/receiver)
|
{::http.client/client (ig/ref ::http.client/client)}
|
||||||
::http.client/client (ig/ref ::http.client/client)}
|
|
||||||
|
|
||||||
:app.loggers.database/reporter
|
:app.loggers.database/reporter
|
||||||
{::lzmq/receiver (ig/ref :app.loggers.zmq/receiver)
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
::db/pool (ig/ref ::db/pool)}
|
|
||||||
|
|
||||||
::sto/storage
|
::sto/storage
|
||||||
{:pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
:executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
|
::sto/backends
|
||||||
:backends
|
|
||||||
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
|
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
|
||||||
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
|
:assets-fs (ig/ref [::assets :app.storage.fs/backend])}}
|
||||||
|
|
||||||
;; keep this for backward compatibility
|
|
||||||
:s3 (ig/ref [::assets :app.storage.s3/backend])
|
|
||||||
:fs (ig/ref [::assets :app.storage.fs/backend])}}
|
|
||||||
|
|
||||||
[::assets :app.storage.s3/backend]
|
[::assets :app.storage.s3/backend]
|
||||||
{:region (cf/get :storage-assets-s3-region)
|
{::sto.s3/region (cf/get :storage-assets-s3-region)
|
||||||
:endpoint (cf/get :storage-assets-s3-endpoint)
|
::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint)
|
||||||
:bucket (cf/get :storage-assets-s3-bucket)
|
::sto.s3/bucket (cf/get :storage-assets-s3-bucket)
|
||||||
:executor (ig/ref ::wrk/executor)}
|
::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
[::assets :app.storage.fs/backend]
|
[::assets :app.storage.fs/backend]
|
||||||
{:directory (cf/get :storage-assets-fs-directory)}
|
{::sto.fs/directory (cf/get :storage-assets-fs-directory)}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
[app.common.media :as cm]
|
[app.common.media :as cm]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.db :as-alias db]
|
||||||
|
[app.storage :as-alias sto]
|
||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
[app.util.svg :as svg]
|
[app.util.svg :as svg]
|
||||||
[buddy.core.bytes :as bb]
|
[buddy.core.bytes :as bb]
|
||||||
@ -297,8 +299,7 @@
|
|||||||
"Given storage map, returns a storage configured with the appropriate
|
"Given storage map, returns a storage configured with the appropriate
|
||||||
backend for assets and optional connection attached."
|
backend for assets and optional connection attached."
|
||||||
([storage]
|
([storage]
|
||||||
(assoc storage :backend (cf/get :assets-storage-backend :assets-fs)))
|
(assoc storage ::sto/backend (cf/get :assets-storage-backend :assets-fs)))
|
||||||
([storage conn]
|
([storage pool-or-conn]
|
||||||
(-> storage
|
(-> (configure-assets-storage storage)
|
||||||
(assoc :conn conn)
|
(assoc ::db/pool-or-conn pool-or-conn))))
|
||||||
(assoc :backend (cf/get :assets-storage-backend :assets-fs)))))
|
|
||||||
|
|||||||
@ -87,6 +87,7 @@
|
|||||||
::definitions definitions
|
::definitions definitions
|
||||||
::registry registry}))
|
::registry registry}))
|
||||||
|
|
||||||
|
|
||||||
(defn- handler
|
(defn- handler
|
||||||
[registry _ respond _]
|
[registry _ respond _]
|
||||||
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
|
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
|
||||||
@ -95,6 +96,18 @@
|
|||||||
(respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
|
(respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
|
||||||
:body (.toString writer)})))
|
:body (.toString writer)})))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(s/def ::routes vector?)
|
||||||
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
|
(s/keys :req [::metrics]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::routes
|
||||||
|
[_ {:keys [::metrics]}]
|
||||||
|
(let [registry (::registry metrics)]
|
||||||
|
["/metrics" {:handler (partial handler registry)
|
||||||
|
:allowed-methods #{:get}}]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Implementation
|
;; Implementation
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -6,8 +6,12 @@
|
|||||||
|
|
||||||
(ns app.migrations
|
(ns app.migrations
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.db :as db]
|
||||||
[app.migrations.clj.migration-0023 :as mg0023]
|
[app.migrations.clj.migration-0023 :as mg0023]
|
||||||
[app.util.migrations :as mg]
|
[app.util.migrations :as mg]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(def migrations
|
(def migrations
|
||||||
@ -302,7 +306,29 @@
|
|||||||
{:name "0098-add-quotes-table"
|
{:name "0098-add-quotes-table"
|
||||||
:fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")}
|
:fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")}
|
||||||
|
|
||||||
|
{:name "0099-add-access-token-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0099-add-access-token-table.sql")}
|
||||||
|
|
||||||
|
{:name "0100-mod-profile-indexes"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0100-mod-profile-indexes.sql")}
|
||||||
|
|
||||||
|
{:name "0101-mod-server-error-report-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0101-mod-server-error-report-table.sql")}
|
||||||
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
(defn apply-migrations!
|
||||||
|
[pool name migrations]
|
||||||
|
(dm/with-open [conn (db/open pool)]
|
||||||
|
(mg/setup! conn)
|
||||||
|
(mg/migrate! conn {:name name :steps migrations})))
|
||||||
|
|
||||||
(defmethod ig/init-key ::migrations [_ _] migrations)
|
(defmethod ig/pre-init-spec ::migrations
|
||||||
|
[_]
|
||||||
|
(s/keys :req [::db/pool]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::migrations
|
||||||
|
[module {:keys [::db/pool]}]
|
||||||
|
(when-not (db/read-only? pool)
|
||||||
|
(l/info :hint "running migrations" :module module)
|
||||||
|
(some->> (seq migrations) (apply-migrations! pool "main"))))
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
DROP TABLE IF EXISTS access_token;
|
||||||
|
CREATE TABLE access_token (
|
||||||
|
id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE,
|
||||||
|
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
name text NOT NULL,
|
||||||
|
token text NOT NULL,
|
||||||
|
perms text[] NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE access_token
|
||||||
|
ALTER COLUMN name SET STORAGE external,
|
||||||
|
ALTER COLUMN token SET STORAGE external,
|
||||||
|
ALTER COLUMN perms SET STORAGE external;
|
||||||
|
|
||||||
|
CREATE INDEX access_token__profile_id__idx ON access_token(profile_id);
|
||||||
31
backend/src/app/migrations/sql/0100-mod-profile-indexes.sql
Normal file
31
backend/src/app/migrations/sql/0100-mod-profile-indexes.sql
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
ALTER TABLE profile
|
||||||
|
ADD COLUMN default_project_id uuid NULL REFERENCES project(id) ON DELETE SET NULL DEFERRABLE,
|
||||||
|
ADD COLUMN default_team_id uuid NULL REFERENCES team(id) ON DELETE SET NULL DEFERRABLE;
|
||||||
|
|
||||||
|
CREATE INDEX profile__default_project__idx ON profile(default_project_id);
|
||||||
|
CREATE INDEX profile__default_team__idx ON profile(default_team_id);
|
||||||
|
|
||||||
|
with profiles as (
|
||||||
|
select p.id,
|
||||||
|
tpr.team_id as default_team_id,
|
||||||
|
ppr.project_id as default_project_id
|
||||||
|
from profile as p
|
||||||
|
join team_profile_rel as tpr
|
||||||
|
on (tpr.profile_id = p.id and
|
||||||
|
tpr.is_owner is true)
|
||||||
|
join project_profile_rel as ppr
|
||||||
|
on (ppr.profile_id = p.id and
|
||||||
|
ppr.is_owner is true)
|
||||||
|
join project as pj
|
||||||
|
on (pj.id = ppr.project_id)
|
||||||
|
join team as tm
|
||||||
|
on (tm.id = tpr.team_id)
|
||||||
|
where pj.is_default is true
|
||||||
|
and tm.is_default is true
|
||||||
|
and pj.team_id = tm.id
|
||||||
|
)
|
||||||
|
update profile
|
||||||
|
set default_team_id = p.default_team_id,
|
||||||
|
default_project_id = p.default_project_id
|
||||||
|
from profiles as p
|
||||||
|
where profile.id = p.id;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE server_error_report
|
||||||
|
ADD COLUMN version integer DEFAULT 1;
|
||||||
@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
(us/verify! ::msgbus msgbus)
|
(us/verify! ::msgbus msgbus)
|
||||||
|
|
||||||
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/async false))
|
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
|
||||||
(set-error-mode! state :continue)
|
(set-error-mode! state :continue)
|
||||||
(start-io-loop! msgbus)
|
(start-io-loop! msgbus)
|
||||||
|
|
||||||
@ -133,7 +133,7 @@
|
|||||||
[nsubs cfg topic chan]
|
[nsubs cfg topic chan]
|
||||||
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
|
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
|
||||||
(when (= 1 (count nsubs))
|
(when (= 1 (count nsubs))
|
||||||
(l/trace :hint "open subscription" :topic topic ::l/async false)
|
(l/trace :hint "open subscription" :topic topic ::l/sync? true)
|
||||||
(redis-sub cfg topic))
|
(redis-sub cfg topic))
|
||||||
nsubs))
|
nsubs))
|
||||||
|
|
||||||
@ -144,7 +144,7 @@
|
|||||||
[nsubs cfg topic chan]
|
[nsubs cfg topic chan]
|
||||||
(let [nsubs (disj nsubs chan)]
|
(let [nsubs (disj nsubs chan)]
|
||||||
(when (empty? nsubs)
|
(when (empty? nsubs)
|
||||||
(l/trace :hint "close subscription" :topic topic ::l/async false)
|
(l/trace :hint "close subscription" :topic topic ::l/sync? true)
|
||||||
(redis-unsub cfg topic))
|
(redis-unsub cfg topic))
|
||||||
nsubs))
|
nsubs))
|
||||||
|
|
||||||
|
|||||||
@ -193,6 +193,7 @@
|
|||||||
|
|
||||||
(defn get-or-connect
|
(defn get-or-connect
|
||||||
[{:keys [::cache] :as state} key options]
|
[{:keys [::cache] :as state} key options]
|
||||||
|
(us/assert! ::redis state)
|
||||||
(-> state
|
(-> state
|
||||||
(assoc ::connection
|
(assoc ::connection
|
||||||
(or (get @cache key)
|
(or (get @cache key)
|
||||||
@ -205,7 +206,6 @@
|
|||||||
|
|
||||||
(defn add-listener!
|
(defn add-listener!
|
||||||
[{:keys [::connection] :as conn} listener]
|
[{:keys [::connection] :as conn} listener]
|
||||||
(us/assert! ::connection-holder conn)
|
|
||||||
(us/assert! ::pubsub-connection connection)
|
(us/assert! ::pubsub-connection connection)
|
||||||
(us/assert! ::pubsub-listener listener)
|
(us/assert! ::pubsub-listener listener)
|
||||||
(.addListener ^StatefulRedisPubSubConnection @connection
|
(.addListener ^StatefulRedisPubSubConnection @connection
|
||||||
@ -213,10 +213,9 @@
|
|||||||
conn)
|
conn)
|
||||||
|
|
||||||
(defn publish!
|
(defn publish!
|
||||||
[{:keys [::connection] :as conn} topic message]
|
[{:keys [::connection]} topic message]
|
||||||
(us/assert! ::us/string topic)
|
(us/assert! ::us/string topic)
|
||||||
(us/assert! ::us/bytes message)
|
(us/assert! ::us/bytes message)
|
||||||
(us/assert! ::connection-holder conn)
|
|
||||||
(us/assert! ::default-connection connection)
|
(us/assert! ::default-connection connection)
|
||||||
|
|
||||||
(let [pcomm (.async ^StatefulRedisConnection @connection)]
|
(let [pcomm (.async ^StatefulRedisConnection @connection)]
|
||||||
@ -224,8 +223,7 @@
|
|||||||
|
|
||||||
(defn subscribe!
|
(defn subscribe!
|
||||||
"Blocking operation, intended to be used on a thread/agent thread."
|
"Blocking operation, intended to be used on a thread/agent thread."
|
||||||
[{:keys [::connection] :as conn} & topics]
|
[{:keys [::connection]} & topics]
|
||||||
(us/assert! ::connection-holder conn)
|
|
||||||
(us/assert! ::pubsub-connection connection)
|
(us/assert! ::pubsub-connection connection)
|
||||||
(try
|
(try
|
||||||
(let [topics (into-array String (map str topics))
|
(let [topics (into-array String (map str topics))
|
||||||
@ -236,8 +234,7 @@
|
|||||||
|
|
||||||
(defn unsubscribe!
|
(defn unsubscribe!
|
||||||
"Blocking operation, intended to be used on a thread/agent thread."
|
"Blocking operation, intended to be used on a thread/agent thread."
|
||||||
[{:keys [::connection] :as conn} & topics]
|
[{:keys [::connection]} & topics]
|
||||||
(us/assert! ::connection-holder conn)
|
|
||||||
(us/assert! ::pubsub-connection connection)
|
(us/assert! ::pubsub-connection connection)
|
||||||
(try
|
(try
|
||||||
(let [topics (into-array String (map str topics))
|
(let [topics (into-array String (map str topics))
|
||||||
@ -247,8 +244,8 @@
|
|||||||
(throw (InterruptedException. (ex-message cause))))))
|
(throw (InterruptedException. (ex-message cause))))))
|
||||||
|
|
||||||
(defn rpush!
|
(defn rpush!
|
||||||
[{:keys [::connection] :as conn} key payload]
|
[{:keys [::connection]} key payload]
|
||||||
(us/assert! ::connection-holder conn)
|
(us/assert! ::default-connection connection)
|
||||||
(us/assert! (or (and (vector? payload)
|
(us/assert! (or (and (vector? payload)
|
||||||
(every? bytes? payload))
|
(every? bytes? payload))
|
||||||
(bytes? payload)))
|
(bytes? payload)))
|
||||||
@ -270,8 +267,8 @@
|
|||||||
(throw (InterruptedException. (ex-message cause))))))
|
(throw (InterruptedException. (ex-message cause))))))
|
||||||
|
|
||||||
(defn blpop!
|
(defn blpop!
|
||||||
[{:keys [::connection] :as conn} timeout & keys]
|
[{:keys [::connection]} timeout & keys]
|
||||||
(us/assert! ::connection-holder conn)
|
(us/assert! ::default-connection connection)
|
||||||
(try
|
(try
|
||||||
(let [keys (into-array Object (map str keys))
|
(let [keys (into-array Object (map str keys))
|
||||||
cmd (.sync ^StatefulRedisConnection @connection)
|
cmd (.sync ^StatefulRedisConnection @connection)
|
||||||
@ -286,8 +283,7 @@
|
|||||||
(throw (InterruptedException. (ex-message cause))))))
|
(throw (InterruptedException. (ex-message cause))))))
|
||||||
|
|
||||||
(defn open?
|
(defn open?
|
||||||
[{:keys [::connection] :as conn}]
|
[{:keys [::connection]}]
|
||||||
(us/assert! ::connection-holder conn)
|
|
||||||
(us/assert! ::pubsub-connection connection)
|
(us/assert! ::pubsub-connection connection)
|
||||||
(.isOpen ^StatefulConnection @connection))
|
(.isOpen ^StatefulConnection @connection))
|
||||||
|
|
||||||
@ -335,7 +331,7 @@
|
|||||||
(defn eval!
|
(defn eval!
|
||||||
[{:keys [::mtx/metrics ::connection] :as state} script]
|
[{:keys [::mtx/metrics ::connection] :as state} script]
|
||||||
(us/assert! ::redis state)
|
(us/assert! ::redis state)
|
||||||
(us/assert! ::connection-holder state)
|
(us/assert! ::default-connection connection)
|
||||||
(us/assert! ::rscript/script script)
|
(us/assert! ::rscript/script script)
|
||||||
|
|
||||||
(let [cmd (.async ^StatefulRedisConnection @connection)
|
(let [cmd (.async ^StatefulRedisConnection @connection)
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
(ns app.rpc
|
(ns app.rpc
|
||||||
(:require
|
(:require
|
||||||
[app.auth.ldap :as-alias ldap]
|
[app.auth.ldap :as-alias ldap]
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
@ -14,10 +15,12 @@
|
|||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
|
[app.http.access-token :as actoken]
|
||||||
[app.http.client :as-alias http.client]
|
[app.http.client :as-alias http.client]
|
||||||
[app.http.session :as-alias http.session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
|
[app.main :as-alias main]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.msgbus :as-alias mbus]
|
[app.msgbus :as-alias mbus]
|
||||||
[app.rpc.climit :as climit]
|
[app.rpc.climit :as climit]
|
||||||
@ -71,71 +74,78 @@
|
|||||||
(defn- rpc-query-handler
|
(defn- rpc-query-handler
|
||||||
"Ring handler that dispatches query requests and convert between
|
"Ring handler that dispatches query requests and convert between
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
|
[methods {:keys [params path-params] :as request} respond raise]
|
||||||
(let [type (keyword (:type path-params))
|
(let [type (keyword (:type path-params))
|
||||||
data (-> params
|
profile-id (or (::session/profile-id request)
|
||||||
(assoc ::request-at (dt/now))
|
(::actoken/profile-id request))
|
||||||
(assoc ::http/request request))
|
|
||||||
data (if profile-id
|
|
||||||
(-> data
|
|
||||||
(assoc :profile-id profile-id)
|
|
||||||
(assoc ::profile-id profile-id)
|
|
||||||
(assoc ::session-id session-id))
|
|
||||||
(dissoc data :profile-id ::profile-id))
|
|
||||||
method (get methods type default-handler)]
|
|
||||||
|
|
||||||
(-> (method data)
|
data (-> params
|
||||||
(p/then (partial handle-response request))
|
(assoc ::request-at (dt/now))
|
||||||
(p/then respond)
|
(assoc ::http/request request))
|
||||||
(p/catch (fn [cause]
|
data (if profile-id
|
||||||
(let [context {:profile-id profile-id}]
|
(-> data
|
||||||
(raise (ex/wrap-with-context cause context))))))))
|
(assoc :profile-id profile-id)
|
||||||
|
(assoc ::profile-id profile-id))
|
||||||
|
(dissoc data :profile-id ::profile-id))
|
||||||
|
method (get methods type default-handler)]
|
||||||
|
|
||||||
|
(->> (method data)
|
||||||
|
(p/mcat (partial handle-response request))
|
||||||
|
(p/fnly (fn [response cause]
|
||||||
|
(if cause
|
||||||
|
(raise cause)
|
||||||
|
(respond response)))))))
|
||||||
|
|
||||||
(defn- rpc-mutation-handler
|
(defn- rpc-mutation-handler
|
||||||
"Ring handler that dispatches mutation requests and convert between
|
"Ring handler that dispatches mutation requests and convert between
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
|
[methods {:keys [params path-params] :as request} respond raise]
|
||||||
(let [type (keyword (:type path-params))
|
(let [type (keyword (:type path-params))
|
||||||
data (-> params
|
profile-id (or (::session/profile-id request)
|
||||||
(assoc ::request-at (dt/now))
|
(::actoken/profile-id request))
|
||||||
(assoc ::http/request request))
|
data (-> params
|
||||||
data (if profile-id
|
(assoc ::request-at (dt/now))
|
||||||
(-> data
|
(assoc ::http/request request))
|
||||||
(assoc :profile-id profile-id)
|
data (if profile-id
|
||||||
(assoc ::profile-id profile-id)
|
(-> data
|
||||||
(assoc ::session-id session-id))
|
(assoc :profile-id profile-id)
|
||||||
(dissoc data :profile-id ::profile-id))
|
(assoc ::profile-id profile-id))
|
||||||
method (get methods type default-handler)]
|
(dissoc data :profile-id))
|
||||||
(-> (method data)
|
method (get methods type default-handler)]
|
||||||
(p/then (partial handle-response request))
|
|
||||||
(p/then respond)
|
(->> (method data)
|
||||||
(p/catch (fn [cause]
|
(p/mcat (partial handle-response request))
|
||||||
(let [context {:profile-id profile-id}]
|
(p/fnly (fn [response cause]
|
||||||
(raise (ex/wrap-with-context cause context))))))))
|
(if cause
|
||||||
|
(raise cause)
|
||||||
|
(respond response)))))))
|
||||||
|
|
||||||
(defn- rpc-command-handler
|
(defn- rpc-command-handler
|
||||||
"Ring handler that dispatches cmd requests and convert between
|
"Ring handler that dispatches cmd requests and convert between
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
|
[methods {:keys [params path-params] :as request} respond raise]
|
||||||
(let [cmd (keyword (:type path-params))
|
(let [type (keyword (:type path-params))
|
||||||
etag (yrq/get-header request "if-none-match")
|
etag (yrq/get-header request "if-none-match")
|
||||||
|
profile-id (or (::session/profile-id request)
|
||||||
|
(::actoken/profile-id request))
|
||||||
|
|
||||||
data (-> params
|
data (-> params
|
||||||
(assoc ::request-at (dt/now))
|
(assoc ::request-at (dt/now))
|
||||||
(assoc ::http/request request)
|
(assoc ::session/id (::session/id request))
|
||||||
(assoc ::cond/key etag)
|
(assoc ::http/request request)
|
||||||
(cond-> (uuid? profile-id)
|
(assoc ::cond/key etag)
|
||||||
(-> (assoc ::profile-id profile-id)
|
(cond-> (uuid? profile-id)
|
||||||
(assoc ::session-id session-id))))
|
(assoc ::profile-id profile-id)))
|
||||||
|
|
||||||
|
method (get methods type default-handler)]
|
||||||
|
|
||||||
method (get methods cmd default-handler)]
|
|
||||||
(binding [cond/*enabled* true]
|
(binding [cond/*enabled* true]
|
||||||
(-> (method data)
|
(->> (method data)
|
||||||
(p/then (partial handle-response request))
|
(p/mcat (partial handle-response request))
|
||||||
(p/then respond)
|
(p/fnly (fn [response cause]
|
||||||
(p/catch (fn [cause]
|
(if cause
|
||||||
(let [context {:profile-id profile-id}]
|
(raise cause)
|
||||||
(raise (ex/wrap-with-context cause context)))))))))
|
(respond response))))))))
|
||||||
|
|
||||||
(defn- wrap-metrics
|
(defn- wrap-metrics
|
||||||
"Wrap service method with metrics measurement."
|
"Wrap service method with metrics measurement."
|
||||||
@ -143,18 +153,46 @@
|
|||||||
(let [labels (into-array String [(::sv/name mdata)])]
|
(let [labels (into-array String [(::sv/name mdata)])]
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(let [tp (dt/tpoint)]
|
(let [tp (dt/tpoint)]
|
||||||
(p/finally
|
(->> (f cfg params)
|
||||||
(f cfg params)
|
(p/fnly (fn [_ _]
|
||||||
(fn [_ _]
|
(mtx/run! metrics
|
||||||
(mtx/run! metrics
|
:id metrics-id
|
||||||
:id metrics-id
|
:val (inst-ms (tp))
|
||||||
:val (inst-ms (tp))
|
:labels labels))))))))
|
||||||
:labels labels)))))))
|
|
||||||
|
|
||||||
|
(defn- wrap-authentication
|
||||||
|
[_ f mdata]
|
||||||
|
(fn [cfg params]
|
||||||
|
(let [profile-id (::profile-id params)]
|
||||||
|
(if (and (::auth mdata true) (not (uuid? profile-id)))
|
||||||
|
(p/rejected
|
||||||
|
(ex/error :type :authentication
|
||||||
|
:code :authentication-required
|
||||||
|
:hint "authentication required for this endpoint"))
|
||||||
|
(f cfg params)))))
|
||||||
|
|
||||||
|
(defn- wrap-access-token
|
||||||
|
"Wraps service method with access token validation."
|
||||||
|
[_ f {:keys [::sv/name] :as mdata}]
|
||||||
|
(if (contains? cf/flags :access-tokens)
|
||||||
|
(fn [cfg params]
|
||||||
|
(let [request (::http/request params)]
|
||||||
|
(if (contains? request ::actoken/id)
|
||||||
|
(let [perms (::actoken/perms request #{})]
|
||||||
|
(if (contains? perms name)
|
||||||
|
(f cfg params)
|
||||||
|
(p/rejected
|
||||||
|
(ex/error :type :authorization
|
||||||
|
:code :operation-not-allowed
|
||||||
|
:allowed perms))))
|
||||||
|
(f cfg params))))
|
||||||
|
f))
|
||||||
|
|
||||||
(defn- wrap-dispatch
|
(defn- wrap-dispatch
|
||||||
"Wraps service method into async flow, with the ability to dispatching
|
"Wraps service method into async flow, with the ability to dispatching
|
||||||
it to a preconfigured executor service."
|
it to a preconfigured executor service."
|
||||||
[{:keys [executor] :as cfg} f mdata]
|
[{:keys [::wrk/executor] :as cfg} f mdata]
|
||||||
(with-meta
|
(with-meta
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(->> (px/submit! executor (px/wrap-bindings #(f cfg params)))
|
(->> (px/submit! executor (px/wrap-bindings #(f cfg params)))
|
||||||
@ -223,37 +261,34 @@
|
|||||||
f))
|
f))
|
||||||
f))
|
f))
|
||||||
|
|
||||||
|
(defn- wrap-spec-conform
|
||||||
|
[_ f mdata]
|
||||||
|
(let [spec (or (::sv/spec mdata) (s/spec any?))]
|
||||||
|
(fn [cfg params]
|
||||||
|
(let [params (ex/try! (us/conform spec params))]
|
||||||
|
(if (ex/exception? params)
|
||||||
|
(p/rejected params)
|
||||||
|
(f cfg params))))))
|
||||||
|
|
||||||
|
(defn- wrap-all
|
||||||
|
[cfg f mdata]
|
||||||
|
(as-> f $
|
||||||
|
(wrap-dispatch cfg $ mdata)
|
||||||
|
(wrap-metrics cfg $ mdata)
|
||||||
|
(cond/wrap cfg $ mdata)
|
||||||
|
(retry/wrap-retry cfg $ mdata)
|
||||||
|
(climit/wrap cfg $ mdata)
|
||||||
|
(rlimit/wrap cfg $ mdata)
|
||||||
|
(wrap-audit cfg $ mdata)
|
||||||
|
(wrap-spec-conform cfg $ mdata)
|
||||||
|
(wrap-authentication cfg $ mdata)
|
||||||
|
(wrap-access-token cfg $ mdata)))
|
||||||
|
|
||||||
(defn- wrap
|
(defn- wrap
|
||||||
[cfg f mdata]
|
[cfg f mdata]
|
||||||
(let [f (as-> f $
|
(l/debug :hint "register method" :name (::sv/name mdata))
|
||||||
(wrap-dispatch cfg $ mdata)
|
(let [f (wrap-all cfg f mdata)]
|
||||||
(cond/wrap cfg $ mdata)
|
(with-meta #(f cfg %) mdata)))
|
||||||
(retry/wrap-retry cfg $ mdata)
|
|
||||||
(wrap-metrics cfg $ mdata)
|
|
||||||
(climit/wrap cfg $ mdata)
|
|
||||||
(rlimit/wrap cfg $ mdata)
|
|
||||||
(wrap-audit cfg $ mdata))
|
|
||||||
|
|
||||||
spec (or (::sv/spec mdata) (s/spec any?))
|
|
||||||
auth? (::auth mdata true)]
|
|
||||||
|
|
||||||
|
|
||||||
(l/debug :hint "register method" :name (::sv/name mdata))
|
|
||||||
(with-meta
|
|
||||||
(fn [params]
|
|
||||||
;; Raise authentication error when rpc method requires auth but
|
|
||||||
;; no profile-id is found in the request.
|
|
||||||
(let [profile-id (if (= "command" (::type cfg))
|
|
||||||
(::profile-id params)
|
|
||||||
(:profile-id params))]
|
|
||||||
(p/do!
|
|
||||||
(if (and auth? (not (uuid? profile-id)))
|
|
||||||
(ex/raise :type :authentication
|
|
||||||
:code :authentication-required
|
|
||||||
:hint "authentication required for this endpoint")
|
|
||||||
(let [params (us/conform spec params)]
|
|
||||||
(f cfg params))))))
|
|
||||||
mdata)))
|
|
||||||
|
|
||||||
(defn- process-method
|
(defn- process-method
|
||||||
[cfg vfn]
|
[cfg vfn]
|
||||||
@ -264,79 +299,76 @@
|
|||||||
(defn- resolve-query-methods
|
(defn- resolve-query-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)]
|
(let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)]
|
||||||
(->> (sv/scan-ns 'app.rpc.queries.projects
|
(->> (sv/scan-ns
|
||||||
'app.rpc.queries.files
|
'app.rpc.queries.projects
|
||||||
'app.rpc.queries.teams
|
'app.rpc.queries.profile
|
||||||
'app.rpc.queries.profile
|
'app.rpc.queries.viewer
|
||||||
'app.rpc.queries.viewer
|
'app.rpc.queries.fonts)
|
||||||
'app.rpc.queries.fonts)
|
|
||||||
(map (partial process-method cfg))
|
(map (partial process-method cfg))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
(defn- resolve-mutation-methods
|
(defn- resolve-mutation-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
|
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
|
||||||
(->> (sv/scan-ns 'app.rpc.mutations.media
|
(->> (sv/scan-ns
|
||||||
'app.rpc.mutations.profile
|
'app.rpc.mutations.media
|
||||||
'app.rpc.mutations.files
|
'app.rpc.mutations.profile
|
||||||
'app.rpc.mutations.projects
|
'app.rpc.mutations.projects
|
||||||
'app.rpc.mutations.teams
|
'app.rpc.mutations.fonts
|
||||||
'app.rpc.mutations.fonts
|
'app.rpc.mutations.share-link)
|
||||||
'app.rpc.mutations.share-link)
|
|
||||||
(map (partial process-method cfg))
|
(map (partial process-method cfg))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
(defn- resolve-command-methods
|
(defn- resolve-command-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
|
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
|
||||||
(->> (sv/scan-ns 'app.rpc.commands.binfile
|
(->> (sv/scan-ns
|
||||||
'app.rpc.commands.comments
|
'app.rpc.commands.access-token
|
||||||
'app.rpc.commands.management
|
'app.rpc.commands.audit
|
||||||
'app.rpc.commands.verify-token
|
'app.rpc.commands.auth
|
||||||
'app.rpc.commands.search
|
'app.rpc.commands.feedback
|
||||||
'app.rpc.commands.media
|
'app.rpc.commands.fonts
|
||||||
'app.rpc.commands.teams
|
'app.rpc.commands.binfile
|
||||||
'app.rpc.commands.auth
|
'app.rpc.commands.comments
|
||||||
'app.rpc.commands.ldap
|
'app.rpc.commands.demo
|
||||||
'app.rpc.commands.demo
|
'app.rpc.commands.files
|
||||||
'app.rpc.commands.webhooks
|
'app.rpc.commands.files-create
|
||||||
'app.rpc.commands.audit
|
'app.rpc.commands.files-share
|
||||||
'app.rpc.commands.files
|
'app.rpc.commands.files-temp
|
||||||
'app.rpc.commands.files.update
|
'app.rpc.commands.files-update
|
||||||
'app.rpc.commands.files.create
|
'app.rpc.commands.ldap
|
||||||
'app.rpc.commands.files.temp)
|
'app.rpc.commands.management
|
||||||
|
'app.rpc.commands.media
|
||||||
|
'app.rpc.commands.profile
|
||||||
|
'app.rpc.commands.projects
|
||||||
|
'app.rpc.commands.search
|
||||||
|
'app.rpc.commands.teams
|
||||||
|
'app.rpc.commands.verify-token
|
||||||
|
'app.rpc.commands.viewer
|
||||||
|
'app.rpc.commands.webhooks)
|
||||||
(map (partial process-method cfg))
|
(map (partial process-method cfg))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
(s/def ::ldap (s/nilable map?))
|
|
||||||
(s/def ::msgbus ::mbus/msgbus)
|
|
||||||
(s/def ::climit (s/nilable ::climit/climit))
|
|
||||||
(s/def ::rlimit (s/nilable ::rlimit/rlimit))
|
|
||||||
|
|
||||||
(s/def ::public-uri ::us/not-empty-string)
|
|
||||||
(s/def ::sprops map?)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::methods [_]
|
(defmethod ig/pre-init-spec ::methods [_]
|
||||||
(s/keys :req [::http.client/client
|
(s/keys :req [::session/manager
|
||||||
|
::http.client/client
|
||||||
::db/pool
|
::db/pool
|
||||||
|
::mbus/msgbus
|
||||||
::ldap/provider
|
::ldap/provider
|
||||||
|
::sto/storage
|
||||||
|
::mtx/metrics
|
||||||
|
::main/props
|
||||||
::wrk/executor]
|
::wrk/executor]
|
||||||
:req-un [::sto/storage
|
:opt [::climit
|
||||||
::http.session/session
|
::rlimit]
|
||||||
::sprops
|
:req-un [::db/pool]))
|
||||||
::public-uri
|
|
||||||
::msgbus
|
|
||||||
::rlimit
|
|
||||||
::climit
|
|
||||||
::wrk/executor
|
|
||||||
::mtx/metrics
|
|
||||||
::db/pool]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::methods
|
(defmethod ig/init-key ::methods
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
{:mutations (resolve-mutation-methods cfg)
|
(let [cfg (d/without-nils cfg)]
|
||||||
:queries (resolve-query-methods cfg)
|
{:mutations (resolve-mutation-methods cfg)
|
||||||
:commands (resolve-command-methods cfg)})
|
:queries (resolve-query-methods cfg)
|
||||||
|
:commands (resolve-command-methods cfg)}))
|
||||||
|
|
||||||
(s/def ::mutations
|
(s/def ::mutations
|
||||||
(s/map-of keyword? fn?))
|
(s/map-of keyword? fn?))
|
||||||
@ -352,12 +384,20 @@
|
|||||||
::queries
|
::queries
|
||||||
::commands]))
|
::commands]))
|
||||||
|
|
||||||
|
(s/def ::routes vector?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::routes [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req-un [::methods]))
|
(s/keys :req [::methods
|
||||||
|
::db/pool
|
||||||
|
::main/props
|
||||||
|
::wrk/executor
|
||||||
|
::session/manager
|
||||||
|
::actoken/manager]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [methods] :as cfg}]
|
[_ {:keys [::methods] :as cfg}]
|
||||||
[["/rpc"
|
[["/rpc" {:middleware [[session/authz cfg]
|
||||||
|
[actoken/authz cfg]]}
|
||||||
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
|
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
|
||||||
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
|
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
|
||||||
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
|
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
(defn- capacity-exception?
|
(defn- capacity-exception?
|
||||||
[o]
|
[o]
|
||||||
(and (ex/ex-info? o)
|
(and (ex/error? o)
|
||||||
(let [data (ex-data o)]
|
(let [data (ex-data o)]
|
||||||
(and (= :bulkhead-error (:type data))
|
(and (= :bulkhead-error (:type data))
|
||||||
(= :capacity-limit-reached (:code data))))))
|
(= :capacity-limit-reached (:code data))))))
|
||||||
@ -46,7 +46,7 @@
|
|||||||
(p/rejected
|
(p/rejected
|
||||||
(ex/error :type :internal
|
(ex/error :type :internal
|
||||||
:code :concurrency-limit-reached
|
:code :concurrency-limit-reached
|
||||||
:queue (-> limiter meta :bkey name)
|
:queue (-> limiter meta ::bkey name)
|
||||||
:cause cause))
|
:cause cause))
|
||||||
|
|
||||||
(some? cause)
|
(some? cause)
|
||||||
@ -56,7 +56,7 @@
|
|||||||
(p/resolved result))))))
|
(p/resolved result))))))
|
||||||
|
|
||||||
(defn- create-limiter
|
(defn- create-limiter
|
||||||
[{:keys [executor metrics concurrency queue-size bkey skey]}]
|
[{:keys [::wrk/executor ::mtx/metrics ::bkey ::skey concurrency queue-size]}]
|
||||||
(let [labels (into-array String [(name bkey)])
|
(let [labels (into-array String [(name bkey)])
|
||||||
on-queue (fn [instance]
|
on-queue (fn [instance]
|
||||||
(l/trace :hint "enqueued"
|
(l/trace :hint "enqueued"
|
||||||
@ -100,10 +100,10 @@
|
|||||||
:on-run on-run}]
|
:on-run on-run}]
|
||||||
|
|
||||||
(-> (pxb/create options)
|
(-> (pxb/create options)
|
||||||
(vary-meta assoc :bkey bkey :skey skey))))
|
(vary-meta assoc ::bkey bkey ::skey skey))))
|
||||||
|
|
||||||
(defn- create-cache
|
(defn- create-cache
|
||||||
[{:keys [executor] :as params} config]
|
[{:keys [::wrk/executor] :as params} config]
|
||||||
(let [listener (reify RemovalListener
|
(let [listener (reify RemovalListener
|
||||||
(onRemoval [_ key _val cause]
|
(onRemoval [_ key _val cause]
|
||||||
(l/trace :hint "cache: remove" :key key :reason (str cause))))
|
(l/trace :hint "cache: remove" :key key :reason (str cause))))
|
||||||
@ -113,8 +113,8 @@
|
|||||||
(let [[bkey skey] key]
|
(let [[bkey skey] key]
|
||||||
(when-let [config (get config bkey)]
|
(when-let [config (get config bkey)]
|
||||||
(-> (merge params config)
|
(-> (merge params config)
|
||||||
(assoc :bkey bkey)
|
(assoc ::bkey bkey)
|
||||||
(assoc :skey skey)
|
(assoc ::skey skey)
|
||||||
(create-limiter))))))]
|
(create-limiter))))))]
|
||||||
|
|
||||||
(.. (Caffeine/newBuilder)
|
(.. (Caffeine/newBuilder)
|
||||||
@ -134,14 +134,16 @@
|
|||||||
|
|
||||||
(defmethod ig/prep-key ::rpc/climit
|
(defmethod ig/prep-key ::rpc/climit
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(merge {:path (cf/get :rpc-climit-config)}
|
(merge {::path (cf/get :rpc-climit-config)}
|
||||||
(d/without-nils cfg)))
|
(d/without-nils cfg)))
|
||||||
|
|
||||||
|
(s/def ::path ::fs/path)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::rpc/climit [_]
|
(defmethod ig/pre-init-spec ::rpc/climit [_]
|
||||||
(s/keys :req-un [::wrk/executor ::mtx/metrics ::fs/path]))
|
(s/keys :req [::wrk/executor ::mtx/metrics ::path]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::rpc/climit
|
(defmethod ig/init-key ::rpc/climit
|
||||||
[_ {:keys [path] :as params}]
|
[_ {:keys [::path] :as params}]
|
||||||
(when (contains? cf/flags :rpc-climit)
|
(when (contains? cf/flags :rpc-climit)
|
||||||
(if-let [config (some->> path slurp edn/read-string)]
|
(if-let [config (some->> path slurp edn/read-string)]
|
||||||
(do
|
(do
|
||||||
@ -163,7 +165,8 @@
|
|||||||
(l/warn :hint "unable to load configuration" :config (str path)))))
|
(l/warn :hint "unable to load configuration" :config (str path)))))
|
||||||
|
|
||||||
|
|
||||||
(s/def ::climit #(satisfies? IConcurrencyManager %))
|
(s/def ::rpc/climit
|
||||||
|
(s/nilable #(satisfies? IConcurrencyManager %)))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; PUBLIC API
|
;; PUBLIC API
|
||||||
@ -176,7 +179,7 @@
|
|||||||
(p/wrap (do ~@body))))
|
(p/wrap (do ~@body))))
|
||||||
|
|
||||||
(defn wrap
|
(defn wrap
|
||||||
[{:keys [climit]} f {:keys [::queue ::key-fn] :as mdata}]
|
[{:keys [::rpc/climit]} f {:keys [::queue ::key-fn] :as mdata}]
|
||||||
(if (and (some? climit)
|
(if (and (some? climit)
|
||||||
(some? queue))
|
(some? queue))
|
||||||
(if-let [config (get @climit queue)]
|
(if-let [config (get @climit queue)]
|
||||||
@ -192,7 +195,6 @@
|
|||||||
(let [key [queue (key-fn params)]
|
(let [key [queue (key-fn params)]
|
||||||
lim (get climit key)]
|
lim (get climit key)]
|
||||||
(invoke! lim (partial f cfg params))))
|
(invoke! lim (partial f cfg params))))
|
||||||
|
|
||||||
(let [lim (get climit queue)]
|
(let [lim (get climit queue)]
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(invoke! lim (partial f cfg params))))))
|
(invoke! lim (partial f cfg params))))))
|
||||||
|
|||||||
87
backend/src/app/rpc/commands/access_token.clj
Normal file
87
backend/src/app/rpc/commands/access_token.clj
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.rpc.commands.access-token
|
||||||
|
(:require
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.main :as-alias main]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.rpc.quotes :as quotes]
|
||||||
|
[app.tokens :as tokens]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
(defn- decode-row
|
||||||
|
[{:keys [perms] :as row}]
|
||||||
|
(cond-> row
|
||||||
|
(db/pgarray? perms "text")
|
||||||
|
(assoc :perms (db/decode-pgarray perms #{}))))
|
||||||
|
|
||||||
|
(defn- create-access-token
|
||||||
|
[{:keys [::conn ::main/props]} profile-id name perms]
|
||||||
|
(let [created-at (dt/now)
|
||||||
|
token-id (uuid/next)
|
||||||
|
token (tokens/generate props {:iss "access-token"
|
||||||
|
:tid token-id
|
||||||
|
:iat created-at})]
|
||||||
|
(db/insert! conn :access-token
|
||||||
|
{:id token-id
|
||||||
|
:name name
|
||||||
|
:token token
|
||||||
|
:profile-id profile-id
|
||||||
|
:created-at created-at
|
||||||
|
:updated-at created-at
|
||||||
|
:perms (db/create-array conn "text" perms)})))
|
||||||
|
|
||||||
|
(defn repl-create-access-token
|
||||||
|
[{:keys [::db/pool] :as system} profile-id name perms]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [props (:app.setup/props system)]
|
||||||
|
(create-access-token {::conn conn ::main/props props}
|
||||||
|
profile-id
|
||||||
|
name
|
||||||
|
perms))))
|
||||||
|
|
||||||
|
(s/def ::name ::us/not-empty-string)
|
||||||
|
(s/def ::perms ::us/set-of-strings)
|
||||||
|
|
||||||
|
(s/def ::create-access-token
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::name ::perms]))
|
||||||
|
|
||||||
|
(sv/defmethod ::create-access-token
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name perms]}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [cfg (assoc cfg ::conn conn)]
|
||||||
|
(quotes/check-quote! conn
|
||||||
|
{::quotes/id ::quotes/access-tokens-per-profile
|
||||||
|
::quotes/profile-id profile-id})
|
||||||
|
(-> (create-access-token cfg profile-id name perms)
|
||||||
|
(decode-row)))))
|
||||||
|
|
||||||
|
(s/def ::delete-access-token
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::us/id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::delete-access-token
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||||
|
(db/delete! pool :access-token {:id id :profile-id profile-id})
|
||||||
|
nil)
|
||||||
|
|
||||||
|
(s/def ::get-access-tokens
|
||||||
|
(s/keys :req [::rpc/profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::get-access-tokens
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
||||||
|
(->> (db/query pool :access-token {:profile-id profile-id})
|
||||||
|
(mapv decode-row)))
|
||||||
@ -42,7 +42,7 @@
|
|||||||
:profile-id :ip-addr :props :context])
|
:profile-id :ip-addr :props :context])
|
||||||
|
|
||||||
(defn- handle-events
|
(defn- handle-events
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request] :as params}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request]}]
|
||||||
(let [ip-addr (audit/parse-client-ip request)
|
(let [ip-addr (audit/parse-client-ip request)
|
||||||
xform (comp
|
xform (comp
|
||||||
(map #(assoc % :profile-id profile-id))
|
(map #(assoc % :profile-id profile-id))
|
||||||
|
|||||||
@ -13,16 +13,16 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as eml]
|
[app.email :as eml]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.climit :as climit]
|
[app.rpc.climit :as climit]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.queries.profile :as profile]
|
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -52,24 +52,10 @@
|
|||||||
(str/split #"@" 2))]
|
(str/split #"@" 2))]
|
||||||
(contains? domains candidate))))
|
(contains? domains candidate))))
|
||||||
|
|
||||||
(def ^:private sql:profile-existence
|
|
||||||
"select exists (select * from profile
|
|
||||||
where email = ?
|
|
||||||
and deleted_at is null) as val")
|
|
||||||
|
|
||||||
(defn check-profile-existence!
|
|
||||||
[conn {:keys [email] :as params}]
|
|
||||||
(let [email (str/lower email)
|
|
||||||
result (db/exec-one! conn [sql:profile-existence email])]
|
|
||||||
(when (:val result)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-already-exists))
|
|
||||||
params))
|
|
||||||
|
|
||||||
;; ---- COMMAND: login with password
|
;; ---- COMMAND: login with password
|
||||||
|
|
||||||
(defn login-with-password
|
(defn login-with-password
|
||||||
[{:keys [::db/pool session] :as cfg} {:keys [email password] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [email password] :as params}]
|
||||||
|
|
||||||
(when-not (or (contains? cf/flags :login)
|
(when-not (or (contains? cf/flags :login)
|
||||||
(contains? cf/flags :login-with-password))
|
(contains? cf/flags :login-with-password))
|
||||||
@ -105,11 +91,10 @@
|
|||||||
profile)]
|
profile)]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
|
(let [profile (->> (profile/get-profile-by-email conn email)
|
||||||
(validate-profile)
|
(validate-profile)
|
||||||
(profile/strip-private-attrs)
|
(profile/decode-row)
|
||||||
(profile/populate-additional-data conn)
|
(profile/strip-private-attrs))
|
||||||
(profile/decode-profile-row))
|
|
||||||
|
|
||||||
invitation (when-let [token (:invitation-token params)]
|
invitation (when-let [token (:invitation-token params)]
|
||||||
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))
|
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))
|
||||||
@ -122,14 +107,13 @@
|
|||||||
(assoc profile :is-admin (let [admins (cf/get :admins)]
|
(assoc profile :is-admin (let [admins (cf/get :admins)]
|
||||||
(contains? admins (:email profile)))))]
|
(contains? admins (:email profile)))))]
|
||||||
(-> response
|
(-> response
|
||||||
(rph/with-transform (session/create-fn session (:id profile)))
|
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||||
(rph/with-meta {::audit/props (audit/profile->props profile)
|
(rph/with-meta {::audit/props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)}))))))
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
(s/def ::scope ::us/string)
|
|
||||||
(s/def ::login-with-password
|
(s/def ::login-with-password
|
||||||
(s/keys :req-un [::email ::password]
|
(s/keys :req-un [::email ::password]
|
||||||
:opt-un [::invitation-token ::scope]))
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
(sv/defmethod ::login-with-password
|
(sv/defmethod ::login-with-password
|
||||||
"Performs authentication using penpot password."
|
"Performs authentication using penpot password."
|
||||||
@ -148,8 +132,8 @@
|
|||||||
"Clears the authentication cookie and logout the current session."
|
"Clears the authentication cookie and logout the current session."
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::doc/added "1.15"}
|
::doc/added "1.15"}
|
||||||
[{:keys [session] :as cfg} _]
|
[cfg _]
|
||||||
(rph/with-transform {} (session/delete-fn session)))
|
(rph/with-transform {} (session/delete-fn cfg)))
|
||||||
|
|
||||||
;; ---- COMMAND: Recover Profile
|
;; ---- COMMAND: Recover Profile
|
||||||
|
|
||||||
@ -226,7 +210,7 @@
|
|||||||
|
|
||||||
(validate-register-attempt! cfg params)
|
(validate-register-attempt! cfg params)
|
||||||
|
|
||||||
(let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))]
|
(let [profile (when-let [profile (profile/get-profile-by-email pool (:email params))]
|
||||||
(cond
|
(cond
|
||||||
(:is-blocked profile)
|
(:is-blocked profile)
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
@ -267,10 +251,11 @@
|
|||||||
|
|
||||||
;; ---- COMMAND: Register Profile
|
;; ---- COMMAND: Register Profile
|
||||||
|
|
||||||
(defn create-profile
|
(defn create-profile!
|
||||||
"Create the profile entry on the database with limited set of input
|
"Create the profile entry on the database with limited set of input
|
||||||
attrs (all the other attrs are filled with default values)."
|
attrs (all the other attrs are filled with default values)."
|
||||||
[conn params]
|
[conn {:keys [email] :as params}]
|
||||||
|
(us/assert! ::us/email email)
|
||||||
(let [id (or (:id params) (uuid/next))
|
(let [id (or (:id params) (uuid/next))
|
||||||
props (-> (audit/extract-utm-params params)
|
props (-> (audit/extract-utm-params params)
|
||||||
(merge (:props params))
|
(merge (:props params))
|
||||||
@ -291,7 +276,7 @@
|
|||||||
is-demo (:is-demo params false)
|
is-demo (:is-demo params false)
|
||||||
is-muted (:is-muted params false)
|
is-muted (:is-muted params false)
|
||||||
is-active (:is-active params false)
|
is-active (:is-active params false)
|
||||||
email (str/lower (:email params))
|
email (str/lower email)
|
||||||
|
|
||||||
params {:id id
|
params {:id id
|
||||||
:fullname (:fullname params)
|
:fullname (:fullname params)
|
||||||
@ -306,7 +291,7 @@
|
|||||||
:is-demo is-demo}]
|
:is-demo is-demo}]
|
||||||
(try
|
(try
|
||||||
(-> (db/insert! conn :profile params)
|
(-> (db/insert! conn :profile params)
|
||||||
(profile/decode-profile-row))
|
(profile/decode-row))
|
||||||
(catch org.postgresql.util.PSQLException e
|
(catch org.postgresql.util.PSQLException e
|
||||||
(let [state (.getSQLState e)]
|
(let [state (.getSQLState e)]
|
||||||
(if (not= state "23505")
|
(if (not= state "23505")
|
||||||
@ -316,15 +301,17 @@
|
|||||||
:hint "email already exists"
|
:hint "email already exists"
|
||||||
:cause e)))))))
|
:cause e)))))))
|
||||||
|
|
||||||
(defn create-profile-relations
|
(defn create-profile-rels!
|
||||||
[conn profile]
|
[conn {:keys [id] :as profile}]
|
||||||
(let [team (teams/create-team conn {:profile-id (:id profile)
|
(let [team (teams/create-team conn {:profile-id id
|
||||||
:name "Default"
|
:name "Default"
|
||||||
:is-default true})]
|
:is-default true})]
|
||||||
(-> profile
|
(-> (db/update! conn :profile
|
||||||
(profile/strip-private-attrs)
|
{:default-team-id (:id team)
|
||||||
(assoc :default-team-id (:id team))
|
:default-project-id (:default-project-id team)}
|
||||||
(assoc :default-project-id (:default-project-id team)))))
|
{:id id})
|
||||||
|
(profile/decode-row))))
|
||||||
|
|
||||||
|
|
||||||
(defn send-email-verification!
|
(defn send-email-verification!
|
||||||
[conn props profile]
|
[conn props profile]
|
||||||
@ -348,22 +335,18 @@
|
|||||||
:extra-data ptoken})))
|
:extra-data ptoken})))
|
||||||
|
|
||||||
(defn register-profile
|
(defn register-profile
|
||||||
[{:keys [::db/conn session] :as cfg} {:keys [token] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [token] :as params}]
|
||||||
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
|
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
|
||||||
params (merge params claims)
|
params (merge params claims)
|
||||||
|
|
||||||
is-active (or (:is-active params)
|
is-active (or (:is-active params)
|
||||||
(not (contains? cf/flags :email-verification))
|
(not (contains? cf/flags :email-verification)))
|
||||||
|
|
||||||
;; DEPRECATED: v1.15
|
|
||||||
(contains? cf/flags :insecure-register))
|
|
||||||
|
|
||||||
profile (if-let [profile-id (:profile-id claims)]
|
profile (if-let [profile-id (:profile-id claims)]
|
||||||
(profile/retrieve-profile conn profile-id)
|
(profile/get-profile conn profile-id)
|
||||||
(->> (assoc params :is-active is-active)
|
(->> (create-profile! conn (assoc params :is-active is-active))
|
||||||
(create-profile conn)
|
(create-profile-rels! conn)))
|
||||||
(create-profile-relations conn)
|
|
||||||
(profile/decode-profile-row)))
|
|
||||||
invitation (when-let [token (:invitation-token params)]
|
invitation (when-let [token (:invitation-token params)]
|
||||||
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
|
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
|
||||||
|
|
||||||
@ -389,7 +372,7 @@
|
|||||||
token (tokens/generate (::main/props cfg) claims)
|
token (tokens/generate (::main/props cfg) claims)
|
||||||
resp {:invitation-token token}]
|
resp {:invitation-token token}]
|
||||||
(-> resp
|
(-> resp
|
||||||
(rph/with-transform (session/create-fn session (:id profile)))
|
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||||
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)})))
|
::audit/profile-id (:id profile)})))
|
||||||
|
|
||||||
@ -398,7 +381,7 @@
|
|||||||
;; we need to mark this session as logged.
|
;; we need to mark this session as logged.
|
||||||
(not= "penpot" (:auth-backend profile))
|
(not= "penpot" (:auth-backend profile))
|
||||||
(-> (profile/strip-private-attrs profile)
|
(-> (profile/strip-private-attrs profile)
|
||||||
(rph/with-transform (session/create-fn session (:id profile)))
|
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||||
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)}))
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
@ -406,7 +389,7 @@
|
|||||||
;; to sign in the user directly, without email verification.
|
;; to sign in the user directly, without email verification.
|
||||||
(true? is-active)
|
(true? is-active)
|
||||||
(-> (profile/strip-private-attrs profile)
|
(-> (profile/strip-private-attrs profile)
|
||||||
(rph/with-transform (session/create-fn session (:id profile)))
|
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||||
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)}))
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
@ -448,7 +431,7 @@
|
|||||||
:exp (dt/in-future {:days 30})})]
|
:exp (dt/in-future {:days 30})})]
|
||||||
(eml/send! {::eml/conn conn
|
(eml/send! {::eml/conn conn
|
||||||
::eml/factory eml/password-recovery
|
::eml/factory eml/password-recovery
|
||||||
:public-uri (:public-uri cfg)
|
:public-uri (cf/get :public-uri)
|
||||||
:to (:email profile)
|
:to (:email profile)
|
||||||
:token (:token profile)
|
:token (:token profile)
|
||||||
:name (:fullname profile)
|
:name (:fullname profile)
|
||||||
@ -456,7 +439,7 @@
|
|||||||
nil))]
|
nil))]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
|
(when-let [profile (profile/get-profile-by-email conn email)]
|
||||||
(when-not (eml/allow-send-emails? conn profile)
|
(when-not (eml/allow-send-emails? conn profile)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :profile-is-muted
|
:code :profile-is-muted
|
||||||
|
|||||||
@ -21,9 +21,9 @@
|
|||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.queries.projects :as projects]
|
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
[app.tasks.file-gc]
|
[app.tasks.file-gc]
|
||||||
@ -109,20 +109,20 @@
|
|||||||
|
|
||||||
(defn write-byte!
|
(defn write-byte!
|
||||||
[^DataOutputStream output data]
|
[^DataOutputStream output data]
|
||||||
(l/trace :fn "write-byte!" :data data :position @*position* ::l/async false)
|
(l/trace :fn "write-byte!" :data data :position @*position* ::l/sync? true)
|
||||||
(.writeByte output (byte data))
|
(.writeByte output (byte data))
|
||||||
(swap! *position* inc))
|
(swap! *position* inc))
|
||||||
|
|
||||||
(defn read-byte!
|
(defn read-byte!
|
||||||
[^DataInputStream input]
|
[^DataInputStream input]
|
||||||
(let [v (.readByte input)]
|
(let [v (.readByte input)]
|
||||||
(l/trace :fn "read-byte!" :val v :position @*position* ::l/async false)
|
(l/trace :fn "read-byte!" :val v :position @*position* ::l/sync? true)
|
||||||
(swap! *position* inc)
|
(swap! *position* inc)
|
||||||
v))
|
v))
|
||||||
|
|
||||||
(defn write-long!
|
(defn write-long!
|
||||||
[^DataOutputStream output data]
|
[^DataOutputStream output data]
|
||||||
(l/trace :fn "write-long!" :data data :position @*position* ::l/async false)
|
(l/trace :fn "write-long!" :data data :position @*position* ::l/sync? true)
|
||||||
(.writeLong output (long data))
|
(.writeLong output (long data))
|
||||||
(swap! *position* + 8))
|
(swap! *position* + 8))
|
||||||
|
|
||||||
@ -130,14 +130,14 @@
|
|||||||
(defn read-long!
|
(defn read-long!
|
||||||
[^DataInputStream input]
|
[^DataInputStream input]
|
||||||
(let [v (.readLong input)]
|
(let [v (.readLong input)]
|
||||||
(l/trace :fn "read-long!" :val v :position @*position* ::l/async false)
|
(l/trace :fn "read-long!" :val v :position @*position* ::l/sync? true)
|
||||||
(swap! *position* + 8)
|
(swap! *position* + 8)
|
||||||
v))
|
v))
|
||||||
|
|
||||||
(defn write-bytes!
|
(defn write-bytes!
|
||||||
[^DataOutputStream output ^bytes data]
|
[^DataOutputStream output ^bytes data]
|
||||||
(let [size (alength data)]
|
(let [size (alength data)]
|
||||||
(l/trace :fn "write-bytes!" :size size :position @*position* ::l/async false)
|
(l/trace :fn "write-bytes!" :size size :position @*position* ::l/sync? true)
|
||||||
(.write output data 0 size)
|
(.write output data 0 size)
|
||||||
(swap! *position* + size)))
|
(swap! *position* + size)))
|
||||||
|
|
||||||
@ -145,7 +145,7 @@
|
|||||||
[^InputStream input ^bytes buff]
|
[^InputStream input ^bytes buff]
|
||||||
(let [size (alength buff)
|
(let [size (alength buff)
|
||||||
readed (.readNBytes input buff 0 size)]
|
readed (.readNBytes input buff 0 size)]
|
||||||
(l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/async false)
|
(l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/sync? true)
|
||||||
(swap! *position* + readed)
|
(swap! *position* + readed)
|
||||||
readed))
|
readed))
|
||||||
|
|
||||||
@ -153,7 +153,7 @@
|
|||||||
|
|
||||||
(defn write-uuid!
|
(defn write-uuid!
|
||||||
[^DataOutputStream output id]
|
[^DataOutputStream output id]
|
||||||
(l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/async false)
|
(l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/sync? true)
|
||||||
|
|
||||||
(doto output
|
(doto output
|
||||||
(write-byte! (get-mark :uuid))
|
(write-byte! (get-mark :uuid))
|
||||||
@ -162,7 +162,7 @@
|
|||||||
|
|
||||||
(defn read-uuid!
|
(defn read-uuid!
|
||||||
[^DataInputStream input]
|
[^DataInputStream input]
|
||||||
(l/trace :fn "read-uuid!" :position @*position* ::l/async false)
|
(l/trace :fn "read-uuid!" :position @*position* ::l/sync? true)
|
||||||
(let [m (read-byte! input)]
|
(let [m (read-byte! input)]
|
||||||
(assert-mark m :uuid)
|
(assert-mark m :uuid)
|
||||||
(let [a (read-long! input)
|
(let [a (read-long! input)
|
||||||
@ -171,7 +171,7 @@
|
|||||||
|
|
||||||
(defn write-obj!
|
(defn write-obj!
|
||||||
[^DataOutputStream output data]
|
[^DataOutputStream output data]
|
||||||
(l/trace :fn "write-obj!" :position @*position* ::l/async false)
|
(l/trace :fn "write-obj!" :position @*position* ::l/sync? true)
|
||||||
(let [^bytes data (fres/encode data)]
|
(let [^bytes data (fres/encode data)]
|
||||||
(doto output
|
(doto output
|
||||||
(write-byte! (get-mark :obj))
|
(write-byte! (get-mark :obj))
|
||||||
@ -180,7 +180,7 @@
|
|||||||
|
|
||||||
(defn read-obj!
|
(defn read-obj!
|
||||||
[^DataInputStream input]
|
[^DataInputStream input]
|
||||||
(l/trace :fn "read-obj!" :position @*position* ::l/async false)
|
(l/trace :fn "read-obj!" :position @*position* ::l/sync? true)
|
||||||
(let [m (read-byte! input)]
|
(let [m (read-byte! input)]
|
||||||
(assert-mark m :obj)
|
(assert-mark m :obj)
|
||||||
(let [size (read-long! input)]
|
(let [size (read-long! input)]
|
||||||
@ -191,14 +191,14 @@
|
|||||||
|
|
||||||
(defn write-label!
|
(defn write-label!
|
||||||
[^DataOutputStream output label]
|
[^DataOutputStream output label]
|
||||||
(l/trace :fn "write-label!" :label label :position @*position* ::l/async false)
|
(l/trace :fn "write-label!" :label label :position @*position* ::l/sync? true)
|
||||||
(doto output
|
(doto output
|
||||||
(write-byte! (get-mark :label))
|
(write-byte! (get-mark :label))
|
||||||
(write-obj! label)))
|
(write-obj! label)))
|
||||||
|
|
||||||
(defn read-label!
|
(defn read-label!
|
||||||
[^DataInputStream input]
|
[^DataInputStream input]
|
||||||
(l/trace :fn "read-label!" :position @*position* ::l/async false)
|
(l/trace :fn "read-label!" :position @*position* ::l/sync? true)
|
||||||
(let [m (read-byte! input)]
|
(let [m (read-byte! input)]
|
||||||
(assert-mark m :label)
|
(assert-mark m :label)
|
||||||
(read-obj! input)))
|
(read-obj! input)))
|
||||||
@ -208,7 +208,7 @@
|
|||||||
(l/trace :fn "write-header!"
|
(l/trace :fn "write-header!"
|
||||||
:version version
|
:version version
|
||||||
:position @*position*
|
:position @*position*
|
||||||
::l/async false)
|
::l/sync? true)
|
||||||
(let [vers (-> version name (subs 1) parse-long)
|
(let [vers (-> version name (subs 1) parse-long)
|
||||||
output (io/data-output-stream output)]
|
output (io/data-output-stream output)]
|
||||||
(doto output
|
(doto output
|
||||||
@ -218,7 +218,7 @@
|
|||||||
|
|
||||||
(defn read-header!
|
(defn read-header!
|
||||||
[^InputStream input]
|
[^InputStream input]
|
||||||
(l/trace :fn "read-header!" :position @*position* ::l/async false)
|
(l/trace :fn "read-header!" :position @*position* ::l/sync? true)
|
||||||
(let [input (io/data-input-stream input)
|
(let [input (io/data-input-stream input)
|
||||||
mark (read-byte! input)
|
mark (read-byte! input)
|
||||||
mnum (read-long! input)
|
mnum (read-long! input)
|
||||||
@ -235,13 +235,13 @@
|
|||||||
(defn copy-stream!
|
(defn copy-stream!
|
||||||
[^OutputStream output ^InputStream input ^long size]
|
[^OutputStream output ^InputStream input ^long size]
|
||||||
(let [written (io/copy! input output :size size)]
|
(let [written (io/copy! input output :size size)]
|
||||||
(l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/async false)
|
(l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/sync? true)
|
||||||
(swap! *position* + written)
|
(swap! *position* + written)
|
||||||
written))
|
written))
|
||||||
|
|
||||||
(defn write-stream!
|
(defn write-stream!
|
||||||
[^DataOutputStream output stream size]
|
[^DataOutputStream output stream size]
|
||||||
(l/trace :fn "write-stream!" :position @*position* ::l/async false :size size)
|
(l/trace :fn "write-stream!" :position @*position* ::l/sync? true :size size)
|
||||||
(doto output
|
(doto output
|
||||||
(write-byte! (get-mark :stream))
|
(write-byte! (get-mark :stream))
|
||||||
(write-long! size))
|
(write-long! size))
|
||||||
@ -250,7 +250,7 @@
|
|||||||
|
|
||||||
(defn read-stream!
|
(defn read-stream!
|
||||||
[^DataInputStream input]
|
[^DataInputStream input]
|
||||||
(l/trace :fn "read-stream!" :position @*position* ::l/async false)
|
(l/trace :fn "read-stream!" :position @*position* ::l/sync? true)
|
||||||
(let [m (read-byte! input)
|
(let [m (read-byte! input)
|
||||||
s (read-long! input)
|
s (read-long! input)
|
||||||
p (tmp/tempfile :prefix "penpot.binfile.")]
|
p (tmp/tempfile :prefix "penpot.binfile.")]
|
||||||
@ -264,7 +264,7 @@
|
|||||||
(if (> s temp-file-threshold)
|
(if (> s temp-file-threshold)
|
||||||
(with-open [^OutputStream output (io/output-stream p)]
|
(with-open [^OutputStream output (io/output-stream p)]
|
||||||
(let [readed (io/copy! input output :offset 0 :size s)]
|
(let [readed (io/copy! input output :offset 0 :size s)]
|
||||||
(l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/async false)
|
(l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/sync? true)
|
||||||
(swap! *position* + readed)
|
(swap! *position* + readed)
|
||||||
[s p]))
|
[s p]))
|
||||||
[s (io/read-as-bytes input :size s)])))
|
[s (io/read-as-bytes input :size s)])))
|
||||||
@ -438,9 +438,8 @@
|
|||||||
(s/def ::embed-assets? (s/nilable ::us/boolean))
|
(s/def ::embed-assets? (s/nilable ::us/boolean))
|
||||||
|
|
||||||
(s/def ::write-export-options
|
(s/def ::write-export-options
|
||||||
(s/keys :req-un [::db/pool ::sto/storage]
|
(s/keys :req [::db/pool ::sto/storage ::output ::file-ids]
|
||||||
:req [::output ::file-ids]
|
:opt [::include-libraries? ::embed-assets?]))
|
||||||
:opt [::include-libraries? ::embed-assets?]))
|
|
||||||
|
|
||||||
(defn write-export!
|
(defn write-export!
|
||||||
"Do the exportation of a specified file in custom penpot binary
|
"Do the exportation of a specified file in custom penpot binary
|
||||||
@ -453,6 +452,7 @@
|
|||||||
`::embed-assets?`: instead of including the libraries, embed in the
|
`::embed-assets?`: instead of including the libraries, embed in the
|
||||||
same file library all assets used from external libraries."
|
same file library all assets used from external libraries."
|
||||||
[{:keys [::include-libraries? ::embed-assets?] :as options}]
|
[{:keys [::include-libraries? ::embed-assets?] :as options}]
|
||||||
|
|
||||||
(us/assert! ::write-export-options options)
|
(us/assert! ::write-export-options options)
|
||||||
(us/verify!
|
(us/verify!
|
||||||
:expr (not (and include-libraries? embed-assets?))
|
:expr (not (and include-libraries? embed-assets?))
|
||||||
@ -466,7 +466,7 @@
|
|||||||
(with-open [output (io/data-output-stream output)]
|
(with-open [output (io/data-output-stream output)]
|
||||||
(binding [*state* (volatile! {})]
|
(binding [*state* (volatile! {})]
|
||||||
(run! (fn [section]
|
(run! (fn [section]
|
||||||
(l/debug :hint "write section" :section section ::l/async false)
|
(l/debug :hint "write section" :section section ::l/sync? true)
|
||||||
(write-label! output section)
|
(write-label! output section)
|
||||||
(let [options (-> options
|
(let [options (-> options
|
||||||
(assoc ::output output)
|
(assoc ::output output)
|
||||||
@ -477,7 +477,7 @@
|
|||||||
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
|
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
|
||||||
|
|
||||||
(defmethod write-section :v1/metadata
|
(defmethod write-section :v1/metadata
|
||||||
[{:keys [pool ::output ::file-ids ::include-libraries?]}]
|
[{:keys [::db/pool ::output ::file-ids ::include-libraries?]}]
|
||||||
(let [libs (when include-libraries?
|
(let [libs (when include-libraries?
|
||||||
(retrieve-libraries pool file-ids))
|
(retrieve-libraries pool file-ids))
|
||||||
files (into file-ids libs)]
|
files (into file-ids libs)]
|
||||||
@ -485,7 +485,7 @@
|
|||||||
(vswap! *state* assoc :files files)))
|
(vswap! *state* assoc :files files)))
|
||||||
|
|
||||||
(defmethod write-section :v1/files
|
(defmethod write-section :v1/files
|
||||||
[{:keys [pool ::output ::embed-assets?]}]
|
[{:keys [::db/pool ::output ::embed-assets?]}]
|
||||||
|
|
||||||
;; Initialize SIDS with empty vector
|
;; Initialize SIDS with empty vector
|
||||||
(vswap! *state* assoc :sids [])
|
(vswap! *state* assoc :sids [])
|
||||||
@ -500,7 +500,7 @@
|
|||||||
(l/debug :hint "write penpot file"
|
(l/debug :hint "write penpot file"
|
||||||
:id file-id
|
:id file-id
|
||||||
:media (count media)
|
:media (count media)
|
||||||
::l/async false)
|
::l/sync? true)
|
||||||
|
|
||||||
(doto output
|
(doto output
|
||||||
(write-obj! file)
|
(write-obj! file)
|
||||||
@ -509,26 +509,26 @@
|
|||||||
(vswap! *state* update :sids into storage-object-id-xf media))))
|
(vswap! *state* update :sids into storage-object-id-xf media))))
|
||||||
|
|
||||||
(defmethod write-section :v1/rels
|
(defmethod write-section :v1/rels
|
||||||
[{:keys [pool ::output ::include-libraries?]}]
|
[{:keys [::db/pool ::output ::include-libraries?]}]
|
||||||
(let [rels (when include-libraries?
|
(let [rels (when include-libraries?
|
||||||
(retrieve-library-relations pool (-> *state* deref :files)))]
|
(retrieve-library-relations pool (-> *state* deref :files)))]
|
||||||
(l/debug :hint "found rels" :total (count rels) ::l/async false)
|
(l/debug :hint "found rels" :total (count rels) ::l/sync? true)
|
||||||
(write-obj! output rels)))
|
(write-obj! output rels)))
|
||||||
|
|
||||||
(defmethod write-section :v1/sobjects
|
(defmethod write-section :v1/sobjects
|
||||||
[{:keys [storage ::output]}]
|
[{:keys [::sto/storage ::output]}]
|
||||||
(let [sids (-> *state* deref :sids)
|
(let [sids (-> *state* deref :sids)
|
||||||
storage (media/configure-assets-storage storage)]
|
storage (media/configure-assets-storage storage)]
|
||||||
(l/debug :hint "found sobjects"
|
(l/debug :hint "found sobjects"
|
||||||
:items (count sids)
|
:items (count sids)
|
||||||
::l/async false)
|
::l/sync? true)
|
||||||
|
|
||||||
;; Write all collected storage objects
|
;; Write all collected storage objects
|
||||||
(write-obj! output sids)
|
(write-obj! output sids)
|
||||||
|
|
||||||
(doseq [id sids]
|
(doseq [id sids]
|
||||||
(let [{:keys [size] :as obj} @(sto/get-object storage id)]
|
(let [{:keys [size] :as obj} @(sto/get-object storage id)]
|
||||||
(l/debug :hint "write sobject" :id id ::l/async false)
|
(l/debug :hint "write sobject" :id id ::l/sync? true)
|
||||||
(doto output
|
(doto output
|
||||||
(write-uuid! id)
|
(write-uuid! id)
|
||||||
(write-obj! (meta obj)))
|
(write-obj! (meta obj)))
|
||||||
@ -557,9 +557,8 @@
|
|||||||
(s/def ::ignore-index-errors? (s/nilable ::us/boolean))
|
(s/def ::ignore-index-errors? (s/nilable ::us/boolean))
|
||||||
|
|
||||||
(s/def ::read-import-options
|
(s/def ::read-import-options
|
||||||
(s/keys :req-un [::db/pool ::sto/storage]
|
(s/keys :req [::db/pool ::sto/storage ::project-id ::input]
|
||||||
:req [::project-id ::input]
|
:opt [::overwrite? ::migrate? ::ignore-index-errors?]))
|
||||||
:opt [::overwrite? ::migrate? ::ignore-index-errors?]))
|
|
||||||
|
|
||||||
(defn read-import!
|
(defn read-import!
|
||||||
"Do the importation of the specified resource in penpot custom binary
|
"Do the importation of the specified resource in penpot custom binary
|
||||||
@ -582,14 +581,14 @@
|
|||||||
(read-import (assoc options ::version version ::timestamp timestamp))))
|
(read-import (assoc options ::version version ::timestamp timestamp))))
|
||||||
|
|
||||||
(defmethod read-import :v1
|
(defmethod read-import :v1
|
||||||
[{:keys [pool ::input] :as options}]
|
[{:keys [::db/pool ::input] :as options}]
|
||||||
(with-open [input (zstd-input-stream input)]
|
(with-open [input (zstd-input-stream input)]
|
||||||
(with-open [input (io/data-input-stream input)]
|
(with-open [input (io/data-input-stream input)]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED;"])
|
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED;"])
|
||||||
(binding [*state* (volatile! {:media [] :index {}})]
|
(binding [*state* (volatile! {:media [] :index {}})]
|
||||||
(run! (fn [section]
|
(run! (fn [section]
|
||||||
(l/debug :hint "reading section" :section section ::l/async false)
|
(l/debug :hint "reading section" :section section ::l/sync? true)
|
||||||
(assert-read-label! input section)
|
(assert-read-label! input section)
|
||||||
(let [options (-> options
|
(let [options (-> options
|
||||||
(assoc ::section section)
|
(assoc ::section section)
|
||||||
@ -607,7 +606,7 @@
|
|||||||
(defmethod read-section :v1/metadata
|
(defmethod read-section :v1/metadata
|
||||||
[{:keys [::input]}]
|
[{:keys [::input]}]
|
||||||
(let [{:keys [version files]} (read-obj! input)]
|
(let [{:keys [version files]} (read-obj! input)]
|
||||||
(l/debug :hint "metadata readed" :version (:full version) :files files ::l/async false)
|
(l/debug :hint "metadata readed" :version (:full version) :files files ::l/sync? true)
|
||||||
(vswap! *state* update :index update-index files)
|
(vswap! *state* update :index update-index files)
|
||||||
(vswap! *state* assoc :version version :files files)))
|
(vswap! *state* assoc :version version :files files)))
|
||||||
|
|
||||||
@ -635,14 +634,14 @@
|
|||||||
:hint "the penpot file seems corrupt, found unexpected uuid (file-id)"))
|
:hint "the penpot file seems corrupt, found unexpected uuid (file-id)"))
|
||||||
|
|
||||||
;; Update index using with media
|
;; Update index using with media
|
||||||
(l/debug :hint "update index with media" ::l/async false)
|
(l/debug :hint "update index with media" ::l/sync? true)
|
||||||
(vswap! *state* update :index update-index (map :id media'))
|
(vswap! *state* update :index update-index (map :id media'))
|
||||||
|
|
||||||
;; Store file media for later insertion
|
;; Store file media for later insertion
|
||||||
(l/debug :hint "update media references" ::l/async false)
|
(l/debug :hint "update media references" ::l/sync? true)
|
||||||
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
|
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
|
||||||
|
|
||||||
(l/debug :hint "processing file" :file-id file-id ::features features ::l/async false)
|
(l/debug :hint "processing file" :file-id file-id ::features features ::l/sync? true)
|
||||||
|
|
||||||
(binding [ffeat/*current* features
|
(binding [ffeat/*current* features
|
||||||
ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity)
|
ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity)
|
||||||
@ -668,7 +667,7 @@
|
|||||||
:created-at timestamp
|
:created-at timestamp
|
||||||
:modified-at timestamp}]
|
:modified-at timestamp}]
|
||||||
|
|
||||||
(l/debug :hint "create file" :id file-id' ::l/async false)
|
(l/debug :hint "create file" :id file-id' ::l/sync? true)
|
||||||
|
|
||||||
(if overwrite?
|
(if overwrite?
|
||||||
(create-or-update-file conn params)
|
(create-or-update-file conn params)
|
||||||
@ -691,11 +690,11 @@
|
|||||||
(l/debug :hint "create file library link"
|
(l/debug :hint "create file library link"
|
||||||
:file-id (:file-id rel)
|
:file-id (:file-id rel)
|
||||||
:lib-id (:library-file-id rel)
|
:lib-id (:library-file-id rel)
|
||||||
::l/async false)
|
::l/sync? true)
|
||||||
(db/insert! conn :file-library-rel rel)))))
|
(db/insert! conn :file-library-rel rel)))))
|
||||||
|
|
||||||
(defmethod read-section :v1/sobjects
|
(defmethod read-section :v1/sobjects
|
||||||
[{:keys [storage conn ::input ::overwrite?]}]
|
[{:keys [::sto/storage conn ::input ::overwrite?]}]
|
||||||
(let [storage (media/configure-assets-storage storage)
|
(let [storage (media/configure-assets-storage storage)
|
||||||
ids (read-obj! input)]
|
ids (read-obj! input)]
|
||||||
|
|
||||||
@ -708,7 +707,7 @@
|
|||||||
:code :inconsistent-penpot-file
|
:code :inconsistent-penpot-file
|
||||||
:hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)"))
|
:hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)"))
|
||||||
|
|
||||||
(l/debug :hint "readed storage object" :id id ::l/async false)
|
(l/debug :hint "readed storage object" :id id ::l/sync? true)
|
||||||
|
|
||||||
(let [[size resource] (read-stream! input)
|
(let [[size resource] (read-stream! input)
|
||||||
hash (sto/calculate-hash resource)
|
hash (sto/calculate-hash resource)
|
||||||
@ -722,18 +721,18 @@
|
|||||||
|
|
||||||
sobject @(sto/put-object! storage params)]
|
sobject @(sto/put-object! storage params)]
|
||||||
|
|
||||||
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/async false)
|
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
|
||||||
(vswap! *state* update :index assoc id (:id sobject)))))
|
(vswap! *state* update :index assoc id (:id sobject)))))
|
||||||
|
|
||||||
(doseq [item (:media @*state*)]
|
(doseq [item (:media @*state*)]
|
||||||
(l/debug :hint "inserting file media object"
|
(l/debug :hint "inserting file media object"
|
||||||
:id (:id item)
|
:id (:id item)
|
||||||
:file-id (:file-id item)
|
:file-id (:file-id item)
|
||||||
::l/async false)
|
::l/sync? true)
|
||||||
|
|
||||||
(let [file-id (lookup-index (:file-id item))]
|
(let [file-id (lookup-index (:file-id item))]
|
||||||
(if (= file-id (:file-id item))
|
(if (= file-id (:file-id item))
|
||||||
(l/warn :hint "ignoring file media object" :file-id (:file-id item) ::l/async false)
|
(l/warn :hint "ignoring file media object" :file-id (:file-id item) ::l/sync? true)
|
||||||
(db/insert! conn :file-media-object
|
(db/insert! conn :file-media-object
|
||||||
(-> item
|
(-> item
|
||||||
(assoc :file-id file-id)
|
(assoc :file-id file-id)
|
||||||
@ -744,7 +743,7 @@
|
|||||||
(defn- lookup-index
|
(defn- lookup-index
|
||||||
[id]
|
[id]
|
||||||
(let [val (get-in @*state* [:index id])]
|
(let [val (get-in @*state* [:index id])]
|
||||||
(l/trace :fn "lookup-index" :id id :val val ::l/async false)
|
(l/trace :fn "lookup-index" :id id :val val ::l/sync? true)
|
||||||
(when (and (not (::ignore-index-errors? *options*)) (not val))
|
(when (and (not (::ignore-index-errors? *options*)) (not val))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :incomplete-index
|
:code :incomplete-index
|
||||||
@ -757,7 +756,7 @@
|
|||||||
index index]
|
index index]
|
||||||
(if-let [id (first items)]
|
(if-let [id (first items)]
|
||||||
(let [new-id (if (::overwrite? *options*) id (uuid/next))]
|
(let [new-id (if (::overwrite? *options*) id (uuid/next))]
|
||||||
(l/trace :fn "update-index" :id id :new-id new-id ::l/async false)
|
(l/trace :fn "update-index" :id id :new-id new-id ::l/sync? true)
|
||||||
(recur (rest items)
|
(recur (rest items)
|
||||||
(assoc index id new-id)))
|
(assoc index id new-id)))
|
||||||
index)))
|
index)))
|
||||||
@ -805,7 +804,7 @@
|
|||||||
(try
|
(try
|
||||||
(process-map-form form)
|
(process-map-form form)
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "failed form" :form (pr-str form) ::l/async false)
|
(l/warn :hint "failed form" :form (pr-str form) ::l/sync? true)
|
||||||
(throw cause)))
|
(throw cause)))
|
||||||
form))
|
form))
|
||||||
data)))
|
data)))
|
||||||
@ -893,13 +892,14 @@
|
|||||||
(s/def ::embed-assets? ::us/boolean)
|
(s/def ::embed-assets? ::us/boolean)
|
||||||
|
|
||||||
(s/def ::export-binfile
|
(s/def ::export-binfile
|
||||||
(s/keys :req [::rpc/profile-id] :req-un [::file-id ::include-libraries? ::embed-assets?]))
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::include-libraries? ::embed-assets?]))
|
||||||
|
|
||||||
(sv/defmethod ::export-binfile
|
(sv/defmethod ::export-binfile
|
||||||
"Export a penpot file in a binary format."
|
"Export a penpot file in a binary format."
|
||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}]
|
||||||
(files/check-read-permissions! pool profile-id file-id)
|
(files/check-read-permissions! pool profile-id file-id)
|
||||||
(let [body (reify yrs/StreamableResponseBody
|
(let [body (reify yrs/StreamableResponseBody
|
||||||
(-write-body-to-stream [_ _ output-stream]
|
(-write-body-to-stream [_ _ output-stream]
|
||||||
@ -914,13 +914,14 @@
|
|||||||
|
|
||||||
(s/def ::file ::media/upload)
|
(s/def ::file ::media/upload)
|
||||||
(s/def ::import-binfile
|
(s/def ::import-binfile
|
||||||
(s/keys :req [::rpc/profile-id] :req-un [::project-id ::file]))
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::project-id ::file]))
|
||||||
|
|
||||||
(sv/defmethod ::import-binfile
|
(sv/defmethod ::import-binfile
|
||||||
"Import a penpot file in a binary format."
|
"Import a penpot file in a binary format."
|
||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(projects/check-read-permissions! conn profile-id project-id)
|
(projects/check-read-permissions! conn profile-id project-id)
|
||||||
(let [ids (import! (assoc cfg
|
(let [ids (import! (assoc cfg
|
||||||
|
|||||||
@ -54,8 +54,8 @@
|
|||||||
:hint "file not found"))))
|
:hint "file not found"))))
|
||||||
|
|
||||||
(defn- get-comment-thread
|
(defn- get-comment-thread
|
||||||
[conn thread-id & {:keys [for-update?]}]
|
[conn thread-id & {:as opts}]
|
||||||
(-> (db/get-by-id conn :comment-thread thread-id {:for-update for-update?})
|
(-> (db/get-by-id conn :comment-thread thread-id opts)
|
||||||
(decode-row)))
|
(decode-row)))
|
||||||
|
|
||||||
(defn- get-comment
|
(defn- get-comment
|
||||||
@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-comment-threads
|
(sv/defmethod ::get-comment-threads
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(get-comment-threads conn profile-id file-id)))
|
(get-comment-threads conn profile-id file-id)))
|
||||||
@ -143,7 +143,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-unread-comment-threads
|
(sv/defmethod ::get-unread-comment-threads
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(get-unread-comment-threads conn profile-id team-id)))
|
(get-unread-comment-threads conn profile-id team-id)))
|
||||||
@ -190,7 +190,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-comment-thread
|
(sv/defmethod ::get-comment-thread
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(let [sql (str "with threads as (" sql:comment-threads ")"
|
(let [sql (str "with threads as (" sql:comment-threads ")"
|
||||||
@ -210,7 +210,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-comments
|
(sv/defmethod ::get-comments
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
@ -262,7 +262,7 @@
|
|||||||
participants on comment threads of the file."
|
participants on comment threads of the file."
|
||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(get-file-comments-users conn file-id profile-id)))
|
(get-file-comments-users conn file-id profile-id)))
|
||||||
@ -372,9 +372,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-comment-thread-status
|
(sv/defmethod ::update-comment-thread-status
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(upsert-comment-thread-status! conn profile-id id))))
|
(upsert-comment-thread-status! conn profile-id id))))
|
||||||
|
|
||||||
@ -389,9 +389,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-comment-thread
|
(sv/defmethod ::update-comment-thread
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(db/update! conn :comment-thread
|
(db/update! conn :comment-thread
|
||||||
{:is-resolved is-resolved}
|
{:is-resolved is-resolved}
|
||||||
@ -412,9 +412,9 @@
|
|||||||
(sv/defmethod ::create-comment
|
(sv/defmethod ::create-comment
|
||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id :for-update? true)
|
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)
|
||||||
{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
|
{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
|
||||||
|
|
||||||
(files/check-comment-permissions! conn profile-id (:id file) share-id)
|
(files/check-comment-permissions! conn profile-id (:id file) share-id)
|
||||||
@ -465,10 +465,10 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-comment
|
(sv/defmethod ::update-comment
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [thread-id] :as comment} (get-comment conn id :for-update? true)
|
(let [{:keys [thread-id] :as comment} (get-comment conn id ::db/for-update? true)
|
||||||
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id :for-update? true)]
|
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)]
|
||||||
|
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
|
|
||||||
@ -498,9 +498,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-comment-thread
|
(sv/defmethod ::delete-comment-thread
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(when-not (= owner-id profile-id)
|
(when-not (= owner-id profile-id)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -518,9 +518,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-comment
|
(sv/defmethod ::delete-comment
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id :for-update? true)
|
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::db/for-update? true)
|
||||||
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(when-not (= owner-id profile-id)
|
(when-not (= owner-id profile-id)
|
||||||
@ -538,9 +538,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-comment-thread-position
|
(sv/defmethod ::update-comment-thread-position
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(db/update! conn :comment-thread
|
(db/update! conn :comment-thread
|
||||||
{:modified-at (::rpc/request-at params)
|
{:modified-at (::rpc/request-at params)
|
||||||
@ -558,9 +558,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-comment-thread-frame
|
(sv/defmethod ::update-comment-thread-frame
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(db/update! conn :comment-thread
|
(db/update! conn :comment-thread
|
||||||
{:modified-at (::rpc/request-at params)
|
{:modified-at (::rpc/request-at params)
|
||||||
|
|||||||
@ -8,12 +8,11 @@
|
|||||||
"A demo specific mutations."
|
"A demo specific mutations."
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.auth :as cmd.auth]
|
[app.rpc.commands.auth :as auth]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -30,32 +29,31 @@
|
|||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::doc/added "1.15"
|
::doc/added "1.15"
|
||||||
::doc/changes ["1.15" "This method is migrated from mutations to commands."]}
|
::doc/changes ["1.15" "This method is migrated from mutations to commands."]}
|
||||||
[{:keys [pool] :as cfg} _]
|
[{:keys [::db/pool] :as cfg} _]
|
||||||
(let [id (uuid/next)
|
|
||||||
sem (System/currentTimeMillis)
|
(when-not (contains? cf/flags :demo-users)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :demo-users-not-allowed
|
||||||
|
:hint "Demo users are disabled by config."))
|
||||||
|
|
||||||
|
(let [sem (System/currentTimeMillis)
|
||||||
email (str "demo-" sem ".demo@example.com")
|
email (str "demo-" sem ".demo@example.com")
|
||||||
fullname (str "Demo User " sem)
|
fullname (str "Demo User " sem)
|
||||||
|
|
||||||
password (-> (bn/random-bytes 16)
|
password (-> (bn/random-bytes 16)
|
||||||
(bc/bytes->b64u)
|
(bc/bytes->b64u)
|
||||||
(bc/bytes->str))
|
(bc/bytes->str))
|
||||||
params {:id id
|
|
||||||
:email email
|
params {:email email
|
||||||
:fullname fullname
|
:fullname fullname
|
||||||
:is-active true
|
:is-active true
|
||||||
:deleted-at (dt/in-future cf/deletion-delay)
|
:deleted-at (dt/in-future cf/deletion-delay)
|
||||||
:password password
|
:password password
|
||||||
:props {}
|
:props {}}]
|
||||||
}]
|
|
||||||
|
|
||||||
(when-not (contains? cf/flags :demo-users)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :demo-users-not-allowed
|
|
||||||
:hint "Demo users are disabled by config."))
|
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(->> (cmd.auth/create-profile conn params)
|
(let [profile (->> (auth/create-profile! conn params)
|
||||||
(cmd.auth/create-profile-relations conn))
|
(auth/create-profile-rels! conn))]
|
||||||
|
(with-meta {:email email
|
||||||
(with-meta {:email email
|
:password password}
|
||||||
:password password}
|
{::audit/profile-id (:id profile)})))))
|
||||||
{::audit/profile-id id}))))
|
|
||||||
|
|||||||
56
backend/src/app/rpc/commands/feedback.clj
Normal file
56
backend/src/app/rpc/commands/feedback.clj
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.rpc.commands.feedback
|
||||||
|
"A general purpose feedback module."
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.email :as eml]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
(declare ^:private send-feedback!)
|
||||||
|
|
||||||
|
(s/def ::content ::us/string)
|
||||||
|
(s/def ::from ::us/email)
|
||||||
|
(s/def ::subject ::us/string)
|
||||||
|
|
||||||
|
(s/def ::send-user-feedback
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::subject
|
||||||
|
::content]))
|
||||||
|
|
||||||
|
(sv/defmethod ::send-user-feedback
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
|
||||||
|
(when-not (contains? cf/flags :user-feedback)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :feedback-disabled
|
||||||
|
:hint "feedback not enabled"))
|
||||||
|
|
||||||
|
(let [profile (profile/get-profile pool profile-id)]
|
||||||
|
(send-feedback! pool profile params)
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(defn- send-feedback!
|
||||||
|
[pool profile params]
|
||||||
|
(let [dest (cf/get :feedback-destination)]
|
||||||
|
(eml/send! {::eml/conn pool
|
||||||
|
::eml/factory eml/feedback
|
||||||
|
:from dest
|
||||||
|
:to dest
|
||||||
|
:profile profile
|
||||||
|
:reply-to (:email profile)
|
||||||
|
:email (:email profile)
|
||||||
|
:subject (:subject params)
|
||||||
|
:content (:content params)})
|
||||||
|
nil))
|
||||||
@ -22,13 +22,12 @@
|
|||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files.thumbnails :as-alias thumbs]
|
[app.rpc.commands.files.thumbnails :as-alias thumbs]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.cond :as-alias cond]
|
[app.rpc.cond :as-alias cond]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.permissions :as perms]
|
||||||
[app.rpc.queries.projects :as projects]
|
|
||||||
[app.rpc.queries.share-link :refer [retrieve-share-link]]
|
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.pointer-map :as pmap]
|
[app.util.pointer-map :as pmap]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
@ -128,7 +127,9 @@
|
|||||||
|
|
||||||
([conn profile-id file-id share-id]
|
([conn profile-id file-id share-id]
|
||||||
(let [perms (get-permissions conn profile-id file-id)
|
(let [perms (get-permissions conn profile-id file-id)
|
||||||
ldata (retrieve-share-link conn file-id share-id)]
|
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
|
||||||
|
(dissoc :flags)
|
||||||
|
(update :pages db/decode-pgarray #{}))]
|
||||||
|
|
||||||
;; NOTE: in a future when share-link becomes more powerful and
|
;; NOTE: in a future when share-link becomes more powerful and
|
||||||
;; will allow us specify which parts of the app is available, we
|
;; will allow us specify which parts of the app is available, we
|
||||||
@ -196,7 +197,7 @@
|
|||||||
(let [row (db/get conn :file-data-fragment
|
(let [row (db/get conn :file-data-fragment
|
||||||
{:id id :file-id file-id}
|
{:id id :file-id file-id}
|
||||||
{:columns [:content]
|
{:columns [:content]
|
||||||
:check-deleted? false})]
|
::db/check-deleted? false})]
|
||||||
(blob/decode (:content row))))
|
(blob/decode (:content row))))
|
||||||
|
|
||||||
(defn persist-pointers!
|
(defn persist-pointers!
|
||||||
@ -258,7 +259,7 @@
|
|||||||
(handle-file-features client-features))))
|
(handle-file-features client-features))))
|
||||||
|
|
||||||
(defn get-minimal-file
|
(defn get-minimal-file
|
||||||
[{:keys [pool] :as cfg} id]
|
[{:keys [::db/pool] :as cfg} id]
|
||||||
(db/get pool :file {:id id} {:columns [:id :modified-at :revn]}))
|
(db/get pool :file {:id id} {:columns [:id :modified-at :revn]}))
|
||||||
|
|
||||||
(defn get-file-etag
|
(defn get-file-etag
|
||||||
@ -275,7 +276,7 @@
|
|||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::cond/get-object #(get-minimal-file %1 (:id %2))
|
::cond/get-object #(get-minimal-file %1 (:id %2))
|
||||||
::cond/key-fn get-file-etag}
|
::cond/key-fn get-file-etag}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(let [perms (get-permissions conn profile-id id)]
|
(let [perms (get-permissions conn profile-id id)]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
@ -303,7 +304,7 @@
|
|||||||
"Retrieve a file by its ID. Only authenticated users."
|
"Retrieve a file by its ID. Only authenticated users."
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::rpc/:auth false}
|
::rpc/:auth false}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(let [perms (get-permissions conn profile-id file-id share-id)]
|
(let [perms (get-permissions conn profile-id file-id share-id)]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
@ -339,7 +340,7 @@
|
|||||||
::cond/get-object #(get-minimal-file %1 (:file-id %2))
|
::cond/get-object #(get-minimal-file %1 (:file-id %2))
|
||||||
::cond/reuse-key? true
|
::cond/reuse-key? true
|
||||||
::cond/key-fn get-file-etag}
|
::cond/key-fn get-file-etag}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-object-thumbnails conn file-id)))
|
(get-object-thumbnails conn file-id)))
|
||||||
@ -370,7 +371,7 @@
|
|||||||
(sv/defmethod ::get-project-files
|
(sv/defmethod ::get-project-files
|
||||||
"Get all files for the specified project."
|
"Get all files for the specified project."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(projects/check-read-permissions! conn profile-id project-id)
|
(projects/check-read-permissions! conn profile-id project-id)
|
||||||
(get-project-files conn project-id)))
|
(get-project-files conn project-id)))
|
||||||
@ -389,7 +390,7 @@
|
|||||||
(sv/defmethod ::has-file-libraries
|
(sv/defmethod ::has-file-libraries
|
||||||
"Checks if the file has libraries. Returns a boolean"
|
"Checks if the file has libraries. Returns a boolean"
|
||||||
{::doc/added "1.15.1"}
|
{::doc/added "1.15.1"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! pool profile-id file-id)
|
(check-read-permissions! pool profile-id file-id)
|
||||||
(get-has-file-libraries conn file-id)))
|
(get-has-file-libraries conn file-id)))
|
||||||
@ -456,7 +457,7 @@
|
|||||||
|
|
||||||
Mainly used for rendering purposes."
|
Mainly used for rendering purposes."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-page conn params)))
|
(get-page conn params)))
|
||||||
@ -509,7 +510,7 @@
|
|||||||
(sv/defmethod ::get-team-shared-files
|
(sv/defmethod ::get-team-shared-files
|
||||||
"Get all file (libraries) for the specified team."
|
"Get all file (libraries) for the specified team."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(get-team-shared-files conn team-id)))
|
(get-team-shared-files conn team-id)))
|
||||||
@ -563,7 +564,7 @@
|
|||||||
(sv/defmethod ::get-file-libraries
|
(sv/defmethod ::get-file-libraries
|
||||||
"Get libraries used by the specified file."
|
"Get libraries used by the specified file."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-file-libraries conn file-id features)))
|
(get-file-libraries conn file-id features)))
|
||||||
@ -589,7 +590,7 @@
|
|||||||
(sv/defmethod ::get-library-file-references
|
(sv/defmethod ::get-library-file-references
|
||||||
"Returns all the file references that use specified file (library) id."
|
"Returns all the file references that use specified file (library) id."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-library-file-references conn file-id)))
|
(get-library-file-references conn file-id)))
|
||||||
@ -626,7 +627,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-team-recent-files
|
(sv/defmethod ::get-team-recent-files
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(get-team-recent-files conn team-id)))
|
(get-team-recent-files conn team-id)))
|
||||||
@ -660,7 +661,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-file-thumbnail
|
(sv/defmethod ::get-file-thumbnail
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool]} {:keys [::rpc/profile-id file-id revn]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(-> (get-file-thumbnail conn file-id revn)
|
(-> (get-file-thumbnail conn file-id revn)
|
||||||
@ -756,7 +757,7 @@
|
|||||||
"Retrieves the data for generate the thumbnail of the file. Used
|
"Retrieves the data for generate the thumbnail of the file. Used
|
||||||
mainly for render thumbnails on dashboard."
|
mainly for render thumbnails on dashboard."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
;; NOTE: we force here the "storage/pointer-map" feature, because
|
;; NOTE: we force here the "storage/pointer-map" feature, because
|
||||||
@ -788,7 +789,7 @@
|
|||||||
(sv/defmethod ::rename-file
|
(sv/defmethod ::rename-file
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id id)
|
(check-edition-permissions! conn profile-id id)
|
||||||
(let [file (rename-file conn params)]
|
(let [file (rename-file conn params)]
|
||||||
@ -819,7 +820,7 @@
|
|||||||
(let [ldata (-> library decode-row pmg/migrate-file :data)]
|
(let [ldata (-> library decode-row pmg/migrate-file :data)]
|
||||||
(->> (db/query conn :file-library-rel {:library-file-id id})
|
(->> (db/query conn :file-library-rel {:library-file-id id})
|
||||||
(map :file-id)
|
(map :file-id)
|
||||||
(keep #(db/get-by-id conn :file % {:check-deleted? false}))
|
(keep #(db/get-by-id conn :file % ::db/check-deleted? false))
|
||||||
(map decode-row)
|
(map decode-row)
|
||||||
(map pmg/migrate-file)
|
(map pmg/migrate-file)
|
||||||
(run! (fn [{:keys [id data revn] :as file}]
|
(run! (fn [{:keys [id data revn] :as file}]
|
||||||
@ -837,7 +838,7 @@
|
|||||||
(sv/defmethod ::set-file-shared
|
(sv/defmethod ::set-file-shared
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id id)
|
(check-edition-permissions! conn profile-id id)
|
||||||
(when-not is-shared
|
(when-not is-shared
|
||||||
@ -866,7 +867,7 @@
|
|||||||
(sv/defmethod ::delete-file
|
(sv/defmethod ::delete-file
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id id)
|
(check-edition-permissions! conn profile-id id)
|
||||||
(absorb-library conn params)
|
(absorb-library conn params)
|
||||||
@ -896,7 +897,7 @@
|
|||||||
(sv/defmethod ::link-file-to-library
|
(sv/defmethod ::link-file-to-library
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||||
(when (= file-id library-id)
|
(when (= file-id library-id)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :invalid-library
|
:code :invalid-library
|
||||||
@ -921,7 +922,7 @@
|
|||||||
(sv/defmethod ::unlink-file-from-library
|
(sv/defmethod ::unlink-file-from-library
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(unlink-file-from-library conn params)))
|
(unlink-file-from-library conn params)))
|
||||||
@ -945,7 +946,7 @@
|
|||||||
(sv/defmethod ::update-file-library-sync-status
|
(sv/defmethod ::update-file-library-sync-status
|
||||||
"Update the synchronization statos of a file->library link"
|
"Update the synchronization statos of a file->library link"
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(update-sync conn params)))
|
(update-sync conn params)))
|
||||||
@ -967,7 +968,7 @@
|
|||||||
(sv/defmethod ::ignore-file-library-sync-status
|
(sv/defmethod ::ignore-file-library-sync-status
|
||||||
"Ignore updates in linked files"
|
"Ignore updates in linked files"
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(-> (ignore-sync conn params)
|
(-> (ignore-sync conn params)
|
||||||
@ -998,7 +999,7 @@
|
|||||||
(sv/defmethod ::upsert-file-object-thumbnail
|
(sv/defmethod ::upsert-file-object-thumbnail
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::audit/skip true}
|
::audit/skip true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(upsert-file-object-thumbnail! conn params)
|
(upsert-file-object-thumbnail! conn params)
|
||||||
@ -1006,13 +1007,13 @@
|
|||||||
|
|
||||||
;; --- MUTATION COMMAND: upsert-file-thumbnail
|
;; --- MUTATION COMMAND: upsert-file-thumbnail
|
||||||
|
|
||||||
(def sql:upsert-file-thumbnail
|
(def ^:private sql:upsert-file-thumbnail
|
||||||
"insert into file_thumbnail (file_id, revn, data, props)
|
"insert into file_thumbnail (file_id, revn, data, props)
|
||||||
values (?, ?, ?, ?::jsonb)
|
values (?, ?, ?, ?::jsonb)
|
||||||
on conflict(file_id, revn) do
|
on conflict(file_id, revn) do
|
||||||
update set data = ?, props=?, updated_at=now();")
|
update set data = ?, props=?, updated_at=now();")
|
||||||
|
|
||||||
(defn upsert-file-thumbnail
|
(defn- upsert-file-thumbnail!
|
||||||
[conn {:keys [file-id revn data props]}]
|
[conn {:keys [file-id revn data props]}]
|
||||||
(let [props (db/tjson (or props {}))]
|
(let [props (db/tjson (or props {}))]
|
||||||
(db/exec-one! conn [sql:upsert-file-thumbnail
|
(db/exec-one! conn [sql:upsert-file-thumbnail
|
||||||
@ -1029,8 +1030,9 @@
|
|||||||
grid thumbnails."
|
grid thumbnails."
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::audit/skip true}
|
::audit/skip true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(upsert-file-thumbnail conn params)
|
(when-not (db/read-only? conn)
|
||||||
|
(upsert-file-thumbnail! conn params))
|
||||||
nil))
|
nil))
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.rpc.commands.files.create
|
(ns app.rpc.commands.files-create
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.files.features :as ffeat]
|
[app.common.files.features :as ffeat]
|
||||||
@ -15,14 +15,15 @@
|
|||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.permissions :as perms]
|
||||||
[app.rpc.queries.projects :as proj]
|
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.objects-map :as omap]
|
[app.util.objects-map :as omap]
|
||||||
[app.util.pointer-map :as pmap]
|
[app.util.pointer-map :as pmap]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
(defn create-file-role!
|
(defn create-file-role!
|
||||||
@ -67,6 +68,10 @@
|
|||||||
(->> (assoc params :file-id id :role :owner)
|
(->> (assoc params :file-id id :role :owner)
|
||||||
(create-file-role! conn))
|
(create-file-role! conn))
|
||||||
|
|
||||||
|
(db/update! conn :project
|
||||||
|
{:modified-at (dt/now)}
|
||||||
|
{:id project-id})
|
||||||
|
|
||||||
(files/decode-row file)))
|
(files/decode-row file)))
|
||||||
|
|
||||||
(s/def ::create-file
|
(s/def ::create-file
|
||||||
@ -80,9 +85,9 @@
|
|||||||
(sv/defmethod ::create-file
|
(sv/defmethod ::create-file
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(proj/check-edition-permissions! conn profile-id project-id)
|
(projects/check-edition-permissions! conn profile-id project-id)
|
||||||
(let [team-id (files/get-team-id conn project-id)
|
(let [team-id (files/get-team-id conn project-id)
|
||||||
params (assoc params :profile-id profile-id)]
|
params (assoc params :profile-id profile-id)]
|
||||||
|
|
||||||
71
backend/src/app/rpc/commands/files_share.clj
Normal file
71
backend/src/app/rpc/commands/files_share.clj
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.rpc.commands.files-share
|
||||||
|
"Share link related rpc mutation methods."
|
||||||
|
(:require
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
;; --- Helpers & Specs
|
||||||
|
|
||||||
|
(s/def ::file-id ::us/uuid)
|
||||||
|
(s/def ::who-comment ::us/string)
|
||||||
|
(s/def ::who-inspect ::us/string)
|
||||||
|
(s/def ::pages (s/every ::us/uuid :kind set?))
|
||||||
|
|
||||||
|
;; --- MUTATION: Create Share Link
|
||||||
|
|
||||||
|
(declare create-share-link)
|
||||||
|
|
||||||
|
(s/def ::create-share-link
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::who-comment ::who-inspect ::pages]))
|
||||||
|
|
||||||
|
(sv/defmethod ::create-share-link
|
||||||
|
"Creates a share-link object.
|
||||||
|
|
||||||
|
Share links are resources that allows external users access to specific
|
||||||
|
pages of a file with specific permissions (who-comment and who-inspect)."
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
|
(create-share-link conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
|
(defn create-share-link
|
||||||
|
[conn {:keys [profile-id file-id pages who-comment who-inspect]}]
|
||||||
|
(let [pages (db/create-array conn "uuid" pages)
|
||||||
|
slink (db/insert! conn :share-link
|
||||||
|
{:id (uuid/next)
|
||||||
|
:file-id file-id
|
||||||
|
:who-comment who-comment
|
||||||
|
:who-inspect who-inspect
|
||||||
|
:pages pages
|
||||||
|
:owner-id profile-id})]
|
||||||
|
|
||||||
|
(update slink :pages db/decode-pgarray #{})))
|
||||||
|
|
||||||
|
;; --- MUTATION: Delete Share Link
|
||||||
|
|
||||||
|
(s/def ::delete-share-link
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::us/id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::delete-share-link
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [slink (db/get-by-id conn :share-link id)]
|
||||||
|
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
||||||
|
(db/delete! conn :share-link {:id id})
|
||||||
|
nil)))
|
||||||
@ -4,7 +4,7 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.rpc.commands.files.temp
|
(ns app.rpc.commands.files-temp
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
@ -13,10 +13,10 @@
|
|||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.files.create :as files.create]
|
[app.rpc.commands.files-create :refer [create-file]]
|
||||||
[app.rpc.commands.files.update :as files.update]
|
[app.rpc.commands.files-update :as-alias files.update]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.queries.projects :as proj]
|
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -37,15 +37,15 @@
|
|||||||
|
|
||||||
(sv/defmethod ::create-temp-file
|
(sv/defmethod ::create-temp-file
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(proj/check-edition-permissions! conn profile-id project-id)
|
(projects/check-edition-permissions! conn profile-id project-id)
|
||||||
(files.create/create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1})))))
|
(create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1})))))
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: update-temp-file
|
;; --- MUTATION COMMAND: update-temp-file
|
||||||
|
|
||||||
(defn update-temp-file
|
(defn update-temp-file
|
||||||
[conn {:keys [::rpc/profile-id session-id id revn changes] :as params}]
|
[conn {:keys [profile-id session-id id revn changes] :as params}]
|
||||||
(db/insert! conn :file-change
|
(db/insert! conn :file-change
|
||||||
{:id (uuid/next)
|
{:id (uuid/next)
|
||||||
:session-id session-id
|
:session-id session-id
|
||||||
@ -57,16 +57,17 @@
|
|||||||
:changes (blob/encode changes)}))
|
:changes (blob/encode changes)}))
|
||||||
|
|
||||||
(s/def ::update-temp-file
|
(s/def ::update-temp-file
|
||||||
(s/keys :req-un [::files.update/changes
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::files.update/changes
|
||||||
::files.update/revn
|
::files.update/revn
|
||||||
::files.update/session-id
|
::files.update/session-id
|
||||||
::files/id]))
|
::files/id]))
|
||||||
|
|
||||||
(sv/defmethod ::update-temp-file
|
(sv/defmethod ::update-temp-file
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} params]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(update-temp-file conn params)
|
(update-temp-file conn (assoc params :profile-id profile-id))
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: persist-temp-file
|
;; --- MUTATION COMMAND: persist-temp-file
|
||||||
@ -101,7 +102,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::persist-temp-file
|
(sv/defmethod ::persist-temp-file
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id id)
|
(files/check-edition-permissions! conn profile-id id)
|
||||||
(persist-temp-file conn params)))
|
(persist-temp-file conn params)))
|
||||||
@ -4,7 +4,7 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.rpc.commands.files.update
|
(ns app.rpc.commands.files-update
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.files.features :as ffeat]
|
[app.common.files.features :as ffeat]
|
||||||
@ -132,7 +132,7 @@
|
|||||||
::webhooks/batch-timeout (dt/duration "2m")
|
::webhooks/batch-timeout (dt/duration "2m")
|
||||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id id)
|
(files/check-edition-permissions! conn profile-id id)
|
||||||
(db/xact-lock! conn id)
|
(db/xact-lock! conn id)
|
||||||
@ -145,7 +145,7 @@
|
|||||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
|
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
|
||||||
|
|
||||||
(defn update-file
|
(defn update-file
|
||||||
[{:keys [conn metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
|
[{:keys [conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
|
||||||
(let [file (get-file conn id)
|
(let [file (get-file conn id)
|
||||||
features (->> (concat (:features file)
|
features (->> (concat (:features file)
|
||||||
(:features params))
|
(:features params))
|
||||||
@ -275,7 +275,7 @@
|
|||||||
(defn- send-notifications!
|
(defn- send-notifications!
|
||||||
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]
|
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]
|
||||||
(let [lchanges (filter library-change? changes)
|
(let [lchanges (filter library-change? changes)
|
||||||
msgbus (:msgbus cfg)]
|
msgbus (::mbus/msgbus cfg)]
|
||||||
|
|
||||||
;; Asynchronously publish message to the msgbus
|
;; Asynchronously publish message to the msgbus
|
||||||
(mbus/pub! msgbus
|
(mbus/pub! msgbus
|
||||||
236
backend/src/app/rpc/commands/fonts.clj
Normal file
236
backend/src/app/rpc/commands/fonts.clj
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.rpc.commands.fonts
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.loggers.audit :as-alias audit]
|
||||||
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
|
[app.media :as media]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.climit :as climit]
|
||||||
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
|
[app.rpc.commands.teams :as teams]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.rpc.helpers :as rph]
|
||||||
|
[app.rpc.quotes :as quotes]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as-alias wrk]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[promesa.core :as p]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||||
|
(def valid-style #{"normal" "italic"})
|
||||||
|
|
||||||
|
(s/def ::data (s/map-of ::us/string any?))
|
||||||
|
(s/def ::file-id ::us/uuid)
|
||||||
|
(s/def ::font-id ::us/uuid)
|
||||||
|
(s/def ::id ::us/uuid)
|
||||||
|
(s/def ::name ::us/not-empty-string)
|
||||||
|
(s/def ::project-id ::us/uuid)
|
||||||
|
(s/def ::style valid-style)
|
||||||
|
(s/def ::team-id ::us/uuid)
|
||||||
|
(s/def ::weight valid-weight)
|
||||||
|
|
||||||
|
;; --- QUERY: Get font variants
|
||||||
|
|
||||||
|
(s/def ::get-font-variants
|
||||||
|
(s/and
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:opt-un [::team-id
|
||||||
|
::file-id
|
||||||
|
::project-id])
|
||||||
|
(fn [o]
|
||||||
|
(or (contains? o :team-id)
|
||||||
|
(contains? o :file-id)
|
||||||
|
(contains? o :project-id)))))
|
||||||
|
|
||||||
|
(sv/defmethod ::get-font-variants
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}]
|
||||||
|
(with-open [conn (db/open pool)]
|
||||||
|
(cond
|
||||||
|
(uuid? team-id)
|
||||||
|
(do
|
||||||
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
|
(db/query conn :team-font-variant
|
||||||
|
{:team-id team-id
|
||||||
|
:deleted-at nil}))
|
||||||
|
|
||||||
|
(uuid? project-id)
|
||||||
|
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
|
||||||
|
(projects/check-read-permissions! conn profile-id project-id)
|
||||||
|
(db/query conn :team-font-variant
|
||||||
|
{:team-id (:team-id project)
|
||||||
|
:deleted-at nil}))
|
||||||
|
|
||||||
|
(uuid? file-id)
|
||||||
|
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
|
||||||
|
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})]
|
||||||
|
(files/check-read-permissions! conn profile-id file-id)
|
||||||
|
(db/query conn :team-font-variant
|
||||||
|
{:team-id (:team-id project)
|
||||||
|
:deleted-at nil})))))
|
||||||
|
|
||||||
|
|
||||||
|
(declare create-font-variant)
|
||||||
|
|
||||||
|
(s/def ::create-font-variant
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::team-id
|
||||||
|
::data
|
||||||
|
::font-id
|
||||||
|
::font-family
|
||||||
|
::font-weight
|
||||||
|
::font-style]))
|
||||||
|
|
||||||
|
(sv/defmethod ::create-font-variant
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||||
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
|
(teams/check-edition-permissions! pool profile-id team-id)
|
||||||
|
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
|
||||||
|
::quotes/profile-id profile-id
|
||||||
|
::quotes/team-id team-id})
|
||||||
|
(create-font-variant cfg (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
|
(defn create-font-variant
|
||||||
|
[{:keys [::sto/storage ::db/pool ::wrk/executor ::rpc/climit]} {:keys [data] :as params}]
|
||||||
|
(letfn [(generate-fonts [data]
|
||||||
|
(climit/with-dispatch (:process-font climit)
|
||||||
|
(media/run {:cmd :generate-fonts :input data})))
|
||||||
|
|
||||||
|
;; Function responsible of calculating cryptographyc hash of
|
||||||
|
;; the provided data.
|
||||||
|
(calculate-hash [data]
|
||||||
|
(px/with-dispatch executor
|
||||||
|
(sto/calculate-hash data)))
|
||||||
|
|
||||||
|
(validate-data [data]
|
||||||
|
(when (and (not (contains? data "font/otf"))
|
||||||
|
(not (contains? data "font/ttf"))
|
||||||
|
(not (contains? data "font/woff"))
|
||||||
|
(not (contains? data "font/woff2")))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-font-upload))
|
||||||
|
data)
|
||||||
|
|
||||||
|
(persist-font-object [data mtype]
|
||||||
|
(when-let [resource (get data mtype)]
|
||||||
|
(p/let [hash (calculate-hash resource)
|
||||||
|
content (-> (sto/content resource)
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
(sto/put-object! storage {::sto/content content
|
||||||
|
::sto/touched-at (dt/now)
|
||||||
|
::sto/deduplicate? true
|
||||||
|
:content-type mtype
|
||||||
|
:bucket "team-font-variant"}))))
|
||||||
|
|
||||||
|
(persist-fonts [data]
|
||||||
|
(p/let [otf (persist-font-object data "font/otf")
|
||||||
|
ttf (persist-font-object data "font/ttf")
|
||||||
|
woff1 (persist-font-object data "font/woff")
|
||||||
|
woff2 (persist-font-object data "font/woff2")]
|
||||||
|
|
||||||
|
(d/without-nils
|
||||||
|
{:otf otf
|
||||||
|
:ttf ttf
|
||||||
|
:woff1 woff1
|
||||||
|
:woff2 woff2})))
|
||||||
|
|
||||||
|
(insert-into-db [{:keys [woff1 woff2 otf ttf]}]
|
||||||
|
(db/insert! pool :team-font-variant
|
||||||
|
{:id (uuid/next)
|
||||||
|
:team-id (:team-id params)
|
||||||
|
:font-id (:font-id params)
|
||||||
|
:font-family (:font-family params)
|
||||||
|
:font-weight (:font-weight params)
|
||||||
|
:font-style (:font-style params)
|
||||||
|
:woff1-file-id (:id woff1)
|
||||||
|
:woff2-file-id (:id woff2)
|
||||||
|
:otf-file-id (:id otf)
|
||||||
|
:ttf-file-id (:id ttf)}))
|
||||||
|
]
|
||||||
|
|
||||||
|
(->> (generate-fonts data)
|
||||||
|
(p/fmap validate-data)
|
||||||
|
(p/mcat executor persist-fonts)
|
||||||
|
(p/fmap executor insert-into-db)
|
||||||
|
(p/fmap (fn [result]
|
||||||
|
(let [params (update params :data (comp vec keys))]
|
||||||
|
(rph/with-meta result {::audit/replace-props params})))))))
|
||||||
|
|
||||||
|
;; --- UPDATE FONT FAMILY
|
||||||
|
|
||||||
|
(s/def ::update-font
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::team-id ::id ::name]))
|
||||||
|
|
||||||
|
(sv/defmethod ::update-font
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id id name]}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
(rph/with-meta
|
||||||
|
(db/update! conn :team-font-variant
|
||||||
|
{:font-family name}
|
||||||
|
{:font-id id
|
||||||
|
:team-id team-id})
|
||||||
|
{::audit/replace-props {:id id
|
||||||
|
:name name
|
||||||
|
:team-id team-id
|
||||||
|
:profile-id profile-id}})))
|
||||||
|
|
||||||
|
;; --- DELETE FONT
|
||||||
|
|
||||||
|
(s/def ::delete-font
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::team-id ::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::delete-font
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
(let [font (db/update! conn :team-font-variant
|
||||||
|
{:deleted-at (dt/now)}
|
||||||
|
{:font-id id :team-id team-id})]
|
||||||
|
(rph/with-meta (rph/wrap)
|
||||||
|
{::audit/props {:id id
|
||||||
|
:team-id team-id
|
||||||
|
:name (:font-family font)
|
||||||
|
:profile-id profile-id}}))))
|
||||||
|
|
||||||
|
;; --- DELETE FONT VARIANT
|
||||||
|
|
||||||
|
(s/def ::delete-font-variant
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::team-id ::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::delete-font-variant
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
(let [variant (db/update! conn :team-font-variant
|
||||||
|
{:deleted-at (dt/now)}
|
||||||
|
{:id id :team-id team-id})]
|
||||||
|
(rph/with-meta (rph/wrap)
|
||||||
|
{::audit/props {:font-family (:font-family variant)
|
||||||
|
:font-id (:font-id variant)}}))))
|
||||||
|
|
||||||
@ -14,10 +14,10 @@
|
|||||||
[app.loggers.audit :as-alias audit]
|
[app.loggers.audit :as-alias audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.auth :as cmd.auth]
|
[app.rpc.commands.auth :as auth]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.queries.profile :as profile]
|
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
@ -39,7 +39,7 @@
|
|||||||
is properly configured and enabled with `login-with-ldap` flag."
|
is properly configured and enabled with `login-with-ldap` flag."
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::doc/added "1.15"}
|
::doc/added "1.15"}
|
||||||
[{:keys [::main/props ::ldap/provider session] :as cfg} params]
|
[{:keys [::main/props ::ldap/provider] :as cfg} params]
|
||||||
(when-not provider
|
(when-not provider
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :ldap-not-initialized
|
:code :ldap-not-initialized
|
||||||
@ -67,24 +67,23 @@
|
|||||||
:member-email (:email profile))
|
:member-email (:email profile))
|
||||||
token (tokens/generate props claims)]
|
token (tokens/generate props claims)]
|
||||||
(-> {:invitation-token token}
|
(-> {:invitation-token token}
|
||||||
(rph/with-transform (session/create-fn session (:id profile)))
|
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||||
(rph/with-meta {::audit/props (:props profile)
|
(rph/with-meta {::audit/props (:props profile)
|
||||||
::audit/profile-id (:id profile)})))
|
::audit/profile-id (:id profile)})))
|
||||||
|
|
||||||
(-> profile
|
(-> profile
|
||||||
(rph/with-transform (session/create-fn session (:id profile)))
|
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||||
(rph/with-meta {::audit/props (:props profile)
|
(rph/with-meta {::audit/props (:props profile)
|
||||||
::audit/profile-id (:id profile)}))))))
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
(defn- login-or-register
|
(defn- login-or-register
|
||||||
[{:keys [pool] :as cfg} info]
|
[{:keys [::db/pool] :as cfg} info]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(or (some->> (:email info)
|
(or (some->> (:email info)
|
||||||
(profile/retrieve-profile-data-by-email conn)
|
(profile/get-profile-by-email conn)
|
||||||
(profile/populate-additional-data conn)
|
(profile/decode-row))
|
||||||
(profile/decode-profile-row))
|
|
||||||
(->> (assoc info :is-active true :is-demo false)
|
(->> (assoc info :is-active true :is-demo false)
|
||||||
(cmd.auth/create-profile conn)
|
(auth/create-profile! conn)
|
||||||
(cmd.auth/create-profile-relations conn)
|
(auth/create-profile-rels! conn)
|
||||||
(profile/strip-private-attrs)))))
|
(profile/strip-private-attrs)))))
|
||||||
|
|
||||||
|
|||||||
@ -17,9 +17,9 @@
|
|||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.binfile :as binfile]
|
[app.rpc.commands.binfile :as binfile]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.commands.projects :as proj]
|
||||||
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
|
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.queries.projects :as proj]
|
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.pointer-map :as pmap]
|
[app.util.pointer-map :as pmap]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"Duplicate a single file in the same team."
|
"Duplicate a single file in the same team."
|
||||||
{::doc/added "1.16"
|
{::doc/added "1.16"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(duplicate-file conn (assoc params :profile-id profile-id))))
|
(duplicate-file conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
@ -221,7 +221,7 @@
|
|||||||
"Duplicate an entire project with all the files"
|
"Duplicate an entire project with all the files"
|
||||||
{::doc/added "1.16"
|
{::doc/added "1.16"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} params]
|
[{:keys [::db/pool] :as cfg} params]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(duplicate-project conn (assoc params :profile-id (::rpc/profile-id params)))))
|
(duplicate-project conn (assoc params :profile-id (::rpc/profile-id params)))))
|
||||||
|
|
||||||
@ -231,12 +231,13 @@
|
|||||||
;; Defer all constraints
|
;; Defer all constraints
|
||||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||||
|
|
||||||
(let [project (db/get-by-id conn :project project-id)
|
(let [project (-> (db/get-by-id conn :project project-id)
|
||||||
|
(assoc :is-pinned false))
|
||||||
|
|
||||||
files (db/query conn :file
|
files (db/query conn :file
|
||||||
{:project-id (:id project)
|
{:project-id (:id project)
|
||||||
:deleted-at nil}
|
:deleted-at nil}
|
||||||
{:columns [:id]})
|
{:columns [:id]})
|
||||||
|
|
||||||
project (cond-> project
|
project (cond-> project
|
||||||
(string? name)
|
(string? name)
|
||||||
@ -329,7 +330,7 @@
|
|||||||
"Move a set of files from one project to other."
|
"Move a set of files from one project to other."
|
||||||
{::doc/added "1.16"
|
{::doc/added "1.16"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(move-files conn (assoc params :profile-id profile-id))))
|
(move-files conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
@ -369,7 +370,7 @@
|
|||||||
"Move projects between teams."
|
"Move projects between teams."
|
||||||
{::doc/added "1.16"
|
{::doc/added "1.16"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(move-project conn (assoc params :profile-id profile-id))))
|
(move-project conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
@ -386,7 +387,7 @@
|
|||||||
"Clone into the specified project the template by its id."
|
"Clone into the specified project the template by its id."
|
||||||
{::doc/added "1.16"
|
{::doc/added "1.16"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(-> (assoc cfg :conn conn)
|
(-> (assoc cfg :conn conn)
|
||||||
(clone-template (assoc params :profile-id profile-id)))))
|
(clone-template (assoc params :profile-id profile-id)))))
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as-alias wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.io :as io]
|
[datoteka.io :as io]
|
||||||
@ -66,8 +67,8 @@
|
|||||||
|
|
||||||
(sv/defmethod ::upload-file-media-object
|
(sv/defmethod ::upload-file-media-object
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(media/validate-media-type! content)
|
(media/validate-media-type! content)
|
||||||
(validate-content-size! content)
|
(validate-content-size! content)
|
||||||
@ -110,7 +111,7 @@
|
|||||||
;; inverse, soft referential integrity).
|
;; inverse, soft referential integrity).
|
||||||
|
|
||||||
(defn create-file-media-object
|
(defn create-file-media-object
|
||||||
[{:keys [storage pool climit executor]}
|
[{:keys [::sto/storage ::db/pool climit ::wrk/executor]}
|
||||||
{:keys [id file-id is-local name content]}]
|
{:keys [id file-id is-local name content]}]
|
||||||
(letfn [;; Function responsible to retrieve the file information, as
|
(letfn [;; Function responsible to retrieve the file information, as
|
||||||
;; it is synchronous operation it should be wrapped into
|
;; it is synchronous operation it should be wrapped into
|
||||||
@ -186,8 +187,8 @@
|
|||||||
|
|
||||||
(sv/defmethod ::create-file-media-object-from-url
|
(sv/defmethod ::create-file-media-object-from-url
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(create-file-media-object-from-url cfg params)))
|
(create-file-media-object-from-url cfg params)))
|
||||||
|
|
||||||
@ -253,7 +254,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::clone-file-media-object
|
(sv/defmethod ::clone-file-media-object
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(-> (assoc cfg :conn conn)
|
(-> (assoc cfg :conn conn)
|
||||||
|
|||||||
426
backend/src/app/rpc/commands/profile.clj
Normal file
426
backend/src/app/rpc/commands/profile.clj
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.rpc.commands.profile
|
||||||
|
(:require
|
||||||
|
[app.auth :as auth]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.email :as eml]
|
||||||
|
[app.http.session :as session]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
|
[app.main :as-alias main]
|
||||||
|
[app.media :as media]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.climit :as climit]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.rpc.helpers :as rph]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.tokens :as tokens]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as-alias wrk]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[promesa.core :as p]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
(declare decode-row)
|
||||||
|
(declare get-profile)
|
||||||
|
(declare strip-private-attrs)
|
||||||
|
(declare filter-props)
|
||||||
|
(declare check-profile-existence!)
|
||||||
|
|
||||||
|
;; --- QUERY: Get profile (own)
|
||||||
|
|
||||||
|
(s/def ::get-profile
|
||||||
|
(s/keys :opt [::rpc/profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::get-profile
|
||||||
|
{::rpc/auth false
|
||||||
|
::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id]}]
|
||||||
|
;; We need to return the anonymous profile object in two cases, when
|
||||||
|
;; no profile-id is in session, and when db call raises not found. In all other
|
||||||
|
;; cases we need to reraise the exception.
|
||||||
|
(try
|
||||||
|
(-> (get-profile pool profile-id)
|
||||||
|
(strip-private-attrs)
|
||||||
|
(update :props filter-props))
|
||||||
|
(catch Throwable _
|
||||||
|
{:id uuid/zero :fullname "Anonymous User"})))
|
||||||
|
|
||||||
|
(defn get-profile
|
||||||
|
"Get profile by id. Throws not-found exception if no profile found."
|
||||||
|
[conn id & {:as attrs}]
|
||||||
|
(-> (db/get-by-id conn :profile id attrs)
|
||||||
|
(decode-row)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- MUTATION: Update Profile (own)
|
||||||
|
|
||||||
|
(s/def ::email ::us/email)
|
||||||
|
(s/def ::fullname ::us/not-empty-string)
|
||||||
|
(s/def ::lang ::us/string)
|
||||||
|
(s/def ::theme ::us/string)
|
||||||
|
|
||||||
|
(s/def ::update-profile
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::fullname]
|
||||||
|
:opt-un [::lang ::theme]))
|
||||||
|
|
||||||
|
(sv/defmethod ::update-profile
|
||||||
|
{::doc/added "1.0"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
;; NOTE: we need to retrieve the profile independently if we use
|
||||||
|
;; it or not for explicit locking and avoid concurrent updates of
|
||||||
|
;; the same row/object.
|
||||||
|
(let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true)
|
||||||
|
(decode-row))
|
||||||
|
|
||||||
|
;; Update the profile map with direct params
|
||||||
|
profile (-> profile
|
||||||
|
(assoc :fullname fullname)
|
||||||
|
(assoc :lang lang)
|
||||||
|
(assoc :theme theme))
|
||||||
|
]
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:fullname fullname
|
||||||
|
:lang lang
|
||||||
|
:theme theme
|
||||||
|
:props (db/tjson (:props profile))}
|
||||||
|
{:id profile-id})
|
||||||
|
|
||||||
|
(-> profile
|
||||||
|
(strip-private-attrs)
|
||||||
|
(d/without-nils)
|
||||||
|
(rph/with-meta {::audit/props (audit/profile->props profile)})))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- MUTATION: Update Password
|
||||||
|
|
||||||
|
(declare validate-password!)
|
||||||
|
(declare update-profile-password!)
|
||||||
|
(declare invalidate-profile-session!)
|
||||||
|
|
||||||
|
(s/def ::password ::us/not-empty-string)
|
||||||
|
(s/def ::old-password (s/nilable ::us/string))
|
||||||
|
|
||||||
|
(s/def ::update-profile-password
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::password ::old-password]))
|
||||||
|
|
||||||
|
(sv/defmethod ::update-profile-password
|
||||||
|
{::climit/queue :auth}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [profile (validate-password! conn (assoc params :profile-id profile-id))
|
||||||
|
session-id (::session/id params)]
|
||||||
|
|
||||||
|
(when (= (str/lower (:email profile))
|
||||||
|
(str/lower (:password params)))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-as-password
|
||||||
|
:hint "you can't use your email as password"))
|
||||||
|
|
||||||
|
(update-profile-password! conn (assoc profile :password password))
|
||||||
|
(invalidate-profile-session! conn profile-id session-id)
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
(defn- invalidate-profile-session!
|
||||||
|
"Removes all sessions except the current one."
|
||||||
|
[conn profile-id session-id]
|
||||||
|
(let [sql "delete from http_session where profile_id = ? and id != ?"]
|
||||||
|
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
|
||||||
|
|
||||||
|
(defn- validate-password!
|
||||||
|
[conn {:keys [profile-id old-password] :as params}]
|
||||||
|
(let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)]
|
||||||
|
(when (and (not= (:password profile) "!")
|
||||||
|
(not (:valid (auth/verify-password old-password (:password profile)))))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :old-password-not-match))
|
||||||
|
profile))
|
||||||
|
|
||||||
|
(defn update-profile-password!
|
||||||
|
[conn {:keys [id password] :as profile}]
|
||||||
|
(when-not (db/read-only? conn)
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:password (auth/derive-password password)}
|
||||||
|
{:id id})))
|
||||||
|
|
||||||
|
;; --- MUTATION: Update Photo
|
||||||
|
|
||||||
|
(declare upload-photo)
|
||||||
|
(declare update-profile-photo)
|
||||||
|
|
||||||
|
(s/def ::file ::media/upload)
|
||||||
|
(s/def ::update-profile-photo
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file]))
|
||||||
|
|
||||||
|
(sv/defmethod ::update-profile-photo
|
||||||
|
[cfg {:keys [::rpc/profile-id file] :as params}]
|
||||||
|
;; Validate incoming mime type
|
||||||
|
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
||||||
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
|
(update-profile-photo cfg (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
|
;; TODO: reimplement it without p/let
|
||||||
|
|
||||||
|
(defn update-profile-photo
|
||||||
|
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}]
|
||||||
|
(letfn [(on-uploaded [photo]
|
||||||
|
(let [profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
|
||||||
|
|
||||||
|
;; Schedule deletion of old photo
|
||||||
|
(when-let [id (:photo-id profile)]
|
||||||
|
(sto/touch-object! storage id))
|
||||||
|
|
||||||
|
;; Save new photo
|
||||||
|
(db/update! pool :profile
|
||||||
|
{:photo-id (:id photo)}
|
||||||
|
{:id profile-id})
|
||||||
|
|
||||||
|
(-> (rph/wrap)
|
||||||
|
(rph/with-meta {::audit/replace-props
|
||||||
|
{:file-name (:filename file)
|
||||||
|
:file-size (:size file)
|
||||||
|
:file-path (str (:path file))
|
||||||
|
:file-mtype (:mtype file)}}))))]
|
||||||
|
(->> (upload-photo cfg params)
|
||||||
|
(p/fmap executor on-uploaded))))
|
||||||
|
|
||||||
|
(defn upload-photo
|
||||||
|
[{:keys [::sto/storage ::wrk/executor climit] :as cfg} {:keys [file]}]
|
||||||
|
(letfn [(get-info [content]
|
||||||
|
(climit/with-dispatch (:process-image climit)
|
||||||
|
(media/run {:cmd :info :input content})))
|
||||||
|
|
||||||
|
(generate-thumbnail [info]
|
||||||
|
(climit/with-dispatch (:process-image climit)
|
||||||
|
(media/run {:cmd :profile-thumbnail
|
||||||
|
:format :jpeg
|
||||||
|
:quality 85
|
||||||
|
:width 256
|
||||||
|
:height 256
|
||||||
|
:input info})))
|
||||||
|
|
||||||
|
;; Function responsible of calculating cryptographyc hash of
|
||||||
|
;; the provided data.
|
||||||
|
(calculate-hash [data]
|
||||||
|
(px/with-dispatch executor
|
||||||
|
(sto/calculate-hash data)))]
|
||||||
|
|
||||||
|
(p/let [info (get-info file)
|
||||||
|
thumb (generate-thumbnail info)
|
||||||
|
hash (calculate-hash (:data thumb))
|
||||||
|
content (-> (sto/content (:data thumb) (:size thumb))
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
(sto/put-object! storage {::sto/content content
|
||||||
|
::sto/deduplicate? true
|
||||||
|
:bucket "profile"
|
||||||
|
:content-type (:mtype thumb)}))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- MUTATION: Request Email Change
|
||||||
|
|
||||||
|
(declare ^:private request-email-change!)
|
||||||
|
(declare ^:private change-email-immediately!)
|
||||||
|
|
||||||
|
(s/def ::request-email-change
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::email]))
|
||||||
|
|
||||||
|
(sv/defmethod ::request-email-change
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [profile (db/get-by-id conn :profile profile-id)
|
||||||
|
cfg (assoc cfg ::conn conn)
|
||||||
|
params (assoc params
|
||||||
|
:profile profile
|
||||||
|
:email (str/lower email))]
|
||||||
|
(if (contains? cf/flags :smtp)
|
||||||
|
(request-email-change! cfg params)
|
||||||
|
(change-email-immediately! cfg params)))))
|
||||||
|
|
||||||
|
(defn- change-email-immediately!
|
||||||
|
[{:keys [::conn]} {:keys [profile email] :as params}]
|
||||||
|
(when (not= email (:email profile))
|
||||||
|
(check-profile-existence! conn params))
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:email email}
|
||||||
|
{:id (:id profile)})
|
||||||
|
|
||||||
|
{:changed true})
|
||||||
|
|
||||||
|
(defn- request-email-change!
|
||||||
|
[{:keys [::conn] :as cfg} {:keys [profile email] :as params}]
|
||||||
|
(let [token (tokens/generate (::main/props cfg)
|
||||||
|
{:iss :change-email
|
||||||
|
:exp (dt/in-future "15m")
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:email email})
|
||||||
|
ptoken (tokens/generate (::main/props cfg)
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:exp (dt/in-future {:days 30})})]
|
||||||
|
|
||||||
|
(when (not= email (:email profile))
|
||||||
|
(check-profile-existence! conn params))
|
||||||
|
|
||||||
|
(when-not (eml/allow-send-emails? conn profile)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :profile-is-muted
|
||||||
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
||||||
|
|
||||||
|
(when (eml/has-bounce-reports? conn email)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||||
|
|
||||||
|
(eml/send! {::eml/conn conn
|
||||||
|
::eml/factory eml/change-email
|
||||||
|
:public-uri (cf/get :public-uri)
|
||||||
|
:to (:email profile)
|
||||||
|
:name (:fullname profile)
|
||||||
|
:pending-email email
|
||||||
|
:token token
|
||||||
|
:extra-data ptoken})
|
||||||
|
nil))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- MUTATION: Update Profile Props
|
||||||
|
|
||||||
|
(s/def ::props map?)
|
||||||
|
(s/def ::update-profile-props
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::props]))
|
||||||
|
|
||||||
|
(sv/defmethod ::update-profile-props
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [profile (get-profile conn profile-id ::db/for-update? true)
|
||||||
|
props (reduce-kv (fn [props k v]
|
||||||
|
;; We don't accept namespaced keys
|
||||||
|
(if (simple-ident? k)
|
||||||
|
(if (nil? v)
|
||||||
|
(dissoc props k)
|
||||||
|
(assoc props k v))
|
||||||
|
props))
|
||||||
|
(:props profile)
|
||||||
|
props)]
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:props (db/tjson props)}
|
||||||
|
{:id profile-id})
|
||||||
|
|
||||||
|
(filter-props props))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- MUTATION: Delete Profile
|
||||||
|
|
||||||
|
(declare ^:private get-owned-teams-with-participants)
|
||||||
|
|
||||||
|
(s/def ::delete-profile
|
||||||
|
(s/keys :req [::rpc/profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::delete-profile
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [teams (get-owned-teams-with-participants conn profile-id)
|
||||||
|
deleted-at (dt/now)]
|
||||||
|
|
||||||
|
;; If we found owned teams with participants, we don't allow
|
||||||
|
;; delete profile until the user properly transfer ownership or
|
||||||
|
;; explicitly removes all participants from the team
|
||||||
|
(when (some pos? (map :participants teams))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :owner-teams-with-people
|
||||||
|
:hint "The user need to transfer ownership of owned teams."
|
||||||
|
:context {:teams (mapv :id teams)}))
|
||||||
|
|
||||||
|
(doseq [{:keys [id]} teams]
|
||||||
|
(db/update! conn :team
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:id id}))
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:id profile-id})
|
||||||
|
|
||||||
|
(rph/with-transform {} (session/delete-fn cfg)))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- HELPERS
|
||||||
|
|
||||||
|
(def sql:owned-teams
|
||||||
|
"with owner_teams as (
|
||||||
|
select tpr.team_id as id
|
||||||
|
from team_profile_rel as tpr
|
||||||
|
where tpr.is_owner is true
|
||||||
|
and tpr.profile_id = ?
|
||||||
|
)
|
||||||
|
select tpr.team_id as id,
|
||||||
|
count(tpr.profile_id) - 1 as participants
|
||||||
|
from team_profile_rel as tpr
|
||||||
|
where tpr.team_id in (select id from owner_teams)
|
||||||
|
and tpr.profile_id != ?
|
||||||
|
group by 1")
|
||||||
|
|
||||||
|
(defn- get-owned-teams-with-participants
|
||||||
|
[conn profile-id]
|
||||||
|
(db/exec! conn [sql:owned-teams profile-id profile-id]))
|
||||||
|
|
||||||
|
(def ^:private sql:profile-existence
|
||||||
|
"select exists (select * from profile
|
||||||
|
where email = ?
|
||||||
|
and deleted_at is null) as val")
|
||||||
|
|
||||||
|
(defn check-profile-existence!
|
||||||
|
[conn {:keys [email] :as params}]
|
||||||
|
(let [email (str/lower email)
|
||||||
|
result (db/exec-one! conn [sql:profile-existence email])]
|
||||||
|
(when (:val result)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-already-exists))
|
||||||
|
params))
|
||||||
|
|
||||||
|
(def ^:private sql:profile-by-email
|
||||||
|
"select p.* from profile as p
|
||||||
|
where p.email = ?
|
||||||
|
and (p.deleted_at is null or
|
||||||
|
p.deleted_at > now())")
|
||||||
|
|
||||||
|
(defn get-profile-by-email
|
||||||
|
"Returns a profile looked up by email or `nil` if not match found."
|
||||||
|
[conn email]
|
||||||
|
(->> (db/exec! conn [sql:profile-by-email (str/lower email)])
|
||||||
|
(map decode-row)
|
||||||
|
(first)))
|
||||||
|
|
||||||
|
(defn strip-private-attrs
|
||||||
|
"Only selects a publicly visible profile attrs."
|
||||||
|
[row]
|
||||||
|
(dissoc row :password :deleted-at))
|
||||||
|
|
||||||
|
(defn filter-props
|
||||||
|
"Removes all namespace qualified props from `props` attr."
|
||||||
|
[props]
|
||||||
|
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
||||||
|
|
||||||
|
(defn decode-row
|
||||||
|
[{:keys [props] :as row}]
|
||||||
|
(cond-> row
|
||||||
|
(db/pgobject? props "jsonb")
|
||||||
|
(assoc :props (db/decode-transit-pgobject props))))
|
||||||
268
backend/src/app/rpc/commands/projects.clj
Normal file
268
backend/src/app/rpc/commands/projects.clj
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.rpc.commands.projects
|
||||||
|
(:require
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.loggers.audit :as-alias audit]
|
||||||
|
[app.loggers.webhooks :as webhooks]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.teams :as teams]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.rpc.helpers :as rph]
|
||||||
|
[app.rpc.permissions :as perms]
|
||||||
|
[app.rpc.quotes :as quotes]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
(s/def ::id ::us/uuid)
|
||||||
|
(s/def ::name ::us/string)
|
||||||
|
|
||||||
|
;; --- Check Project Permissions
|
||||||
|
|
||||||
|
(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- get-permissions
|
||||||
|
[conn profile-id project-id]
|
||||||
|
(let [rows (db/exec! conn [sql:project-permissions
|
||||||
|
project-id profile-id
|
||||||
|
project-id profile-id])
|
||||||
|
is-owner (boolean (some :is-owner rows))
|
||||||
|
is-admin (boolean (some :is-admin rows))
|
||||||
|
can-edit (boolean (some :can-edit rows))]
|
||||||
|
(when (seq rows)
|
||||||
|
{:is-owner is-owner
|
||||||
|
:is-admin (or is-owner is-admin)
|
||||||
|
:can-edit (or is-owner is-admin can-edit)
|
||||||
|
:can-read true})))
|
||||||
|
|
||||||
|
(def has-edit-permissions?
|
||||||
|
(perms/make-edition-predicate-fn get-permissions))
|
||||||
|
|
||||||
|
(def has-read-permissions?
|
||||||
|
(perms/make-read-predicate-fn get-permissions))
|
||||||
|
|
||||||
|
(def check-edition-permissions!
|
||||||
|
(perms/make-check-fn has-edit-permissions?))
|
||||||
|
|
||||||
|
(def check-read-permissions!
|
||||||
|
(perms/make-check-fn has-read-permissions?))
|
||||||
|
|
||||||
|
;; --- QUERY: Get projects
|
||||||
|
|
||||||
|
(declare get-projects)
|
||||||
|
|
||||||
|
(s/def ::team-id ::us/uuid)
|
||||||
|
(s/def ::get-projects
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::team-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::get-projects
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
|
||||||
|
(with-open [conn (db/open pool)]
|
||||||
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
|
(get-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) as count
|
||||||
|
from project as p
|
||||||
|
inner join team as t on (t.id = p.team_id)
|
||||||
|
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
|
||||||
|
and t.deleted_at is null
|
||||||
|
order by p.modified_at desc")
|
||||||
|
|
||||||
|
(defn get-projects
|
||||||
|
[conn profile-id team-id]
|
||||||
|
(db/exec! conn [sql:projects profile-id team-id]))
|
||||||
|
|
||||||
|
;; --- QUERY: Get all projects
|
||||||
|
|
||||||
|
(declare get-all-projects)
|
||||||
|
|
||||||
|
(s/def ::get-all-projects
|
||||||
|
(s/keys :req [::rpc/profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::get-all-projects
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
||||||
|
(with-open [conn (db/open pool)]
|
||||||
|
(get-all-projects conn profile-id)))
|
||||||
|
|
||||||
|
(def sql:all-projects
|
||||||
|
"select p1.*, t.name as team_name, t.is_default as is_default_team
|
||||||
|
from project as p1
|
||||||
|
inner join team as t on (t.id = p1.team_id)
|
||||||
|
where t.id in (select team_id
|
||||||
|
from team_profile_rel as tpr
|
||||||
|
where tpr.profile_id = ?
|
||||||
|
and (tpr.can_edit = true or
|
||||||
|
tpr.is_owner = true or
|
||||||
|
tpr.is_admin = true))
|
||||||
|
and t.deleted_at is null
|
||||||
|
and p1.deleted_at is null
|
||||||
|
union
|
||||||
|
select p2.*, t.name as team_name, t.is_default as is_default_team
|
||||||
|
from project as p2
|
||||||
|
inner join team as t on (t.id = p2.team_id)
|
||||||
|
where p2.id in (select project_id
|
||||||
|
from project_profile_rel as ppr
|
||||||
|
where ppr.profile_id = ?
|
||||||
|
and (ppr.can_edit = true or
|
||||||
|
ppr.is_owner = true or
|
||||||
|
ppr.is_admin = true))
|
||||||
|
and t.deleted_at is null
|
||||||
|
and p2.deleted_at is null
|
||||||
|
order by team_name, name;")
|
||||||
|
|
||||||
|
(defn get-all-projects
|
||||||
|
[conn profile-id]
|
||||||
|
(db/exec! conn [sql:all-projects profile-id profile-id]))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- QUERY: Get project
|
||||||
|
|
||||||
|
(s/def ::get-project
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::get-project
|
||||||
|
{::doc/added "1.18"}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||||
|
(with-open [conn (db/open pool)]
|
||||||
|
(let [project (db/get-by-id conn :project id)]
|
||||||
|
(check-read-permissions! conn profile-id id)
|
||||||
|
project)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; --- MUTATION: Create Project
|
||||||
|
|
||||||
|
(s/def ::create-project
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::team-id ::name]
|
||||||
|
:opt-un [::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::create-project
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
|
||||||
|
::quotes/profile-id profile-id
|
||||||
|
::quotes/team-id team-id})
|
||||||
|
|
||||||
|
(let [params (assoc params :profile-id profile-id)
|
||||||
|
project (teams/create-project conn params)]
|
||||||
|
(teams/create-project-role conn profile-id (:id project) :owner)
|
||||||
|
(db/insert! conn :team-project-profile-rel
|
||||||
|
{:project-id (:id project)
|
||||||
|
:profile-id profile-id
|
||||||
|
:team-id team-id
|
||||||
|
:is-pinned true})
|
||||||
|
(assoc project :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 [::rpc/profile-id]
|
||||||
|
:req-un [::id ::team-id ::is-pinned]))
|
||||||
|
|
||||||
|
(sv/defmethod ::update-project-pin
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/batch-timeout (dt/duration "5s")
|
||||||
|
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(check-edition-permissions! conn profile-id id)
|
||||||
|
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||||
|
nil))
|
||||||
|
|
||||||
|
;; --- MUTATION: Rename Project
|
||||||
|
|
||||||
|
(declare rename-project)
|
||||||
|
|
||||||
|
(s/def ::rename-project
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::name ::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::rename-project
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(check-edition-permissions! conn profile-id id)
|
||||||
|
(let [project (db/get-by-id conn :project id ::db/for-update? true)]
|
||||||
|
(db/update! conn :project
|
||||||
|
{:name name}
|
||||||
|
{:id id})
|
||||||
|
(rph/with-meta (rph/wrap)
|
||||||
|
{::audit/props {:team-id (:team-id project)
|
||||||
|
:prev-name (:name project)}}))))
|
||||||
|
|
||||||
|
;; --- MUTATION: Delete Project
|
||||||
|
|
||||||
|
(s/def ::delete-project
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::id]))
|
||||||
|
|
||||||
|
;; TODO: right now, we just don't allow delete default projects, in a
|
||||||
|
;; future we need to ensure raise a correct exception signaling that
|
||||||
|
;; this is not allowed.
|
||||||
|
|
||||||
|
(sv/defmethod ::delete-project
|
||||||
|
{::doc/added "1.18"
|
||||||
|
::webhooks/event? true}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(check-edition-permissions! conn profile-id id)
|
||||||
|
(let [project (db/update! conn :project
|
||||||
|
{:deleted-at (dt/now)}
|
||||||
|
{:id id :is-default false})]
|
||||||
|
(rph/with-meta (rph/wrap)
|
||||||
|
{::audit/props {:team-id (:team-id project)
|
||||||
|
:name (:name project)
|
||||||
|
:created-at (:created-at project)
|
||||||
|
:modified-at (:modified-at project)}}))))
|
||||||
|
|
||||||
|
|
||||||
@ -45,6 +45,7 @@
|
|||||||
from file as f
|
from file as f
|
||||||
inner join projects as pr on (f.project_id = pr.id)
|
inner join projects as pr on (f.project_id = pr.id)
|
||||||
where f.name ilike ('%' || ? || '%')
|
where f.name ilike ('%' || ? || '%')
|
||||||
|
and (f.deleted_at is null or f.deleted_at > now())
|
||||||
order by f.created_at asc")
|
order by f.created_at asc")
|
||||||
|
|
||||||
(defn search-files
|
(defn search-files
|
||||||
@ -64,5 +65,5 @@
|
|||||||
|
|
||||||
(sv/defmethod ::search-files
|
(sv/defmethod ::search-files
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool]} {:keys [::rpc/profile-id team-id search-term]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}]
|
||||||
(some->> search-term (search-files pool profile-id team-id)))
|
(some->> search-term (search-files pool profile-id team-id)))
|
||||||
|
|||||||
@ -13,21 +13,21 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as eml]
|
[app.email :as eml]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.climit :as climit]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.permissions :as perms]
|
||||||
[app.rpc.queries.profile :as profile]
|
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as-alias wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
@ -62,12 +62,18 @@
|
|||||||
:can-edit (or is-owner is-admin can-edit)
|
:can-edit (or is-owner is-admin can-edit)
|
||||||
:can-read true})))
|
:can-read true})))
|
||||||
|
|
||||||
|
(def has-admin-permissions?
|
||||||
|
(perms/make-admin-predicate-fn get-permissions))
|
||||||
|
|
||||||
(def has-edit-permissions?
|
(def has-edit-permissions?
|
||||||
(perms/make-edition-predicate-fn get-permissions))
|
(perms/make-edition-predicate-fn get-permissions))
|
||||||
|
|
||||||
(def has-read-permissions?
|
(def has-read-permissions?
|
||||||
(perms/make-read-predicate-fn get-permissions))
|
(perms/make-read-predicate-fn get-permissions))
|
||||||
|
|
||||||
|
(def check-admin-permissions!
|
||||||
|
(perms/make-check-fn has-admin-permissions?))
|
||||||
|
|
||||||
(def check-edition-permissions!
|
(def check-edition-permissions!
|
||||||
(perms/make-check-fn has-edit-permissions?))
|
(perms/make-check-fn has-edit-permissions?))
|
||||||
|
|
||||||
@ -83,7 +89,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-teams
|
(sv/defmethod ::get-teams
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(retrieve-teams conn profile-id)))
|
(retrieve-teams conn profile-id)))
|
||||||
|
|
||||||
@ -114,8 +120,8 @@
|
|||||||
|
|
||||||
(defn retrieve-teams
|
(defn retrieve-teams
|
||||||
[conn profile-id]
|
[conn profile-id]
|
||||||
(let [defaults (profile/retrieve-additional-data conn profile-id)]
|
(let [profile (profile/get-profile conn profile-id)]
|
||||||
(->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id])
|
(->> (db/exec! conn [sql:teams (:default-team-id profile) profile-id])
|
||||||
(mapv process-permissions))))
|
(mapv process-permissions))))
|
||||||
|
|
||||||
;; --- Query: Team (by ID)
|
;; --- Query: Team (by ID)
|
||||||
@ -128,20 +134,21 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-team
|
(sv/defmethod ::get-team
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(retrieve-team conn profile-id id)))
|
(retrieve-team conn profile-id id)))
|
||||||
|
|
||||||
(defn retrieve-team
|
(defn retrieve-team
|
||||||
[conn profile-id team-id]
|
[conn profile-id team-id]
|
||||||
(let [defaults (profile/retrieve-additional-data conn profile-id)
|
(let [profile (profile/get-profile conn profile-id)
|
||||||
sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE 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])]
|
result (db/exec-one! conn [sql (:default-team-id profile) profile-id team-id])]
|
||||||
|
|
||||||
(when-not result
|
(when-not result
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
:code :team-does-not-exist))
|
:code :team-does-not-exist))
|
||||||
(process-permissions result)))
|
|
||||||
|
|
||||||
|
(process-permissions result)))
|
||||||
|
|
||||||
;; --- Query: Team Members
|
;; --- Query: Team Members
|
||||||
|
|
||||||
@ -439,7 +446,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::leave-team
|
(sv/defmethod ::leave-team
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(leave-team conn (assoc params :profile-id profile-id))))
|
(leave-team conn (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
@ -455,7 +462,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-team
|
(sv/defmethod ::delete-team
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [perms (get-permissions conn profile-id id)]
|
(let [perms (get-permissions conn profile-id id)]
|
||||||
(when-not (:is-owner perms)
|
(when-not (:is-owner perms)
|
||||||
@ -551,7 +558,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-team-member
|
(sv/defmethod ::delete-team-member
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [perms (get-permissions conn profile-id team-id)]
|
(let [perms (get-permissions conn profile-id team-id)]
|
||||||
(when-not (or (:is-owner perms)
|
(when-not (or (:is-owner perms)
|
||||||
@ -570,7 +577,7 @@
|
|||||||
|
|
||||||
;; --- Mutation: Update Team Photo
|
;; --- Mutation: Update Team Photo
|
||||||
|
|
||||||
(declare ^:private upload-photo)
|
(declare upload-photo)
|
||||||
(declare ^:private update-team-photo)
|
(declare ^:private update-team-photo)
|
||||||
|
|
||||||
(s/def ::file ::media/upload)
|
(s/def ::file ::media/upload)
|
||||||
@ -583,57 +590,28 @@
|
|||||||
[cfg {:keys [::rpc/profile-id file] :as params}]
|
[cfg {:keys [::rpc/profile-id file] :as params}]
|
||||||
;; Validate incoming mime type
|
;; Validate incoming mime type
|
||||||
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(update-team-photo cfg (assoc params :profile-id profile-id))))
|
(update-team-photo cfg (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
(defn update-team-photo
|
(defn update-team-photo
|
||||||
[{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}]
|
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||||
(p/let [team (px/with-dispatch executor
|
(p/let [team (px/with-dispatch executor
|
||||||
(retrieve-team pool profile-id team-id))
|
(retrieve-team pool profile-id team-id))
|
||||||
photo (upload-photo cfg params)]
|
photo (profile/upload-photo cfg params)]
|
||||||
|
|
||||||
;; Mark object as touched for make it ellegible for tentative
|
(db/with-atomic [conn pool]
|
||||||
;; garbage collection.
|
(check-admin-permissions! conn profile-id team-id)
|
||||||
(when-let [id (:photo-id team)]
|
;; Mark object as touched for make it ellegible for tentative
|
||||||
(sto/touch-object! storage id))
|
;; garbage collection.
|
||||||
|
(when-let [id (:photo-id team)]
|
||||||
|
(sto/touch-object! storage id))
|
||||||
|
|
||||||
;; Save new photo
|
;; Save new photo
|
||||||
(db/update! pool :team
|
(db/update! pool :team
|
||||||
{:photo-id (:id photo)}
|
{:photo-id (:id photo)}
|
||||||
{:id team-id})
|
{:id team-id})
|
||||||
|
|
||||||
(assoc team :photo-id (:id photo))))
|
(assoc team :photo-id (:id photo)))))
|
||||||
|
|
||||||
(defn upload-photo
|
|
||||||
[{:keys [storage executor climit] :as cfg} {:keys [file]}]
|
|
||||||
(letfn [(get-info [content]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run {:cmd :info :input content})))
|
|
||||||
|
|
||||||
(generate-thumbnail [info]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run {:cmd :profile-thumbnail
|
|
||||||
:format :jpeg
|
|
||||||
:quality 85
|
|
||||||
:width 256
|
|
||||||
:height 256
|
|
||||||
:input info})))
|
|
||||||
|
|
||||||
;; Function responsible of calculating cryptographyc hash of
|
|
||||||
;; the provided data.
|
|
||||||
(calculate-hash [data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(sto/calculate-hash data)))]
|
|
||||||
|
|
||||||
(p/let [info (get-info file)
|
|
||||||
thumb (generate-thumbnail info)
|
|
||||||
hash (calculate-hash (:data thumb))
|
|
||||||
content (-> (sto/content (:data thumb) (:size thumb))
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage {::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
:bucket "profile"
|
|
||||||
:content-type (:mtype thumb)}))))
|
|
||||||
|
|
||||||
;; --- Mutation: Create Team Invitation
|
;; --- Mutation: Create Team Invitation
|
||||||
|
|
||||||
@ -664,7 +642,7 @@
|
|||||||
|
|
||||||
(defn- create-invitation
|
(defn- create-invitation
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||||
(let [member (profile/retrieve-profile-data-by-email conn email)]
|
(let [member (profile/get-profile-by-email conn email)]
|
||||||
|
|
||||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -753,13 +731,18 @@
|
|||||||
"A rpc call that allow to send a single or multiple invitations to
|
"A rpc call that allow to send a single or multiple invitations to
|
||||||
join the team."
|
join the team."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [perms (get-permissions conn profile-id team-id)
|
(let [perms (get-permissions conn profile-id team-id)
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
profile (db/get-by-id conn :profile profile-id)
|
||||||
team (db/get-by-id conn :team team-id)
|
team (db/get-by-id conn :team team-id)
|
||||||
emails (cond-> (or emails #{}) (string? email) (conj email))]
|
|
||||||
|
|
||||||
|
;; Members emails. We don't re-send inviation to already existing members
|
||||||
|
member? (into #{}
|
||||||
|
(map :email)
|
||||||
|
(db/exec! conn [sql:team-members team-id]))
|
||||||
|
|
||||||
|
emails (cond-> (or emails #{}) (string? email) (conj email))]
|
||||||
|
|
||||||
(run! (partial quotes/check-quote! conn)
|
(run! (partial quotes/check-quote! conn)
|
||||||
(list {::quotes/id ::quotes/invitations-per-team
|
(list {::quotes/id ::quotes/invitations-per-team
|
||||||
@ -783,6 +766,7 @@
|
|||||||
|
|
||||||
(let [cfg (assoc cfg ::db/conn conn)
|
(let [cfg (assoc cfg ::db/conn conn)
|
||||||
invitations (->> emails
|
invitations (->> emails
|
||||||
|
(remove member?)
|
||||||
(map (fn [email]
|
(map (fn [email]
|
||||||
{:email (str/lower email)
|
{:email (str/lower email)
|
||||||
:team team
|
:team team
|
||||||
@ -802,7 +786,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::create-team-with-invitations
|
(sv/defmethod ::create-team-with-invitations
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [params (assoc params :profile-id profile-id)
|
(let [params (assoc params :profile-id profile-id)
|
||||||
team (create-team conn params)
|
team (create-team conn params)
|
||||||
@ -855,7 +839,7 @@
|
|||||||
{:team-id team-id
|
{:team-id team-id
|
||||||
:email-to (str/lower email)})
|
:email-to (str/lower email)})
|
||||||
(update :role keyword))
|
(update :role keyword))
|
||||||
member (profile/retrieve-profile-data-by-email pool (:email-to invit))
|
member (profile/get-profile-by-email pool (:email-to invit))
|
||||||
token (create-invitation-token cfg {:team-id (:team-id invit)
|
token (create-invitation-token cfg {:team-id (:team-id invit)
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:valid-until (:valid-until invit)
|
:valid-until (:valid-until invit)
|
||||||
@ -872,7 +856,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-team-invitation-role
|
(sv/defmethod ::update-team-invitation-role
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [perms (get-permissions conn profile-id team-id)]
|
(let [perms (get-permissions conn profile-id team-id)]
|
||||||
|
|
||||||
@ -893,7 +877,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-team-invitation
|
(sv/defmethod ::delete-team-invitation
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [perms (get-permissions conn profile-id team-id)]
|
(let [perms (get-permissions conn profile-id team-id)]
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,12 @@
|
|||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
|
[app.main :as-alias main]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.queries.profile :as profile]
|
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
||||||
@ -34,15 +35,15 @@
|
|||||||
(sv/defmethod ::verify-token
|
(sv/defmethod ::verify-token
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::doc/added "1.15"}
|
::doc/added "1.15"}
|
||||||
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [claims (tokens/verify sprops {:token token})
|
(let [claims (tokens/verify (::main/props cfg) {:token token})
|
||||||
cfg (assoc cfg :conn conn)]
|
cfg (assoc cfg :conn conn)]
|
||||||
(process-token cfg params claims))))
|
(process-token cfg params claims))))
|
||||||
|
|
||||||
(defmethod process-token :change-email
|
(defmethod process-token :change-email
|
||||||
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||||
(when (profile/retrieve-profile-data-by-email conn email)
|
(when (profile/get-profile-by-email conn email)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-already-exists))
|
:code :email-already-exists))
|
||||||
|
|
||||||
@ -56,8 +57,8 @@
|
|||||||
::audit/profile-id profile-id}))
|
::audit/profile-id profile-id}))
|
||||||
|
|
||||||
(defmethod process-token :verify-email
|
(defmethod process-token :verify-email
|
||||||
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
|
[{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||||
(let [profile (profile/retrieve-profile conn profile-id)
|
(let [profile (profile/get-profile conn profile-id)
|
||||||
claims (assoc claims :profile profile)]
|
claims (assoc claims :profile profile)]
|
||||||
|
|
||||||
(when-not (:is-active profile)
|
(when-not (:is-active profile)
|
||||||
@ -71,14 +72,14 @@
|
|||||||
{:id (:id profile)}))
|
{:id (:id profile)}))
|
||||||
|
|
||||||
(-> claims
|
(-> claims
|
||||||
(rph/with-transform (session/create-fn session profile-id))
|
(rph/with-transform (session/create-fn cfg profile-id))
|
||||||
(rph/with-meta {::audit/name "verify-profile-email"
|
(rph/with-meta {::audit/name "verify-profile-email"
|
||||||
::audit/props (audit/profile->props profile)
|
::audit/props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)}))))
|
::audit/profile-id (:id profile)}))))
|
||||||
|
|
||||||
(defmethod process-token :auth
|
(defmethod process-token :auth
|
||||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||||
(let [profile (profile/retrieve-profile conn profile-id)]
|
(let [profile (profile/get-profile conn profile-id)]
|
||||||
(assoc claims :profile profile)))
|
(assoc claims :profile profile)))
|
||||||
|
|
||||||
;; --- Team Invitation
|
;; --- Team Invitation
|
||||||
|
|||||||
@ -13,11 +13,10 @@
|
|||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.cond :as-alias cond]
|
[app.rpc.cond :as-alias cond]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.queries.share-link :as slnk]
|
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
;; --- Query: View Only Bundle
|
;; --- QUERY: View Only Bundle
|
||||||
|
|
||||||
(defn- get-project
|
(defn- get-project
|
||||||
[conn id]
|
[conn id]
|
||||||
@ -31,7 +30,15 @@
|
|||||||
users (comments/get-file-comments-users conn file-id profile-id)
|
users (comments/get-file-comments-users conn file-id profile-id)
|
||||||
|
|
||||||
links (->> (db/query conn :share-link {:file-id file-id})
|
links (->> (db/query conn :share-link {:file-id file-id})
|
||||||
(mapv slnk/decode-share-link-row))
|
(mapv (fn [row]
|
||||||
|
(-> row
|
||||||
|
(update :pages db/decode-pgarray #{})
|
||||||
|
;; NOTE: the flags are deprecated but are still present
|
||||||
|
;; on the table on old rows. The flags are pgarray and
|
||||||
|
;; for avoid decoding it (because they are no longer used
|
||||||
|
;; on frontend) we just dissoc the column attribute from
|
||||||
|
;; row.
|
||||||
|
(dissoc :flags)))))
|
||||||
|
|
||||||
fonts (db/query conn :team-font-variant
|
fonts (db/query conn :team-font-variant
|
||||||
{:team-id (:team-id project)
|
{:team-id (:team-id project)
|
||||||
@ -84,6 +91,6 @@
|
|||||||
::cond/key-fn files/get-file-etag
|
::cond/key-fn files/get-file-etag
|
||||||
::cond/reuse-key? true
|
::cond/reuse-key? true
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[{:keys [pool]} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(get-view-only-bundle conn (assoc params :profile-id profile-id))))
|
(get-view-only-bundle conn (assoc params :profile-id profile-id))))
|
||||||
|
|||||||
@ -148,7 +148,7 @@
|
|||||||
from webhook where team_id = ? order by uri")
|
from webhook where team_id = ? order by uri")
|
||||||
|
|
||||||
(sv/defmethod ::get-webhooks
|
(sv/defmethod ::get-webhooks
|
||||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id team-id)
|
(check-read-permissions! conn profile-id team-id)
|
||||||
(->> (db/exec! conn [sql:get-webhooks team-id])
|
(->> (db/exec! conn [sql:get-webhooks team-id])
|
||||||
|
|||||||
@ -70,6 +70,8 @@
|
|||||||
(respond (yrs/response 404)))))
|
(respond (yrs/response 404)))))
|
||||||
|
|
||||||
|
|
||||||
|
(s/def ::routes vector?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::routes [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req-un [::rpc/methods]))
|
(s/keys :req-un [::rpc/methods]))
|
||||||
|
|
||||||
|
|||||||
@ -1,239 +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/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.rpc.mutations.files
|
|
||||||
(:require
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.db :as db]
|
|
||||||
[app.loggers.audit :as-alias audit]
|
|
||||||
[app.rpc.climit :as-alias climit]
|
|
||||||
[app.rpc.commands.files :as cmd.files]
|
|
||||||
[app.rpc.commands.files.create :as cmd.files.create]
|
|
||||||
[app.rpc.commands.files.temp :as cmd.files.temp]
|
|
||||||
[app.rpc.commands.files.update :as cmd.files.update]
|
|
||||||
[app.rpc.doc :as-alias doc]
|
|
||||||
[app.rpc.helpers :as rph]
|
|
||||||
[app.rpc.queries.projects :as proj]
|
|
||||||
[app.util.services :as sv]
|
|
||||||
[app.util.time :as dt]
|
|
||||||
[clojure.spec.alpha :as s]))
|
|
||||||
|
|
||||||
;; --- Mutation: Create File
|
|
||||||
|
|
||||||
(s/def ::create-file ::cmd.files.create/create-file)
|
|
||||||
|
|
||||||
(sv/defmethod ::create-file
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id features components-v2] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(proj/check-edition-permissions! conn profile-id project-id)
|
|
||||||
(let [team-id (cmd.files/get-team-id conn project-id)
|
|
||||||
features (cond-> (or features #{})
|
|
||||||
;; BACKWARD COMPATIBILITY with the components-v2 param
|
|
||||||
components-v2 (conj "components/v2"))
|
|
||||||
params (assoc params :features features)]
|
|
||||||
(-> (cmd.files.create/create-file conn params)
|
|
||||||
(vary-meta assoc ::audit/props {:team-id team-id})))))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Rename File
|
|
||||||
|
|
||||||
(s/def ::rename-file ::cmd.files/rename-file)
|
|
||||||
|
|
||||||
(sv/defmethod ::rename-file
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id id)
|
|
||||||
(cmd.files/rename-file conn params)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Set File shared
|
|
||||||
|
|
||||||
(s/def ::set-file-shared ::cmd.files/set-file-shared)
|
|
||||||
|
|
||||||
(sv/defmethod ::set-file-shared
|
|
||||||
{::doc/added "1.2"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id is-shared] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id id)
|
|
||||||
(when-not is-shared
|
|
||||||
(cmd.files/absorb-library conn params)
|
|
||||||
(cmd.files/unlink-files conn params))
|
|
||||||
(cmd.files/set-file-shared conn params)))
|
|
||||||
|
|
||||||
;; --- Mutation: Delete File
|
|
||||||
|
|
||||||
(s/def ::delete-file ::cmd.files/delete-file)
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-file
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id id)
|
|
||||||
(cmd.files/absorb-library conn params)
|
|
||||||
(cmd.files/mark-file-deleted conn params)
|
|
||||||
nil))
|
|
||||||
|
|
||||||
;; --- Mutation: Link file to library
|
|
||||||
|
|
||||||
(s/def ::link-file-to-library ::cmd.files/link-file-to-library)
|
|
||||||
|
|
||||||
(sv/defmethod ::link-file-to-library
|
|
||||||
{::doc/added "1.3"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}]
|
|
||||||
(when (= file-id library-id)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-library
|
|
||||||
:hint "A file cannot be linked to itself"))
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id library-id)
|
|
||||||
(cmd.files/link-file-to-library conn params)))
|
|
||||||
|
|
||||||
;; --- Mutation: Unlink file from library
|
|
||||||
|
|
||||||
(s/def ::unlink-file-from-library ::cmd.files/unlink-file-from-library)
|
|
||||||
|
|
||||||
(sv/defmethod ::unlink-file-from-library
|
|
||||||
{::doc/added "1.3"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
|
||||||
(cmd.files/unlink-file-from-library conn params)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Update synchronization status of a link
|
|
||||||
|
|
||||||
(s/def ::update-sync ::cmd.files/update-file-library-sync-status)
|
|
||||||
|
|
||||||
(sv/defmethod ::update-sync
|
|
||||||
{::doc/added "1.10"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
|
||||||
(cmd.files/update-sync conn params)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Ignore updates in linked files
|
|
||||||
|
|
||||||
(declare ignore-sync)
|
|
||||||
|
|
||||||
(s/def ::ignore-sync ::cmd.files/ignore-file-library-sync-status)
|
|
||||||
|
|
||||||
(sv/defmethod ::ignore-sync
|
|
||||||
{::doc/added "1.10"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
|
||||||
(cmd.files/ignore-sync conn params)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION: update-file
|
|
||||||
|
|
||||||
(s/def ::components-v2 ::us/boolean)
|
|
||||||
(s/def ::update-file
|
|
||||||
(s/and ::cmd.files.update/update-file
|
|
||||||
(s/keys :opt-un [::components-v2])))
|
|
||||||
|
|
||||||
(sv/defmethod ::update-file
|
|
||||||
{::climit/queue :update-file
|
|
||||||
::climit/key-fn :id
|
|
||||||
::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id features components-v2] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(db/xact-lock! conn id)
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id id)
|
|
||||||
|
|
||||||
(let [;; BACKWARD COMPATIBILITY with the components-v2 parameter
|
|
||||||
features (cond-> (or features #{})
|
|
||||||
components-v2 (conj "components/v2"))
|
|
||||||
tpoint (dt/tpoint)
|
|
||||||
params (assoc params :features features)
|
|
||||||
cfg (assoc cfg :conn conn)]
|
|
||||||
|
|
||||||
(-> (cmd.files.update/update-file cfg params)
|
|
||||||
(rph/with-defer #(let [elapsed (tpoint)]
|
|
||||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
|
|
||||||
|
|
||||||
;; --- Mutation: upsert object thumbnail
|
|
||||||
|
|
||||||
(s/def ::upsert-file-object-thumbnail ::cmd.files/upsert-file-object-thumbnail)
|
|
||||||
|
|
||||||
(sv/defmethod ::upsert-file-object-thumbnail
|
|
||||||
{::doc/added "1.13"
|
|
||||||
::doc/deprecated "1.17"
|
|
||||||
::audit/skip true}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
|
||||||
(cmd.files/upsert-file-object-thumbnail! conn params)
|
|
||||||
nil))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: upsert file thumbnail
|
|
||||||
|
|
||||||
(s/def ::upsert-file-thumbnail ::cmd.files/upsert-file-thumbnail)
|
|
||||||
|
|
||||||
(sv/defmethod ::upsert-file-thumbnail
|
|
||||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
|
||||||
grid thumbnails."
|
|
||||||
{::doc/added "1.13"
|
|
||||||
::doc/deprecated "1.17"
|
|
||||||
::audit/skip true}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
|
||||||
(cmd.files/upsert-file-thumbnail conn params)
|
|
||||||
nil))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: create-temp-file
|
|
||||||
|
|
||||||
(s/def ::create-temp-file ::cmd.files.temp/create-temp-file)
|
|
||||||
|
|
||||||
(sv/defmethod ::create-temp-file
|
|
||||||
{::doc/added "1.7"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(proj/check-edition-permissions! conn profile-id project-id)
|
|
||||||
(cmd.files.create/create-file conn (assoc params :deleted-at (dt/in-future {:days 1})))))
|
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: update-temp-file
|
|
||||||
|
|
||||||
(s/def ::update-temp-file ::cmd.files.temp/update-temp-file)
|
|
||||||
|
|
||||||
(sv/defmethod ::update-temp-file
|
|
||||||
{::doc/added "1.7"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files.temp/update-temp-file conn params)
|
|
||||||
nil))
|
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: persist-temp-file
|
|
||||||
|
|
||||||
(s/def ::persist-temp-file ::cmd.files.temp/persist-temp-file)
|
|
||||||
|
|
||||||
(sv/defmethod ::persist-temp-file
|
|
||||||
{::doc/added "1.7"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.files/check-edition-permissions! conn profile-id id)
|
|
||||||
(cmd.files.temp/persist-temp-file conn params)))
|
|
||||||
@ -6,15 +6,12 @@
|
|||||||
|
|
||||||
(ns app.rpc.mutations.fonts
|
(ns app.rpc.mutations.fonts
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.audit :as-alias audit]
|
[app.loggers.audit :as-alias audit]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc.climit :as-alias climit]
|
[app.rpc.commands.fonts :as fonts]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
@ -22,9 +19,7 @@
|
|||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]))
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
(declare create-font-variant)
|
(declare create-font-variant)
|
||||||
|
|
||||||
@ -44,82 +39,19 @@
|
|||||||
(s/keys :req-un [::profile-id ::team-id ::data
|
(s/keys :req-un [::profile-id ::team-id ::data
|
||||||
::font-id ::font-family ::font-weight ::font-style]))
|
::font-id ::font-family ::font-weight ::font-style]))
|
||||||
|
|
||||||
|
(declare create-font-variant)
|
||||||
|
|
||||||
(sv/defmethod ::create-font-variant
|
(sv/defmethod ::create-font-variant
|
||||||
{::doc/added "1.3"
|
{::doc/added "1.3"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(teams/check-edition-permissions! pool profile-id team-id)
|
(teams/check-edition-permissions! pool profile-id team-id)
|
||||||
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
|
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
|
||||||
::quotes/profile-id profile-id
|
::quotes/profile-id profile-id
|
||||||
::quotes/team-id team-id})
|
::quotes/team-id team-id})
|
||||||
(create-font-variant cfg params)))
|
(fonts/create-font-variant cfg params)))
|
||||||
|
|
||||||
(defn create-font-variant
|
|
||||||
[{:keys [storage pool executor climit] :as cfg} {:keys [data] :as params}]
|
|
||||||
(letfn [(generate-fonts [data]
|
|
||||||
(climit/with-dispatch (:process-font climit)
|
|
||||||
(media/run {:cmd :generate-fonts :input data})))
|
|
||||||
|
|
||||||
;; Function responsible of calculating cryptographyc hash of
|
|
||||||
;; the provided data.
|
|
||||||
(calculate-hash [data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(sto/calculate-hash data)))
|
|
||||||
|
|
||||||
(validate-data [data]
|
|
||||||
(when (and (not (contains? data "font/otf"))
|
|
||||||
(not (contains? data "font/ttf"))
|
|
||||||
(not (contains? data "font/woff"))
|
|
||||||
(not (contains? data "font/woff2")))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-font-upload))
|
|
||||||
data)
|
|
||||||
|
|
||||||
(persist-font-object [data mtype]
|
|
||||||
(when-let [resource (get data mtype)]
|
|
||||||
(p/let [hash (calculate-hash resource)
|
|
||||||
content (-> (sto/content resource)
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage {::sto/content content
|
|
||||||
::sto/touched-at (dt/now)
|
|
||||||
::sto/deduplicate? true
|
|
||||||
:content-type mtype
|
|
||||||
:bucket "team-font-variant"}))))
|
|
||||||
|
|
||||||
(persist-fonts [data]
|
|
||||||
(p/let [otf (persist-font-object data "font/otf")
|
|
||||||
ttf (persist-font-object data "font/ttf")
|
|
||||||
woff1 (persist-font-object data "font/woff")
|
|
||||||
woff2 (persist-font-object data "font/woff2")]
|
|
||||||
|
|
||||||
(d/without-nils
|
|
||||||
{:otf otf
|
|
||||||
:ttf ttf
|
|
||||||
:woff1 woff1
|
|
||||||
:woff2 woff2})))
|
|
||||||
|
|
||||||
(insert-into-db [{:keys [woff1 woff2 otf ttf]}]
|
|
||||||
(db/insert! pool :team-font-variant
|
|
||||||
{:id (uuid/next)
|
|
||||||
:team-id (:team-id params)
|
|
||||||
:font-id (:font-id params)
|
|
||||||
:font-family (:font-family params)
|
|
||||||
:font-weight (:font-weight params)
|
|
||||||
:font-style (:font-style params)
|
|
||||||
:woff1-file-id (:id woff1)
|
|
||||||
:woff2-file-id (:id woff2)
|
|
||||||
:otf-file-id (:id otf)
|
|
||||||
:ttf-file-id (:id ttf)}))
|
|
||||||
]
|
|
||||||
|
|
||||||
(->> (generate-fonts data)
|
|
||||||
(p/fmap validate-data)
|
|
||||||
(p/mcat executor persist-fonts)
|
|
||||||
(p/fmap executor insert-into-db)
|
|
||||||
(p/fmap (fn [result]
|
|
||||||
(let [params (update params :data (comp vec keys))]
|
|
||||||
(rph/with-meta result {::audit/replace-props params})))))))
|
|
||||||
|
|
||||||
;; --- UPDATE FONT FAMILY
|
;; --- UPDATE FONT FAMILY
|
||||||
|
|
||||||
@ -128,6 +60,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-font
|
(sv/defmethod ::update-font
|
||||||
{::doc/added "1.3"
|
{::doc/added "1.3"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}]
|
[{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
@ -149,6 +82,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-font
|
(sv/defmethod ::delete-font
|
||||||
{::doc/added "1.3"
|
{::doc/added "1.3"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
@ -169,6 +103,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-font-variant
|
(sv/defmethod ::delete-font-variant
|
||||||
{::doc/added "1.3"
|
{::doc/added "1.3"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.media :as cmd.media]
|
[app.rpc.commands.media :as cmd.media]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.storage :as-alias sto]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
@ -20,9 +21,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::upload-file-media-object
|
(sv/defmethod ::upload-file-media-object
|
||||||
{::doc/added "1.2"
|
{::doc/added "1.2"
|
||||||
::doc/deprecated "1.17"}
|
::doc/deprecated "1.18"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(media/validate-media-type! content)
|
(media/validate-media-type! content)
|
||||||
(cmd.media/validate-content-size! content)
|
(cmd.media/validate-content-size! content)
|
||||||
@ -34,9 +35,9 @@
|
|||||||
|
|
||||||
(sv/defmethod ::create-file-media-object-from-url
|
(sv/defmethod ::create-file-media-object-from-url
|
||||||
{::doc/added "1.3"
|
{::doc/added "1.3"
|
||||||
::doc/deprecated "1.17"}
|
::doc/deprecated "1.18"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(#'cmd.media/create-file-media-object-from-url cfg params)))
|
(#'cmd.media/create-file-media-object-from-url cfg params)))
|
||||||
|
|
||||||
@ -46,7 +47,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::clone-file-media-object
|
(sv/defmethod ::clone-file-media-object
|
||||||
{::doc/added "1.2"
|
{::doc/added "1.2"
|
||||||
::doc/deprecated "1.17"}
|
::doc/deprecated "1.18"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
|
|||||||
@ -6,31 +6,23 @@
|
|||||||
|
|
||||||
(ns app.rpc.mutations.profile
|
(ns app.rpc.mutations.profile
|
||||||
(:require
|
(:require
|
||||||
[app.auth :as auth]
|
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as eml]
|
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc :as-alias rpc]
|
|
||||||
[app.rpc.climit :as-alias climit]
|
[app.rpc.climit :as-alias climit]
|
||||||
[app.rpc.commands.auth :as cmd.auth]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.queries.profile :as profile]
|
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tokens :as tokens]
|
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]))
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
;; --- Helpers & Specs
|
||||||
|
|
||||||
@ -40,7 +32,7 @@
|
|||||||
(s/def ::path ::us/string)
|
(s/def ::path ::us/string)
|
||||||
(s/def ::profile-id ::us/uuid)
|
(s/def ::profile-id ::us/uuid)
|
||||||
(s/def ::password ::us/not-empty-string)
|
(s/def ::password ::us/not-empty-string)
|
||||||
(s/def ::old-password ::us/not-empty-string)
|
(s/def ::old-password (s/nilable ::us/string))
|
||||||
(s/def ::theme ::us/string)
|
(s/def ::theme ::us/string)
|
||||||
|
|
||||||
;; --- MUTATION: Update Profile (own)
|
;; --- MUTATION: Update Profile (own)
|
||||||
@ -50,14 +42,15 @@
|
|||||||
:opt-un [::lang ::theme]))
|
:opt-un [::lang ::theme]))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile
|
(sv/defmethod ::update-profile
|
||||||
{::doc/added "1.0"}
|
{::doc/added "1.0"
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id fullname lang theme] :as params}]
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id fullname lang theme] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
;; NOTE: we need to retrieve the profile independently if we use
|
;; NOTE: we need to retrieve the profile independently if we use
|
||||||
;; it or not for explicit locking and avoid concurrent updates of
|
;; it or not for explicit locking and avoid concurrent updates of
|
||||||
;; the same row/object.
|
;; the same row/object.
|
||||||
(let [profile (-> (db/get-by-id conn :profile profile-id {:for-update true})
|
(let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true)
|
||||||
(profile/decode-profile-row))
|
(profile/decode-row))
|
||||||
|
|
||||||
;; Update the profile map with direct params
|
;; Update the profile map with direct params
|
||||||
profile (-> profile
|
profile (-> profile
|
||||||
@ -74,161 +67,68 @@
|
|||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
||||||
(-> profile
|
(-> profile
|
||||||
profile/strip-private-attrs
|
(profile/strip-private-attrs)
|
||||||
d/without-nils
|
(d/without-nils)
|
||||||
(rph/with-meta {::audit/props (audit/profile->props profile)})))))
|
(rph/with-meta {::audit/props (audit/profile->props profile)})))))
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION: Update Password
|
;; --- MUTATION: Update Password
|
||||||
|
|
||||||
(declare validate-password!)
|
|
||||||
(declare update-profile-password!)
|
|
||||||
(declare invalidate-profile-session!)
|
|
||||||
|
|
||||||
(s/def ::update-profile-password
|
(s/def ::update-profile-password
|
||||||
(s/keys :req-un [::profile-id ::password ::old-password]))
|
(s/keys :req-un [::profile-id ::password ::old-password]))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-password
|
(sv/defmethod ::update-profile-password
|
||||||
{::climit/queue :auth}
|
{::climit/queue :auth
|
||||||
[{:keys [pool] :as cfg} {:keys [password] :as params}]
|
::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [password] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (validate-password! conn params)
|
(let [profile (#'profile/validate-password! conn params)
|
||||||
session-id (::rpc/session-id params)]
|
session-id (::session/id params)]
|
||||||
(when (= (str/lower (:email profile))
|
(when (= (str/lower (:email profile))
|
||||||
(str/lower (:password params)))
|
(str/lower (:password params)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-as-password
|
:code :email-as-password
|
||||||
:hint "you can't use your email as password"))
|
:hint "you can't use your email as password"))
|
||||||
(update-profile-password! conn (assoc profile :password password))
|
(profile/update-profile-password! conn (assoc profile :password password))
|
||||||
(invalidate-profile-session! conn (:id profile) session-id)
|
(#'profile/invalidate-profile-session! conn (:id profile) session-id)
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(defn- invalidate-profile-session!
|
|
||||||
"Removes all sessions except the current one."
|
|
||||||
[conn profile-id session-id]
|
|
||||||
(let [sql "delete from http_session where profile_id = ? and id != ?"]
|
|
||||||
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
|
|
||||||
|
|
||||||
(defn- validate-password!
|
|
||||||
[conn {:keys [profile-id old-password] :as params}]
|
|
||||||
(let [profile (db/get-by-id conn :profile profile-id)]
|
|
||||||
(when-not (:valid (auth/verify-password old-password (:password profile)))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :old-password-not-match))
|
|
||||||
profile))
|
|
||||||
|
|
||||||
(defn update-profile-password!
|
|
||||||
[conn {:keys [id password] :as profile}]
|
|
||||||
(db/update! conn :profile
|
|
||||||
{:password (auth/derive-password password)}
|
|
||||||
{:id id}))
|
|
||||||
|
|
||||||
;; --- MUTATION: Update Photo
|
;; --- MUTATION: Update Photo
|
||||||
|
|
||||||
(declare update-profile-photo)
|
|
||||||
|
|
||||||
(s/def ::file ::media/upload)
|
(s/def ::file ::media/upload)
|
||||||
(s/def ::update-profile-photo
|
(s/def ::update-profile-photo
|
||||||
(s/keys :req-un [::profile-id ::file]))
|
(s/keys :req-un [::profile-id ::file]))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-photo
|
(sv/defmethod ::update-profile-photo
|
||||||
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
[cfg {:keys [file] :as params}]
|
[cfg {:keys [file] :as params}]
|
||||||
;; Validate incoming mime type
|
;; Validate incoming mime type
|
||||||
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(update-profile-photo cfg params)))
|
(profile/update-profile-photo cfg params)))
|
||||||
|
|
||||||
(defn update-profile-photo
|
|
||||||
[{:keys [pool storage executor] :as cfg} {:keys [profile-id file] :as params}]
|
|
||||||
(p/let [profile (px/with-dispatch executor
|
|
||||||
(db/get-by-id pool :profile profile-id))
|
|
||||||
photo (teams/upload-photo cfg params)]
|
|
||||||
|
|
||||||
;; Schedule deletion of old photo
|
|
||||||
(when-let [id (:photo-id profile)]
|
|
||||||
(sto/touch-object! storage id))
|
|
||||||
|
|
||||||
;; Save new photo
|
|
||||||
(db/update! pool :profile
|
|
||||||
{:photo-id (:id photo)}
|
|
||||||
{:id profile-id})
|
|
||||||
|
|
||||||
(-> (rph/wrap)
|
|
||||||
(rph/with-meta {::audit/replace-props
|
|
||||||
{:file-name (:filename file)
|
|
||||||
:file-size (:size file)
|
|
||||||
:file-path (str (:path file))
|
|
||||||
:file-mtype (:mtype file)}}))))
|
|
||||||
|
|
||||||
;; --- MUTATION: Request Email Change
|
;; --- MUTATION: Request Email Change
|
||||||
|
|
||||||
(declare request-email-change)
|
|
||||||
(declare change-email-immediately)
|
|
||||||
|
|
||||||
(s/def ::request-email-change
|
(s/def ::request-email-change
|
||||||
(s/keys :req-un [::email]))
|
(s/keys :req-un [::email]))
|
||||||
|
|
||||||
(sv/defmethod ::request-email-change
|
(sv/defmethod ::request-email-change
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id email] :as params}]
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id email] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (db/get-by-id conn :profile profile-id)
|
(let [profile (db/get-by-id conn :profile profile-id)
|
||||||
cfg (assoc cfg :conn conn)
|
cfg (assoc cfg ::profile/conn conn)
|
||||||
params (assoc params
|
params (assoc params
|
||||||
:profile profile
|
:profile profile
|
||||||
:email (str/lower email))]
|
:email (str/lower email))]
|
||||||
|
|
||||||
(if (contains? cf/flags :smtp)
|
(if (contains? cf/flags :smtp)
|
||||||
(request-email-change cfg params)
|
(#'profile/request-email-change! cfg params)
|
||||||
(change-email-immediately cfg params)))))
|
(#'profile/change-email-immediately! cfg params)))))
|
||||||
|
|
||||||
(defn- change-email-immediately
|
|
||||||
[{:keys [conn]} {:keys [profile email] :as params}]
|
|
||||||
(when (not= email (:email profile))
|
|
||||||
(cmd.auth/check-profile-existence! conn params))
|
|
||||||
(db/update! conn :profile
|
|
||||||
{:email email}
|
|
||||||
{:id (:id profile)})
|
|
||||||
{:changed true})
|
|
||||||
|
|
||||||
(defn- request-email-change
|
|
||||||
[{:keys [conn sprops] :as cfg} {:keys [profile email] :as params}]
|
|
||||||
(let [token (tokens/generate sprops
|
|
||||||
{:iss :change-email
|
|
||||||
:exp (dt/in-future "15m")
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:email email})
|
|
||||||
ptoken (tokens/generate sprops
|
|
||||||
{:iss :profile-identity
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:exp (dt/in-future {:days 30})})]
|
|
||||||
|
|
||||||
(when (not= email (:email profile))
|
|
||||||
(cmd.auth/check-profile-existence! conn params))
|
|
||||||
|
|
||||||
(when-not (eml/allow-send-emails? conn profile)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :profile-is-muted
|
|
||||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
|
||||||
|
|
||||||
(when (eml/has-bounce-reports? conn email)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-has-permanent-bounces
|
|
||||||
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
|
||||||
|
|
||||||
(eml/send! {::eml/conn conn
|
|
||||||
::eml/factory eml/change-email
|
|
||||||
:public-uri (:public-uri cfg)
|
|
||||||
:to (:email profile)
|
|
||||||
:name (:fullname profile)
|
|
||||||
:pending-email email
|
|
||||||
:token token
|
|
||||||
:extra-data ptoken})
|
|
||||||
nil))
|
|
||||||
|
|
||||||
|
|
||||||
(defn select-profile-for-update
|
|
||||||
[conn id]
|
|
||||||
(db/get-by-id conn :profile id {:for-update true}))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION: Update Profile Props
|
;; --- MUTATION: Update Profile Props
|
||||||
|
|
||||||
@ -237,9 +137,11 @@
|
|||||||
(s/keys :req-un [::profile-id ::props]))
|
(s/keys :req-un [::profile-id ::props]))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-props
|
(sv/defmethod ::update-profile-props
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id props]}]
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id props]}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (profile/retrieve-profile-data conn profile-id)
|
(let [profile (profile/get-profile conn profile-id ::db/for-update? true)
|
||||||
props (reduce-kv (fn [props k v]
|
props (reduce-kv (fn [props k v]
|
||||||
;; We don't accept namespaced keys
|
;; We don't accept namespaced keys
|
||||||
(if (simple-ident? k)
|
(if (simple-ident? k)
|
||||||
@ -254,22 +156,20 @@
|
|||||||
{:props (db/tjson props)}
|
{:props (db/tjson props)}
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
||||||
(profile/filter-profile-props props))))
|
(profile/filter-props props))))
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION: Delete Profile
|
;; --- MUTATION: Delete Profile
|
||||||
|
|
||||||
(declare get-owned-teams-with-participants)
|
|
||||||
(declare check-can-delete-profile!)
|
|
||||||
(declare mark-profile-as-deleted!)
|
|
||||||
|
|
||||||
(s/def ::delete-profile
|
(s/def ::delete-profile
|
||||||
(s/keys :req-un [::profile-id]))
|
(s/keys :req-un [::profile-id]))
|
||||||
|
|
||||||
(sv/defmethod ::delete-profile
|
(sv/defmethod ::delete-profile
|
||||||
[{:keys [pool session] :as cfg} {:keys [profile-id] :as params}]
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [teams (get-owned-teams-with-participants conn profile-id)
|
(let [teams (#'profile/get-owned-teams-with-participants conn profile-id)
|
||||||
deleted-at (dt/now)]
|
deleted-at (dt/now)]
|
||||||
|
|
||||||
;; If we found owned teams with participants, we don't allow
|
;; If we found owned teams with participants, we don't allow
|
||||||
@ -290,22 +190,4 @@
|
|||||||
{:deleted-at deleted-at}
|
{:deleted-at deleted-at}
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
||||||
(rph/with-transform {} (session/delete-fn session)))))
|
(rph/with-transform {} (session/delete-fn cfg)))))
|
||||||
|
|
||||||
(def sql:owned-teams
|
|
||||||
"with owner_teams as (
|
|
||||||
select tpr.team_id as id
|
|
||||||
from team_profile_rel as tpr
|
|
||||||
where tpr.is_owner is true
|
|
||||||
and tpr.profile_id = ?
|
|
||||||
)
|
|
||||||
select tpr.team_id as id,
|
|
||||||
count(tpr.profile_id) - 1 as participants
|
|
||||||
from team_profile_rel as tpr
|
|
||||||
where tpr.team_id in (select id from owner_teams)
|
|
||||||
and tpr.profile_id != ?
|
|
||||||
group by 1")
|
|
||||||
|
|
||||||
(defn- get-owned-teams-with-participants
|
|
||||||
[conn profile-id]
|
|
||||||
(db/exec! conn [sql:owned-teams profile-id profile-id]))
|
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.audit :as-alias audit]
|
[app.loggers.audit :as-alias audit]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.queries.projects :as proj]
|
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::create-project
|
(sv/defmethod ::create-project
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
@ -70,12 +71,13 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-project-pin
|
(sv/defmethod ::update-project-pin
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/batch-timeout (dt/duration "5s")
|
::webhooks/batch-timeout (dt/duration "5s")
|
||||||
::webhooks/batch-key :id
|
::webhooks/batch-key :id
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id team-id is-pinned] :as params}]
|
[{:keys [pool] :as cfg} {:keys [id profile-id team-id is-pinned] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(proj/check-edition-permissions! conn profile-id id)
|
(projects/check-edition-permissions! conn profile-id id)
|
||||||
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
@ -88,10 +90,11 @@
|
|||||||
|
|
||||||
(sv/defmethod ::rename-project
|
(sv/defmethod ::rename-project
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id name] :as params}]
|
[{:keys [pool] :as cfg} {:keys [id profile-id name] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(proj/check-edition-permissions! conn profile-id id)
|
(projects/check-edition-permissions! conn profile-id id)
|
||||||
(let [project (db/get-by-id conn :project id)]
|
(let [project (db/get-by-id conn :project id)]
|
||||||
(db/update! conn :project
|
(db/update! conn :project
|
||||||
{:name name}
|
{:name name}
|
||||||
@ -112,10 +115,11 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-project
|
(sv/defmethod ::delete-project
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(proj/check-edition-permissions! conn profile-id id)
|
(projects/check-edition-permissions! conn profile-id id)
|
||||||
(let [project (db/update! conn :project
|
(let [project (db/update! conn :project
|
||||||
{:deleted-at (dt/now)}
|
{:deleted-at (dt/now)}
|
||||||
{:id id :is-default false})]
|
{:id id :is-default false})]
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
@ -35,8 +36,9 @@
|
|||||||
|
|
||||||
Share links are resources that allows external users access to specific
|
Share links are resources that allows external users access to specific
|
||||||
pages of a file with specific permissions (who-comment and who-inspect)."
|
pages of a file with specific permissions (who-comment and who-inspect)."
|
||||||
|
{::doc/added "1.5"
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(create-share-link conn params)))
|
(create-share-link conn params)))
|
||||||
@ -51,18 +53,17 @@
|
|||||||
:who-inspect who-inspect
|
:who-inspect who-inspect
|
||||||
:pages pages
|
:pages pages
|
||||||
:owner-id profile-id})]
|
:owner-id profile-id})]
|
||||||
(-> slink
|
(update slink :pages db/decode-pgarray #{})))
|
||||||
(update :pages db/decode-pgarray #{}))))
|
|
||||||
|
|
||||||
;; --- Mutation: Delete Share Link
|
;; --- Mutation: Delete Share Link
|
||||||
|
|
||||||
(declare delete-share-link)
|
|
||||||
|
|
||||||
(s/def ::delete-share-link
|
(s/def ::delete-share-link
|
||||||
(s/keys :req-un [::profile-id ::id]))
|
(s/keys :req-un [::profile-id ::id]))
|
||||||
|
|
||||||
(sv/defmethod ::delete-share-link
|
(sv/defmethod ::delete-share-link
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
{::doc/added "1.5"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [slink (db/get-by-id conn :share-link id)]
|
(let [slink (db/get-by-id conn :share-link id)]
|
||||||
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
||||||
|
|||||||
@ -1,240 +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/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.rpc.mutations.teams
|
|
||||||
(:require
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.db :as db]
|
|
||||||
[app.emails :as eml]
|
|
||||||
[app.loggers.audit :as audit]
|
|
||||||
[app.media :as media]
|
|
||||||
[app.rpc.commands.teams :as cmd.teams]
|
|
||||||
[app.rpc.doc :as-alias doc]
|
|
||||||
[app.rpc.helpers :as rph]
|
|
||||||
[app.util.services :as sv]
|
|
||||||
[app.util.time :as dt]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]))
|
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
|
||||||
|
|
||||||
(s/def ::id ::us/uuid)
|
|
||||||
(s/def ::name ::us/string)
|
|
||||||
(s/def ::profile-id ::us/uuid)
|
|
||||||
|
|
||||||
;; --- Mutation: Create Team
|
|
||||||
|
|
||||||
(s/def ::create-team ::cmd.teams/create-team)
|
|
||||||
|
|
||||||
(sv/defmethod ::create-team
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.teams/create-team conn params)))
|
|
||||||
|
|
||||||
;; --- Mutation: Update Team
|
|
||||||
|
|
||||||
(s/def ::update-team ::cmd.teams/update-team)
|
|
||||||
|
|
||||||
(sv/defmethod ::update-team
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [id name profile-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.teams/check-edition-permissions! conn profile-id id)
|
|
||||||
(db/update! conn :team
|
|
||||||
{:name name}
|
|
||||||
{:id id})
|
|
||||||
nil))
|
|
||||||
|
|
||||||
;; --- Mutation: Leave Team
|
|
||||||
|
|
||||||
(s/def ::leave-team ::cmd.teams/leave-team)
|
|
||||||
|
|
||||||
(sv/defmethod ::leave-team
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.teams/leave-team conn params)))
|
|
||||||
|
|
||||||
;; --- Mutation: Delete Team
|
|
||||||
|
|
||||||
(s/def ::delete-team ::cmd.teams/delete-team)
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-team
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [id profile-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [perms (cmd.teams/get-permissions conn profile-id id)]
|
|
||||||
(when-not (:is-owner perms)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :only-owner-can-delete-team))
|
|
||||||
(db/update! conn :team
|
|
||||||
{:deleted-at (dt/now)}
|
|
||||||
{:id id :is-default false})
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Team Update Role
|
|
||||||
|
|
||||||
(s/def ::update-team-member-role ::cmd.teams/update-team-member-role)
|
|
||||||
|
|
||||||
(sv/defmethod ::update-team-member-role
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(cmd.teams/update-team-member-role conn params)))
|
|
||||||
|
|
||||||
;; --- Mutation: Delete Team Member
|
|
||||||
|
|
||||||
(s/def ::delete-team-member ::cmd.teams/delete-team-member)
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-team-member
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [team-id profile-id member-id] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [perms (cmd.teams/get-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
|
|
||||||
|
|
||||||
(s/def ::update-team-photo ::cmd.teams/update-team-photo)
|
|
||||||
|
|
||||||
(sv/defmethod ::update-team-photo
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[cfg {:keys [file] :as params}]
|
|
||||||
;; Validate incoming mime type
|
|
||||||
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
|
||||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
|
||||||
(cmd.teams/update-team-photo cfg params)))
|
|
||||||
|
|
||||||
;; --- Mutation: Invite Member
|
|
||||||
|
|
||||||
(s/def ::invite-team-member ::cmd.teams/create-team-invitations)
|
|
||||||
|
|
||||||
(sv/defmethod ::invite-team-member
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [perms (cmd.teams/get-permissions conn profile-id team-id)
|
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
|
||||||
team (db/get-by-id conn :team team-id)
|
|
||||||
emails (cond-> (or emails #{}) (string? email) (conj email))]
|
|
||||||
|
|
||||||
(when-not (:is-admin perms)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :insufficient-permissions))
|
|
||||||
|
|
||||||
;; First check if the current profile is allowed to send emails.
|
|
||||||
(when-not (eml/allow-send-emails? conn profile)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :profile-is-muted
|
|
||||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
|
||||||
|
|
||||||
(let [cfg (assoc cfg ::cmd.teams/conn conn)
|
|
||||||
invitations (->> emails
|
|
||||||
(map (fn [email]
|
|
||||||
{:email (str/lower email)
|
|
||||||
:team team
|
|
||||||
:profile profile
|
|
||||||
:role role}))
|
|
||||||
(map (partial #'cmd.teams/create-invitation cfg)))]
|
|
||||||
(with-meta (vec invitations)
|
|
||||||
{::audit/props {:invitations (count invitations)}})))))
|
|
||||||
|
|
||||||
;; --- Mutation: Create Team & Invite Members
|
|
||||||
|
|
||||||
(s/def ::create-team-and-invite-members ::cmd.teams/create-team-with-invitations)
|
|
||||||
|
|
||||||
(sv/defmethod ::create-team-and-invite-members
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [team (cmd.teams/create-team conn params)
|
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
|
||||||
cfg (assoc cfg ::cmd.teams/conn conn)]
|
|
||||||
|
|
||||||
;; Create invitations for all provided emails.
|
|
||||||
(->> emails
|
|
||||||
(map (fn [email]
|
|
||||||
{:team team
|
|
||||||
:profile profile
|
|
||||||
:email (str/lower email)
|
|
||||||
:role role}))
|
|
||||||
(run! (partial #'cmd.teams/create-invitation cfg)))
|
|
||||||
|
|
||||||
(-> team
|
|
||||||
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
|
||||||
(rph/with-defer
|
|
||||||
#(when-let [collector (::audit/collector cfg)]
|
|
||||||
(audit/submit! collector
|
|
||||||
{:type "mutation"
|
|
||||||
:name "invite-team-member"
|
|
||||||
:profile-id profile-id
|
|
||||||
:props {:emails emails
|
|
||||||
:role role
|
|
||||||
:profile-id profile-id
|
|
||||||
:invitations (count emails)}})))))))
|
|
||||||
|
|
||||||
;; --- Mutation: Update invitation role
|
|
||||||
|
|
||||||
(s/def ::update-team-invitation-role
|
|
||||||
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
|
|
||||||
|
|
||||||
(sv/defmethod ::update-team-invitation-role
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [perms (cmd.teams/get-permissions conn profile-id team-id)]
|
|
||||||
|
|
||||||
(when-not (:is-admin perms)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :insufficient-permissions))
|
|
||||||
|
|
||||||
(db/update! conn :team-invitation
|
|
||||||
{:role (name role) :updated-at (dt/now)}
|
|
||||||
{:team-id team-id :email-to (str/lower email)})
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
;; --- Mutation: Delete invitation
|
|
||||||
|
|
||||||
(s/def ::delete-team-invitation ::cmd.teams/delete-team-invitation)
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-team-invitation
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [perms (cmd.teams/get-permissions conn profile-id team-id)]
|
|
||||||
|
|
||||||
(when-not (:is-admin perms)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :insufficient-permissions))
|
|
||||||
|
|
||||||
(db/delete! conn :team-invitation
|
|
||||||
{:team-id team-id :email-to (str/lower email)})
|
|
||||||
nil)))
|
|
||||||
@ -37,6 +37,14 @@
|
|||||||
:is-admin false
|
:is-admin false
|
||||||
:can-edit false)))
|
:can-edit false)))
|
||||||
|
|
||||||
|
(defn make-admin-predicate-fn
|
||||||
|
"A simple factory for admin permission predicate functions."
|
||||||
|
[qfn]
|
||||||
|
(us/assert fn? qfn)
|
||||||
|
(fn check
|
||||||
|
([perms] (:is-admin perms))
|
||||||
|
([conn & args] (check (apply qfn conn args)))))
|
||||||
|
|
||||||
(defn make-edition-predicate-fn
|
(defn make-edition-predicate-fn
|
||||||
"A simple factory for edition permission predicate functions."
|
"A simple factory for edition permission predicate functions."
|
||||||
[qfn]
|
[qfn]
|
||||||
|
|||||||
@ -1,183 +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/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.rpc.queries.files
|
|
||||||
(:require
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.db :as db]
|
|
||||||
[app.rpc.commands.files :as files]
|
|
||||||
[app.rpc.commands.search :as search]
|
|
||||||
[app.rpc.commands.teams :as teams]
|
|
||||||
[app.rpc.doc :as-alias doc]
|
|
||||||
[app.rpc.helpers :as rph]
|
|
||||||
[app.rpc.queries.projects :as projects]
|
|
||||||
[app.util.services :as sv]
|
|
||||||
[clojure.spec.alpha :as s]))
|
|
||||||
|
|
||||||
;; --- Query: Project Files
|
|
||||||
|
|
||||||
(s/def ::project-files ::files/get-project-files)
|
|
||||||
|
|
||||||
(sv/defmethod ::project-files
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(projects/check-read-permissions! conn profile-id project-id)
|
|
||||||
(files/get-project-files conn project-id)))
|
|
||||||
|
|
||||||
;; --- Query: File (By ID)
|
|
||||||
|
|
||||||
(s/def ::components-v2 ::us/boolean)
|
|
||||||
(s/def ::file
|
|
||||||
(s/and ::files/get-file
|
|
||||||
(s/keys :opt-un [::components-v2])))
|
|
||||||
|
|
||||||
(defn get-file
|
|
||||||
[conn id features]
|
|
||||||
(let [file (files/get-file conn id features)
|
|
||||||
thumbs (files/get-object-thumbnails conn id)]
|
|
||||||
(assoc file :thumbnails thumbs)))
|
|
||||||
|
|
||||||
(sv/defmethod ::file
|
|
||||||
"Retrieve a file by its ID. Only authenticated users."
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id id features components-v2] :as params}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(let [perms (files/get-permissions pool profile-id id)
|
|
||||||
;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
|
||||||
features (cond-> (or features #{})
|
|
||||||
components-v2 (conj "components/v2"))]
|
|
||||||
|
|
||||||
(files/check-read-permissions! perms)
|
|
||||||
(-> (get-file conn id features)
|
|
||||||
(assoc :permissions perms)))))
|
|
||||||
|
|
||||||
;; --- QUERY: page
|
|
||||||
|
|
||||||
(s/def ::page
|
|
||||||
(s/and ::files/get-page
|
|
||||||
(s/keys :opt-un [::components-v2])))
|
|
||||||
|
|
||||||
(sv/defmethod ::page
|
|
||||||
"Retrieves the page data from file and returns it. If no page-id is
|
|
||||||
specified, the first page will be returned. If object-id is
|
|
||||||
specified, only that object and its children will be returned in the
|
|
||||||
page objects data structure.
|
|
||||||
|
|
||||||
If you specify the object-id, the page-id parameter becomes
|
|
||||||
mandatory.
|
|
||||||
|
|
||||||
Mainly used for rendering purposes."
|
|
||||||
{::doc/added "1.5"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as params}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
|
||||||
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
|
||||||
features (cond-> (or features #{})
|
|
||||||
components-v2 (conj "components/v2"))
|
|
||||||
params (assoc params :features features)]
|
|
||||||
|
|
||||||
(files/get-page conn params))))
|
|
||||||
|
|
||||||
;; --- QUERY: file-data-for-thumbnail
|
|
||||||
|
|
||||||
(s/def ::file-data-for-thumbnail
|
|
||||||
(s/and ::files/get-file-data-for-thumbnail
|
|
||||||
(s/keys :opt-un [::components-v2])))
|
|
||||||
|
|
||||||
(sv/defmethod ::file-data-for-thumbnail
|
|
||||||
"Retrieves the data for generate the thumbnail of the file. Used
|
|
||||||
mainly for render thumbnails on dashboard."
|
|
||||||
{::doc/added "1.11"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as props}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
|
||||||
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
|
||||||
features (cond-> (or features #{})
|
|
||||||
components-v2 (conj "components/v2"))
|
|
||||||
file (files/get-file conn file-id features)]
|
|
||||||
{:file-id file-id
|
|
||||||
:revn (:revn file)
|
|
||||||
:page (files/get-file-data-for-thumbnail conn file)})))
|
|
||||||
|
|
||||||
;; --- Query: Shared Library Files
|
|
||||||
|
|
||||||
(s/def ::team-shared-files ::files/get-team-shared-files)
|
|
||||||
|
|
||||||
(sv/defmethod ::team-shared-files
|
|
||||||
{::doc/added "1.3"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
|
||||||
(files/get-team-shared-files conn params)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Query: File Libraries used by a File
|
|
||||||
|
|
||||||
(s/def ::file-libraries ::files/get-file-libraries)
|
|
||||||
|
|
||||||
(sv/defmethod ::file-libraries
|
|
||||||
{::doc/added "1.3"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id features] :as params}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
|
||||||
(files/get-file-libraries conn file-id features)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Query: Files that use this File library
|
|
||||||
|
|
||||||
(s/def ::library-using-files ::files/get-library-file-references)
|
|
||||||
|
|
||||||
(sv/defmethod ::library-using-files
|
|
||||||
{::doc/added "1.13"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
|
||||||
(files/get-library-file-references conn file-id)))
|
|
||||||
|
|
||||||
;; --- QUERY: team-recent-files
|
|
||||||
|
|
||||||
(s/def ::team-recent-files ::files/get-team-recent-files)
|
|
||||||
|
|
||||||
(sv/defmethod ::team-recent-files
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
|
||||||
(files/get-team-recent-files conn team-id)))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- QUERY: get file thumbnail
|
|
||||||
|
|
||||||
(s/def ::file-thumbnail ::files/get-file-thumbnail)
|
|
||||||
|
|
||||||
(sv/defmethod ::file-thumbnail
|
|
||||||
{::doc/added "1.13"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool]} {:keys [profile-id file-id revn]}]
|
|
||||||
(with-open [conn (db/open pool)]
|
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
|
||||||
(-> (files/get-file-thumbnail conn file-id revn)
|
|
||||||
(rph/with-http-cache files/long-cache-duration))))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- QUERY: search files
|
|
||||||
|
|
||||||
(s/def ::search-files ::search/search-files)
|
|
||||||
|
|
||||||
(sv/defmethod ::search-files
|
|
||||||
{::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.17"}
|
|
||||||
[{:keys [pool]} {:keys [profile-id team-id search-term]}]
|
|
||||||
(some->> search-term (search/search-files pool profile-id team-id)))
|
|
||||||
@ -9,9 +9,9 @@
|
|||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.queries.projects :as projects]
|
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
|||||||
@ -6,112 +6,27 @@
|
|||||||
|
|
||||||
(ns app.rpc.queries.profile
|
(ns app.rpc.queries.profile
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]))
|
||||||
[cuerdas.core :as str]))
|
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
(s/def ::profile ::profile/get-profile)
|
||||||
|
|
||||||
(declare strip-private-attrs)
|
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
|
||||||
(s/def ::fullname ::us/string)
|
|
||||||
(s/def ::old-password ::us/string)
|
|
||||||
(s/def ::password ::us/string)
|
|
||||||
(s/def ::path ::us/string)
|
|
||||||
(s/def ::user ::us/uuid)
|
|
||||||
(s/def ::profile-id ::us/uuid)
|
|
||||||
(s/def ::theme ::us/string)
|
|
||||||
|
|
||||||
;; --- Query: Profile (own)
|
|
||||||
|
|
||||||
(declare retrieve-profile)
|
|
||||||
(declare retrieve-additional-data)
|
|
||||||
|
|
||||||
(s/def ::profile
|
|
||||||
(s/keys :opt-un [::profile-id]))
|
|
||||||
|
|
||||||
(sv/defmethod ::profile
|
(sv/defmethod ::profile
|
||||||
{::rpc/auth false}
|
{::rpc/auth false
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id] :as params}]
|
::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id]}]
|
||||||
;; We need to return the anonymous profile object in two cases, when
|
;; We need to return the anonymous profile object in two cases, when
|
||||||
;; no profile-id is in session, and when db call raises not found. In all other
|
;; no profile-id is in session, and when db call raises not found. In all other
|
||||||
;; cases we need to reraise the exception.
|
;; cases we need to reraise the exception.
|
||||||
(or (ex/try*
|
(try
|
||||||
#(some->> profile-id (retrieve-profile pool))
|
(-> (profile/get-profile pool profile-id)
|
||||||
#(when (not= :not-found (:type (ex-data %))) (throw %)))
|
(profile/strip-private-attrs)
|
||||||
{:id uuid/zero
|
(update :props profile/filter-props))
|
||||||
:fullname "Anonymous User"}))
|
(catch Throwable _
|
||||||
|
{:id uuid/zero :fullname "Anonymous User"})))
|
||||||
(def ^:private sql:default-profile-team
|
|
||||||
"select t.id, name
|
|
||||||
from team as t
|
|
||||||
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")
|
|
||||||
|
|
||||||
(def ^:private sql:default-profile-project
|
|
||||||
"select p.id, name
|
|
||||||
from project as p
|
|
||||||
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
|
|
||||||
and p.team_id = ?")
|
|
||||||
|
|
||||||
(defn retrieve-additional-data
|
|
||||||
[conn id]
|
|
||||||
(let [team (db/exec-one! conn [sql:default-profile-team id])
|
|
||||||
project (db/exec-one! conn [sql:default-profile-project id (:id team)])]
|
|
||||||
{:default-team-id (:id team)
|
|
||||||
:default-project-id (:id project)}))
|
|
||||||
|
|
||||||
(defn populate-additional-data
|
|
||||||
[conn profile]
|
|
||||||
(merge profile (retrieve-additional-data conn (:id profile))))
|
|
||||||
|
|
||||||
(defn filter-profile-props
|
|
||||||
[props]
|
|
||||||
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
|
||||||
|
|
||||||
(defn decode-profile-row
|
|
||||||
[{:keys [props] :as row}]
|
|
||||||
(cond-> row
|
|
||||||
(db/pgobject? props "jsonb")
|
|
||||||
(assoc :props (db/decode-transit-pgobject props))))
|
|
||||||
|
|
||||||
(defn retrieve-profile-data
|
|
||||||
[conn id]
|
|
||||||
(-> (db/get-by-id conn :profile id)
|
|
||||||
(decode-profile-row)))
|
|
||||||
|
|
||||||
(defn retrieve-profile
|
|
||||||
[conn id]
|
|
||||||
(let [profile (->> (retrieve-profile-data conn id)
|
|
||||||
(strip-private-attrs)
|
|
||||||
(populate-additional-data conn))]
|
|
||||||
(update profile :props filter-profile-props)))
|
|
||||||
|
|
||||||
(def ^:private sql:profile-by-email
|
|
||||||
"select p.* from profile as p
|
|
||||||
where p.email = ?
|
|
||||||
and (p.deleted_at is null or
|
|
||||||
p.deleted_at > now())")
|
|
||||||
|
|
||||||
(defn retrieve-profile-data-by-email
|
|
||||||
[conn email]
|
|
||||||
(ex/ignoring
|
|
||||||
(db/exec-one! conn [sql:profile-by-email (str/lower email)])))
|
|
||||||
|
|
||||||
;; --- Attrs Helpers
|
|
||||||
|
|
||||||
(defn strip-private-attrs
|
|
||||||
"Only selects a publicly visible profile attrs."
|
|
||||||
[row]
|
|
||||||
(dissoc row :password :deleted-at))
|
|
||||||
|
|||||||
@ -8,135 +8,39 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
;; --- Check Project Permissions
|
|
||||||
|
|
||||||
(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- get-permissions
|
|
||||||
[conn profile-id project-id]
|
|
||||||
(let [rows (db/exec! conn [sql:project-permissions
|
|
||||||
project-id profile-id
|
|
||||||
project-id profile-id])
|
|
||||||
is-owner (boolean (some :is-owner rows))
|
|
||||||
is-admin (boolean (some :is-admin rows))
|
|
||||||
can-edit (boolean (some :can-edit rows))]
|
|
||||||
(when (seq rows)
|
|
||||||
{:is-owner is-owner
|
|
||||||
:is-admin (or is-owner is-admin)
|
|
||||||
:can-edit (or is-owner is-admin can-edit)
|
|
||||||
:can-read true})))
|
|
||||||
|
|
||||||
(def has-edit-permissions?
|
|
||||||
(perms/make-edition-predicate-fn get-permissions))
|
|
||||||
|
|
||||||
(def has-read-permissions?
|
|
||||||
(perms/make-read-predicate-fn get-permissions))
|
|
||||||
|
|
||||||
(def check-edition-permissions!
|
|
||||||
(perms/make-check-fn has-edit-permissions?))
|
|
||||||
|
|
||||||
(def check-read-permissions!
|
|
||||||
(perms/make-check-fn has-read-permissions?))
|
|
||||||
|
|
||||||
;; --- Query: Projects
|
;; --- Query: Projects
|
||||||
|
|
||||||
(declare retrieve-projects)
|
|
||||||
|
|
||||||
(s/def ::team-id ::us/uuid)
|
(s/def ::team-id ::us/uuid)
|
||||||
(s/def ::profile-id ::us/uuid)
|
(s/def ::profile-id ::us/uuid)
|
||||||
(s/def ::projects
|
(s/def ::projects
|
||||||
(s/keys :req-un [::profile-id ::team-id]))
|
(s/keys :req-un [::profile-id ::team-id]))
|
||||||
|
|
||||||
(sv/defmethod ::projects
|
(sv/defmethod ::projects
|
||||||
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
[{:keys [pool]} {:keys [profile-id team-id]}]
|
[{:keys [pool]} {:keys [profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(retrieve-projects conn profile-id team-id)))
|
(projects/get-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) as count
|
|
||||||
from project as p
|
|
||||||
inner join team as t on (t.id = p.team_id)
|
|
||||||
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
|
|
||||||
and t.deleted_at is null
|
|
||||||
order by p.modified_at desc")
|
|
||||||
|
|
||||||
(defn retrieve-projects
|
|
||||||
[conn profile-id team-id]
|
|
||||||
(db/exec! conn [sql:projects profile-id team-id]))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Query: All projects
|
;; --- Query: All projects
|
||||||
|
|
||||||
(declare retrieve-all-projects)
|
|
||||||
|
|
||||||
(s/def ::profile-id ::us/uuid)
|
(s/def ::profile-id ::us/uuid)
|
||||||
(s/def ::all-projects
|
(s/def ::all-projects
|
||||||
(s/keys :req-un [::profile-id]))
|
(s/keys :req-un [::profile-id]))
|
||||||
|
|
||||||
(sv/defmethod ::all-projects
|
(sv/defmethod ::all-projects
|
||||||
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
[{:keys [pool]} {:keys [profile-id]}]
|
[{:keys [pool]} {:keys [profile-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(retrieve-all-projects conn profile-id)))
|
(projects/get-all-projects conn profile-id)))
|
||||||
|
|
||||||
(def sql:all-projects
|
|
||||||
"select p1.*, t.name as team_name, t.is_default as is_default_team
|
|
||||||
from project as p1
|
|
||||||
inner join team as t on (t.id = p1.team_id)
|
|
||||||
where t.id in (select team_id
|
|
||||||
from team_profile_rel as tpr
|
|
||||||
where tpr.profile_id = ?
|
|
||||||
and (tpr.can_edit = true or
|
|
||||||
tpr.is_owner = true or
|
|
||||||
tpr.is_admin = true))
|
|
||||||
and t.deleted_at is null
|
|
||||||
and p1.deleted_at is null
|
|
||||||
union
|
|
||||||
select p2.*, t.name as team_name, t.is_default as is_default_team
|
|
||||||
from project as p2
|
|
||||||
inner join team as t on (t.id = p2.team_id)
|
|
||||||
where p2.id in (select project_id
|
|
||||||
from project_profile_rel as ppr
|
|
||||||
where ppr.profile_id = ?
|
|
||||||
and (ppr.can_edit = true or
|
|
||||||
ppr.is_owner = true or
|
|
||||||
ppr.is_admin = true))
|
|
||||||
and t.deleted_at is null
|
|
||||||
and p2.deleted_at is null
|
|
||||||
order by team_name, name;")
|
|
||||||
|
|
||||||
(defn retrieve-all-projects
|
|
||||||
[conn profile-id]
|
|
||||||
(db/exec! conn [sql:all-projects profile-id profile-id]))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Query: Project
|
;; --- Query: Project
|
||||||
|
|
||||||
@ -145,9 +49,11 @@
|
|||||||
(s/keys :req-un [::profile-id ::id]))
|
(s/keys :req-un [::profile-id ::id]))
|
||||||
|
|
||||||
(sv/defmethod ::project
|
(sv/defmethod ::project
|
||||||
|
{::doc/added "1.0"
|
||||||
|
::doc/deprecated "1.18"}
|
||||||
[{:keys [pool]} {:keys [profile-id id]}]
|
[{:keys [pool]} {:keys [profile-id id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(with-open [conn (db/open pool)]
|
||||||
(let [project (db/get-by-id conn :project id)]
|
(let [project (db/get-by-id conn :project id)]
|
||||||
(check-read-permissions! conn profile-id id)
|
(projects/check-read-permissions! conn profile-id id)
|
||||||
project)))
|
project)))
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user