mirror of
https://github.com/penpot/penpot.git
synced 2026-02-27 04:07:59 -05:00
Compare commits
4 Commits
1.19.2
...
azazeln28-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79b06aa092 | ||
|
|
7d8e985877 | ||
|
|
6f8e8da466 | ||
|
|
eb1bc480ca |
@@ -2,10 +2,10 @@
|
||||
{promesa.core/let clojure.core/let
|
||||
promesa.core/->> clojure.core/->>
|
||||
promesa.core/-> clojure.core/->
|
||||
promesa.exec.csp/go-loop clojure.core/loop
|
||||
rumext.v2/defc clojure.core/defn
|
||||
rumext.v2/fnc clojure.core/fn
|
||||
app.common.data/export clojure.core/def
|
||||
app.db/with-atomic clojure.core/with-open
|
||||
app.common.data.macros/get-in clojure.core/get-in
|
||||
app.common.data.macros/with-open clojure.core/with-open
|
||||
app.common.data.macros/select-keys clojure.core/select-keys
|
||||
@@ -16,7 +16,6 @@
|
||||
{app.common.data.macros/export hooks.export/export
|
||||
potok.core/reify hooks.export/potok-reify
|
||||
app.util.services/defmethod hooks.export/service-defmethod
|
||||
app.db/with-atomic hooks.export/penpot-with-atomic
|
||||
}}
|
||||
|
||||
:output
|
||||
|
||||
@@ -39,43 +39,6 @@
|
||||
other))]
|
||||
{:node result})))
|
||||
|
||||
(defn penpot-with-atomic
|
||||
[{:keys [node]}]
|
||||
(let [[_ params & other] (:children node)
|
||||
|
||||
result (if (api/vector-node? params)
|
||||
(api/list-node
|
||||
(into [(api/token-node (symbol "clojure.core" "with-open")) params] other))
|
||||
(api/list-node
|
||||
(into [(api/token-node (symbol "clojure.core" "with-open"))
|
||||
(api/vector-node [params params])]
|
||||
other)))
|
||||
|
||||
]
|
||||
{:node result}))
|
||||
|
||||
(defn penpot-defrecord
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype rparams & other] (:children node)
|
||||
|
||||
nodes [(api/token-node (symbol "do"))
|
||||
(api/list-node
|
||||
(into [(api/token-node (symbol (name (:value rnode)))) rtype rparams] other))
|
||||
(api/list-node
|
||||
[(api/token-node (symbol "defn"))
|
||||
(api/token-node (symbol (str "pos->" (:string-value rtype))))
|
||||
(api/vector-node
|
||||
(->> (:children rparams)
|
||||
(mapv (fn [t]
|
||||
(api/token-node (symbol (str "_" (:string-value t))))))))
|
||||
(api/token-node nil)])]
|
||||
|
||||
result (api/list-node nodes)]
|
||||
|
||||
;; (prn "=====>" (into {} rparams))
|
||||
;; (prn (api/sexpr result))
|
||||
{:node result}))
|
||||
|
||||
(defn clojure-specify
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype & other] (:children node)
|
||||
@@ -85,6 +48,7 @@
|
||||
other))]
|
||||
{:node result}))
|
||||
|
||||
|
||||
(defn service-defmethod
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype ?meta & other] (:children node)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*.{cljs,cljc,clj,js,css,scss,html,yml,yaml,json,mustache}]
|
||||
charset = utf-8
|
||||
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
end_of_line = lf
|
||||
|
||||
insert_final_newline = true
|
||||
|
||||
trim_trailing_whitespace = true
|
||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/.clj-kondo": true,
|
||||
"**/.cpcache": true,
|
||||
"**/.lsp": true,
|
||||
"**/.shadow-cljs": true,
|
||||
"**/node_modules": true
|
||||
}
|
||||
"files.associations": {
|
||||
"stdint.h": "c"
|
||||
}
|
||||
}
|
||||
|
||||
278
CHANGES.md
278
CHANGES.md
@@ -1,283 +1,21 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.19.2
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Navigate up in layer hierarchy with Shift+Enter shortcut [Taiga #5734](https://tree.taiga.io/project/penpot/us/5734)
|
||||
- Click on the flow tags open viewer with the selected frame [Taiga #5044](https://tree.taiga.io/project/penpot/us/5044)
|
||||
- Add Dutch language & update translation files with weblate
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix unexpected output on get-page rpc method when invalid object-id is provided [Github #3546](https://github.com/penpot/penpot/issues/3546)
|
||||
- Fix Invalid files amount after moving file from Project to Drafts [Taiga #5638](https://tree.taiga.io/project/penpot/us/5638)
|
||||
- Fix deleted pages comments shown in right sidebar [Taiga #5648](https://tree.taiga.io/project/penpot/us/5648)
|
||||
- Fix tooltip on toggle visibility and toggle lock buttons [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
|
||||
|
||||
|
||||
## 1.19.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix components not registered as updated [Taiga #5725](https://tree.taiga.io/project/penpot/issue/5725)
|
||||
|
||||
## 1.19.0
|
||||
## :rocket: Next
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Default naming of text layers [Taiga #2836](https://tree.taiga.io/project/penpot/us/2836)
|
||||
- Create typography style from a selected text layer [Taiga #3041](https://tree.taiga.io/project/penpot/us/3041)
|
||||
- Board as ruler origin [Taiga #4833](https://tree.taiga.io/project/penpot/us/4833)
|
||||
- Access tokens support [Taiga #4460](https://tree.taiga.io/project/penpot/us/4460)
|
||||
- Show interactions setting at the view mode [Taiga #1330](https://tree.taiga.io/project/penpot/issue/1330)
|
||||
- Improve dashboard performance related to thumbnails; now the thumbnails are
|
||||
rendered as bitmap images.
|
||||
- Add the ability to disable google fonts provider with the `disable-google-fonts-provider` flag
|
||||
- Add the ability to disable dashboard templates section with the `disable-dashboard-templates-section` flag
|
||||
- Add the ability to use the registration whitelist with OICD [Github #3348](https://github.com/penpot/penpot/issues/3348)
|
||||
- Add support for local caching of google fonts (this avoids exposing the final user IP to
|
||||
goolge and reduces the amount of request sent to google)
|
||||
- Set smooth/instant autoscroll depending on distance [GitHub #3377](https://github.com/penpot/penpot/issues/3377)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix files can be opened from multiple urls [Taiga #5310](https://tree.taiga.io/project/penpot/issue/5310)
|
||||
- Fix asset color item was created from the selected layer [Taiga #5180](https://tree.taiga.io/project/penpot/issue/5180)
|
||||
- Fix unpublish more than one library at the same time [Taiga #5532](https://tree.taiga.io/project/penpot/issue/5532)
|
||||
- Fix drag projects on dahsboard [Taiga #5531](https://tree.taiga.io/project/penpot/issue/5531)
|
||||
- Fix allow team name to be all blank [Taiga #5527](https://tree.taiga.io/project/penpot/issue/5527)
|
||||
- Fix search font visualitation [Taiga #5523](https://tree.taiga.io/project/penpot/issue/5523)
|
||||
- Fix create and account only with spaces [Taiga #5518](https://tree.taiga.io/project/penpot/issue/5518)
|
||||
- Fix context menu outside screen [Taiga #5524](https://tree.taiga.io/project/penpot/issue/5524)
|
||||
- Fix graphic item rename on assets pannel [Taiga #5556](https://tree.taiga.io/project/penpot/issue/5556)
|
||||
- Fix component and media name validation on assets panel [Taiga #5555](https://tree.taiga.io/project/penpot/issue/5555)
|
||||
- Fix problem with selection shortcuts [Taiga #5492](https://tree.taiga.io/project/penpot/issue/5492)
|
||||
- Fix issue with paths line to curve and concurrent editing [Taiga #5191](https://tree.taiga.io/project/penpot/issue/5191)
|
||||
- Fix problems with locked layers [Taiga #5139](https://tree.taiga.io/project/penpot/issue/5139)
|
||||
- Fix export from shared prototype [Taiga #5565](https://tree.taiga.io/project/penpot/issue/5565)
|
||||
- Fix email change: validation error displaying even after both fields are identical [Taiga #5514](https://tree.taiga.io/project/penpot/issue/5514)
|
||||
- Fix scroll on viewer comment list [Taiga #5563](https://tree.taiga.io/project/penpot/issue/5563)
|
||||
- Fix context menu z-index [Taiga #5561](https://tree.taiga.io/project/penpot/issue/5561)
|
||||
- Fix select all checkbox on shared link config [Taiga #5566](https://tree.taiga.io/project/penpot/issue/5566)
|
||||
- Fix validation on full name input on account creation [Taiga #5516](https://tree.taiga.io/project/penpot/issue/5516)
|
||||
- Fix validation on team name input [Taiga #5510](https://tree.taiga.io/project/penpot/issue/5510)
|
||||
- Fix incorrect uri generation issues on share-link modal [Taiga #5564](https://tree.taiga.io/project/penpot/issue/5564)
|
||||
- Fix cache issues with share-links [Taiga #5559](https://tree.taiga.io/project/penpot/issue/5559)
|
||||
- Makes height priority for the rows/columns grids [#2774](https://github.com/penpot/penpot/issues/2774)
|
||||
- Fix problem with comments mode not staying [#3363](https://github.com/penpot/penpot/issues/3363)
|
||||
- Fix problem with comments when user left the team [Taiga #5562](https://tree.taiga.io/project/penpot/issue/5562)
|
||||
- Fix problem with images patterns repeating [#3372](https://github.com/penpot/penpot/issues/3372)
|
||||
- Fix grid not being clipped in frames [#3365](https://github.com/penpot/penpot/issues/3365)
|
||||
- Fix cut/delete text layer when while creating text [Taiga #5602](https://tree.taiga.io/project/penpot/issue/5602)
|
||||
- Fix picking a gradient color in recent colors for a new color in the assets tab [Taiga #5601](https://tree.taiga.io/project/penpot/issue/5601)
|
||||
- Fix problem with importation process [Taiga #5597](https://tree.taiga.io/project/penpot/issue/5597)
|
||||
- Fix problem with HSV color picker [#3317](https://github.com/penpot/penpot/issues/3317)
|
||||
- Fix problem with slashes in layers names for exporter [#3276](https://github.com/penpot/penpot/issues/3276)
|
||||
- Fix incorrect modified data on moving files on dashboard [Taiga #5530](https://tree.taiga.io/project/penpot/issue/5530)
|
||||
- Fix focus handling on comments edition [Taiga #5560](https://tree.taiga.io/project/penpot/issue/5560)
|
||||
- Fix incorrect fullname use on registring user after OIDC authentication [Taiga #5517](https://tree.taiga.io/project/penpot/issue/5517)
|
||||
- Fix incorrect modified-at on project after import file [Taiga #5268](https://tree.taiga.io/project/penpot/issue/5268)
|
||||
- Fix incorrect message after sending invitation to already member [Taiga 5599](https://tree.taiga.io/project/penpot/issue/5599)
|
||||
- Fix text decoration on button [Taiga #5301](https://tree.taiga.io/project/penpot/issue/5301)
|
||||
- Fix menu order on design tab [Taiga #5195](https://tree.taiga.io/project/penpot/issue/5195)
|
||||
- Fix search bar width on layer tab [Taiga #5445](https://tree.taiga.io/project/penpot/issue/5445)
|
||||
- Fix border radius values with decimals [Taiga #5283](https://tree.taiga.io/project/penpot/issue/5283)
|
||||
- Fix shortcuts translations not homogenized [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
|
||||
- Fix overlay manual position in nested boards [Taiga #5135](https://tree.taiga.io/project/penpot/issue/5135)
|
||||
- Fix close overlay from a nested board [Taiga #5587](https://tree.taiga.io/project/penpot/issue/5587)
|
||||
- Fix overlay position when it has shadow or blur [Taiga #4752](https://tree.taiga.io/project/penpot/issue/4752)
|
||||
- Fix overlay position when there are elements fixed when scrolling [Taiga #4383](https://tree.taiga.io/project/penpot/issue/4383)
|
||||
- Fix problem when sliding color picker in selected-colors [#3150](https://github.com/penpot/penpot/issues/3150)
|
||||
- Fix error screen on upload image error [Taiga #5608](https://tree.taiga.io/project/penpot/issue/5608)
|
||||
- Fix bad frame-id for certain componentes [#3205](https://github.com/penpot/penpot/issues/3205)
|
||||
- Fix paste elements at bottom of frame [Taig #5253](https://tree.taiga.io/project/penpot/issue/5253)
|
||||
- Fix new-file button on project not redirecting to the new file [Taiga #5610](https://tree.taiga.io/project/penpot/issue/5610)
|
||||
- Fix retrieve user comments in dashboard [Taiga #5607](https://tree.taiga.io/project/penpot/issue/5607)
|
||||
- Locks shapes when moved inside a locked parent [Taiga #5252](https://tree.taiga.io/project/penpot/issue/5252)
|
||||
- Fix rotate several elements in bulk [Taiga #5165](https://tree.taiga.io/project/penpot/issue/5165)
|
||||
- Fix onboarding slides height [Taiga #5373](https://tree.taiga.io/project/penpot/issue/5373)
|
||||
- Fix create typography with section closed [Taiga #5574](https://tree.taiga.io/project/penpot/issue/5574)
|
||||
- Fix exports menu on viewer mode [Taiga #5568](https://tree.taiga.io/project/penpot/issue/5568)
|
||||
- Fix create empty comments [Taiga #5536](https://tree.taiga.io/project/penpot/issue/5536)
|
||||
- Fix position of text cursor is a bit too high in Invitations section [Taiga #5511](https://tree.taiga.io/project/penpot/issue/5511)
|
||||
- Fix undo when updating several texts [Taiga #5197](https://tree.taiga.io/project/penpot/issue/5197)
|
||||
- Fix assets right click button for multiple selection [Taiga #5545](https://tree.taiga.io/project/penpot/issue/5545)
|
||||
- Fix problem with precision in resizes [Taiga #5623](https://tree.taiga.io/project/penpot/issue/5623)
|
||||
- Fix absolute positioned layouts not showing flex properties [Taiga #5630](https://tree.taiga.io/project/penpot/issue/5630)
|
||||
- Fix text gradient handlers [Taiga #4047](https://tree.taiga.io/project/penpot/issue/4047)
|
||||
- Fix when user deletes one file during import it is impossible to finish importing of second file [Taiga #5656](https://tree.taiga.io/project/penpot/issue/5656)
|
||||
- Fix export multiple images when only one of them has export settings [Taiga #5649](https://tree.taiga.io/project/penpot/issue/5649)
|
||||
- Fix error when a user different than the thread creator edits a comment [Taiga #5647](https://tree.taiga.io/project/penpot/issue/5647)
|
||||
- Fix unnecessary button [Taiga #3312](https://tree.taiga.io/project/penpot/issue/3312)
|
||||
- Fix copy color information in several formats [Taiga #4723](https://tree.taiga.io/project/penpot/issue/4723)
|
||||
- Fix dropdown width [Taiga #5541](https://tree.taiga.io/project/penpot/issue/5541)
|
||||
- Fix enable comment mode and insert image keeps on comment mode [Taiga #5678](https://tree.taiga.io/project/penpot/issue/5678)
|
||||
- Fix enable undo just after using pencil [Taiga #5674](https://tree.taiga.io/project/penpot/issue/5674)
|
||||
- Fix 400 error when user changes password [Taiga #5643](https://tree.taiga.io/project/penpot/issue/5643)
|
||||
- Fix cannot undo layer styles [Taiga #5676](https://tree.taiga.io/project/penpot/issue/5676)
|
||||
- Fix unexpected exception on boolean shapes [Taiga #5685](https://tree.taiga.io/project/penpot/issue/5685)
|
||||
- Fix ctrl+z on select not working [Taiga #5677](https://tree.taiga.io/project/penpot/issue/5677)
|
||||
- Fix thubmnail rendering flashing [Taiga #5675](https://tree.taiga.io/project/penpot/issue/5675)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)
|
||||
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
|
||||
- Palettes (color, typographies) empty state (by @akshay-gupta7) [Github #3160](https://github.com/penpot/penpot/pull/3160)
|
||||
- Duplicate objects via drag + alt (by @akshay-gupta7) [Github #3147](https://github.com/penpot/penpot/pull/3147)
|
||||
- Set line-height to auto as 1.2 (by @akshay-gupta7) [Github #3185](https://github.com/penpot/penpot/pull/3185)
|
||||
- Click to select full values at the design sidebar (by @akshay-gupta7) [Github #3179](https://github.com/penpot/penpot/pull/3179)
|
||||
- Fix rect filter bounds math (by @ryanbreen) [Github #3180](https://github.com/penpot/penpot/pull/3180)
|
||||
- Removed sizing variables from radius (by @ondrejkonec) [Github #3184](https://github.com/penpot/penpot/pull/3184)
|
||||
- Dashboard search, set focus after shortcut (by @akshay-gupta7) [Github #3196](https://github.com/penpot/penpot/pull/3196)
|
||||
- Library name dropdown arrow is overlapped by library name (by @ondrejkonec) [Taiga #5200](https://tree.taiga.io/project/penpot/issue/5200)
|
||||
- Reorder shadows (by @akshay-gupta7) [Github #3236](https://github.com/penpot/penpot/pull/3236)
|
||||
- Open project in new tab from workspace (by @akshay-gupta7) [Github #3246](https://github.com/penpot/penpot/pull/3246)
|
||||
- Distribute fix enabled when two elements were selected (by @dfelinto) [Github #3266](https://github.com/penpot/penpot/pull/3266)
|
||||
- Distribute vertical spacing failing for overlapped text (by @dfelinto) [Github #3267](https://github.com/penpot/penpot/pull/3267)
|
||||
- bug Change independent corner radius input tooltips #3332 (by @astudentinearth) [Github #3332](https://github.com/penpot/penpot/pull/3332)
|
||||
|
||||
## 1.18.6
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix comments navigation from workspace [Taiga #5504](https://tree.taiga.io/project/penpot/issue/5504)
|
||||
|
||||
### :sparkles: Enhancements
|
||||
|
||||
- Add the ability to overwrite internal resolver with `PENPOT_INTERNAL_RESOLVER` environment
|
||||
variable [GH #3310](https://github.com/penpot/penpot/issues/3310)
|
||||
|
||||
## 1.18.5
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix add flow option in contextual menu for frames
|
||||
- Fix issues related with invitations
|
||||
- Fix problem with undefined gaps
|
||||
- Add deleted fonts auto match mechanism
|
||||
|
||||
## 1.18.4
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix zooming while color picker breaks UI [GH #3214](https://github.com/penpot/penpot/issues/3214)
|
||||
- Fix problem with layout not reflowing on shape deletion [Taiga #5289](https://tree.taiga.io/project/penpot/issue/5289)
|
||||
- Fix extra long typography names on assets and palette [Taiga #5199](https://tree.taiga.io/project/penpot/issue/5199)
|
||||
- Fix background-color property on inspect code [Taiga #5300](https://tree.taiga.io/project/penpot/issue/5300)
|
||||
- Preview layer blend modes (by @akshay-gupta7) [Github #3235](https://github.com/penpot/penpot/pull/3235)
|
||||
|
||||
## 1.18.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with rulers not placing correctly [Taiga #5093](https://tree.taiga.io/project/penpot/issue/5093)
|
||||
- Fix page context menu [Taiga #5145](https://tree.taiga.io/project/penpot/issue/5145)
|
||||
- Fix project file count [Taiga #5148](https://tree.taiga.io/project/penpot/issue/5148)
|
||||
- Fix OIDC roles checking mechanism [GH #3152](https://github.com/penpot/penpot/issues/3152)
|
||||
- Import updated translation strings from weblate
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
## 1.18.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with frame title rotation
|
||||
- Fix first level board "Show in view mode" is automatically unchecked [Taiga #5136](https://tree.taiga.io/project/penpot/issue/5136)
|
||||
|
||||
## 1.18.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problems with imported SVG shadows [Taiga #4922](https://tree.taiga.io/project/penpot/issue/4922)
|
||||
- Fix problems with imported SVG embedded images and transforms [Taiga #4639](https://tree.taiga.io/project/penpot/issue/4639)
|
||||
|
||||
## 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)
|
||||
- Add visualization and mouse control to paddings 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)
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
- To @ondrejkonec: for contributing to the code with:
|
||||
@@ -288,16 +26,6 @@
|
||||
### :bug: Bugs fixed
|
||||
- Fix copy and paste very nested inside itself [Taiga #4848](https://tree.taiga.io/project/penpot/issue/4848)
|
||||
- Fix custom fonts not rendered correctly [Taiga #4874](https://tree.taiga.io/project/penpot/issue/4874)
|
||||
- Fix problem with shadows and blur on multiple selection
|
||||
- Fix problem with redo shortcut
|
||||
- Fix Component texts not displayed in assets panel [Taiga #4907](https://tree.taiga.io/project/penpot/issue/4907)
|
||||
- Fix search field has implemented shared styles for "close icon" and "search icon" [Taiga #4927](https://tree.taiga.io/project/penpot/issue/4927)
|
||||
- Fix Handling correctly slashes "/" in emails [Taiga #4906](https://tree.taiga.io/project/penpot/issue/4906)
|
||||
- Fix Change text color from selected colors [Taiga #4933](https://tree.taiga.io/project/penpot/issue/4933)
|
||||
|
||||
### :sparkles: Enhancements
|
||||
|
||||
- Adds environment variables for specifying the export and backend URI for the frontend docker image, thanks to @Supernova3339 for the initial PR and suggestion [Github #2984](https://github.com/penpot/penpot/issues/2984)
|
||||
|
||||
## 1.17.2
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
|
||||

|
||||
|
||||
**:tada: [Important Notice!] :tada:** Our very first **Penpot Fest** is happening on June 28-30, Barcelona (Spain). **Secure yourself a ticket** to know everything about the present and future of Penpot and be part of the conversation! See details on the amazing venue and speakers lineup at [penpotfest.org](https://penpotfest.org)! :zap:
|
||||
|
||||
Penpot is the first **Open Source** design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return.
|
||||
|
||||
## Table of contents ##
|
||||
@@ -126,7 +124,7 @@ You can ask and answer questions, have open-ended conversations, and follow alon
|
||||
|
||||
✏️ [Tutorials](https://www.youtube.com/playlist?list=PLgcCPfOv5v54WpXhHmNO7T-YC7AE-SRsr)
|
||||
|
||||
🏘️ [Architecture](https://help.penpot.app/technical-guide/developer/architecture/)
|
||||
🏘️ [Architecture](https://help.penpot.app/technical-guide/architecture/)
|
||||
|
||||
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)
|
||||
|
||||
|
||||
11
THANKYOU.md
11
THANKYOU.md
@@ -5,25 +5,24 @@ We want to thank to the amazing people that help us! Thank you! You're the best!
|
||||
## Security
|
||||
* Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD)
|
||||
* [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/)
|
||||
* Vaibhav Shukla
|
||||
|
||||
## Internationalization
|
||||
* [00ff88](https://hosted.weblate.org/user/00ff88)
|
||||
* [AhmadHB](https://hosted.weblate.org/user/AhmadHB)
|
||||
* [Aimee](https://hosted.weblate.org/user/Aimee)
|
||||
* [alejandro.alonso](https://hosted.weblate.org/user/alejandro.alonso)
|
||||
* [alejandro.alonso](alejandro.https://hosted.weblate.org/user/alonso)
|
||||
* [alexpawlak](https://hosted.weblate.org/user/alexpawlak)
|
||||
* [allytiago](https://hosted.weblate.org/user/allytiago)
|
||||
* [alonso.torres](https://hosted.weblate.org/user/alonso.torres)
|
||||
* [andres.moya](https://hosted.weblate.org/user/andres.moya)
|
||||
* [alonso.torres](alonso.https://hosted.weblate.org/user/torres)
|
||||
* [andres.moya](andres.https://hosted.weblate.org/user/moya)
|
||||
* [antoniofsm](https://hosted.weblate.org/user/antoniofsm)
|
||||
* [ascarida](https://hosted.weblate.org/user/ascarida)
|
||||
* [Bechii](https://hosted.weblate.org/user/Bechii)
|
||||
* [Beeby](https://hosted.weblate.org/user/Beeby)
|
||||
* [bingling-sama](https://hosted.weblate.org/user/bingling-sama)
|
||||
* [bingling-sama](bingling-https://hosted.weblate.org/user/sama)
|
||||
* [devadarta](https://hosted.weblate.org/user/devadarta)
|
||||
* [diacritica](https://hosted.weblate.org/user/diacritica)
|
||||
* [dundzys.vincas](https://hosted.weblate.org/user/dundzys.vincas)
|
||||
* [dundzys.vincas](dundzys.https://hosted.weblate.org/user/vincas)
|
||||
* [Eranot](https://hosted.weblate.org/user/Eranot)
|
||||
* [erral](https://hosted.weblate.org/user/erral)
|
||||
* [ersen](https://hosted.weblate.org/user/ersen)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{:mvn/repos
|
||||
{"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
|
||||
|
||||
:deps
|
||||
{:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/clojure {:mvn/version "1.11.1"}
|
||||
org.clojure/core.async {:mvn/version "1.6.673"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.5-4"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
|
||||
org.clojure/data.fressian {:mvn/version "1.0.0"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
@@ -17,30 +15,28 @@
|
||||
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.2.4.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.2.2.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v9.16"
|
||||
:git/sha "7df3e08"
|
||||
{:git/tag "v9.12"
|
||||
:git/sha "51646d8"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.883"}
|
||||
metosin/reitit-core {:mvn/version "0.6.0"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.6.0"}
|
||||
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
|
||||
metosin/reitit-core {:mvn/version "0.5.18"}
|
||||
org.postgresql/postgresql {:mvn/version "42.5.2"}
|
||||
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
|
||||
buddy/buddy-hashers {:mvn/version "2.0.167"}
|
||||
buddy/buddy-sign {:mvn/version "3.5.351"}
|
||||
buddy/buddy-hashers {:mvn/version "1.8.158"}
|
||||
buddy/buddy-sign {:mvn/version "3.4.333"}
|
||||
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.6"}
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.2"}
|
||||
|
||||
org.jsoup/jsoup {:mvn/version "1.16.1"}
|
||||
org.jsoup/jsoup {:mvn/version "1.15.3"}
|
||||
org.im4java/im4java
|
||||
{:git/tag "1.4.0-penpot-2"
|
||||
:git/sha "e2b3e16"
|
||||
@@ -49,14 +45,14 @@
|
||||
org.lz4/lz4-java {:mvn/version "1.8.0"}
|
||||
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
integrant/integrant {:mvn/version "0.8.1"}
|
||||
integrant/integrant {:mvn/version "0.8.0"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.11.4"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.20.96"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.19.29"}
|
||||
}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
|
||||
@@ -8,15 +8,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.logging :as l]
|
||||
[app.common.perf :as perf]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-js-like :as smdj]
|
||||
[app.common.schema.desc-native :as smdn]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.spec :as us]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -25,6 +20,7 @@
|
||||
[app.srepl.helpers]
|
||||
[app.srepl.main :as srepl]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.fressian :as fres]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[clj-async-profiler.core :as prof]
|
||||
@@ -35,20 +31,13 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.stacktrace :as trace]
|
||||
[clojure.test :as test]
|
||||
[clojure.test.check.generators :as tgen]
|
||||
[clojure.test.check.generators :as gen]
|
||||
[clojure.tools.namespace.repl :as repl]
|
||||
[clojure.walk :refer [macroexpand-all]]
|
||||
[criterium.core :as crit]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core]
|
||||
[integrant.core :as ig]
|
||||
[malli.core :as m]
|
||||
[malli.dev.pretty :as mdp]
|
||||
[malli.error :as me]
|
||||
[malli.generator :as mg]
|
||||
[malli.registry :as mr]
|
||||
[malli.transform :as mt]
|
||||
[malli.util :as mu]))
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(repl/disable-reload! (find-ns 'integrant.core))
|
||||
(set! *warn-on-reflection* true)
|
||||
@@ -141,39 +130,3 @@
|
||||
(add-tap #(locking debug-tap
|
||||
(prn "tap debug:" %)))
|
||||
1))
|
||||
|
||||
|
||||
(sm/def! ::test
|
||||
[:map {:title "Foo"}
|
||||
[:x :int]
|
||||
[:y {:min 0} :double]
|
||||
[:bar
|
||||
[:map {:title "Bar"}
|
||||
[:z :string]
|
||||
[:v ::sm/uuid]]]
|
||||
[:items
|
||||
[:vector ::dt/instant]]])
|
||||
|
||||
(sm/def! ::test2
|
||||
[:multi {:title "Foo" :dispatch :type}
|
||||
[:x
|
||||
[:map {:title "FooX"}
|
||||
[:type [:= :x]]
|
||||
[:x :int]]]
|
||||
[:y
|
||||
[:map
|
||||
[:type [:= :x]]
|
||||
[:y [::sm/one-of #{:a :b :c}]]]]
|
||||
[:z
|
||||
[:map {:title "FooZ"}
|
||||
[:z
|
||||
[:multi {:title "Bar" :dispatch :type}
|
||||
[:a
|
||||
[:map
|
||||
[:type [:= :a]]
|
||||
[:a :int]]]
|
||||
[:b
|
||||
[:map
|
||||
[:type [:= :b]]
|
||||
[:b :int]]]]]]]])
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,30 +1,36 @@
|
||||
[{:id "material-design-3"
|
||||
:name "Material Design 3"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Material%20Design%203.penpot"}
|
||||
{:id "tutorial-for-beginners"
|
||||
[{:id "tutorial-for-beginners"
|
||||
:name "Tutorial for beginners"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/tutorial-for-beginners.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
|
||||
{:id "penpot-design-system"
|
||||
:name "Penpot Design System"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
|
||||
{:id "flex-layout-playground"
|
||||
:name "Flex Layout Playground"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
|
||||
{:id "wireframing-kit"
|
||||
:name "Wireframing Kit"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-wireframes.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
|
||||
{:id "ant-design"
|
||||
:name "Ant Design UI Kit (lite)"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-ant-design.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Ant-Design-UI-Kit-Lite.penpot"}
|
||||
{:id "cocomaterial"
|
||||
:name "Cocomaterial"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-cocomaterial.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Cocomaterial.penpot"}
|
||||
{:id "circum-icons"
|
||||
:name "Circum Icons pack"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-circum.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/CircumIcons.penpot"}
|
||||
{:id "coreui"
|
||||
:name "CoreUI"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-coreui.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/CoreUI%20DesignSystem%20(DEMO).penpot"}
|
||||
{:id "whiteboarding-kit"
|
||||
:name "Whiteboarding Kit"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}]
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-whiteboards.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}
|
||||
{:id "material-design-baseline"
|
||||
:name "Material Design (baseline)"
|
||||
:thumbnail-uri "https://penpot.app/images/libraries/cover-material.jpg"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Material-Design-Kit.penpot"}]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<li class="rpc-item">
|
||||
<div class="rpc-row-info">
|
||||
{# <div class="type">{{item.type}}</div> #}
|
||||
<div class="module">{{item.module}}:</div>
|
||||
<div class="name">{{item.name}}</div>
|
||||
<div class="tags">
|
||||
@@ -14,23 +15,19 @@
|
||||
<span>AUTH</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if item.webhook %}
|
||||
<span class="tag">
|
||||
<span>WEBHOOK</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if item.params-schema-js %}
|
||||
<span class="tag">
|
||||
<span>SCHEMA</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpc-row-detail hidden">
|
||||
<h4>DOCSTRING:</h4>
|
||||
<h3>DOCSTRING:</h3>
|
||||
|
||||
<section class="padded-section">
|
||||
|
||||
{% if item.added %}
|
||||
<p class="small"><strong>Added:</strong> on v{{item.added}}</p>
|
||||
{% endif %}
|
||||
@@ -39,18 +36,13 @@
|
||||
<p class="small"><strong>Deprecated:</strong> since v{{item.deprecated}}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if item.entrypoint %}
|
||||
<p class="small"><strong>URI:</strong> <a href="{{item.entrypoint}}">{{item.entrypoint}}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if item.docs %}
|
||||
<p class="docstring"> {{item.docs}}</p>
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
|
||||
{% if item.changes %}
|
||||
<h4>CHANGES:</h4>
|
||||
<h3>CHANGES:</h3>
|
||||
<section class="padded-section">
|
||||
|
||||
<ul class="changes">
|
||||
@@ -61,55 +53,9 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if item.spec %}
|
||||
<h4>PARAMS (SPEC):</h4>
|
||||
<section class="padded-section">
|
||||
<pre class="spec-explain">{{item.spec}}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if param-style = "js" %}
|
||||
{% if item.params-schema-js %}
|
||||
<h4>PARAMS:</h4>
|
||||
<section class="padded-section">
|
||||
<pre class="params-schema">{{item.params-schema-js}}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if item.result-schema-js %}
|
||||
<h4>RESPONSE:</h4>
|
||||
<section class="padded-section">
|
||||
<pre class="result">{{item.result-schema-js}}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if item.webhook-schema-js %}
|
||||
<h4>WEBHOOK PAYLOAD:</h4>
|
||||
<section class="padded-section">
|
||||
<pre class="webhook">{{item.webhook-schema-js}}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if item.params-schema-clj %}
|
||||
<h4>PARAMS:</h4>
|
||||
<section class="padded-section">
|
||||
<pre class="params-schema">{{item.params-schema-clj}}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if item.result-schema-clj %}
|
||||
<h4>RESPONSE:</h4>
|
||||
<section class="padded-section">
|
||||
<pre class="result">{{item.result-schema-clj}}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if item.webhook-schema-clj %}
|
||||
<h4>WEBHOOK PAYLOAD:</h4>
|
||||
<section class="padded-section">
|
||||
<pre class="webhook">{{item.webhook-schema-clj}}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<h3>SPEC EXPLAIN:</h3>
|
||||
<section class="padded-section">
|
||||
<pre class="spec-explain">{{item.spec}}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -27,78 +27,12 @@ main {
|
||||
header {
|
||||
border-bottom: 1px solid #c0c0c0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header .menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header .menu nav {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
display: flex;
|
||||
width: 45px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
header .menu nav > a {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header .menu nav > a.selected {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
margin-top: 20px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 300;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.doc-content {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* border: 1px solid red; */
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.doc-content p {
|
||||
line-height: 22px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.doc-content h3 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.rpc-doc-content {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -131,7 +65,7 @@ h4 {
|
||||
.rpc-row-info {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
background-color: #e5e5e5;
|
||||
background-color: #eeeeee;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
@@ -174,8 +108,6 @@ h4 {
|
||||
.rpc-row-detail {
|
||||
padding: 5px 10px;
|
||||
padding-bottom: 20px;
|
||||
border-left: 2px solid #e5e5e5;
|
||||
border-right: 2px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.rpc-row-detail p {
|
||||
@@ -211,7 +143,3 @@ h4 {
|
||||
p.small strong {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
p.small a {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@@ -20,70 +20,26 @@
|
||||
<main>
|
||||
<header>
|
||||
<h1>Penpot API Documentation (v{{version}})</h1>
|
||||
<small class="menu">
|
||||
[
|
||||
<nav>
|
||||
<a href="?type=js" {% if param-style = "js" %}class="selected"{% endif %}>JS</a>
|
||||
<a href="?type=clj" {% if param-style = "cljs" %}class="selected"{% endif %}>CLJ</a>
|
||||
</nav>
|
||||
]
|
||||
</small>
|
||||
</header>
|
||||
<section class="doc-content">
|
||||
<h2>INTRODUCTION</h2>
|
||||
<p>This documentation is intended to be a general overview of the penpot RPC API.
|
||||
If you prefer, you can use <a href="/api/openapi.json">OpenAPI</a>
|
||||
and/or <a href="/api/openapi">SwaggerUI</a> as alternative.</p>
|
||||
|
||||
<h2>GENERAL NOTES</h2>
|
||||
|
||||
<h3>Authentication</h3>
|
||||
<p>The penpot backend right now offers two way for authenticate the request:
|
||||
<b>cookies</b> (the same mechanism that we use ourselves on accessing the API from the
|
||||
web application) and <b>access tokens</b>.</p>
|
||||
|
||||
<p>The cookie can be obtained using the <b>`login-with-password`</b> rpc method,
|
||||
on successful login it sets the <b>`auth-token`</b> cookie with the session
|
||||
token.</p>
|
||||
|
||||
<p>The access token can be obtained on the appropriate section on profile settings
|
||||
and it should be provided using <b>`Authorization`</b> header with <b>`Token
|
||||
<token-string>`</b> value.</p>
|
||||
|
||||
<h3>Content Negotiation</h3>
|
||||
<p>The penpot API by default operates indistinctly with: <b>`application/json`</b>
|
||||
and <b>`application/transit+json`</b> content types. You should specify the
|
||||
desired content-type on the <b>`Accept`</b> header, the transit encoding is used
|
||||
by default.</p>
|
||||
|
||||
|
||||
<h3>Limits</h3>
|
||||
<p>The rate limit work per user basis (this means that different api keys share
|
||||
the same rate limit). For now the limits are not documented because we are
|
||||
studying and analyzing the data. As a general rule, it should not be abused, if an
|
||||
abusive use is detected, we will proceed to block the user's access to the
|
||||
API.</p>
|
||||
|
||||
<h3>Webhooks</h3>
|
||||
<p>All methods that emit webhook events are marked with flag <b>WEBHOOK</b>, the
|
||||
data structure defined on each method represents the <i>payload</i> of the
|
||||
event.</p>
|
||||
<p>The webhook event structure has this aspect:</p>
|
||||
<br/>
|
||||
|
||||
<pre>
|
||||
{
|
||||
"id": "db601c95-045f-808b-8002-362f08fcb621",
|
||||
"name": "rename-file",
|
||||
"props": <payload>,
|
||||
"profileId": "db601c95-045f-808b-8002-361312e63531"
|
||||
}
|
||||
</pre>
|
||||
</section>
|
||||
<section class="rpc-doc-content">
|
||||
<h2>RPC METHODS REFERENCE:</h2>
|
||||
|
||||
<h2>RPC COMMAND METHODS:</h2>
|
||||
<ul class="rpc-items">
|
||||
{% for item in methods %}
|
||||
{% for item in command-methods %}
|
||||
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>RPC QUERY METHODS:</h2>
|
||||
<ul class="rpc-items">
|
||||
{% for item in query-methods %}
|
||||
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>RPC MUTATION METHODS:</h2>
|
||||
<ul class="rpc-items">
|
||||
{% for item in mutation-methods %}
|
||||
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -6,19 +6,13 @@ penpot - error list
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div class="title">
|
||||
<h1>Error reports (last 200)</h1>
|
||||
</div>
|
||||
<h1>Latest error reports:</h1>
|
||||
</nav>
|
||||
<main class="horizontal-list">
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li>
|
||||
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
|
||||
<a class="hint" href="/dbg/error/{{item.id}}">
|
||||
<span class="title">{{item.hint|abbreviate:150}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li><a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
|
||||
<span class="title">{{item.hint|abbreviate:150}}</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</main>
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
{% extends "app/templates/base.tmpl" %}
|
||||
|
||||
{% block title %}
|
||||
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div>[<a href="/dbg/error">⮜</a>]</div>
|
||||
<div>[<a href="#head">head</a>]</div>
|
||||
<div>[<a href="#props">props</a>]</div>
|
||||
<div>[<a href="#context">context</a>]</div>
|
||||
{% if params %}
|
||||
<div>[<a href="#params">params</a>]</div>
|
||||
{% endif %}
|
||||
{% if data %}
|
||||
<div>[<a href="#edata">data</a>]</div>
|
||||
{% endif %}
|
||||
{% if explain %}
|
||||
<div>[<a href="#explain">explain</a>]</div>
|
||||
{% endif %}
|
||||
{% if value %}
|
||||
<div>[<a href="#value">value</a>]</div>
|
||||
{% endif %}
|
||||
{% if trace %}
|
||||
<div>[<a href="#trace">trace</a>]</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<main>
|
||||
<div class="table">
|
||||
<div class="table-row multiline">
|
||||
<div id="head" class="table-key">HEAD</div>
|
||||
<div class="table-val">
|
||||
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
|
||||
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
|
||||
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
|
||||
</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">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">DATA: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{data}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if value %}
|
||||
<div class="table-row multiline">
|
||||
<div id="value" class="table-key">VALUE: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{value}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if explain %}
|
||||
<div class="table-row multiline">
|
||||
<div id="explain" class="table-key">EXPLAIN: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{explain}}</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 %}
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="SwaggerUI"
|
||||
/>
|
||||
<title>PENPOT Swagger UI</title>
|
||||
<style>{{swagger-css|safe}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script>{{swagger-js|safe}}</script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: '{{public-uri}}/api/openapi.json',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
],
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,11 +36,6 @@ small {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.not-important {
|
||||
color: #888;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
small > strong {
|
||||
font-size: 9px;
|
||||
}
|
||||
@@ -55,13 +50,7 @@ nav {
|
||||
background: #e3e3e3;
|
||||
}
|
||||
|
||||
nav > .title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav > .title > h1 {
|
||||
nav > h1 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
font-size: 11px;
|
||||
@@ -162,6 +151,7 @@ nav > div:not(:last-child) {
|
||||
line-height: 18px;
|
||||
min-width: 210px;
|
||||
margin: 0px 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
;; Example climit.edn file
|
||||
;; Required: permits
|
||||
;; Optional: queue, ommited means Integer/MAX_VALUE
|
||||
;; Optional: timeout, ommited means no timeout
|
||||
;; Note: queue and timeout are excluding
|
||||
{:update-file-by-id {:permits 1 :queue 3}
|
||||
:update-file {:permits 20}
|
||||
|
||||
:derive-password {:permits 8}
|
||||
:process-font {:permits 4 :queue 32}
|
||||
:process-image {:permits 8 :queue 32}
|
||||
|
||||
:submit-audit-events-by-profile
|
||||
{:permits 1 :queue 3}}
|
||||
;; Required: concurrency
|
||||
;; Optional: queue-size, ommited means Integer/MAX_VALUE
|
||||
{:update-file {:concurrency 1 :queue-size 3}
|
||||
:auth {:concurrency 128}
|
||||
:process-font {:concurrency 4 :queue-size 32}
|
||||
:process-image {:concurrency 8 :queue-size 32}
|
||||
:push-audit-events
|
||||
{:concurrency 1 :queue-size 3}}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||
alwaysWriteExceptions="true" />
|
||||
alwaysWriteExceptions="false" />
|
||||
</Console>
|
||||
|
||||
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||
alwaysWriteExceptions="true" />
|
||||
alwaysWriteExceptions="false" />
|
||||
<Policies>
|
||||
<SizeBasedTriggeringPolicy size="50M"/>
|
||||
</Policies>
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
{:default
|
||||
[[:default :window "200000/h"]]
|
||||
|
||||
;; #{:command/get-teams}
|
||||
;; [[:burst :bucket "5/5/5s"]]
|
||||
#{:command/get-teams}
|
||||
[[:burst :bucket "5/1/5s"]]
|
||||
|
||||
;; #{:command/get-profile}
|
||||
;; [[:burst :bucket "60/60/1m"]]
|
||||
}
|
||||
#{:command/get-profile}
|
||||
[[:burst :bucket "60/60/1m"]]}
|
||||
|
||||
@@ -18,8 +18,6 @@ cp scripts/manage.py target/dist/manage.py
|
||||
chmod +x target/dist/run.sh;
|
||||
chmod +x target/dist/manage.py
|
||||
|
||||
# Prefetch templates
|
||||
rm -rf builtin-templates;
|
||||
mkdir builtin-templates;
|
||||
# Prefetch
|
||||
bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/
|
||||
cp -r builtin-templates target/dist/
|
||||
|
||||
@@ -11,7 +11,6 @@ import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from tabulate import tabulate
|
||||
from getpass import getpass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -59,17 +58,13 @@ def print_error(res):
|
||||
break
|
||||
|
||||
def run_cmd(params):
|
||||
try:
|
||||
expr = "(app.srepl.ext/run-json-cmd {})".format(encode(params))
|
||||
res, failed = send_eval(expr)
|
||||
if failed:
|
||||
print_error(res)
|
||||
sys.exit(-1)
|
||||
expr = "(app.srepl.ext/run-json-cmd {})".format(encode(params))
|
||||
res, failed = send_eval(expr)
|
||||
if failed:
|
||||
print_error(res)
|
||||
sys.exit(-1)
|
||||
|
||||
return res
|
||||
except Exception as cause:
|
||||
print("EXC:", str(cause))
|
||||
sys.exit(-2)
|
||||
return res
|
||||
|
||||
def create_profile(fullname, email, password):
|
||||
params = {
|
||||
@@ -101,34 +96,6 @@ def update_profile(email, fullname, password, is_active):
|
||||
else:
|
||||
print(f"No profile found with email {email}")
|
||||
|
||||
def delete_profile(email, soft):
|
||||
params = {
|
||||
"cmd": "delete-profile",
|
||||
"params": {
|
||||
"email": email,
|
||||
"soft": soft
|
||||
}
|
||||
}
|
||||
|
||||
res = run_cmd(params)
|
||||
if res is True:
|
||||
print(f"Deleted")
|
||||
else:
|
||||
print(f"No profile found with email {email}")
|
||||
|
||||
def search_profile(email):
|
||||
params = {
|
||||
"cmd": "search-profile",
|
||||
"params": {
|
||||
"email": email,
|
||||
}
|
||||
}
|
||||
|
||||
res = run_cmd(params)
|
||||
|
||||
if isinstance(res, list):
|
||||
print(tabulate(res, headers="keys"))
|
||||
|
||||
def derive_password(password):
|
||||
params = {
|
||||
"cmd": "derive-password",
|
||||
@@ -140,13 +107,11 @@ def derive_password(password):
|
||||
res = run_cmd(params)
|
||||
print(f"Derived password: \"{res}\"")
|
||||
|
||||
available_commands = (
|
||||
available_commands = [
|
||||
"create-profile",
|
||||
"update-profile",
|
||||
"delete-profile",
|
||||
"search-profile",
|
||||
"derive-password",
|
||||
)
|
||||
"derive-password"
|
||||
]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
@@ -156,11 +121,10 @@ parser = argparse.ArgumentParser(
|
||||
|
||||
parser.add_argument("-V", "--version", action="version", version="Penpot CLI %%develop%%")
|
||||
parser.add_argument("action", action="store", choices=available_commands)
|
||||
parser.add_argument("-f", "--force", help="force operation", action="store_true")
|
||||
parser.add_argument("-n", "--fullname", help="fullname", action="store")
|
||||
parser.add_argument("-e", "--email", help="email", action="store")
|
||||
parser.add_argument("-p", "--password", help="password", action="store")
|
||||
parser.add_argument("-c", "--connect", help="connect to PREPL", action="store", default="tcp://localhost:6063")
|
||||
parser.add_argument("-n", "--fullname", help="Fullname", action="store")
|
||||
parser.add_argument("-e", "--email", help="Email", action="store")
|
||||
parser.add_argument("-p", "--password", help="Password", action="store")
|
||||
parser.add_argument("-c", "--connect", help="Connect to PREPL", action="store", default="tcp://localhost:6063")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -201,19 +165,3 @@ elif args.action == "derive-password":
|
||||
password = getpass("Password: ")
|
||||
|
||||
derive_password(password)
|
||||
|
||||
elif args.action == "delete-profile":
|
||||
email = args.email
|
||||
soft = not args.force
|
||||
|
||||
if email is None:
|
||||
email = input("Email: ")
|
||||
|
||||
delete_profile(email, soft)
|
||||
|
||||
elif args.action == "search-profile":
|
||||
email = args.email
|
||||
if email is None:
|
||||
email = input("Email: ")
|
||||
|
||||
search_profile(email)
|
||||
|
||||
@@ -4,15 +4,7 @@ export PENPOT_HOST=devenv
|
||||
export PENPOT_TENANT=dev
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
enable-registration
|
||||
enable-login-with-password
|
||||
enable-login-with-oidc \
|
||||
enable-login-with-google \
|
||||
enable-login-with-github \
|
||||
enable-login-with-gitlab \
|
||||
enable-backend-asserts \
|
||||
enable-fdata-storage-pointer-map \
|
||||
enable-fdata-storage-objets-map \
|
||||
enable-audit-log \
|
||||
enable-transit-readable-response \
|
||||
enable-demo-users \
|
||||
@@ -50,39 +42,19 @@ export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
|
||||
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
|
||||
|
||||
#-J-Djdk.virtualThreadScheduler.parallelism=16
|
||||
|
||||
export OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-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:+DebugNonSafepoints \
|
||||
-J-Djdk.tracePinnedThreads=full \
|
||||
-J--enable-preview";
|
||||
-J-XX:+DebugNonSafepoints";
|
||||
|
||||
# Setup HEAP
|
||||
export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m"
|
||||
# export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch"
|
||||
|
||||
# Increase virtual thread pool size
|
||||
# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16"
|
||||
|
||||
# Disable C2 Compiler
|
||||
# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1"
|
||||
|
||||
# Disable all compilers
|
||||
# export OPTIONS="$OPTIONS -J-Xint"
|
||||
|
||||
# Setup GC
|
||||
export OPTIONS="$OPTIONS -J-XX:+UseG1GC"
|
||||
|
||||
# Setup GC
|
||||
# export OPTIONS="$OPTIONS -J-XX:+UseZGC"
|
||||
|
||||
# Enable ImageMagick v7.x support
|
||||
# Uncomment for use the ImageMagick v7.x
|
||||
# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
|
||||
|
||||
export OPTIONS_EVAL="nil"
|
||||
|
||||
@@ -18,7 +18,7 @@ if [ -f ./environ ]; then
|
||||
source ./environ
|
||||
fi
|
||||
|
||||
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --enable-preview $JVM_OPTS"
|
||||
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow $JVM_OPTS"
|
||||
|
||||
set -x
|
||||
exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main
|
||||
|
||||
@@ -2,20 +2,7 @@
|
||||
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_TENANT=dev
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
enable-prepl-server \
|
||||
enable-urepl-server \
|
||||
enable-webhooks \
|
||||
enable-backend-asserts \
|
||||
enable-audit-log \
|
||||
enable-transit-readable-response \
|
||||
enable-demo-users \
|
||||
enable-fdata-storage-pointer-map \
|
||||
enable-fdata-storage-objets-map \
|
||||
disable-secure-session-cookies \
|
||||
enable-smtp \
|
||||
enable-access-tokens";
|
||||
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp enable-webhooks"
|
||||
|
||||
set -ex
|
||||
|
||||
|
||||
@@ -6,20 +6,15 @@
|
||||
|
||||
(ns app.auth
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[buddy.hashers :as hashers]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def default-params
|
||||
{:alg :argon2id
|
||||
:memory (* 32768 2) ;; 64 MiB
|
||||
:iterations 7
|
||||
:parallelism (px/get-available-processors)})
|
||||
[buddy.hashers :as hashers]))
|
||||
|
||||
(defn derive-password
|
||||
[password]
|
||||
(hashers/derive password default-params))
|
||||
(hashers/derive password
|
||||
{:alg :argon2id
|
||||
:memory 16384
|
||||
:iterations 20
|
||||
:parallelism 2}))
|
||||
|
||||
(defn verify-password
|
||||
[attempt password]
|
||||
@@ -29,16 +24,3 @@
|
||||
{:update false
|
||||
:valid false})))
|
||||
|
||||
(defn email-domain-in-whitelist?
|
||||
"Returns true if email's domain is in the given whitelist or if
|
||||
given whitelist is an empty string."
|
||||
([email]
|
||||
(let [domains (cf/get :registration-domain-whitelist)]
|
||||
(email-domain-in-whitelist? domains email)))
|
||||
([domains email]
|
||||
(if (or (nil? domains) (empty? domains))
|
||||
true
|
||||
(let [[_ candidate] (-> (str/lower email)
|
||||
(str/split #"@" 2))]
|
||||
(contains? domains candidate)))))
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.auth.oidc
|
||||
"OIDC client implementation."
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.auth.oidc.providers :as-alias providers]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
@@ -18,6 +17,7 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.http.middleware :as hmw]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
@@ -25,13 +25,14 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[buddy.sign.jwk :as jwk]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[yetti.response :as-alias yrs]))
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[yetti.response :as yrs]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
@@ -50,29 +51,36 @@
|
||||
|
||||
(defn- discover-oidc-config
|
||||
[cfg {:keys [base-uri] :as opts}]
|
||||
(let [uri (dm/str (u/join base-uri ".well-known/openid-configuration"))
|
||||
rsp (http/req! cfg {:method :get :uri uri} {:sync? true})]
|
||||
(if (= 200 (:status rsp))
|
||||
(let [data (-> rsp :body json/decode)
|
||||
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
|
||||
response (ex/try! (http/req! cfg
|
||||
{:method :get :uri (str discovery-uri)}
|
||||
{:sync? true}))]
|
||||
(cond
|
||||
(ex/exception? response)
|
||||
(do
|
||||
(l/warn :hint "unable to discover oidc configuration"
|
||||
:discover-uri (str discovery-uri)
|
||||
:cause response)
|
||||
nil)
|
||||
|
||||
(= 200 (:status response))
|
||||
(let [data (json/decode (:body response))
|
||||
token-uri (get data :token_endpoint)
|
||||
auth-uri (get data :authorization_endpoint)
|
||||
user-uri (get data :userinfo_endpoint)
|
||||
jwks-uri (get data :jwks_uri)]
|
||||
|
||||
user-uri (get data :userinfo_endpoint)]
|
||||
(l/debug :hint "oidc uris discovered"
|
||||
:token-uri token-uri
|
||||
:auth-uri auth-uri
|
||||
:user-uri user-uri
|
||||
:jwks-uri jwks-uri)
|
||||
|
||||
:user-uri user-uri)
|
||||
{:token-uri token-uri
|
||||
:auth-uri auth-uri
|
||||
:user-uri user-uri
|
||||
:jwks-uri jwks-uri})
|
||||
:user-uri user-uri})
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/warn :hint "unable to discover OIDC configuration"
|
||||
:discover-uri uri
|
||||
:http-status (:status rsp))
|
||||
:uri (str discovery-uri)
|
||||
:response-status-code (:status response))
|
||||
nil))))
|
||||
|
||||
(defn- prepare-oidc-opts
|
||||
@@ -83,7 +91,6 @@
|
||||
:token-uri (cf/get :oidc-token-uri)
|
||||
:auth-uri (cf/get :oidc-auth-uri)
|
||||
:user-uri (cf/get :oidc-user-uri)
|
||||
:jwks-uri (cf/get :oidc-jwks-uri)
|
||||
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
|
||||
:roles-attr (cf/get :oidc-roles-attr)
|
||||
:roles (cf/get :oidc-roles)
|
||||
@@ -98,42 +105,8 @@
|
||||
(string? (:user-uri opts))
|
||||
(string? (:auth-uri opts)))
|
||||
opts
|
||||
(try
|
||||
(-> (discover-oidc-config cfg opts)
|
||||
(merge opts {:discover? true}))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unable to discover OIDC configuration"
|
||||
:cause cause)))))))
|
||||
|
||||
(defn- process-oidc-jwks
|
||||
[keys]
|
||||
(reduce (fn [result {:keys [kid] :as kdata}]
|
||||
(let [pkey (ex/try! (jwk/public-key kdata))]
|
||||
(if (ex/exception? pkey)
|
||||
(do
|
||||
(l/warn :hint "unable to create public key"
|
||||
:kid (:kid kdata)
|
||||
:cause pkey)
|
||||
result)
|
||||
(assoc result kid pkey))))
|
||||
{}
|
||||
keys))
|
||||
|
||||
(defn- fetch-oidc-jwks
|
||||
[cfg {:keys [jwks-uri]}]
|
||||
(when jwks-uri
|
||||
(try
|
||||
(let [{:keys [status body]} (http/req! cfg {:method :get :uri jwks-uri} {:sync? true})]
|
||||
(if (= 200 status)
|
||||
(-> body json/decode :keys process-oidc-jwks)
|
||||
(do
|
||||
(l/warn :hint "unable to retrieve JWKs (unexpected response status code)"
|
||||
:http-status status
|
||||
:http-body body)
|
||||
nil)))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unable to retrieve JWKs (unexpected exception)"
|
||||
:cause cause)))))
|
||||
(some-> (discover-oidc-config cfg opts)
|
||||
(merge opts {:discover? true}))))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::providers/generic [_]
|
||||
(s/keys :req [::http/client]))
|
||||
@@ -142,7 +115,7 @@
|
||||
[_ cfg]
|
||||
(when (contains? cf/flags :login-with-oidc)
|
||||
(if-let [opts (prepare-oidc-opts cfg)]
|
||||
(let [jwks (fetch-oidc-jwks cfg opts)]
|
||||
(do
|
||||
(l/info :hint "provider initialized"
|
||||
:provider "oidc"
|
||||
:method (if (:discover? opts) "discover" "manual")
|
||||
@@ -153,9 +126,8 @@
|
||||
:user-uri (:user-uri opts)
|
||||
:token-uri (:token-uri opts)
|
||||
:roles-attr (:roles-attr opts)
|
||||
:roles (:roles opts)
|
||||
:keys (str/join "," (map str (keys jwks))))
|
||||
(assoc opts :jwks jwks))
|
||||
:roles (:roles opts))
|
||||
opts)
|
||||
(do
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
|
||||
nil))))
|
||||
@@ -193,23 +165,21 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- retrieve-github-email
|
||||
[cfg tdata props]
|
||||
(or (some-> props :github/email)
|
||||
(let [params {:uri "https://api.github.com/user/emails"
|
||||
:headers {"Authorization" (dm/str (:token/type tdata) " " (:token/access tdata))}
|
||||
:timeout 6000
|
||||
:method :get}
|
||||
|
||||
{:keys [status body]} (http/req! cfg params {:sync? true})]
|
||||
|
||||
(when-not (s/int-in-range? 200 300 status)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-github-emails
|
||||
:hint "unable to retrieve github emails"
|
||||
:http-status status
|
||||
:http-body body))
|
||||
|
||||
(->> body json/decode (filter :primary) first :email))))
|
||||
[cfg tdata info]
|
||||
(or (some-> info :email p/resolved)
|
||||
(->> (http/req! cfg
|
||||
{:uri "https://api.github.com/user/emails"
|
||||
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
|
||||
:timeout 6000
|
||||
:method :get})
|
||||
(p/map (fn [{:keys [status body] :as response}]
|
||||
(when-not (s/int-in-range? 200 300 status)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-github-emails
|
||||
:hint "unable to retrieve github emails"
|
||||
:http-status status
|
||||
:http-body body))
|
||||
(->> response :body json/decode (filter :primary) first :email))))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::providers/github [_]
|
||||
(s/keys :req [::http/client]))
|
||||
@@ -226,7 +196,7 @@
|
||||
|
||||
;; Additional hooks for provider specific way of
|
||||
;; retrieve emails.
|
||||
:get-email-fn (partial retrieve-github-email cfg)}]
|
||||
:get-email-fn (partial retrieve-github-email cfg)}]
|
||||
|
||||
(when (contains? cf/flags :login-with-github)
|
||||
(if (and (string? (:client-id opts))
|
||||
@@ -276,11 +246,6 @@
|
||||
;; HANDLERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- parse-attr-path
|
||||
[provider path]
|
||||
(let [[fitem & items] (str/split path "__")]
|
||||
(into [(keyword (:name provider) fitem)] (map keyword) items)))
|
||||
|
||||
(defn- build-redirect-uri
|
||||
[{:keys [provider] :as cfg}]
|
||||
(let [public (u/uri (cf/get :public-uri))]
|
||||
@@ -305,7 +270,7 @@
|
||||
{}
|
||||
props))
|
||||
|
||||
(defn fetch-access-token
|
||||
(defn retrieve-access-token
|
||||
[{:keys [provider] :as cfg} code]
|
||||
(let [params {:client_id (:client-id provider)
|
||||
:client_secret (:client-secret provider)
|
||||
@@ -325,83 +290,80 @@
|
||||
:grant-type (:grant_type params)
|
||||
:redirect-uri (:redirect_uri params))
|
||||
|
||||
(let [{:keys [status body]} (http/req! cfg req {:sync? true})]
|
||||
(l/trace :hint "access token response" :status status :body body)
|
||||
(if (= status 200)
|
||||
(let [data (json/decode body)]
|
||||
{:token/access (get data :access_token)
|
||||
:token/id (get data :id_token)
|
||||
:token/type (get data :token_type)})
|
||||
(->> (http/req! cfg req)
|
||||
(p/map (fn [{:keys [status body] :as res}]
|
||||
(l/trace :hint "access token response"
|
||||
:status status
|
||||
:body body)
|
||||
(if (= status 200)
|
||||
(let [data (json/decode body)]
|
||||
{:token (get data :access_token)
|
||||
:type (get data :token_type)})
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-token
|
||||
:http-status status
|
||||
:http-body body)))))))
|
||||
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-token
|
||||
:hint "unable to retrieve token"
|
||||
:http-status status
|
||||
:http-body body)))))
|
||||
(defn- retrieve-user-info
|
||||
[{:keys [provider] :as cfg} tdata]
|
||||
(letfn [(retrieve []
|
||||
(l/trace :hint "request user info"
|
||||
:uri (:user-uri provider)
|
||||
:token (obfuscate-string (:token tdata))
|
||||
:token-type (:type tdata))
|
||||
(http/req! cfg
|
||||
{:uri (:user-uri provider)
|
||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
||||
:timeout 6000
|
||||
:method :get}))
|
||||
(validate-response [response]
|
||||
(l/trace :hint "user info response"
|
||||
:status (:status response)
|
||||
:body (:body response))
|
||||
(when-not (s/int-in-range? 200 300 (:status response))
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-user-info
|
||||
:hint "unable to retrieve user info"
|
||||
:http-status (:status response)
|
||||
:http-body (:body response)))
|
||||
response)
|
||||
|
||||
(defn- process-user-info
|
||||
[provider tdata info]
|
||||
(letfn [(get-email [props]
|
||||
(get-email [info]
|
||||
;; Allow providers hook into this for custom email
|
||||
;; retrieval method.
|
||||
(if-let [get-email-fn (:get-email-fn provider)]
|
||||
(get-email-fn tdata props)
|
||||
(let [attr-kw (cf/get :oidc-email-attr "email")
|
||||
attr-ph (parse-attr-path provider attr-kw)]
|
||||
(get-in props attr-ph))))
|
||||
(get-email-fn tdata info)
|
||||
(let [attr-kw (cf/get :oidc-email-attr :email)]
|
||||
(p/resolved (get info attr-kw)))))
|
||||
|
||||
(get-name [props]
|
||||
(let [attr-kw (cf/get :oidc-name-attr "name")
|
||||
attr-ph (parse-attr-path provider attr-kw)]
|
||||
(get-in props attr-ph)))
|
||||
]
|
||||
(get-name [info]
|
||||
(let [attr-kw (cf/get :oidc-name-attr :name)]
|
||||
(get info attr-kw)))
|
||||
|
||||
(let [props (qualify-props provider info)
|
||||
email (get-email props)]
|
||||
{:backend (:name provider)
|
||||
:fullname (or (get-name props) email)
|
||||
:email email
|
||||
:props props})))
|
||||
(process-response [response]
|
||||
(p/let [info (-> response :body json/decode)
|
||||
email (get-email info)]
|
||||
{:backend (:name provider)
|
||||
:email email
|
||||
:fullname (or (get-name info) email)
|
||||
:props (->> (dissoc info :name :email)
|
||||
(qualify-props provider))}))
|
||||
|
||||
(defn- fetch-user-info
|
||||
[{:keys [provider] :as cfg} tdata]
|
||||
(l/trace :hint "fetch user info"
|
||||
:uri (:user-uri provider)
|
||||
:token (obfuscate-string (:token/access tdata)))
|
||||
(validate-info [info]
|
||||
(l/trace :hint "authentication info" :info info)
|
||||
(when-not (s/valid? ::info info)
|
||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
|
||||
:info (pr-str info))
|
||||
(ex/raise :type :internal
|
||||
:code :incomplete-user-info
|
||||
:hint "inconmplete user info"
|
||||
:info info))
|
||||
info)]
|
||||
|
||||
(let [params {:uri (:user-uri provider)
|
||||
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
|
||||
:timeout 6000
|
||||
:method :get}
|
||||
response (http/req! cfg params {:sync? true})]
|
||||
|
||||
(l/trace :hint "user info response"
|
||||
:status (:status response)
|
||||
:body (:body response))
|
||||
|
||||
(when-not (s/int-in-range? 200 300 (:status response))
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-user-info
|
||||
:hint "unable to retrieve user info"
|
||||
:http-status (:status response)
|
||||
:http-body (:body response)))
|
||||
|
||||
(-> response :body json/decode)))
|
||||
|
||||
(defn- get-user-info
|
||||
[{:keys [provider]} tdata]
|
||||
(try
|
||||
(when (:token/id tdata)
|
||||
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
|
||||
(when-let [key (if (str/starts-with? (name alg) "hs")
|
||||
(:client-secret provider)
|
||||
(get-in provider [:jwks kid]))]
|
||||
|
||||
(let [claims (jwt/unsign (:token/id tdata) key {:alg alg})]
|
||||
(dissoc claims :exp :iss :iat :sid :aud :sub)))))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unable to get user info from JWT token (unexpected exception)"
|
||||
:cause cause))))
|
||||
(->> (retrieve)
|
||||
(p/fmap validate-response)
|
||||
(p/mcat process-response)
|
||||
(p/fmap validate-info))))
|
||||
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
(s/def ::email ::us/not-empty-string)
|
||||
@@ -414,93 +376,68 @@
|
||||
::props]))
|
||||
|
||||
(defn get-info
|
||||
[{:keys [provider ::main/props] :as cfg} {:keys [params] :as request}]
|
||||
(when-let [error (get params :error)]
|
||||
(ex/raise :type :internal
|
||||
:code :error-on-retrieving-code
|
||||
:error-id error
|
||||
:error-desc (get params :error_description)))
|
||||
[{:keys [provider] :as cfg} {:keys [params] :as request}]
|
||||
(letfn [(validate-oidc [info]
|
||||
;; If the provider is OIDC, we can proceed to check
|
||||
;; roles if they are defined.
|
||||
(when (and (= "oidc" (:name provider))
|
||||
(seq (:roles provider)))
|
||||
(let [provider-roles (into #{} (:roles provider))
|
||||
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
|
||||
roles (get info attr)]
|
||||
(cond
|
||||
(string? roles) (into #{} (str/words roles))
|
||||
(vector? roles) (into #{} roles)
|
||||
:else #{}))]
|
||||
|
||||
(let [state (get params :state)
|
||||
code (get params :code)
|
||||
state (tokens/verify props {:token state :iss :oauth})
|
||||
tdata (fetch-access-token cfg code)
|
||||
info (case (cf/get :oidc-user-info-source)
|
||||
:token (get-user-info cfg tdata)
|
||||
:userinfo (fetch-user-info cfg tdata)
|
||||
(or (get-user-info cfg tdata)
|
||||
(fetch-user-info cfg tdata)))
|
||||
;; check if profile has a configured set of roles
|
||||
(when-not (set/subset? provider-roles profile-roles)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-auth
|
||||
:hint "not enough permissions"))))
|
||||
info)
|
||||
|
||||
info (process-user-info provider tdata info)]
|
||||
(post-process [state info]
|
||||
(cond-> info
|
||||
(some? (:invitation-token state))
|
||||
(assoc :invitation-token (:invitation-token state))
|
||||
|
||||
(l/trace :hint "user info" :info info)
|
||||
;; If state token comes with props, merge them. The state token
|
||||
;; props can contain pm_ and utm_ prefixed query params.
|
||||
(map? (:props state))
|
||||
(update :props merge (:props state))))]
|
||||
|
||||
(when-not (s/valid? ::info info)
|
||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
|
||||
(when-let [error (get params :error)]
|
||||
(ex/raise :type :internal
|
||||
:code :incomplete-user-info
|
||||
:hint "inconmplete user info"
|
||||
:info info))
|
||||
:code :error-on-retrieving-code
|
||||
:error-id error
|
||||
:error-desc (get params :error_description)))
|
||||
|
||||
;; If the provider is OIDC, we can proceed to check
|
||||
;; roles if they are defined.
|
||||
(when (and (= "oidc" (:name provider))
|
||||
(seq (:roles provider)))
|
||||
|
||||
(let [expected-roles (into #{} (:roles provider))
|
||||
current-roles (let [roles-kw (cf/get :oidc-roles-attr "roles")
|
||||
roles-ph (parse-attr-path provider roles-kw)
|
||||
roles (get-in (:props info) roles-ph)]
|
||||
(cond
|
||||
(string? roles) (into #{} (str/words roles))
|
||||
(vector? roles) (into #{} roles)
|
||||
:else #{}))]
|
||||
|
||||
;; check if profile has a configured set of roles
|
||||
(when-not (set/subset? expected-roles current-roles)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-auth
|
||||
:hint "not enough permissions"))))
|
||||
|
||||
(cond-> info
|
||||
(some? (:invitation-token state))
|
||||
(assoc :invitation-token (:invitation-token state))
|
||||
|
||||
;; If state token comes with props, merge them. The state token
|
||||
;; props can contain pm_ and utm_ prefixed query params.
|
||||
(map? (:props state))
|
||||
(update :props merge (:props state)))))
|
||||
(let [state (get params :state)
|
||||
code (get params :code)
|
||||
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})]
|
||||
(-> (p/resolved code)
|
||||
(p/then #(retrieve-access-token cfg %))
|
||||
(p/then #(retrieve-user-info cfg %))
|
||||
(p/then' validate-oidc)
|
||||
(p/then' (partial post-process state))))))
|
||||
|
||||
(defn- get-profile
|
||||
[{:keys [::db/pool] :as cfg} info]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(some->> (:email info)
|
||||
(profile/get-profile-by-email conn))))
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} info]
|
||||
(px/with-dispatch executor
|
||||
(with-open [conn (db/open pool)]
|
||||
(some->> (:email info)
|
||||
(profile/get-profile-by-email conn)))))
|
||||
|
||||
(defn- redirect-response
|
||||
[uri]
|
||||
{::yrs/status 302
|
||||
::yrs/headers {"location" (str uri)}})
|
||||
(yrs/response :status 302 :headers {"location" (str uri)}))
|
||||
|
||||
(defn- generate-error-redirect
|
||||
[_ cause]
|
||||
(let [data (if (ex/error? cause) (ex-data cause) nil)
|
||||
code (or (:code data) :unexpected)
|
||||
type (or (:type data) :internal)
|
||||
hint (or (:hint data)
|
||||
(if (ex/exception? cause)
|
||||
(ex-message cause)
|
||||
(str cause)))
|
||||
|
||||
params {:error "unable-to-auth"
|
||||
:hint hint
|
||||
:type type
|
||||
:code code}
|
||||
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/login")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
[_ error]
|
||||
(let [uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/login")
|
||||
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
|
||||
(redirect-response uri)))
|
||||
|
||||
(defn- generate-redirect
|
||||
@@ -521,32 +458,27 @@
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(audit/submit! cfg {::audit/type "command"
|
||||
::audit/name "login-with-oidc"
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/ip-addr (audit/parse-client-ip request)
|
||||
::audit/props (audit/profile->props profile)})
|
||||
(audit/submit! cfg {:type "command"
|
||||
:name "login-with-password"
|
||||
:profile-id (:id profile)
|
||||
:ip-addr (audit/parse-client-ip request)
|
||||
:props (audit/profile->props profile)})
|
||||
|
||||
(->> (redirect-response uri)
|
||||
(sxf request)))
|
||||
|
||||
|
||||
(if (auth/email-domain-in-whitelist? (:email info))
|
||||
(let [info (assoc info
|
||||
:iss :prepared-register
|
||||
:is-active true
|
||||
:exp (dt/in-future {:hours 48}))
|
||||
token (tokens/generate (::main/props cfg) info)
|
||||
params (d/without-nils
|
||||
{:token token
|
||||
:fullname (:fullname info)})
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/register/validate")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
(redirect-response uri))
|
||||
(generate-error-redirect cfg "email-domain-not-allowed"))))
|
||||
|
||||
(let [info (assoc info
|
||||
:iss :prepared-register
|
||||
:is-active true
|
||||
:exp (dt/in-future {:hours 48}))
|
||||
token (tokens/generate (::main/props cfg) info)
|
||||
params (d/without-nils
|
||||
{:token token
|
||||
:fullname (:fullname info)})
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/register/validate")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(redirect-response uri))))
|
||||
|
||||
(defn- auth-handler
|
||||
[cfg {:keys [params] :as request}]
|
||||
@@ -557,24 +489,27 @@
|
||||
:props props
|
||||
:exp (dt/in-future "4h")})
|
||||
uri (build-auth-uri cfg state)]
|
||||
{::yrs/status 200
|
||||
::yrs/body {:redirect-uri uri}}))
|
||||
(yrs/response 200 {:redirect-uri uri})))
|
||||
|
||||
(defn- callback-handler
|
||||
[cfg request]
|
||||
(try
|
||||
(let [info (get-info cfg request)
|
||||
profile (get-profile cfg info)]
|
||||
(generate-redirect cfg request info profile))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "error on oauth process" :cause cause)
|
||||
(generate-error-redirect cfg cause))))
|
||||
(letfn [(process-request []
|
||||
(p/let [info (get-info cfg request)
|
||||
profile (get-profile cfg info)]
|
||||
(generate-redirect cfg request info profile)))
|
||||
|
||||
(handle-error [cause]
|
||||
(l/error :hint "error on oauth process" :cause cause)
|
||||
(generate-error-redirect cfg cause))]
|
||||
|
||||
(-> (process-request)
|
||||
(p/catch handle-error))))
|
||||
|
||||
(def provider-lookup
|
||||
{:compile
|
||||
(fn [& _]
|
||||
(fn [handler {:keys [::providers] :as cfg}]
|
||||
(fn [request]
|
||||
(fn [handler]
|
||||
(fn [{:keys [::providers] :as cfg} request]
|
||||
(let [provider (some-> request :path-params :provider keyword)]
|
||||
(if-let [provider (get providers provider)]
|
||||
(handler (assoc cfg :provider provider) request)
|
||||
@@ -618,15 +553,18 @@
|
||||
[_]
|
||||
(s/keys :req [::session/manager
|
||||
::http/client
|
||||
::wrk/executor
|
||||
::main/props
|
||||
::db/pool
|
||||
::providers]))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
[_ {:keys [::wrk/executor] :as cfg}]
|
||||
(let [cfg (update cfg :provider d/without-nils)]
|
||||
["" {:middleware [[session/authz cfg]
|
||||
[provider-lookup cfg]]}
|
||||
[hmw/with-dispatch executor]
|
||||
[hmw/with-config cfg]
|
||||
[provider-lookup]]}
|
||||
["/auth/oauth"
|
||||
["/:provider"
|
||||
{:handler auth-handler
|
||||
|
||||
169
backend/src/app/cli/manage.clj
Normal file
169
backend/src/app/cli/manage.clj
Normal file
@@ -0,0 +1,169 @@
|
||||
;; 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.cli.manage
|
||||
"A manage cli api."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[clojure.string :as str]
|
||||
[clojure.tools.cli :refer [parse-opts]]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
java.io.Console))
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn init-system
|
||||
[]
|
||||
(let [data (-> main/system-config
|
||||
(select-keys [:app.db/pool :app.metrics/metrics])
|
||||
(assoc :app.migrations/all {}))]
|
||||
(-> data ig/prep ig/init)))
|
||||
|
||||
(defn- read-from-console
|
||||
[{:keys [label type] :or {type :text}}]
|
||||
(let [^Console console (System/console)]
|
||||
(when-not console
|
||||
(l/error :hint "no console found, can proceed")
|
||||
(System/exit 1))
|
||||
|
||||
(binding [*out* (.writer console)]
|
||||
(print label " ")
|
||||
(.flush *out*))
|
||||
|
||||
(case type
|
||||
:text (.readLine console)
|
||||
:password (String. (.readPassword console)))))
|
||||
|
||||
(defn create-profile
|
||||
[options]
|
||||
(let [system (init-system)
|
||||
email (or (:email options)
|
||||
(read-from-console {:label "Email:"}))
|
||||
fullname (or (:fullname options)
|
||||
(read-from-console {:label "Full Name:"}))
|
||||
password (or (:password options)
|
||||
(read-from-console {:label "Password:"
|
||||
:type :password}))]
|
||||
(try
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(->> (auth/create-profile! conn
|
||||
{:fullname fullname
|
||||
:email email
|
||||
:password password
|
||||
:is-active true
|
||||
:is-demo false})
|
||||
(auth/create-profile-rels! conn)))
|
||||
|
||||
(when (pos? (:verbosity options))
|
||||
(println "User created successfully."))
|
||||
|
||||
(System/exit 0)
|
||||
|
||||
(catch Exception _e
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Unable to create user, already exists."))
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn reset-password
|
||||
[options]
|
||||
(let [system (init-system)]
|
||||
(try
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [email (or (:email options)
|
||||
(read-from-console {:label "Email:"}))
|
||||
profile (profile/get-profile-by-email conn email)]
|
||||
(when-not profile
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Profile does not exists."))
|
||||
(System/exit 1))
|
||||
|
||||
(let [password (or (:password options)
|
||||
(read-from-console {:label "Password:"
|
||||
:type :password}))]
|
||||
(profile/update-profile-password! conn (assoc profile :password password))
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Password changed successfully.")))))
|
||||
(System/exit 0)
|
||||
(catch Exception e
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Unable to change password."))
|
||||
(when (= 2 (:verbosity options))
|
||||
(.printStackTrace e))
|
||||
(System/exit 1)))))
|
||||
|
||||
;; --- CLI PARSE
|
||||
|
||||
(def cli-options
|
||||
;; An option with a required argument
|
||||
[["-u" "--email EMAIL" "Email Address"]
|
||||
["-p" "--password PASSWORD" "Password"]
|
||||
["-n" "--name FULLNAME" "Full Name"
|
||||
:id :fullname]
|
||||
["-v" nil "Verbosity level"
|
||||
:id :verbosity
|
||||
:default 1
|
||||
:update-fn inc]
|
||||
["-q" nil "Don't print to console"
|
||||
:id :verbosity
|
||||
:update-fn (constantly 0)]
|
||||
["-h" "--help"]])
|
||||
|
||||
(defn usage
|
||||
[options-summary]
|
||||
(->> ["Penpot CLI management."
|
||||
""
|
||||
"Usage: manage [options] action"
|
||||
""
|
||||
"Options:"
|
||||
options-summary
|
||||
""
|
||||
"Actions:"
|
||||
" create-profile Create new profile."
|
||||
" reset-password Reset profile password."
|
||||
""]
|
||||
(str/join \newline)))
|
||||
|
||||
(defn error-msg [errors]
|
||||
(str "The following errors occurred while parsing your command:\n\n"
|
||||
(str/join \newline errors)))
|
||||
|
||||
(defn validate-args
|
||||
"Validate command line arguments. Either return a map indicating the program
|
||||
should exit (with a error message, and optional ok status), or a map
|
||||
indicating the action the program should take and the options provided."
|
||||
[args]
|
||||
(let [{:keys [options arguments errors summary] :as opts} (parse-opts args cli-options)]
|
||||
(cond
|
||||
(:help options) ; help => exit OK with usage summary
|
||||
{:exit-message (usage summary) :ok? true}
|
||||
|
||||
errors ; errors => exit with description of errors
|
||||
{:exit-message (error-msg errors)}
|
||||
|
||||
;; custom validation on arguments
|
||||
:else
|
||||
(let [action (first arguments)]
|
||||
(if (#{"create-profile" "reset-password"} action)
|
||||
{:action (first arguments) :options options}
|
||||
{:exit-message (usage summary)})))))
|
||||
|
||||
(defn exit [status msg]
|
||||
(println msg)
|
||||
(System/exit status))
|
||||
|
||||
(defn -main
|
||||
[& args]
|
||||
(let [{:keys [action options exit-message ok?]} (validate-args args)]
|
||||
(if exit-message
|
||||
(exit (if ok? 0 1) exit-message)
|
||||
(case action
|
||||
"create-profile" (create-profile options)
|
||||
"reset-password" (reset-password options)))))
|
||||
@@ -146,18 +146,16 @@
|
||||
(s/def ::google-client-id ::us/string)
|
||||
(s/def ::google-client-secret ::us/string)
|
||||
(s/def ::oidc-client-id ::us/string)
|
||||
(s/def ::oidc-user-info-source ::us/keyword)
|
||||
(s/def ::oidc-client-secret ::us/string)
|
||||
(s/def ::oidc-base-uri ::us/string)
|
||||
(s/def ::oidc-token-uri ::us/string)
|
||||
(s/def ::oidc-auth-uri ::us/string)
|
||||
(s/def ::oidc-user-uri ::us/string)
|
||||
(s/def ::oidc-jwks-uri ::us/string)
|
||||
(s/def ::oidc-scopes ::us/set-of-strings)
|
||||
(s/def ::oidc-roles ::us/set-of-strings)
|
||||
(s/def ::oidc-roles-attr ::us/string)
|
||||
(s/def ::oidc-email-attr ::us/string)
|
||||
(s/def ::oidc-name-attr ::us/string)
|
||||
(s/def ::oidc-roles-attr ::us/keyword)
|
||||
(s/def ::oidc-email-attr ::us/keyword)
|
||||
(s/def ::oidc-name-attr ::us/keyword)
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::http-server-port ::us/integer)
|
||||
(s/def ::http-server-host ::us/string)
|
||||
@@ -243,12 +241,10 @@
|
||||
::google-client-secret
|
||||
::oidc-client-id
|
||||
::oidc-client-secret
|
||||
::oidc-user-info-source
|
||||
::oidc-base-uri
|
||||
::oidc-token-uri
|
||||
::oidc-auth-uri
|
||||
::oidc-user-uri
|
||||
::oidc-jwks-uri
|
||||
::oidc-scopes
|
||||
::oidc-roles-attr
|
||||
::oidc-email-attr
|
||||
@@ -327,7 +323,6 @@
|
||||
|
||||
(def default-flags
|
||||
[:enable-backend-api-doc
|
||||
:enable-backend-openapi-doc
|
||||
:enable-backend-worker
|
||||
:enable-secure-session-cookies
|
||||
:enable-email-verification])
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.db
|
||||
(:refer-clojure :exclude [get run!])
|
||||
(:refer-clojure :exclude [get])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -155,18 +155,8 @@
|
||||
(.isClosed ^HikariDataSource pool))
|
||||
|
||||
(defn read-only?
|
||||
[pool-or-conn]
|
||||
(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")))
|
||||
[pool]
|
||||
(.isReadOnly ^HikariDataSource pool))
|
||||
|
||||
(defn create-pool
|
||||
[cfg]
|
||||
@@ -218,13 +208,7 @@
|
||||
|
||||
(defmacro with-atomic
|
||||
[& args]
|
||||
(if (symbol? (first args))
|
||||
(let [cfgs (first args)
|
||||
body (rest args)]
|
||||
`(jdbc/with-transaction [conn# (::pool ~cfgs)]
|
||||
(let [~cfgs (assoc ~cfgs ::conn conn#)]
|
||||
~@body)))
|
||||
`(jdbc/with-transaction ~@args)))
|
||||
`(jdbc/with-transaction ~@args))
|
||||
|
||||
(defn open
|
||||
[pool]
|
||||
@@ -299,10 +283,6 @@
|
||||
:hint "database object not found"))
|
||||
row))
|
||||
|
||||
(defn plan
|
||||
[ds sql]
|
||||
(jdbc/plan ds sql sql/default-opts))
|
||||
|
||||
(defn get-by-id
|
||||
[ds table id & {:as opts}]
|
||||
(get ds table {:id id} opts))
|
||||
@@ -371,72 +351,18 @@
|
||||
[data]
|
||||
(org.postgresql.util.PGInterval. ^String data))
|
||||
|
||||
(defn connection?
|
||||
[conn]
|
||||
(instance? Connection conn))
|
||||
|
||||
(defn savepoint
|
||||
([^Connection conn]
|
||||
(.setSavepoint conn))
|
||||
([^Connection conn label]
|
||||
(.setSavepoint conn (name label))))
|
||||
|
||||
(defn release!
|
||||
[^Connection conn ^Savepoint sp ]
|
||||
(.releaseSavepoint conn sp))
|
||||
|
||||
(defn rollback!
|
||||
([^Connection conn]
|
||||
(.rollback conn))
|
||||
([^Connection conn ^Savepoint sp]
|
||||
(.rollback conn sp)))
|
||||
|
||||
(defn tx-run!
|
||||
[cfg f]
|
||||
(cond
|
||||
(connection? cfg)
|
||||
(tx-run! {::conn cfg} f)
|
||||
|
||||
(pool? cfg)
|
||||
(tx-run! {::pool cfg} f)
|
||||
|
||||
(::conn cfg)
|
||||
(let [conn (::conn cfg)
|
||||
sp (savepoint conn)]
|
||||
(try
|
||||
(let [result (f cfg)]
|
||||
(release! conn sp)
|
||||
result)
|
||||
(catch Throwable cause
|
||||
(rollback! sp)
|
||||
(throw cause))))
|
||||
|
||||
(::pool cfg)
|
||||
(with-atomic [conn (::pool cfg)]
|
||||
(f (assoc cfg ::conn conn)))
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "invalid arguments"))))
|
||||
|
||||
(defn run!
|
||||
[cfg f]
|
||||
(cond
|
||||
(connection? cfg)
|
||||
(run! {::conn cfg} f)
|
||||
|
||||
(pool? cfg)
|
||||
(run! {::pool cfg} f)
|
||||
|
||||
(::conn cfg)
|
||||
(f cfg)
|
||||
|
||||
(::pool cfg)
|
||||
(with-open [^Connection conn (open (::pool cfg))]
|
||||
(f (assoc cfg ::conn conn)))
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "invalid arguments"))))
|
||||
|
||||
(defn interval
|
||||
[o]
|
||||
(cond
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- parse-address
|
||||
^"[Ljakarta.mail.internet.InternetAddress;"
|
||||
[v]
|
||||
(InternetAddress/parse ^String v))
|
||||
|
||||
@@ -65,7 +64,7 @@
|
||||
(some? bcc) (assign-recipient :bcc bcc)))
|
||||
|
||||
(defn- assign-from
|
||||
[mmsg {:keys [::default-from] :as cfg} {:keys [from] :as params}]
|
||||
[mmsg {:keys [default-from]} {:keys [from] :as props}]
|
||||
(let [from (or from default-from)]
|
||||
(when from
|
||||
(let [from (parse-address from)]
|
||||
@@ -150,7 +149,6 @@
|
||||
"mail.smtp.connectiontimeout" timeout}))
|
||||
|
||||
(defn- create-smtp-session
|
||||
^Session
|
||||
[cfg]
|
||||
(let [props (opts->props cfg)]
|
||||
(Session/getInstance props)))
|
||||
@@ -305,7 +303,7 @@
|
||||
(fn [params]
|
||||
(when (contains? cf/flags :smtp)
|
||||
(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
|
||||
^String (::username cfg)
|
||||
^String (::password cfg))
|
||||
@@ -341,7 +339,7 @@
|
||||
(map :content)
|
||||
first)))
|
||||
(println "******** end email" (:id email) "**********"))]
|
||||
(l/raw! :info out)))
|
||||
(l/info ::l/raw out)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; EMAIL FACTORIES
|
||||
|
||||
@@ -19,21 +19,19 @@
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.session :as session]
|
||||
[app.http.websocket :as-alias ws]
|
||||
[app.main :as-alias main]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as-alias rpc.doc]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[reitit.core :as r]
|
||||
[reitit.middleware :as rr]
|
||||
[yetti.adapter :as yt]
|
||||
[yetti.request :as yrq]
|
||||
[yetti.response :as-alias yrs]))
|
||||
[yetti.response :as yrs]))
|
||||
|
||||
(declare router-handler)
|
||||
(declare wrap-router)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HTTP SERVER
|
||||
@@ -73,17 +71,13 @@
|
||||
:http/host host
|
||||
:http/max-body-size (::max-body-size cfg)
|
||||
:http/max-multipart-body-size (::max-multipart-body-size cfg)
|
||||
:xnio/io-threads (or (::io-threads cfg)
|
||||
(max 3 (px/get-available-processors)))
|
||||
:xnio/worker-threads (or (::worker-threads cfg)
|
||||
(max 6 (px/get-available-processors)))
|
||||
:xnio/dispatch true
|
||||
:socket/backlog 4069
|
||||
:xnio/io-threads (::io-threads cfg)
|
||||
:xnio/dispatch (::wrk/executor cfg)
|
||||
:ring/async true}
|
||||
|
||||
handler (cond
|
||||
(some? router)
|
||||
(router-handler router)
|
||||
(wrap-router router)
|
||||
|
||||
(some? handler)
|
||||
handler
|
||||
@@ -103,35 +97,32 @@
|
||||
|
||||
(defn- not-found-handler
|
||||
[_ respond _]
|
||||
(respond {::yrs/status 404}))
|
||||
(respond (yrs/response 404)))
|
||||
|
||||
(defn- router-handler
|
||||
(defn- wrap-router
|
||||
[router]
|
||||
(letfn [(resolve-handler [request]
|
||||
(letfn [(handler [request respond raise]
|
||||
(if-let [match (r/match-by-path router (yrq/path request))]
|
||||
(let [params (:path-params match)
|
||||
result (:result match)
|
||||
handler (or (:handler result) not-found-handler)
|
||||
request (assoc request :path-params params)]
|
||||
(partial handler request))
|
||||
(partial not-found-handler request)))
|
||||
(handler request respond raise))
|
||||
(not-found-handler request respond raise)))
|
||||
|
||||
(on-error [cause request]
|
||||
(on-error [cause request respond]
|
||||
(let [{:keys [body] :as response} (errors/handle cause request)]
|
||||
(cond-> response
|
||||
(map? body)
|
||||
(-> (update ::yrs/headers assoc "content-type" "application/transit+json")
|
||||
(assoc ::yrs/body (t/encode-str body {:type :json-verbose}))))))]
|
||||
(respond
|
||||
(cond-> response
|
||||
(map? body)
|
||||
(-> (update :headers assoc "content-type" "application/transit+json")
|
||||
(assoc :body (t/encode-str body {:type :json-verbose})))))))]
|
||||
|
||||
(fn [request respond _]
|
||||
(let [handler (resolve-handler request)
|
||||
exchange (yrq/exchange request)]
|
||||
(handler
|
||||
(fn [response]
|
||||
(yt/dispatch! exchange (partial respond response)))
|
||||
(fn [cause]
|
||||
(let [response (on-error cause request)]
|
||||
(yt/dispatch! exchange (partial respond response)))))))))
|
||||
(try
|
||||
(handler request respond #(on-error % request respond))
|
||||
(catch Throwable cause
|
||||
(on-error cause request respond))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HTTP ROUTER
|
||||
@@ -139,11 +130,11 @@
|
||||
|
||||
(defmethod ig/pre-init-spec ::router [_]
|
||||
(s/keys :req [::session/manager
|
||||
::actoken/manager
|
||||
::ws/routes
|
||||
::rpc/routes
|
||||
::rpc.doc/routes
|
||||
::oidc/routes
|
||||
::main/props
|
||||
::assets/routes
|
||||
::debug/routes
|
||||
::db/pool
|
||||
@@ -154,14 +145,13 @@
|
||||
[_ cfg]
|
||||
(rr/router
|
||||
[["" {:middleware [[mw/server-timing]
|
||||
[mw/params]
|
||||
[mw/format-response]
|
||||
[mw/params]
|
||||
[mw/parse-request]
|
||||
[session/soft-auth cfg]
|
||||
[actoken/soft-auth cfg]
|
||||
[mw/errors errors/handle]
|
||||
[mw/restrict-methods]
|
||||
[mw/with-dispatch :vthread]]}
|
||||
[mw/restrict-methods]]}
|
||||
|
||||
(::mtx/routes cfg)
|
||||
(::assets/routes cfg)
|
||||
|
||||
@@ -7,12 +7,26 @@
|
||||
(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
|
||||
@@ -26,50 +40,48 @@
|
||||
(when token
|
||||
(tokens/verify props {:token token :iss "access-token"})))
|
||||
|
||||
(def sql:get-token-data
|
||||
"SELECT perms, profile_id, expires_at
|
||||
FROM access_token
|
||||
WHERE id = ?
|
||||
AND (expires_at IS NULL
|
||||
OR (expires_at > now()));")
|
||||
|
||||
(defn- get-token-data
|
||||
(defn- get-token-perms
|
||||
[pool token-id]
|
||||
(when-not (db/read-only? pool)
|
||||
(some-> (db/exec-one! pool [sql:get-token-data token-id])
|
||||
(update :perms db/decode-pgarray #{}))))
|
||||
(when-let [token (db/get* pool :access-token {:id token-id} {:columns [:perms]})]
|
||||
(some-> (:perms token)
|
||||
(db/decode-pgarray #{})))))
|
||||
|
||||
(defn- wrap-soft-auth
|
||||
"Soft Authentication, will be executed synchronously on the undertow
|
||||
worker thread."
|
||||
[handler {:keys [::main/props]}]
|
||||
(letfn [(handle-request [request]
|
||||
(try
|
||||
(let [token (get-token request)
|
||||
claims (decode-token props token)]
|
||||
(cond-> request
|
||||
(map? claims)
|
||||
(assoc ::id (:tid claims))))
|
||||
(catch Throwable cause
|
||||
(l/trace :hint "exception on decoding malformed token" :cause cause)
|
||||
request)))]
|
||||
[handler {:keys [::manager]}]
|
||||
(us/assert! ::manager manager)
|
||||
|
||||
(let [{:keys [::wrk/executor ::main/props]} manager]
|
||||
(fn [request respond raise]
|
||||
(let [request (handle-request request)]
|
||||
(handler 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
|
||||
"Authorization middleware, will be executed synchronously on vthread."
|
||||
[handler {:keys [::db/pool]}]
|
||||
(fn [request]
|
||||
(let [{:keys [perms profile-id expires-at]} (some->> (::id request) (get-token-data pool))]
|
||||
(handler (cond-> request
|
||||
(some? perms)
|
||||
(assoc ::perms perms)
|
||||
(some? profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(some? expires-at)
|
||||
(assoc ::expires-at expires-at))))))
|
||||
[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
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[yetti.response :as-alias yrs]))
|
||||
[promesa.core :as p]
|
||||
[yetti.response :as yrs]))
|
||||
|
||||
(def ^:private cache-max-age
|
||||
(dt/duration {:hours 24}))
|
||||
@@ -26,9 +28,10 @@
|
||||
|
||||
(defn get-id
|
||||
[{:keys [path-params]}]
|
||||
(or (some-> path-params :id d/parse-uuid)
|
||||
(ex/raise :type :not-found
|
||||
:hunt "object not found")))
|
||||
(if-let [id (some-> path-params :id d/parse-uuid)]
|
||||
(p/resolved id)
|
||||
(p/rejected (ex/error :type :not-found
|
||||
:hunt "object not found"))))
|
||||
|
||||
(defn- get-file-media-object
|
||||
[pool id]
|
||||
@@ -36,12 +39,16 @@
|
||||
|
||||
(defn- serve-object-from-s3
|
||||
[{:keys [::sto/storage] :as cfg} obj]
|
||||
(let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
|
||||
{::yrs/status 307
|
||||
::yrs/headers {"location" (str url)
|
||||
"x-host" (cond-> host port (str ":" port))
|
||||
"x-mtype" (-> obj meta :content-type)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}}))
|
||||
(let [mdata (meta obj)]
|
||||
(->> (sto/get-object-url storage obj {:max-age signature-max-age})
|
||||
(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]
|
||||
@@ -51,8 +58,8 @@
|
||||
headers {"x-accel-redirect" (:path purl)
|
||||
"content-type" (:content-type mdata)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
|
||||
{::yrs/status 204
|
||||
::yrs/headers headers}))
|
||||
(p/resolved
|
||||
(yrs/response :status 204 :headers headers))))
|
||||
|
||||
(defn- serve-object
|
||||
"Helper function that returns the appropriate response depending on
|
||||
@@ -65,34 +72,42 @@
|
||||
|
||||
(defn objects-handler
|
||||
"Handler that servers storage objects by id."
|
||||
[{:keys [::sto/storage] :as cfg} request]
|
||||
(let [id (get-id request)
|
||||
obj (sto/get-object storage id)]
|
||||
(if obj
|
||||
(serve-object cfg obj)
|
||||
{::yrs/status 404})))
|
||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} request respond raise]
|
||||
(->> (get-id request)
|
||||
(p/mcat executor (fn [id] (sto/get-object storage id)))
|
||||
(p/mcat executor (fn [obj]
|
||||
(if (some? obj)
|
||||
(serve-object cfg obj)
|
||||
(p/resolved (yrs/response 404)))))
|
||||
(p/fnly executor (fn [result cause]
|
||||
(if cause (raise cause) (respond result))))))
|
||||
|
||||
(defn- generic-handler
|
||||
"A generic handler helper/common code for file-media based handlers."
|
||||
[{:keys [::sto/storage] :as cfg} request kf]
|
||||
(let [pool (::db/pool storage)
|
||||
id (get-id request)
|
||||
mobj (get-file-media-object pool id)
|
||||
sobj (sto/get-object storage (kf mobj))]
|
||||
(if sobj
|
||||
(serve-object cfg sobj)
|
||||
{::yrs/status 404})))
|
||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} request kf]
|
||||
(let [pool (::db/pool storage)]
|
||||
(->> (get-id request)
|
||||
(p/fmap executor (fn [id] (get-file-media-object pool id)))
|
||||
(p/mcat executor (fn [mobj] (sto/get-object storage (kf mobj))))
|
||||
(p/mcat executor (fn [sobj]
|
||||
(if sobj
|
||||
(serve-object cfg sobj)
|
||||
(p/resolved (yrs/response 404))))))))
|
||||
|
||||
(defn file-objects-handler
|
||||
"Handler that serves storage objects by file media id."
|
||||
[cfg request]
|
||||
(generic-handler cfg request :media-id))
|
||||
[cfg request respond raise]
|
||||
(->> (generic-handler cfg request :media-id)
|
||||
(p/fnly (fn [result cause]
|
||||
(if cause (raise cause) (respond result))))))
|
||||
|
||||
(defn file-thumbnails-handler
|
||||
"Handler that serves storage objects by thumbnail-id and quick
|
||||
fallback to file-media-id if no thumbnail is available."
|
||||
[cfg request]
|
||||
(generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))))
|
||||
[cfg request respond raise]
|
||||
(->> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
|
||||
(p/fnly (fn [result cause]
|
||||
(if cause (raise cause) (respond result))))))
|
||||
|
||||
;; --- Initialization
|
||||
|
||||
@@ -100,7 +115,7 @@
|
||||
(s/def ::routes vector?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::sto/storage ::path]))
|
||||
(s/keys :req [::sto/storage ::wrk/executor ::path]))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
[jsonista.core :as j]
|
||||
[promesa.exec :as px]
|
||||
[yetti.request :as yrq]
|
||||
[yetti.response :as-alias yrs]))
|
||||
[yetti.response :as yrs]))
|
||||
|
||||
(declare parse-json)
|
||||
(declare handle-request)
|
||||
@@ -36,10 +36,10 @@
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::wrk/executor] :as cfg}]
|
||||
(letfn [(handler [request]
|
||||
(letfn [(handler [request respond _]
|
||||
(let [data (-> request yrq/body slurp)]
|
||||
(px/run! executor #(handle-request cfg data)))
|
||||
{::yrs/status 200})]
|
||||
(respond (yrs/response 200)))]
|
||||
["/sns" {:handler handler
|
||||
:allowed-methods #{:post}}]))
|
||||
|
||||
|
||||
@@ -40,25 +40,12 @@
|
||||
(catch Throwable cause
|
||||
(p/rejected cause))))))
|
||||
|
||||
(defn- resolve-client
|
||||
[params]
|
||||
(cond
|
||||
(instance? HttpClient params)
|
||||
params
|
||||
|
||||
(map? params)
|
||||
(resolve-client (::client params))
|
||||
|
||||
:else
|
||||
(throw (UnsupportedOperationException. "invalid arguments"))))
|
||||
|
||||
(defn req!
|
||||
"A convencience toplevel function for gradual migration to a new API
|
||||
convention."
|
||||
([cfg-or-client request]
|
||||
(let [client (resolve-client cfg-or-client)]
|
||||
(send! client request {})))
|
||||
([cfg-or-client request options]
|
||||
(let [client (resolve-client cfg-or-client)]
|
||||
(send! client request options))))
|
||||
|
||||
([{:keys [::client]} request]
|
||||
(us/assert! ::client client)
|
||||
(send! client request {}))
|
||||
([{:keys [::client]} request options]
|
||||
(us/assert! ::client client)
|
||||
(send! client request options)))
|
||||
|
||||
@@ -13,14 +13,15 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.session :as session]
|
||||
[app.rpc.commands.binfile :as binf]
|
||||
[app.rpc.commands.files-create :refer [create-file]]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.storage :as-alias sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.template :as tmpl]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
@@ -47,17 +48,13 @@
|
||||
(defn prepare-response
|
||||
[body]
|
||||
(let [headers {"content-type" "application/transit+json"}]
|
||||
{::yrs/status 200
|
||||
::yrs/body body
|
||||
::yrs/headers headers}))
|
||||
(yrs/response :status 200 :body body :headers headers)))
|
||||
|
||||
(defn prepare-download-response
|
||||
[body filename]
|
||||
(let [headers {"content-disposition" (str "attachment; filename=" filename)
|
||||
"content-type" "application/octet-stream"}]
|
||||
{::yrs/status 200
|
||||
::yrs/body body
|
||||
::yrs/headers headers}))
|
||||
(yrs/response :status 200 :body body :headers headers)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INDEX
|
||||
@@ -68,10 +65,10 @@
|
||||
(when-not (authorized? pool request)
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed))
|
||||
{::yrs/status 200
|
||||
::yrs/headers {"content-type" "text/html"}
|
||||
::yrs/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {}))})
|
||||
(yrs/response :status 200
|
||||
:headers {"content-type" "text/html"}
|
||||
:body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {}))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FILE CHANGES
|
||||
@@ -118,8 +115,7 @@
|
||||
:project-id project-id
|
||||
:profile-id profile-id
|
||||
:data data})
|
||||
{::yrs/status 201
|
||||
::yrs/body "OK CREATED"})
|
||||
(yrs/response 201 "OK CREATED"))
|
||||
|
||||
:else
|
||||
(prepare-response (blob/decode data))))))
|
||||
@@ -147,8 +143,7 @@
|
||||
(db/update! pool :file
|
||||
{:data (blob/encode data)}
|
||||
{:id file-id})
|
||||
{::yrs/status 200
|
||||
::yrs/body "OK UPDATED"})
|
||||
(yrs/response 200 "OK UPDATED"))
|
||||
|
||||
(do
|
||||
(create-file pool {:id file-id
|
||||
@@ -156,11 +151,9 @@
|
||||
:project-id project-id
|
||||
:profile-id profile-id
|
||||
:data data})
|
||||
{::yrs/status 201
|
||||
::yrs/body "OK CREATED"})))
|
||||
(yrs/response 201 "OK CREATED"))))
|
||||
|
||||
{::yrs/status 500
|
||||
::yrs/body "ERROR"})))
|
||||
(yrs/response 500 "ERROR"))))
|
||||
|
||||
(defn file-data-handler
|
||||
[cfg request]
|
||||
@@ -238,11 +231,6 @@
|
||||
(-> (io/resource "app/templates/error-report.v2.tmpl")
|
||||
(tmpl/render report)))
|
||||
|
||||
(render-template-v3 [{:keys [content id created-at]}]
|
||||
(-> (io/resource "app/templates/error-report.v3.tmpl")
|
||||
(tmpl/render (-> content
|
||||
(assoc :id id)
|
||||
(assoc :created-at (dt/format-instant created-at :rfc1123))))))
|
||||
]
|
||||
|
||||
(when-not (authorized? pool request)
|
||||
@@ -250,23 +238,21 @@
|
||||
:code :only-admins-allowed))
|
||||
|
||||
(if-let [report (get-report request)]
|
||||
(let [result (case (:version report)
|
||||
1 (render-template-v1 report)
|
||||
2 (render-template-v2 report)
|
||||
3 (render-template-v3 report))]
|
||||
{::yrs/status 200
|
||||
::yrs/body result
|
||||
::yrs/headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}})
|
||||
{::yrs/status 404
|
||||
::yrs/body "not found"})))
|
||||
(let [result (if (= 1 (:version report))
|
||||
(render-template-v1 report)
|
||||
(render-template-v2 report))]
|
||||
(yrs/response :status 200
|
||||
:body result
|
||||
:headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}))
|
||||
(yrs/response 404 "not found"))))
|
||||
|
||||
(def sql:error-reports
|
||||
"SELECT id, created_at,
|
||||
content->>'~:hint' AS hint
|
||||
FROM server_error_report
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 200")
|
||||
LIMIT 100")
|
||||
|
||||
(defn error-list-handler
|
||||
[{:keys [::db/pool]} request]
|
||||
@@ -275,11 +261,11 @@
|
||||
:code :only-admins-allowed))
|
||||
(let [items (->> (db/exec! pool [sql:error-reports])
|
||||
(map #(update % :created-at dt/format-instant :rfc1123)))]
|
||||
{::yrs/status 200
|
||||
::yrs/body (-> (io/resource "app/templates/error-list.tmpl")
|
||||
(tmpl/render {:items items}))
|
||||
::yrs/headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}}))
|
||||
(yrs/response :status 200
|
||||
:body (-> (io/resource "app/templates/error-list.tmpl")
|
||||
(tmpl/render {:items items}))
|
||||
:headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; EXPORT/IMPORT
|
||||
@@ -315,15 +301,16 @@
|
||||
::binf/profile-id profile-id
|
||||
::binf/project-id project-id))
|
||||
|
||||
{::yrs/status 200
|
||||
::yrs/headers {"content-type" "text/plain"}
|
||||
::yrs/body "OK CLONED"})
|
||||
|
||||
{::yrs/status 200
|
||||
::yrs/body (io/input-stream path)
|
||||
::yrs/headers {"content-type" "application/octet-stream"
|
||||
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}}))))
|
||||
(yrs/response
|
||||
:status 200
|
||||
:headers {"content-type" "text/plain"}
|
||||
:body "OK CLONED"))
|
||||
|
||||
(yrs/response
|
||||
:status 200
|
||||
:headers {"content-type" "application/octet-stream"
|
||||
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}
|
||||
:body (io/input-stream path))))))
|
||||
|
||||
|
||||
(defn import-handler
|
||||
@@ -353,9 +340,10 @@
|
||||
::binf/profile-id profile-id
|
||||
::binf/project-id project-id))
|
||||
|
||||
{::yrs/status 200
|
||||
::yrs/headers {"content-type" "text/plain"}
|
||||
::yrs/body "OK"}))
|
||||
(yrs/response
|
||||
:status 200
|
||||
:headers {"content-type" "text/plain"}
|
||||
:body "OK")))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OTHER SMALL VIEWS/HANDLERS
|
||||
@@ -366,13 +354,11 @@
|
||||
[{:keys [::db/pool]} _]
|
||||
(try
|
||||
(db/exec-one! pool ["select count(*) as count from server_prop;"])
|
||||
{::yrs/status 200
|
||||
::yrs/body "OK"}
|
||||
(yrs/response 200 "OK")
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unable to execute query on health handler"
|
||||
:cause cause)
|
||||
{::yrs/status 503
|
||||
::yrs/body "KO"})))
|
||||
(yrs/response 503 "KO"))))
|
||||
|
||||
(defn changelog-handler
|
||||
[_ _]
|
||||
@@ -381,11 +367,10 @@
|
||||
(md->html [text]
|
||||
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
|
||||
(if-let [clog (io/resource "changelog.md")]
|
||||
{::yrs/status 200
|
||||
::yrs/headers {"content-type" "text/html; charset=utf-8"}
|
||||
::yrs/body (-> clog slurp md->html)}
|
||||
{::yrs/status 404
|
||||
::yrs/body "NOT FOUND"})))
|
||||
(yrs/response :status 200
|
||||
:headers {"content-type" "text/html; charset=utf-8"}
|
||||
:body (-> clog slurp md->html))
|
||||
(yrs/response :status 404 :body "NOT FOUND"))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INIT
|
||||
@@ -395,26 +380,33 @@
|
||||
{:compile
|
||||
(fn [& _]
|
||||
(fn [handler pool]
|
||||
(fn [request]
|
||||
(fn [request respond raise]
|
||||
(if (authorized? pool request)
|
||||
(handler request)
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed)))))})
|
||||
(handler request respond raise)
|
||||
(raise (ex/error :type :authentication
|
||||
:code :only-admins-allowed))))))})
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::db/pool ::session/manager]))
|
||||
(s/keys :req [::db/pool
|
||||
::wrk/executor
|
||||
::session/manager]))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::db/pool] :as cfg}]
|
||||
[["/readyz" {:handler (partial health-handler cfg)}]
|
||||
[_ {:keys [::db/pool ::wrk/executor] :as cfg}]
|
||||
[["/readyz" {:middleware [[mw/with-dispatch executor]
|
||||
[mw/with-config cfg]]
|
||||
:handler health-handler}]
|
||||
["/dbg" {:middleware [[session/authz cfg]
|
||||
[with-authorization pool]]}
|
||||
["" {:handler (partial index-handler cfg)}]
|
||||
["/health" {:handler (partial health-handler cfg)}]
|
||||
["/changelog" {:handler (partial changelog-handler cfg)}]
|
||||
["/error/:id" {:handler (partial error-handler cfg)}]
|
||||
["/error" {:handler (partial error-list-handler cfg)}]
|
||||
["/file/export" {:handler (partial export-handler cfg)}]
|
||||
["/file/import" {:handler (partial import-handler cfg)}]
|
||||
["/file/data" {:handler (partial file-data-handler cfg)}]
|
||||
["/file/changes" {:handler (partial file-changes-handler cfg)}]]])
|
||||
[with-authorization pool]
|
||||
[mw/with-dispatch executor]
|
||||
[mw/with-config cfg]]}
|
||||
["" {:handler index-handler}]
|
||||
["/health" {:handler health-handler}]
|
||||
["/changelog" {:handler changelog-handler}]
|
||||
;; ["/error-by-id/:id" {:handler error-handler}]
|
||||
["/error/:id" {:handler error-handler}]
|
||||
["/error" {:handler error-list-handler}]
|
||||
["/file/export" {:handler export-handler}]
|
||||
["/file/import" {:handler import-handler}]
|
||||
["/file/data" {:handler file-data-handler}]
|
||||
["/file/changes" {:handler file-changes-handler}]]])
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as-alias actoken]
|
||||
[app.http.session :as-alias session]
|
||||
@@ -31,14 +29,14 @@
|
||||
(let [claims (-> {}
|
||||
(into (::session/token-claims request))
|
||||
(into (::actoken/token-claims request)))]
|
||||
{:request/path (:path request)
|
||||
:request/method (:method request)
|
||||
:request/params (:params request)
|
||||
:request/user-agent (yrq/get-header request "user-agent")
|
||||
:request/ip-addr (parse-client-ip request)
|
||||
:request/profile-id (:uid claims)
|
||||
:version/frontend (or (yrq/get-header request "x-frontend-version") "unknown")
|
||||
:version/backend (:full cf/version)}))
|
||||
{:path (:path request)
|
||||
:method (:method request)
|
||||
:params (:params request)
|
||||
:ip-addr (parse-client-ip request)
|
||||
:user-agent (yrq/get-header request "user-agent")
|
||||
:profile-id (:uid claims)
|
||||
:version (or (yrq/get-header request "x-frontend-version")
|
||||
"unknown")}))
|
||||
|
||||
(defmulti handle-exception
|
||||
(fn [err & _rest]
|
||||
@@ -48,110 +46,69 @@
|
||||
|
||||
(defmethod handle-exception :authentication
|
||||
[err _]
|
||||
{::yrs/status 401
|
||||
::yrs/body (ex-data err)})
|
||||
(yrs/response 401 (ex-data err)))
|
||||
|
||||
(defmethod handle-exception :authorization
|
||||
[err _]
|
||||
{::yrs/status 403
|
||||
::yrs/body (ex-data err)})
|
||||
(yrs/response 403 (ex-data err)))
|
||||
|
||||
(defmethod handle-exception :restriction
|
||||
[err _]
|
||||
{::yrs/status 400
|
||||
::yrs/body (ex-data err)})
|
||||
(yrs/response 400 (ex-data err)))
|
||||
|
||||
(defmethod handle-exception :rate-limit
|
||||
[err _]
|
||||
(let [headers (-> err ex-data ::http/headers)]
|
||||
{::yrs/status 429
|
||||
::yrs/headers headers}))
|
||||
|
||||
(defmethod handle-exception :concurrency-limit
|
||||
[err _]
|
||||
(let [headers (-> err ex-data ::http/headers)]
|
||||
{::yrs/status 429
|
||||
::yrs/headers headers}))
|
||||
(yrs/response :status 429 :body "" :headers headers)))
|
||||
|
||||
(defmethod handle-exception :validation
|
||||
[err request]
|
||||
[err _]
|
||||
(let [{:keys [code] :as data} (ex-data err)]
|
||||
(cond
|
||||
(= code :spec-validation)
|
||||
(let [explain (ex/explain data)]
|
||||
{::yrs/status 400
|
||||
::yrs/body (-> data
|
||||
(dissoc ::s/problems ::s/value ::s/spec)
|
||||
(cond-> explain (assoc :explain explain)))})
|
||||
|
||||
(= code :params-validation)
|
||||
(let [explain (::sm/explain data)
|
||||
payload (sm/humanize-data explain)]
|
||||
{::yrs/status 400
|
||||
::yrs/body (-> data
|
||||
(dissoc ::sm/explain)
|
||||
(assoc :data payload))})
|
||||
(yrs/response :status 400
|
||||
:body (-> data
|
||||
(dissoc ::s/problems ::s/value)
|
||||
(cond-> explain (assoc :explain explain)))))
|
||||
|
||||
(= code :request-body-too-large)
|
||||
{::yrs/status 413 ::yrs/body data}
|
||||
|
||||
(= code :invalid-image)
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "unexpected error on processing image" :cause err)
|
||||
{::yrs/status 400 ::yrs/body data})
|
||||
(yrs/response :status 413 :body data)
|
||||
|
||||
:else
|
||||
{::yrs/status 400 ::yrs/body data})))
|
||||
(yrs/response :status 400 :body data))))
|
||||
|
||||
(defmethod handle-exception :assertion
|
||||
[error request]
|
||||
(binding [l/*context* (request->context request)]
|
||||
(let [{:keys [code] :as data} (ex-data error)]
|
||||
(cond
|
||||
(= code :data-validation)
|
||||
(let [explain (::sm/explain data)
|
||||
payload (sm/humanize-data explain)]
|
||||
(l/error :hint "Data assertion error" :message (ex-message error) :cause error)
|
||||
{::yrs/status 500
|
||||
::yrs/body {:type :server-error
|
||||
:code :assertion
|
||||
:data (-> data
|
||||
(dissoc ::sm/explain)
|
||||
(assoc :data payload))}})
|
||||
|
||||
(= code :spec-validation)
|
||||
(let [explain (ex/explain data)]
|
||||
(l/error :hint "Spec assertion error" :message (ex-message error) :cause error)
|
||||
{::yrs/status 500
|
||||
::yrs/body {:type :server-error
|
||||
:code :assertion
|
||||
:data (-> data
|
||||
(dissoc ::s/problems ::s/value ::s/spec)
|
||||
(cond-> explain (assoc :explain explain)))}})
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
|
||||
{::yrs/status 500
|
||||
::yrs/body {:type :server-error
|
||||
:code :assertion
|
||||
:data data}})))))
|
||||
|
||||
(let [edata (ex-data error)
|
||||
explain (ex/explain edata)]
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
|
||||
(yrs/response :status 500
|
||||
:body {:type :server-error
|
||||
:code :assertion
|
||||
:data (-> edata
|
||||
(dissoc ::s/problems ::s/value ::s/spec)
|
||||
(cond-> explain (assoc :explain explain)))}))))
|
||||
|
||||
(defmethod handle-exception :not-found
|
||||
[err _]
|
||||
{::yrs/status 404
|
||||
::yrs/body (ex-data err)})
|
||||
(yrs/response 404 (ex-data err)))
|
||||
|
||||
(defmethod handle-exception :internal
|
||||
[error request]
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "Internal error" :message (ex-message error) :cause error)
|
||||
{::yrs/status 500
|
||||
::yrs/body {:type :server-error
|
||||
:code :unhandled
|
||||
:hint (ex-message error)
|
||||
:data (ex-data error)}}))
|
||||
(let [{:keys [code] :as edata} (ex-data error)]
|
||||
(cond
|
||||
(= :concurrency-limit-reached code)
|
||||
(yrs/response 429)
|
||||
|
||||
:else
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "Internal error" :message (ex-message error) :cause error)
|
||||
(yrs/response 500 {:type :server-error
|
||||
:code :unhandled
|
||||
:hint (ex-message error)
|
||||
:data edata})))))
|
||||
|
||||
(defmethod handle-exception org.postgresql.util.PSQLException
|
||||
[error request]
|
||||
@@ -160,23 +117,20 @@
|
||||
(l/error :hint "PSQL error" :message (ex-message error) :cause error)
|
||||
(cond
|
||||
(= state "57014")
|
||||
{::yrs/status 504
|
||||
::yrs/body {:type :server-error
|
||||
:code :statement-timeout
|
||||
:hint (ex-message error)}}
|
||||
(yrs/response 504 {:type :server-error
|
||||
:code :statement-timeout
|
||||
:hint (ex-message error)})
|
||||
|
||||
(= state "25P03")
|
||||
{::yrs/status 504
|
||||
::yrs/body {:type :server-error
|
||||
:code :idle-in-transaction-timeout
|
||||
:hint (ex-message error)}}
|
||||
(yrs/response 504 {:type :server-error
|
||||
:code :idle-in-transaction-timeout
|
||||
:hint (ex-message error)})
|
||||
|
||||
:else
|
||||
{::yrs/status 500
|
||||
::yrs/body {:type :server-error
|
||||
:code :unexpected
|
||||
:hint (ex-message error)
|
||||
:state state}}))))
|
||||
(yrs/response 500 {:type :server-error
|
||||
:code :unexpected
|
||||
:hint (ex-message error)
|
||||
:state state})))))
|
||||
|
||||
(defmethod handle-exception :default
|
||||
[error request]
|
||||
@@ -186,10 +140,9 @@
|
||||
(nil? edata)
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "Unexpected error" :message (ex-message error) :cause error)
|
||||
{::yrs/status 500
|
||||
::yrs/body {:type :server-error
|
||||
:code :unexpected
|
||||
:hint (ex-message error)}})
|
||||
(yrs/response 500 {:type :server-error
|
||||
:code :unexpected
|
||||
:hint (ex-message error)}))
|
||||
|
||||
;; This is a special case for the idle-in-transaction error;
|
||||
;; when it happens, the connection is automatically closed and
|
||||
@@ -203,11 +156,10 @@
|
||||
:else
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "Unhandled error" :message (ex-message error) :cause error)
|
||||
{::yrs/status 500
|
||||
::yrs/body {:type :server-error
|
||||
:code :unhandled
|
||||
:hint (ex-message error)
|
||||
:data edata}}))))
|
||||
(yrs/response 500 {:type :server-error
|
||||
:code :unhandled
|
||||
:hint (ex-message error)
|
||||
:data edata})))))
|
||||
|
||||
(defn handle
|
||||
[cause request]
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[promesa.util :as pu]
|
||||
[yetti.adapter :as yt]
|
||||
[yetti.middleware :as ymw]
|
||||
[yetti.request :as yrq]
|
||||
@@ -22,13 +21,9 @@
|
||||
(:import
|
||||
com.fasterxml.jackson.core.JsonParseException
|
||||
com.fasterxml.jackson.core.io.JsonEOFException
|
||||
com.fasterxml.jackson.databind.exc.MismatchedInputException
|
||||
io.undertow.server.RequestTooBigException
|
||||
java.io.InputStream
|
||||
java.io.OutputStream))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(def server-timing
|
||||
{:name ::server-timing
|
||||
:compile (constantly ymw/wrap-server-timing)})
|
||||
@@ -49,14 +44,14 @@
|
||||
(let [header (yrq/get-header request "content-type")]
|
||||
(cond
|
||||
(str/starts-with? header "application/transit+json")
|
||||
(with-open [^InputStream is (yrq/body request)]
|
||||
(with-open [is (yrq/body request)]
|
||||
(let [params (t/read! (t/reader is))]
|
||||
(-> request
|
||||
(assoc :body-params params)
|
||||
(update :params merge params))))
|
||||
|
||||
(str/starts-with? header "application/json")
|
||||
(with-open [^InputStream is (yrq/body request)]
|
||||
(with-open [is (yrq/body request)]
|
||||
(let [params (json/decode is json-mapper)]
|
||||
(-> request
|
||||
(assoc :body-params params)
|
||||
@@ -67,11 +62,6 @@
|
||||
|
||||
(handle-error [raise cause]
|
||||
(cond
|
||||
(instance? RuntimeException cause)
|
||||
(if-let [cause (ex-cause cause)]
|
||||
(handle-error raise cause)
|
||||
(raise cause))
|
||||
|
||||
(instance? RequestTooBigException cause)
|
||||
(raise (ex/error :type :validation
|
||||
:code :request-body-too-large
|
||||
@@ -79,23 +69,21 @@
|
||||
|
||||
|
||||
(or (instance? JsonEOFException cause)
|
||||
(instance? JsonParseException cause)
|
||||
(instance? MismatchedInputException cause))
|
||||
(instance? JsonParseException cause))
|
||||
(raise (ex/error :type :validation
|
||||
:code :malformed-json
|
||||
:hint (ex-message cause)
|
||||
:cause cause))
|
||||
|
||||
:else
|
||||
(raise cause)))]
|
||||
|
||||
(fn [request respond raise]
|
||||
(if (= (yrq/method request) :post)
|
||||
(let [request (ex/try! (process-request request))]
|
||||
(if (ex/exception? request)
|
||||
(handle-error raise request)
|
||||
(handler request respond raise)))
|
||||
(handler request respond raise)))))
|
||||
(let [request (ex/try! (process-request request))]
|
||||
(if (ex/exception? request)
|
||||
(if (ex/runtime-exception? request)
|
||||
(handle-error raise (or (ex-cause request) request))
|
||||
(handle-error raise request))
|
||||
(handler request respond raise))))))
|
||||
|
||||
(def parse-request
|
||||
{:name ::parse-request
|
||||
@@ -106,7 +94,12 @@
|
||||
needed because transit-java calls flush very aggresivelly on each
|
||||
object write."
|
||||
[^java.io.OutputStream os ^long chunk-size]
|
||||
(yetti.util.BufferedOutputStream. os (int chunk-size)))
|
||||
(proxy [java.io.BufferedOutputStream] [os (int chunk-size)]
|
||||
;; Explicitly do not forward flush
|
||||
(flush [])
|
||||
(close []
|
||||
(proxy-super flush)
|
||||
(proxy-super close))))
|
||||
|
||||
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
|
||||
|
||||
@@ -116,14 +109,16 @@
|
||||
(reify yrs/StreamableResponseBody
|
||||
(-write-body-to-stream [_ _ output-stream]
|
||||
(try
|
||||
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||
(with-open [bos (buffered-output-stream output-stream buffer-size)]
|
||||
(let [tw (t/writer bos opts)]
|
||||
(t/write! tw data)))
|
||||
(catch java.io.IOException _)
|
||||
|
||||
(catch java.io.IOException _cause
|
||||
;; Do nothing, EOF means client closes connection abruptly
|
||||
nil)
|
||||
(catch Throwable cause
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(l/warn :hint "unexpected error on encoding response"
|
||||
:cause cause))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
|
||||
@@ -131,27 +126,29 @@
|
||||
(reify yrs/StreamableResponseBody
|
||||
(-write-body-to-stream [_ _ output-stream]
|
||||
(try
|
||||
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||
|
||||
(with-open [bos (buffered-output-stream output-stream buffer-size)]
|
||||
(json/write! bos data json-mapper))
|
||||
|
||||
(catch java.io.IOException _)
|
||||
(catch java.io.IOException _cause
|
||||
;; Do nothing, EOF means client closes connection abruptly
|
||||
nil)
|
||||
(catch Throwable cause
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(l/warn :hint "unexpected error on encoding response"
|
||||
:cause cause))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
|
||||
(format-response-with-json [response _]
|
||||
(let [body (::yrs/body response)]
|
||||
(let [body (yrs/body response)]
|
||||
(if (or (boolean? body) (coll? body))
|
||||
(-> response
|
||||
(update ::yrs/headers assoc "content-type" "application/json")
|
||||
(assoc ::yrs/body (json-streamable-body body)))
|
||||
(update :headers assoc "content-type" "application/json")
|
||||
(assoc :body (json-streamable-body body)))
|
||||
response)))
|
||||
|
||||
(format-response-with-transit [response request]
|
||||
(let [body (::yrs/body response)]
|
||||
(let [body (yrs/body response)]
|
||||
(if (or (boolean? body) (coll? body))
|
||||
(let [qs (yrq/query request)
|
||||
opts (if (or (contains? cf/flags :transit-readable-response)
|
||||
@@ -159,17 +156,12 @@
|
||||
{:type :json-verbose}
|
||||
{:type :json})]
|
||||
(-> response
|
||||
(update ::yrs/headers assoc "content-type" "application/transit+json")
|
||||
(assoc ::yrs/body (transit-streamable-body body opts))))
|
||||
(update :headers assoc "content-type" "application/transit+json")
|
||||
(assoc :body (transit-streamable-body body opts))))
|
||||
response)))
|
||||
|
||||
(format-from-params [{:keys [query-params] :as request}]
|
||||
(and (= "json" (get query-params :_fmt))
|
||||
"application/json"))
|
||||
|
||||
(format-response [response request]
|
||||
(let [accept (or (format-from-params request)
|
||||
(yrq/get-header request "accept"))]
|
||||
(let [accept (yrq/get-header request "accept")]
|
||||
(cond
|
||||
(or (= accept "application/transit+json")
|
||||
(str/includes? accept "application/transit+json"))
|
||||
@@ -189,7 +181,8 @@
|
||||
(fn [request respond raise]
|
||||
(handler request
|
||||
(fn [response]
|
||||
(respond (process-response response request)))
|
||||
(let [response (process-response response request)]
|
||||
(respond response)))
|
||||
raise))))
|
||||
|
||||
(def format-response
|
||||
@@ -198,59 +191,74 @@
|
||||
|
||||
(defn wrap-errors
|
||||
[handler on-error]
|
||||
(fn [request respond raise]
|
||||
(fn [request respond _]
|
||||
(handler request respond (fn [cause]
|
||||
(try
|
||||
(respond (on-error cause request))
|
||||
(catch Throwable cause
|
||||
(raise cause)))))))
|
||||
(-> cause (on-error request) respond)))))
|
||||
|
||||
(def errors
|
||||
{:name ::errors
|
||||
:compile (constantly wrap-errors)})
|
||||
|
||||
(defn- with-cors-headers
|
||||
[headers origin]
|
||||
(-> headers
|
||||
(assoc "access-control-allow-origin" origin)
|
||||
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
|
||||
(assoc "access-control-allow-credentials" "true")
|
||||
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
|
||||
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width")))
|
||||
|
||||
(defn wrap-cors
|
||||
[handler]
|
||||
(fn [request]
|
||||
(let [response (if (= (yrq/method request) :options)
|
||||
{::yrs/status 200}
|
||||
(handler request))
|
||||
origin (yrq/get-header request "origin")]
|
||||
(update response ::yrs/headers with-cors-headers origin))))
|
||||
(if-not (contains? cf/flags :cors)
|
||||
handler
|
||||
(letfn [(add-headers [headers request]
|
||||
(let [origin (yrq/get-header request "origin")]
|
||||
(-> headers
|
||||
(assoc "access-control-allow-origin" origin)
|
||||
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
|
||||
(assoc "access-control-allow-credentials" "true")
|
||||
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
|
||||
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))
|
||||
|
||||
(update-response [response request]
|
||||
(update response :headers add-headers request))]
|
||||
|
||||
(fn [request respond raise]
|
||||
(if (= (yrq/method request) :options)
|
||||
(-> (yrs/response 200)
|
||||
(update-response request)
|
||||
(respond))
|
||||
(handler request
|
||||
(fn [response]
|
||||
(respond (update-response response request)))
|
||||
raise))))))
|
||||
|
||||
(def cors
|
||||
{:name ::cors
|
||||
:compile (fn [& _]
|
||||
(when (contains? cf/flags :cors)
|
||||
wrap-cors))})
|
||||
:compile (constantly wrap-cors)})
|
||||
|
||||
(defn compile-restrict-methods
|
||||
[data _]
|
||||
(when-let [allowed (:allowed-methods data)]
|
||||
(fn [handler]
|
||||
(fn [request respond raise]
|
||||
(let [method (yrq/method request)]
|
||||
(if (contains? allowed method)
|
||||
(handler request respond raise)
|
||||
(respond (yrs/response 405))))))))
|
||||
|
||||
(def restrict-methods
|
||||
{:name ::restrict-methods
|
||||
:compile
|
||||
(fn [data _]
|
||||
(when-let [allowed (:allowed-methods data)]
|
||||
(fn [handler]
|
||||
(fn [request respond raise]
|
||||
(let [method (yrq/method request)]
|
||||
(if (contains? allowed method)
|
||||
(handler request respond raise)
|
||||
(respond {::yrs/status 405})))))))})
|
||||
:compile compile-restrict-methods})
|
||||
|
||||
(def with-dispatch
|
||||
{:name ::with-dispatch
|
||||
:compile
|
||||
(fn [& _]
|
||||
(fn [handler executor]
|
||||
(let [executor (px/resolve-executor executor)]
|
||||
(fn [request respond raise]
|
||||
(->> (px/submit! executor (partial handler request))
|
||||
(p/fnly (pu/handler respond raise)))))))})
|
||||
(fn [request respond raise]
|
||||
(-> (px/submit! executor #(handler request))
|
||||
(p/bind p/wrap)
|
||||
(p/then respond)
|
||||
(p/catch raise)))))})
|
||||
|
||||
(def with-config
|
||||
{:name ::with-config
|
||||
:compile
|
||||
(fn [& _]
|
||||
(fn [handler config]
|
||||
(fn
|
||||
([request] (handler config request))
|
||||
([request respond raise] (handler config request respond raise)))))})
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:refer-clojure :exclude [read])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
@@ -17,9 +18,12 @@
|
||||
[app.main :as-alias main]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[yetti.request :as yrq]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -72,56 +76,69 @@
|
||||
:id key})
|
||||
|
||||
(defn- database-manager
|
||||
[pool]
|
||||
[{:keys [::db/pool ::wrk/executor ::main/props]}]
|
||||
^{::wrk/executor executor
|
||||
::db/pool pool
|
||||
::main/props props}
|
||||
(reify ISessionManager
|
||||
(read [_ token]
|
||||
(db/exec-one! pool (sql/select :http-session {:id token})))
|
||||
(px/with-dispatch executor
|
||||
(db/exec-one! pool (sql/select :http-session {:id token}))))
|
||||
|
||||
(write! [_ key params]
|
||||
(let [params (prepare-session-params key params)]
|
||||
(db/insert! pool :http-session params)
|
||||
params))
|
||||
(px/with-dispatch executor
|
||||
(let [params (prepare-session-params key params)]
|
||||
(db/insert! pool :http-session params)
|
||||
params)))
|
||||
|
||||
(update! [_ params]
|
||||
(let [updated-at (dt/now)]
|
||||
(db/update! pool :http-session
|
||||
{:updated-at updated-at}
|
||||
{:id (:id params)})
|
||||
(assoc params :updated-at updated-at)))
|
||||
(px/with-dispatch executor
|
||||
(db/update! pool :http-session
|
||||
{:updated-at updated-at}
|
||||
{:id (:id params)})
|
||||
(assoc params :updated-at updated-at))))
|
||||
|
||||
(delete! [_ token]
|
||||
(db/delete! pool :http-session {:id token})
|
||||
nil)))
|
||||
|
||||
(defn inmemory-manager
|
||||
[]
|
||||
(let [cache (atom {})]
|
||||
(reify ISessionManager
|
||||
(read [_ token]
|
||||
(get @cache token))
|
||||
|
||||
(write! [_ key params]
|
||||
(let [params (prepare-session-params key params)]
|
||||
(swap! cache assoc key params)
|
||||
params))
|
||||
|
||||
(update! [_ params]
|
||||
(let [updated-at (dt/now)]
|
||||
(swap! cache update (:id params) assoc :updated-at updated-at)
|
||||
(assoc params :updated-at updated-at)))
|
||||
|
||||
(delete! [_ token]
|
||||
(swap! cache dissoc token)
|
||||
(px/with-dispatch executor
|
||||
(db/delete! pool :http-session {:id token})
|
||||
nil))))
|
||||
|
||||
(defn inmemory-manager
|
||||
[{:keys [::db/pool ::wrk/executor ::main/props]}]
|
||||
(let [cache (atom {})]
|
||||
^{::main/props props
|
||||
::wrk/executor executor
|
||||
::db/pool pool}
|
||||
(reify ISessionManager
|
||||
(read [_ token]
|
||||
(p/do (get @cache token)))
|
||||
|
||||
(write! [_ key params]
|
||||
(p/do
|
||||
(let [params (prepare-session-params key params)]
|
||||
(swap! cache assoc key params)
|
||||
params)))
|
||||
|
||||
(update! [_ params]
|
||||
(p/do
|
||||
(let [updated-at (dt/now)]
|
||||
(swap! cache update (:id params) assoc :updated-at updated-at)
|
||||
(assoc params :updated-at updated-at))))
|
||||
|
||||
(delete! [_ token]
|
||||
(p/do
|
||||
(swap! cache dissoc token)
|
||||
nil)))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::manager [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
|
||||
|
||||
(defmethod ig/init-key ::manager
|
||||
[_ {:keys [::db/pool]}]
|
||||
[_ {:keys [::db/pool] :as cfg}]
|
||||
(if (db/read-only? pool)
|
||||
(inmemory-manager)
|
||||
(database-manager pool)))
|
||||
(inmemory-manager cfg)
|
||||
(database-manager cfg)))
|
||||
|
||||
(defmethod ig/halt-key! ::manager
|
||||
[_ _])
|
||||
@@ -137,35 +154,40 @@
|
||||
(declare ^:private gen-token)
|
||||
|
||||
(defn create-fn
|
||||
[{:keys [::manager ::main/props]} profile-id]
|
||||
[{:keys [::manager]} profile-id]
|
||||
(us/assert! ::manager manager)
|
||||
(us/assert! ::us/uuid profile-id)
|
||||
|
||||
(fn [request response]
|
||||
(let [uagent (yrq/get-header request "user-agent")
|
||||
params {:profile-id profile-id
|
||||
:user-agent uagent
|
||||
:created-at (dt/now)}
|
||||
token (gen-token props params)
|
||||
session (write! manager token params)]
|
||||
(l/trace :hint "create" :profile-id (str profile-id))
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)
|
||||
(assign-authenticated-cookie session)))))
|
||||
(let [props (-> manager meta ::main/props)]
|
||||
(fn [request response]
|
||||
(let [uagent (yrq/get-header request "user-agent")
|
||||
params {:profile-id profile-id
|
||||
:user-agent uagent
|
||||
:created-at (dt/now)}
|
||||
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
|
||||
[{:keys [::manager]}]
|
||||
(us/assert! ::manager manager)
|
||||
(fn [request response]
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
cookie (yrq/get-cookie request cname)]
|
||||
(l/trace :hint "delete" :profile-id (:profile-id request))
|
||||
(some->> (:value cookie) (delete! manager))
|
||||
(-> response
|
||||
(assoc :status 204)
|
||||
(assoc :body nil)
|
||||
(clear-auth-token-cookie)
|
||||
(clear-authenticated-cookie)))))
|
||||
(letfn [(delete [{:keys [profile-id] :as request}]
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
cookie (yrq/get-cookie request cname)]
|
||||
(l/trace :hint "delete" :profile-id profile-id)
|
||||
(some->> (:value cookie) (delete! manager))))]
|
||||
(fn [request response]
|
||||
(p/do
|
||||
(delete request)
|
||||
(-> response
|
||||
(assoc :status 204)
|
||||
(assoc :body nil)
|
||||
(clear-auth-token-cookie)
|
||||
(clear-authenticated-cookie))))))
|
||||
|
||||
(defn- gen-token
|
||||
[props {:keys [profile-id created-at]}]
|
||||
@@ -194,39 +216,58 @@
|
||||
(let [elapsed (dt/diff updated-at (dt/now))]
|
||||
(neg? (compare default-renewal-max-age elapsed)))))
|
||||
|
||||
(defn- wrap-soft-auth
|
||||
[handler {:keys [::manager ::main/props]}]
|
||||
(us/assert! ::manager manager)
|
||||
(letfn [(handle-request [request]
|
||||
(try
|
||||
(let [token (get-token request)
|
||||
claims (decode-token props token)]
|
||||
(cond-> request
|
||||
(map? claims)
|
||||
(-> (assoc ::token-claims claims)
|
||||
(assoc ::token token))))
|
||||
(catch Throwable cause
|
||||
(l/trace :hint "exception on decoding malformed token" :cause cause)
|
||||
request)))]
|
||||
(defn- wrap-reneval
|
||||
[respond manager session]
|
||||
(fn [response]
|
||||
(p/let [session (update! manager session)]
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)
|
||||
(assign-authenticated-cookie session)
|
||||
(respond)))))
|
||||
|
||||
(defn- wrap-soft-auth
|
||||
[handler {:keys [::manager]}]
|
||||
(us/assert! ::manager manager)
|
||||
|
||||
(let [{:keys [::wrk/executor ::main/props]} (meta manager)]
|
||||
(fn [request respond raise]
|
||||
(let [request (handle-request request)]
|
||||
(handler request respond raise)))))
|
||||
(let [token (ex/try! (get-token request))]
|
||||
(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))))))))))
|
||||
|
||||
(defn- wrap-authz
|
||||
[handler {:keys [::manager]}]
|
||||
(us/assert! ::manager manager)
|
||||
(fn [request]
|
||||
(let [session (get-session manager (::token request))
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(assoc ::profile-id (:profile-id session)
|
||||
::id (:id session)))]
|
||||
(fn [request respond raise]
|
||||
(if-let [token (::token request)]
|
||||
(->> (get-session manager token)
|
||||
(p/fnly (fn [session cause]
|
||||
(cond
|
||||
(some? cause)
|
||||
(raise cause)
|
||||
|
||||
(cond-> (handler request)
|
||||
(renew-session? session)
|
||||
(-> (assign-auth-token-cookie session)
|
||||
(assign-authenticated-cookie session))))))
|
||||
(nil? session)
|
||||
(handler request respond raise)
|
||||
|
||||
: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
|
||||
|
||||
@@ -11,16 +11,15 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http.session :as session]
|
||||
[app.metrics :as mtx]
|
||||
[app.msgbus :as mbus]
|
||||
[app.util.time :as dt]
|
||||
[app.util.websocket :as ws]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec.csp :as sp]
|
||||
[yetti.websocket :as yws]))
|
||||
|
||||
(def recv-labels
|
||||
@@ -35,38 +34,70 @@
|
||||
|
||||
(def state (atom {}))
|
||||
|
||||
(defn- on-connect
|
||||
[{:keys [::mtx/metrics]} wsp]
|
||||
(let [created-at (dt/now)]
|
||||
(swap! state assoc (::ws/id @wsp) wsp)
|
||||
(mtx/run! metrics
|
||||
:id :websocket-active-connections
|
||||
:inc 1)
|
||||
(fn []
|
||||
(swap! state dissoc (::ws/id @wsp))
|
||||
(mtx/run! metrics :id :websocket-active-connections :dec 1)
|
||||
(mtx/run! metrics
|
||||
:id :websocket-session-timing
|
||||
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)))))
|
||||
|
||||
(defn- on-rcv-message
|
||||
[{:keys [::mtx/metrics]} _ message]
|
||||
(mtx/run! metrics
|
||||
:id :websocket-messages-total
|
||||
:labels recv-labels
|
||||
:inc 1)
|
||||
message)
|
||||
|
||||
(defn- on-snd-message
|
||||
[{:keys [::mtx/metrics]} _ message]
|
||||
(mtx/run! metrics
|
||||
:id :websocket-messages-total
|
||||
:labels send-labels
|
||||
:inc 1)
|
||||
message)
|
||||
|
||||
;; REPL HELPERS
|
||||
|
||||
(defn repl-get-connections-for-file
|
||||
[file-id]
|
||||
(->> (vals @state)
|
||||
(filter #(= file-id (-> % deref ::file-subscription :file-id)))
|
||||
(map deref)
|
||||
(map ::ws/id)))
|
||||
|
||||
(defn repl-get-connections-for-team
|
||||
[team-id]
|
||||
(->> (vals @state)
|
||||
(filter #(= team-id (-> % deref ::team-subscription :team-id)))
|
||||
(map deref)
|
||||
(map ::ws/id)))
|
||||
|
||||
(defn repl-close-connection
|
||||
[id]
|
||||
(when-let [{:keys [::ws/close-ch] :as wsp} (get @state id)]
|
||||
(sp/put! close-ch [8899 "closed from server"])
|
||||
(sp/close! close-ch)))
|
||||
(when-let [wsp (get @state id)]
|
||||
(a/>!! (::ws/close-ch @wsp) [8899 "closed from server"])
|
||||
(a/close! (::ws/close-ch @wsp))))
|
||||
|
||||
(defn repl-get-connection-info
|
||||
[id]
|
||||
(when-let [wsp (get @state id)]
|
||||
{:id id
|
||||
:created-at (::created-at wsp)
|
||||
:profile-id (::profile-id wsp)
|
||||
:session-id (::session-id wsp)
|
||||
:user-agent (::ws/user-agent wsp)
|
||||
:ip-addr (::ws/remote-addr wsp)
|
||||
:last-activity-at (::ws/last-activity-at wsp)
|
||||
:subscribed-file (-> wsp ::file-subscription :file-id)
|
||||
:subscribed-team (-> wsp ::team-subscription :team-id)}))
|
||||
:created-at (::created-at @wsp)
|
||||
:profile-id (::profile-id @wsp)
|
||||
:session-id (::session-id @wsp)
|
||||
:user-agent (::ws/user-agent @wsp)
|
||||
:ip-addr (::ws/remote-addr @wsp)
|
||||
:last-activity-at (::ws/last-activity-at @wsp)
|
||||
:subscribed-file (-> wsp deref ::file-subscription :file-id)
|
||||
:subscribed-team (-> wsp deref ::team-subscription :team-id)}))
|
||||
|
||||
(defn repl-print-connection-info
|
||||
[id]
|
||||
@@ -86,218 +117,223 @@
|
||||
(fn [_ _ message]
|
||||
(:type message)))
|
||||
|
||||
(defmethod handle-message :open
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/output-ch ::ws/state ::profile-id ::session-id] :as wsp} _]
|
||||
(l/trace :fn "handle-message" :event "open" :conn-id id)
|
||||
(let [ch (sp/chan :buf (sp/dropping-buffer 16)
|
||||
:xf (remove #(= (:session-id %) session-id)))]
|
||||
(defmethod handle-message :connect
|
||||
[cfg wsp _]
|
||||
|
||||
;; Subscribe to the profile channel and forward all messages to websocket output
|
||||
;; channel (send them to the client).
|
||||
(swap! state assoc ::profile-subscription {:channel ch})
|
||||
(let [msgbus (::mbus/msgbus cfg)
|
||||
conn-id (::ws/id @wsp)
|
||||
profile-id (::profile-id @wsp)
|
||||
session-id (::session-id @wsp)
|
||||
output-ch (::ws/output-ch @wsp)
|
||||
|
||||
;; Forward the subscription messages directly to the websocket output channel
|
||||
(sp/pipe ch output-ch false)
|
||||
xform (remove #(= (:session-id %) session-id))
|
||||
channel (a/chan (a/dropping-buffer 16) xform)]
|
||||
|
||||
;; Subscribe to the profile topic on msgbus/redis
|
||||
(mbus/sub! msgbus :topic profile-id :chan ch)
|
||||
(l/trace :fn "handle-message" :event "connect" :conn-id conn-id)
|
||||
|
||||
;; Subscribe to the system topic on msgbus/redis
|
||||
(mbus/sub! msgbus :topic (str uuid/zero) :chan ch)))
|
||||
;; Subscribe to the profile channel and forward all messages to
|
||||
;; websocket output channel (send them to the client).
|
||||
(swap! wsp assoc ::profile-subscription channel)
|
||||
(a/pipe channel output-ch false)
|
||||
(mbus/sub! msgbus :topic profile-id :chan channel)))
|
||||
|
||||
(defmethod handle-message :close
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::profile-id ::session-id]} _]
|
||||
(l/trace :fn "handle-message" :event "close" :conn-id id)
|
||||
(let [psub (::profile-subscription @state)
|
||||
fsub (::file-subscription @state)
|
||||
tsub (::team-subscription @state)
|
||||
msg {:type :disconnect
|
||||
:subs-id profile-id
|
||||
:profile-id profile-id
|
||||
:session-id session-id}]
|
||||
(defmethod handle-message :disconnect
|
||||
[cfg wsp _]
|
||||
(let [msgbus (::mbus/msgbus cfg)
|
||||
conn-id (::ws/id @wsp)
|
||||
profile-id (::profile-id @wsp)
|
||||
session-id (::session-id @wsp)
|
||||
profile-ch (::profile-subscription @wsp)
|
||||
fsub (::file-subscription @wsp)
|
||||
tsub (::team-subscription @wsp)
|
||||
|
||||
;; Close profile subscription if exists
|
||||
(when-let [ch (:channel psub)]
|
||||
(sp/close! ch)
|
||||
(mbus/purge! msgbus [ch]))
|
||||
message {:type :disconnect
|
||||
:subs-id profile-id
|
||||
:profile-id profile-id
|
||||
:session-id session-id}]
|
||||
|
||||
;; Close team subscription if exists
|
||||
(when-let [ch (:channel tsub)]
|
||||
(sp/close! ch)
|
||||
(mbus/purge! msgbus [ch]))
|
||||
(l/trace :fn "handle-message"
|
||||
:event :disconnect
|
||||
:conn-id conn-id)
|
||||
|
||||
(a/go
|
||||
;; Close the main profile subscription
|
||||
(a/close! profile-ch)
|
||||
(a/<! (mbus/purge! msgbus [profile-ch]))
|
||||
|
||||
;; Close tram subscription if exists
|
||||
(when-let [channel (:channel tsub)]
|
||||
(a/close! channel)
|
||||
(a/<! (mbus/purge! msgbus channel)))
|
||||
|
||||
;; Close file subscription if exists
|
||||
(when-let [{:keys [topic channel]} fsub]
|
||||
(sp/close! channel)
|
||||
(mbus/purge! msgbus [channel])
|
||||
(mbus/pub! msgbus :topic topic :message msg))))
|
||||
(a/close! channel)
|
||||
(a/<! (mbus/purge! msgbus channel))
|
||||
(a/<! (mbus/pub! msgbus :topic topic :message message))))))
|
||||
|
||||
(defmethod handle-message :subscribe-team
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id]} {:keys [team-id] :as params}]
|
||||
(l/trace :fn "handle-message" :event "subscribe-team" :team-id team-id :conn-id id)
|
||||
(let [prev-subs (get @state ::team-subscription)
|
||||
channel (sp/chan :buf (sp/dropping-buffer 64)
|
||||
:xf (comp
|
||||
(remove #(= (:session-id %) session-id))
|
||||
(map #(assoc % :subs-id team-id))))]
|
||||
[cfg wsp {:keys [team-id] :as params}]
|
||||
(let [msgbus (::mbus/msgbus cfg)
|
||||
conn-id (::ws/id @wsp)
|
||||
session-id (::session-id @wsp)
|
||||
output-ch (::ws/output-ch @wsp)
|
||||
prev-subs (get @wsp ::team-subscription)
|
||||
xform (comp
|
||||
(remove #(= (:session-id %) session-id))
|
||||
(map #(assoc % :subs-id team-id)))
|
||||
|
||||
(sp/pipe channel output-ch false)
|
||||
(mbus/sub! msgbus :topic team-id :chan channel)
|
||||
channel (a/chan (a/dropping-buffer 64) xform)]
|
||||
|
||||
(let [subs {:team-id team-id :channel channel :topic team-id}]
|
||||
(swap! state assoc ::team-subscription subs))
|
||||
(l/trace :fn "handle-message"
|
||||
:event :subscribe-team
|
||||
:team-id team-id
|
||||
:conn-id conn-id)
|
||||
|
||||
;; Close previous subscription if exists
|
||||
(when-let [ch (:channel prev-subs)]
|
||||
(sp/close! ch)
|
||||
(mbus/purge! msgbus [ch]))))
|
||||
(a/pipe channel output-ch false)
|
||||
|
||||
(let [state {:team-id team-id :channel channel :topic team-id}]
|
||||
(swap! wsp assoc ::team-subscription state))
|
||||
|
||||
(a/go
|
||||
;; Close previous subscription if exists
|
||||
(when-let [channel (:channel prev-subs)]
|
||||
(a/close! channel)
|
||||
(a/<! (mbus/purge! msgbus channel))))
|
||||
|
||||
(a/go
|
||||
(a/<! (mbus/sub! msgbus :topic team-id :chan channel)))))
|
||||
|
||||
(defmethod handle-message :subscribe-file
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id ::profile-id]} {:keys [file-id] :as params}]
|
||||
(l/trace :fn "handle-message" :event "subscribe-file" :file-id file-id :conn-id id)
|
||||
(let [psub (::file-subscription @state)
|
||||
fch (sp/chan :buf (sp/dropping-buffer 64)
|
||||
:xf (comp (remove #(= (:session-id %) session-id))
|
||||
(map #(assoc % :subs-id file-id))))]
|
||||
[cfg wsp {:keys [file-id] :as params}]
|
||||
(let [msgbus (::mbus/msgbus cfg)
|
||||
conn-id (::ws/id @wsp)
|
||||
profile-id (::profile-id @wsp)
|
||||
session-id (::session-id @wsp)
|
||||
output-ch (::ws/output-ch @wsp)
|
||||
prev-subs (::file-subscription @wsp)
|
||||
xform (comp (remove #(= (:session-id %) session-id))
|
||||
(map #(assoc % :subs-id file-id)))
|
||||
channel (a/chan (a/dropping-buffer 64) xform)]
|
||||
|
||||
(let [subs {:file-id file-id :channel fch :topic file-id}]
|
||||
(swap! state assoc ::file-subscription subs))
|
||||
(l/trace :fn "handle-message"
|
||||
:event :subscribe-file
|
||||
:file-id file-id
|
||||
:conn-id conn-id)
|
||||
|
||||
;; Close previous subscription if exists
|
||||
(when-let [ch (:channel psub)]
|
||||
(sp/close! ch)
|
||||
(mbus/purge! msgbus [ch]))
|
||||
(let [state {:file-id file-id :channel channel :topic file-id}]
|
||||
(swap! wsp assoc ::file-subscription state))
|
||||
|
||||
(sp/go-loop []
|
||||
(when-let [{:keys [type] :as message} (sp/take! fch)]
|
||||
(sp/put! output-ch message)
|
||||
(when (or (= :join-file type)
|
||||
(= :leave-file type)
|
||||
(= :disconnect type))
|
||||
(let [message {:type :presence
|
||||
:file-id file-id
|
||||
:session-id session-id
|
||||
(a/go
|
||||
;; Close previous subscription if exists
|
||||
(when-let [channel (:channel prev-subs)]
|
||||
(a/close! channel)
|
||||
(a/<! (mbus/purge! msgbus channel))))
|
||||
|
||||
;; Message forwarding
|
||||
(a/go
|
||||
(loop []
|
||||
(when-let [{:keys [type] :as message} (a/<! channel)]
|
||||
(when (or (= :join-file type)
|
||||
(= :leave-file type)
|
||||
(= :disconnect type))
|
||||
(let [message {:type :presence
|
||||
:file-id file-id
|
||||
:session-id session-id
|
||||
:profile-id profile-id}]
|
||||
(mbus/pub! msgbus
|
||||
:topic file-id
|
||||
:message message)))
|
||||
(recur)))
|
||||
(a/<! (mbus/pub! msgbus :topic file-id :message message))))
|
||||
(a/>! output-ch message)
|
||||
(recur))))
|
||||
|
||||
;; Subscribe to file topic
|
||||
(mbus/sub! msgbus :topic file-id :chan fch)
|
||||
(a/go
|
||||
;; Subscribe to file topic
|
||||
(a/<! (mbus/sub! msgbus :topic file-id :chan channel))
|
||||
|
||||
;; Notifify the rest of participants of the new connection.
|
||||
(let [message {:type :join-file
|
||||
:file-id file-id
|
||||
:subs-id file-id
|
||||
:session-id session-id
|
||||
:profile-id profile-id}]
|
||||
(mbus/pub! msgbus :topic file-id :message message))))
|
||||
;; Notifify the rest of participants of the new connection.
|
||||
(let [message {:type :join-file
|
||||
:file-id file-id
|
||||
:subs-id file-id
|
||||
:session-id session-id
|
||||
:profile-id profile-id}]
|
||||
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
|
||||
|
||||
(defmethod handle-message :unsubscribe-file
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::session-id ::profile-id]} {:keys [file-id] :as params}]
|
||||
(l/trace :fn "handle-message" :event "unsubscribe-file" :file-id file-id :conn-id id)
|
||||
[cfg wsp {:keys [file-id] :as params}]
|
||||
(let [msgbus (::mbus/msgbus cfg)
|
||||
conn-id (::ws/id @wsp)
|
||||
session-id (::session-id @wsp)
|
||||
profile-id (::profile-id @wsp)
|
||||
subs (::file-subscription @wsp)
|
||||
|
||||
(let [subs (::file-subscription @state)
|
||||
message {:type :leave-file
|
||||
:file-id file-id
|
||||
:session-id session-id
|
||||
:profile-id profile-id}]
|
||||
message {:type :leave-file
|
||||
:file-id file-id
|
||||
:session-id session-id
|
||||
:profile-id profile-id}]
|
||||
|
||||
(when (= (:file-id subs) file-id)
|
||||
(mbus/pub! msgbus :topic file-id :message message)
|
||||
(let [ch (:channel subs)]
|
||||
(sp/close! ch)
|
||||
(mbus/purge! msgbus [ch])))))
|
||||
(l/trace :fn "handle-message"
|
||||
:event :unsubscribe-file
|
||||
:file-id file-id
|
||||
:conn-id conn-id)
|
||||
|
||||
(a/go
|
||||
(when (= (:file-id subs) file-id)
|
||||
(let [channel (:channel subs)]
|
||||
(a/close! channel)
|
||||
(a/<! (mbus/purge! msgbus channel))
|
||||
(a/<! (mbus/pub! msgbus :topic file-id :message message)))))))
|
||||
|
||||
(defmethod handle-message :keepalive
|
||||
[_ _ _]
|
||||
(l/trace :fn "handle-message" :event :keepalive))
|
||||
|
||||
(defmethod handle-message :broadcast
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::session-id ::profile-id]} message]
|
||||
(l/trace :fn "handle-message" :event "broadcast" :conn-id id)
|
||||
(let [message (-> message
|
||||
(assoc :subs-id profile-id)
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :session-id session-id))]
|
||||
(mbus/pub! msgbus :topic profile-id :message message)))
|
||||
(l/trace :fn "handle-message" :event :keepalive)
|
||||
(a/go :nothing))
|
||||
|
||||
(defmethod handle-message :pointer-update
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/state ::session-id ::profile-id]} {:keys [file-id] :as message}]
|
||||
(when (::file-subscription @state)
|
||||
(let [message (-> message
|
||||
(assoc :subs-id file-id)
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :session-id session-id))]
|
||||
(mbus/pub! msgbus :topic file-id :message message))))
|
||||
[cfg wsp {:keys [file-id] :as message}]
|
||||
(let [msgbus (::mbus/msgbus cfg)
|
||||
profile-id (::profile-id @wsp)
|
||||
session-id (::session-id @wsp)
|
||||
subs (::file-subscription @wsp)
|
||||
message (-> message
|
||||
(assoc :subs-id file-id)
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :session-id session-id))]
|
||||
(a/go
|
||||
;; Only allow receive pointer updates when active subscription
|
||||
(when subs
|
||||
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
|
||||
|
||||
(defmethod handle-message :default
|
||||
[_ {:keys [::ws/id]} message]
|
||||
(l/warn :hint "received unexpected message"
|
||||
:message message
|
||||
:conn-id id))
|
||||
[_ wsp message]
|
||||
(let [conn-id (::ws/id @wsp)]
|
||||
(l/warn :hint "received unexpected message"
|
||||
:message message
|
||||
:conn-id conn-id)
|
||||
(a/go :none)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HTTP HANDLER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- on-connect
|
||||
[{:keys [::mtx/metrics]} {:keys [::ws/id] :as wsp}]
|
||||
(let [created-at (dt/now)]
|
||||
(l/trace :fn "on-connect" :conn-id id)
|
||||
(swap! state assoc id wsp)
|
||||
(mtx/run! metrics
|
||||
:id :websocket-active-connections
|
||||
:inc 1)
|
||||
|
||||
(assoc wsp ::ws/on-disconnect
|
||||
(fn []
|
||||
(l/trace :fn "on-disconnect" :conn-id id)
|
||||
(swap! state dissoc id)
|
||||
(mtx/run! metrics :id :websocket-active-connections :dec 1)
|
||||
(mtx/run! metrics
|
||||
:id :websocket-session-timing
|
||||
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))))))
|
||||
|
||||
(defn- on-rcv-message
|
||||
[{:keys [::mtx/metrics ::profile-id ::session-id]} message]
|
||||
(mtx/run! metrics
|
||||
:id :websocket-messages-total
|
||||
:labels recv-labels
|
||||
:inc 1)
|
||||
(assoc message :profile-id profile-id :session-id session-id))
|
||||
|
||||
(defn- on-snd-message
|
||||
[{:keys [::mtx/metrics]} message]
|
||||
(mtx/run! metrics
|
||||
:id :websocket-messages-total
|
||||
:labels send-labels
|
||||
:inc 1)
|
||||
message)
|
||||
|
||||
|
||||
(s/def ::session-id ::us/uuid)
|
||||
(s/def ::handler-params
|
||||
(s/keys :req-un [::session-id]))
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
[cfg {:keys [params ::session/profile-id] :as request} respond raise]
|
||||
(let [{:keys [session-id]} (us/conform ::handler-params params)]
|
||||
(cond
|
||||
(not profile-id)
|
||||
(ex/raise :type :authentication
|
||||
:hint "Authentication required.")
|
||||
(raise (ex/error :type :authentication
|
||||
:hint "Authentication required."))
|
||||
|
||||
(not (yws/upgrade-request? request))
|
||||
(ex/raise :type :validation
|
||||
:code :websocket-request-expected
|
||||
:hint "this endpoint only accepts websocket connections")
|
||||
(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)
|
||||
@@ -305,7 +341,8 @@
|
||||
::ws/handler (partial handle-message cfg)
|
||||
::profile-id profile-id
|
||||
::session-id session-id)
|
||||
(yws/upgrade request))))))
|
||||
(yws/upgrade request)
|
||||
(respond))))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::mbus/msgbus
|
||||
|
||||
@@ -16,16 +16,13 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as-alias actoken]
|
||||
[app.http.client :as http.client]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.audit.tasks :as-alias tasks]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.retry :as rtry]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.retry :as rtry]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -95,15 +92,6 @@
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; COLLECTOR
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Defines a service that collects the audit/activity log using
|
||||
;; internal database. Later this audit log can be transferred to
|
||||
;; an external storage and data cleared.
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::type ::us/string)
|
||||
@@ -116,13 +104,20 @@
|
||||
(s/or :fn fn? :str string? :kw keyword?))
|
||||
|
||||
(s/def ::event
|
||||
(s/keys :req [::type ::name ::profile-id]
|
||||
:opt [::ip-addr
|
||||
::props
|
||||
::webhooks/event?
|
||||
(s/keys :req-un [::type ::name ::profile-id]
|
||||
:opt-un [::ip-addr ::props]
|
||||
:opt [::webhooks/event?
|
||||
::webhooks/batch-timeout
|
||||
::webhooks/batch-key]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; COLLECTOR
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Defines a service that collects the audit/activity log using
|
||||
;; internal database. Later this audit log can be transferred to
|
||||
;; an external storage and data cleared.
|
||||
|
||||
(s/def ::collector
|
||||
(s/keys :req [::wrk/executor ::db/pool]))
|
||||
|
||||
@@ -138,64 +133,15 @@
|
||||
:else
|
||||
cfg))
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
(let [resultm (meta result)
|
||||
request (-> params meta ::http/request)
|
||||
profile-id (or (::profile-id resultm)
|
||||
(:profile-id result)
|
||||
(::rpc/profile-id params)
|
||||
uuid/zero)
|
||||
|
||||
props (-> (or (::replace-props resultm)
|
||||
(-> params
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (d/without-nils
|
||||
{:access-token-id (some-> token-id str)})]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
(::rpc/type cfg))
|
||||
::name (or (::name resultm)
|
||||
(::sv/name mdata))
|
||||
::profile-id profile-id
|
||||
::ip-addr (some-> request parse-client-ip)
|
||||
::props props
|
||||
::context context
|
||||
|
||||
;; NOTE: for batch-key lookup we need the params as-is
|
||||
;; because the rpc api does not need to know the
|
||||
;; audit/webhook specific object layout.
|
||||
::rpc/params params
|
||||
|
||||
::webhooks/batch-key
|
||||
(or (::webhooks/batch-key mdata)
|
||||
(::webhooks/batch-key resultm))
|
||||
|
||||
::webhooks/batch-timeout
|
||||
(or (::webhooks/batch-timeout mdata)
|
||||
(::webhooks/batch-timeout resultm))
|
||||
|
||||
::webhooks/event?
|
||||
(or (::webhooks/event? mdata)
|
||||
(::webhooks/event? resultm)
|
||||
false)}))
|
||||
|
||||
(defn- handle-event!
|
||||
[conn-or-pool event]
|
||||
(us/verify! ::event event)
|
||||
(let [params {:id (uuid/next)
|
||||
:name (::name event)
|
||||
:type (::type event)
|
||||
:profile-id (::profile-id event)
|
||||
:ip-addr (::ip-addr event)
|
||||
:context (::context event)
|
||||
:props (::props event)}]
|
||||
:name (:name event)
|
||||
:type (:type event)
|
||||
:profile-id (:profile-id event)
|
||||
:ip-addr (:ip-addr event)
|
||||
:props (:props event)}]
|
||||
|
||||
(when (contains? cf/flags :audit-log)
|
||||
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||
@@ -203,13 +149,11 @@
|
||||
;; this case we just retry the operation.
|
||||
(rtry/with-retry {::rtry/when rtry/conflict-exception?
|
||||
::rtry/max-retries 6
|
||||
::rtry/label "persist-audit-log"
|
||||
::db/conn (dm/check db/connection? conn-or-pool)}
|
||||
::rtry/label "persist-audit-log"}
|
||||
(let [now (dt/now)]
|
||||
(db/insert! conn-or-pool :audit-log
|
||||
(-> params
|
||||
(update :props db/tjson)
|
||||
(update :context db/tjson)
|
||||
(update :ip-addr db/inet)
|
||||
(assoc :created-at now)
|
||||
(assoc :tracked-at now)
|
||||
@@ -242,8 +186,9 @@
|
||||
|
||||
(defn submit!
|
||||
"Submit audit event to the collector."
|
||||
[cfg params]
|
||||
[{:keys [::wrk/executor] :as cfg} params]
|
||||
(let [conn (or (::db/conn cfg) (::db/pool cfg))]
|
||||
(us/assert! ::wrk/executor executor)
|
||||
(us/assert! ::db/pool-or-conn conn)
|
||||
(try
|
||||
(handle-event! conn (d/without-nils params))
|
||||
@@ -262,7 +207,7 @@
|
||||
(s/def ::tasks/uri ::us/string)
|
||||
|
||||
(defmethod ig/pre-init-spec ::tasks/archive-task [_]
|
||||
(s/keys :req [::db/pool ::main/props ::http.client/client]))
|
||||
(s/keys :req [::db/pool ::main/props ::http/client]))
|
||||
|
||||
(defmethod ig/init-key ::tasks/archive
|
||||
[_ cfg]
|
||||
@@ -286,7 +231,7 @@
|
||||
(if n
|
||||
(do
|
||||
(px/sleep 100)
|
||||
(recur (+ total ^long n)))
|
||||
(recur (+ total n)))
|
||||
(when (pos? total)
|
||||
(l/debug :hint "events archived" :total total)))))))))
|
||||
|
||||
@@ -336,7 +281,7 @@
|
||||
:method :post
|
||||
:headers headers
|
||||
:body body}
|
||||
resp (http.client/req! cfg params {:sync? true})]
|
||||
resp (http/req! cfg params {:sync? true})]
|
||||
(if (= (:status resp) 204)
|
||||
true
|
||||
(do
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -33,44 +32,35 @@
|
||||
(when-not (db/read-only? pool)
|
||||
(db/insert! pool :server-error-report
|
||||
{:id id
|
||||
:version 3
|
||||
:version 2
|
||||
:content (db/tjson report)})))
|
||||
|
||||
(defn record->report
|
||||
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
|
||||
(us/assert! ::l/record record)
|
||||
|
||||
(let [data (ex-data cause)
|
||||
ctx (-> context
|
||||
(merge
|
||||
{:context (-> context
|
||||
(assoc :tenant (cf/get :tenant))
|
||||
(assoc :host (cf/get :host))
|
||||
(assoc :public-uri (cf/get :public-uri))
|
||||
(assoc :logger/name logger)
|
||||
(assoc :logger/level level)
|
||||
(dissoc :request/params :value :params :data))]
|
||||
(merge
|
||||
{:context (-> (into (sorted-map) ctx)
|
||||
(pp/pprint-str :width 200 :length 50 :level 10))
|
||||
:props (pp/pprint-str props :width 200 :length 50)
|
||||
:hint (or (ex-message cause) @message)
|
||||
:trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)}
|
||||
(assoc :version (:full cf/version))
|
||||
(assoc :logger-name logger)
|
||||
(assoc :logger-level level)
|
||||
(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 [params (or (:request/params context) (:params context))]
|
||||
{:params (pp/pprint-str params :width 200 :length 50 :level 10)})
|
||||
|
||||
(when-let [value (:value context)]
|
||||
{:value (pp/pprint-str value :width 200 :length 50 :level 10)})
|
||||
|
||||
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
|
||||
{:data (pp/pprint-str data :width 200)})
|
||||
|
||||
(when-let [explain (ex/explain data {:level 10 :length 50})]
|
||||
{:explain explain}))))
|
||||
|
||||
(defn error-record?
|
||||
[{:keys [::l/level ::l/cause]}]
|
||||
(and (= :error level)
|
||||
(ex/exception? cause)))
|
||||
(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
|
||||
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
|
||||
@@ -84,16 +74,20 @@
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||
|
||||
(defn error-record?
|
||||
[{:keys [::l/level ::l/cause]}]
|
||||
(and (= :error level)
|
||||
(ex/exception? cause)))
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ cfg]
|
||||
(let [input (sp/chan :buf (sp/sliding-buffer 32)
|
||||
:xf (filter error-record?))]
|
||||
(let [input (sp/chan (sp/sliding-buffer 32) (filter error-record?))]
|
||||
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||
|
||||
(px/thread {:name "penpot/database-reporter" :virtual true}
|
||||
(px/thread
|
||||
{:name "penpot/database-reporter" :virtual true}
|
||||
(l/info :hint "initializing database error persistence")
|
||||
(try
|
||||
(loop []
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.csp :as sp]))
|
||||
|
||||
(defonce enabled (atom true))
|
||||
(defonce enabled (atom false))
|
||||
|
||||
(defn- send-mattermost-notification!
|
||||
[cfg {:keys [id public-uri] :as report}]
|
||||
@@ -30,9 +30,7 @@
|
||||
"```\n"
|
||||
"- host: `" (:host report) "`\n"
|
||||
"- tenant: `" (:tenant report) "`\n"
|
||||
"- request-path: `" (:request-path report) "`\n"
|
||||
"- frontend-version: `" (:frontend-version report) "`\n"
|
||||
"- backend-version: `" (:backend-version report) "`\n"
|
||||
"- version: `" (:version report) "`\n"
|
||||
"\n"
|
||||
"Trace:\n"
|
||||
(:trace report)
|
||||
@@ -52,15 +50,13 @@
|
||||
(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)
|
||||
:backend-version (or (:version/backend context) (:full cf/version))
|
||||
:frontend-version (:version/frontend context)
|
||||
:profile-id (:request/profile-id context)
|
||||
:request-path (:request/path context)
|
||||
:trace (ex/format-throwable cause :detail? false :header? false)})
|
||||
{: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
|
||||
[cfg record]
|
||||
@@ -81,8 +77,7 @@
|
||||
{:name "penpot/mattermost-reporter"
|
||||
:virtual true}
|
||||
(l/info :hint "initializing error reporter" :uri uri)
|
||||
(let [input (sp/chan :buf (sp/sliding-buffer 128)
|
||||
:xf (filter ldb/error-record?))]
|
||||
(let [input (sp/chan (sp/sliding-buffer 128) (filter ldb/error-record?))]
|
||||
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||
(try
|
||||
(loop []
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[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]
|
||||
@@ -29,7 +30,6 @@
|
||||
[app.redis :as-alias rds]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as-alias rpc.doc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.srepl :as-alias srepl]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.fs :as-alias sto.fs]
|
||||
@@ -37,8 +37,7 @@
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px])
|
||||
[integrant.core :as ig])
|
||||
(:gen-class))
|
||||
|
||||
(def default-metrics
|
||||
@@ -103,15 +102,15 @@
|
||||
::mdef/labels ["name"]
|
||||
::mdef/type :summary}
|
||||
|
||||
:rpc-climit-queue
|
||||
{::mdef/name "penpot_rpc_climit_queue"
|
||||
::mdef/help "Current number of queued submissions."
|
||||
:rpc-climit-queue-size
|
||||
{::mdef/name "penpot_rpc_climit_queue_size"
|
||||
::mdef/help "Current number of queued submissions on the CLIMIT."
|
||||
::mdef/labels ["name"]
|
||||
::mdef/type :gauge}
|
||||
|
||||
:rpc-climit-permits
|
||||
{::mdef/name "penpot_rpc_climit_permits"
|
||||
::mdef/help "Current number of available permits"
|
||||
:rpc-climit-concurrency
|
||||
{::mdef/name "penpot_rpc_climit_concurrency"
|
||||
::mdef/help "Current number of used concurrency capacity on the CLIMIT"
|
||||
::mdef/labels ["name"]
|
||||
::mdef/type :gauge}
|
||||
|
||||
@@ -175,8 +174,10 @@
|
||||
|
||||
;; Default thread pool for IO operations
|
||||
::wrk/executor
|
||||
{::wrk/parallelism (cf/get :default-executor-parallelism
|
||||
(+ 3 (* (px/get-available-processors) 3)))}
|
||||
{::wrk/parallelism (cf/get :default-executor-parallelism 100)}
|
||||
|
||||
::wrk/scheduled-executor
|
||||
{::wrk/parallelism (cf/get :scheduled-executor-parallelism 20)}
|
||||
|
||||
::wrk/monitor
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
@@ -193,16 +194,17 @@
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
|
||||
::rds/redis
|
||||
{::rds/uri (cf/get :redis-uri)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
{::rds/uri (cf/get :redis-uri)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
|
||||
::mbus/msgbus
|
||||
{::wrk/executor (ig/ref ::wrk/executor)
|
||||
::rds/redis (ig/ref ::rds/redis)}
|
||||
{:backend (cf/get :msgbus-backend :redis)
|
||||
:executor (ig/ref ::wrk/executor)
|
||||
:redis (ig/ref ::rds/redis)}
|
||||
|
||||
:app.storage.tmp/cleaner
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
{::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
||||
|
||||
::sto/gc-deleted-task
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
@@ -215,13 +217,20 @@
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
::session/manager
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::props (ig/ref :app.setup/props)}
|
||||
|
||||
::actoken/manager
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::props (ig/ref :app.setup/props)}
|
||||
|
||||
::session.tasks/gc
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
::http.awsns/routes
|
||||
{::props (ig/ref ::setup/props)
|
||||
{::props (ig/ref :app.setup/props)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::http.client/client (ig/ref ::http.client/client)
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
@@ -230,7 +239,8 @@
|
||||
{::http/port (cf/get :http-server-port)
|
||||
::http/host (cf/get :http-server-host)
|
||||
::http/router (ig/ref ::http/router)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::http/metrics (ig/ref ::mtx/metrics)
|
||||
::http/executor (ig/ref ::wrk/executor)
|
||||
::http/io-threads (cf/get :http-server-io-threads)
|
||||
::http/max-body-size (cf/get :http-server-max-body-size)
|
||||
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||
@@ -264,7 +274,8 @@
|
||||
::oidc/routes
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::props (ig/ref ::setup/props)
|
||||
::props (ig/ref :app.setup/props)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::oidc/providers {:google (ig/ref ::oidc.providers/google)
|
||||
:github (ig/ref ::oidc.providers/github)
|
||||
:gitlab (ig/ref ::oidc.providers/gitlab)
|
||||
@@ -273,10 +284,12 @@
|
||||
|
||||
:app.http/router
|
||||
{::session/manager (ig/ref ::session/manager)
|
||||
::actoken/manager (ig/ref ::actoken/manager)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::rpc/routes (ig/ref ::rpc/routes)
|
||||
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
|
||||
::props (ig/ref ::setup/props)
|
||||
::props (ig/ref :app.setup/props)
|
||||
::mtx/routes (ig/ref ::mtx/routes)
|
||||
::oidc/routes (ig/ref ::oidc/routes)
|
||||
::http.debug/routes (ig/ref ::http.debug/routes)
|
||||
@@ -287,13 +300,12 @@
|
||||
:app.http.debug/routes
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
::session/manager (ig/ref ::session/manager)}
|
||||
|
||||
::http.ws/routes
|
||||
:app.http.websocket/routes
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||
::mbus/msgbus (ig/ref :app.msgbus/msgbus)
|
||||
::session/manager (ig/ref ::session/manager)}
|
||||
|
||||
:app.http.assets/routes
|
||||
@@ -308,7 +320,8 @@
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
:app.rpc/rlimit
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
{::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
||||
|
||||
:app.rpc/methods
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
@@ -323,10 +336,11 @@
|
||||
|
||||
::rpc/climit (ig/ref ::rpc/climit)
|
||||
::rpc/rlimit (ig/ref ::rpc/rlimit)
|
||||
::setup/templates (ig/ref ::setup/templates)
|
||||
::props (ig/ref ::setup/props)
|
||||
|
||||
::props (ig/ref :app.setup/props)
|
||||
|
||||
:pool (ig/ref ::db/pool)
|
||||
:templates (ig/ref :app.setup/builtin-templates)
|
||||
}
|
||||
|
||||
:app.rpc.doc/routes
|
||||
@@ -337,7 +351,8 @@
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::props (ig/ref ::setup/props)}
|
||||
::actoken/manager (ig/ref ::actoken/manager)
|
||||
::props (ig/ref :app.setup/props)}
|
||||
|
||||
::wrk/registry
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
@@ -381,8 +396,7 @@
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
:app.tasks.file-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.tasks.file-xlog-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
@@ -390,7 +404,7 @@
|
||||
:app.tasks.telemetry/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::http.client/client (ig/ref ::http.client/client)
|
||||
::props (ig/ref ::setup/props)}
|
||||
::props (ig/ref :app.setup/props)}
|
||||
|
||||
[::srepl/urepl ::srepl/server]
|
||||
{::srepl/port (cf/get :urepl-port 6062)
|
||||
@@ -400,9 +414,10 @@
|
||||
{::srepl/port (cf/get :prepl-port 6063)
|
||||
::srepl/host (cf/get :prepl-host "localhost")}
|
||||
|
||||
::setup/templates {}
|
||||
:app.setup/builtin-templates
|
||||
{::http.client/client (ig/ref ::http.client/client)}
|
||||
|
||||
::setup/props
|
||||
:app.setup/props
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::key (cf/get :secret-key)
|
||||
|
||||
@@ -411,7 +426,7 @@
|
||||
::migrations (ig/ref :app.migrations/migrations)}
|
||||
|
||||
::audit.tasks/archive
|
||||
{::props (ig/ref ::setup/props)
|
||||
{::props (ig/ref :app.setup/props)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::http.client/client (ig/ref ::http.client/client)}
|
||||
|
||||
@@ -452,7 +467,8 @@
|
||||
|
||||
(def worker-config
|
||||
{::wrk/cron
|
||||
{::wrk/registry (ig/ref ::wrk/registry)
|
||||
{::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)
|
||||
::wrk/registry (ig/ref ::wrk/registry)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/entries
|
||||
[{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||
|
||||
@@ -10,16 +10,12 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.svg :as svg]
|
||||
[app.util.time :as dt]
|
||||
[buddy.core.bytes :as bb]
|
||||
[buddy.core.codecs :as bc]
|
||||
[clojure.java.shell :as sh]
|
||||
@@ -32,9 +28,6 @@
|
||||
org.im4java.core.IMOperation
|
||||
org.im4java.core.Info))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 30)) ; 30 MiB
|
||||
|
||||
(s/def ::path fs/path?)
|
||||
(s/def ::filename string?)
|
||||
(s/def ::size integer?)
|
||||
@@ -50,27 +43,6 @@
|
||||
(s/keys :req-un [::path]
|
||||
:opt-un [::mtype]))
|
||||
|
||||
(sm/def! ::fs/path
|
||||
{:type ::fs/path
|
||||
:pred fs/path?
|
||||
:type-properties
|
||||
{:title "path"
|
||||
:description "filesystem path"
|
||||
:error/message "expected a valid fs path instance"
|
||||
:gen/gen (sg/generator :string)
|
||||
::oapi/type "string"
|
||||
::oapi/format "unix-path"
|
||||
::oapi/decode fs/path}})
|
||||
|
||||
(sm/def! ::upload
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size :int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]])
|
||||
|
||||
(defn validate-media-type!
|
||||
([upload] (validate-media-type! upload cm/valid-image-types))
|
||||
([upload allowed]
|
||||
@@ -81,16 +53,6 @@
|
||||
|
||||
upload))
|
||||
|
||||
(defn validate-media-size!
|
||||
[upload]
|
||||
(when (> (:size upload) (cf/get :media-max-file-size default-max-file-size))
|
||||
(ex/raise :type :restriction
|
||||
:code :media-max-file-size-reached
|
||||
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
||||
(:size upload)
|
||||
default-max-file-size)))
|
||||
upload)
|
||||
|
||||
(defmulti process :cmd)
|
||||
(defmulti process-error class)
|
||||
|
||||
@@ -206,7 +168,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-svg-file
|
||||
:hint "uploaded svg does not provides dimensions"))
|
||||
(merge input info {:ts (dt/now)}))
|
||||
(merge input info))
|
||||
|
||||
(let [instance (Info. (str path))
|
||||
mtype' (.getProperty instance "Mime type")]
|
||||
@@ -221,8 +183,7 @@
|
||||
;; any frame.
|
||||
(assoc input
|
||||
:width (.getPageWidth instance)
|
||||
:height (.getPageHeight instance)
|
||||
:ts (dt/now))))))
|
||||
:height (.getPageHeight instance))))))
|
||||
|
||||
(defmethod process-error org.im4java.core.InfoException
|
||||
[error]
|
||||
|
||||
@@ -89,12 +89,12 @@
|
||||
|
||||
|
||||
(defn- handler
|
||||
[registry _]
|
||||
[registry _ respond _]
|
||||
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
|
||||
writer (StringWriter.)]
|
||||
(TextFormat/write004 writer samples)
|
||||
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
|
||||
:body (.toString writer)}))
|
||||
(respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
|
||||
:body (.toString writer)})))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -315,19 +315,7 @@
|
||||
{:name "0101-mod-server-error-report-table"
|
||||
:fn (mg/resource "app/migrations/sql/0101-mod-server-error-report-table.sql")}
|
||||
|
||||
{:name "0102-mod-access-token-table"
|
||||
:fn (mg/resource "app/migrations/sql/0102-mod-access-token-table.sql")}
|
||||
|
||||
{:name "0103-mod-file-object-thumbnail-table"
|
||||
:fn (mg/resource "app/migrations/sql/0103-mod-file-object-thumbnail-table.sql")}
|
||||
|
||||
{:name "0104-mod-file-thumbnail-table"
|
||||
:fn (mg/resource "app/migrations/sql/0104-mod-file-thumbnail-table.sql")}
|
||||
|
||||
{:name "0105-mod-server-error-report-table"
|
||||
:fn (mg/resource "app/migrations/sql/0105-mod-server-error-report-table.sql")}
|
||||
|
||||
])
|
||||
])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE access_token
|
||||
ADD COLUMN expires_at timestamptz NULL;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE file_object_thumbnail
|
||||
ADD COLUMN media_id uuid NULL REFERENCES storage_object(id) ON DELETE CASCADE DEFERRABLE;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE file_thumbnail
|
||||
ADD COLUMN media_id uuid NULL REFERENCES storage_object(id) ON DELETE CASCADE DEFERRABLE;
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX server_error_report__created_at__idx
|
||||
ON server_error_report ( created_at );
|
||||
@@ -8,18 +8,20 @@
|
||||
"The msgbus abstraction implemented using redis as underlying backend."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cfg]
|
||||
[app.redis :as rds]
|
||||
[app.redis :as redis]
|
||||
[app.util.async :as aa]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.csp :as sp]))
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
@@ -32,116 +34,132 @@
|
||||
(def ^:private xform-prefix-topic
|
||||
(map (fn [obj] (update obj :topic prefix-topic))))
|
||||
|
||||
(declare ^:private redis-pub!)
|
||||
(declare ^:private redis-sub!)
|
||||
(declare ^:private redis-unsub!)
|
||||
(declare ^:private redis-connect)
|
||||
(declare ^:private redis-disconnect)
|
||||
(declare ^:private redis-pub)
|
||||
(declare ^:private redis-sub)
|
||||
(declare ^:private redis-unsub)
|
||||
(declare ^:private start-io-loop!)
|
||||
(declare ^:private subscribe-to-topics)
|
||||
(declare ^:private unsubscribe-channels)
|
||||
|
||||
(s/def ::cmd-ch sp/chan?)
|
||||
(s/def ::rcv-ch sp/chan?)
|
||||
(s/def ::pub-ch sp/chan?)
|
||||
(defmethod ig/prep-key ::msgbus
|
||||
[_ cfg]
|
||||
(merge {:buffer-size 128
|
||||
:timeout (dt/duration {:seconds 30})}
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(s/def ::cmd-ch ::aa/channel)
|
||||
(s/def ::rcv-ch ::aa/channel)
|
||||
(s/def ::pub-ch ::aa/channel)
|
||||
(s/def ::state ::us/agent)
|
||||
(s/def ::pconn ::rds/connection-holder)
|
||||
(s/def ::sconn ::rds/connection-holder)
|
||||
(s/def ::pconn ::redis/connection-holder)
|
||||
(s/def ::sconn ::redis/connection-holder)
|
||||
(s/def ::msgbus
|
||||
(s/keys :req [::cmd-ch ::rcv-ch ::pub-ch ::state ::pconn ::sconn ::wrk/executor]))
|
||||
|
||||
(defmethod ig/pre-init-spec ::msgbus [_]
|
||||
(s/keys :req [::rds/redis ::wrk/executor]))
|
||||
(s/def ::buffer-size ::us/integer)
|
||||
|
||||
(defmethod ig/prep-key ::msgbus
|
||||
[_ cfg]
|
||||
(-> cfg
|
||||
(assoc ::buffer-size 128)
|
||||
(assoc ::timeout (dt/duration {:seconds 30}))))
|
||||
(defmethod ig/pre-init-spec ::msgbus [_]
|
||||
(s/keys :req-un [::buffer-size ::redis/timeout ::redis/redis ::wrk/executor]))
|
||||
|
||||
(defmethod ig/init-key ::msgbus
|
||||
[_ {:keys [::buffer-size ::wrk/executor ::timeout ::rds/redis] :as cfg}]
|
||||
[_ {:keys [buffer-size executor] :as cfg}]
|
||||
(l/info :hint "initialize msgbus" :buffer-size buffer-size)
|
||||
(let [cmd-ch (sp/chan :buf buffer-size)
|
||||
rcv-ch (sp/chan :buf (sp/dropping-buffer buffer-size))
|
||||
pub-ch (sp/chan :buf (sp/dropping-buffer buffer-size)
|
||||
:xf xform-prefix-topic)
|
||||
(let [cmd-ch (a/chan buffer-size)
|
||||
rcv-ch (a/chan (a/dropping-buffer buffer-size))
|
||||
pub-ch (a/chan (a/dropping-buffer buffer-size) xform-prefix-topic)
|
||||
state (agent {})
|
||||
|
||||
pconn (rds/connect redis :timeout timeout)
|
||||
sconn (rds/connect redis :type :pubsub :timeout timeout)
|
||||
msgbus (-> cfg
|
||||
(assoc ::pconn pconn)
|
||||
(assoc ::sconn sconn)
|
||||
msgbus (-> (redis-connect cfg)
|
||||
(assoc ::cmd-ch cmd-ch)
|
||||
(assoc ::rcv-ch rcv-ch)
|
||||
(assoc ::pub-ch pub-ch)
|
||||
(assoc ::state state)
|
||||
(assoc ::wrk/executor executor))]
|
||||
|
||||
(us/verify! ::msgbus msgbus)
|
||||
|
||||
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
|
||||
(set-error-mode! state :continue)
|
||||
(start-io-loop! msgbus)
|
||||
|
||||
(assoc msgbus ::io-thr (start-io-loop! msgbus))))
|
||||
|
||||
(defmethod ig/halt-key! ::msgbus
|
||||
[_ msgbus]
|
||||
(px/interrupt! (::io-thr msgbus))
|
||||
(sp/close! (::cmd-ch msgbus))
|
||||
(sp/close! (::rcv-ch msgbus))
|
||||
(sp/close! (::pub-ch msgbus))
|
||||
(d/close! (::pconn msgbus))
|
||||
(d/close! (::sconn msgbus)))
|
||||
msgbus))
|
||||
|
||||
(defn sub!
|
||||
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
|
||||
(let [topics (into [] (map prefix-topic) (if topic [topic] topics))]
|
||||
(l/debug :hint "subscribe" :topics topics :chan (hash chan))
|
||||
(send-via executor state subscribe-to-topics cfg topics chan)
|
||||
nil))
|
||||
(let [done-ch (a/chan)
|
||||
topics (into [] (map prefix-topic) (if topic [topic] topics))]
|
||||
(l/debug :hint "subscribe" :topics topics)
|
||||
(send-via executor state subscribe-to-topics cfg topics chan done-ch)
|
||||
done-ch))
|
||||
|
||||
(defn pub!
|
||||
[{::keys [pub-ch]} & {:as params}]
|
||||
(sp/put! pub-ch params))
|
||||
(a/go
|
||||
(a/>! pub-ch params)))
|
||||
|
||||
(defn purge!
|
||||
[{:keys [::state ::wrk/executor] :as msgbus} chans]
|
||||
(l/debug :hint "purge" :chans (count chans))
|
||||
(send-via executor state unsubscribe-channels msgbus chans)
|
||||
nil)
|
||||
(l/trace :hint "purge" :chans (count chans))
|
||||
(let [done-ch (a/chan)]
|
||||
(send-via executor state unsubscribe-channels msgbus chans done-ch)
|
||||
done-ch))
|
||||
|
||||
(defmethod ig/halt-key! ::msgbus
|
||||
[_ msgbus]
|
||||
(redis-disconnect msgbus)
|
||||
(a/close! (::cmd-ch msgbus))
|
||||
(a/close! (::rcv-ch msgbus))
|
||||
(a/close! (::pub-ch msgbus)))
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn- redis-connect
|
||||
[{:keys [timeout redis] :as cfg}]
|
||||
(let [pconn (redis/connect redis :timeout timeout)
|
||||
sconn (redis/connect redis :type :pubsub :timeout timeout)]
|
||||
{::pconn pconn
|
||||
::sconn sconn}))
|
||||
|
||||
(defn- redis-disconnect
|
||||
[{:keys [::pconn ::sconn] :as cfg}]
|
||||
(d/close! pconn)
|
||||
(d/close! sconn))
|
||||
|
||||
(defn- conj-subscription
|
||||
"A low level function that is responsible to create on-demand
|
||||
subscriptions on redis. It reuses the same subscription if it is
|
||||
already established."
|
||||
already established. Intended to be executed in agent."
|
||||
[nsubs cfg topic chan]
|
||||
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
|
||||
(when (= 1 (count nsubs))
|
||||
(l/trace :hint "open subscription" :topic topic ::l/sync? true)
|
||||
(redis-sub! cfg topic))
|
||||
(redis-sub cfg topic))
|
||||
nsubs))
|
||||
|
||||
(defn- disj-subscription
|
||||
"A low level function responsible on removing subscriptions. The
|
||||
subscription is truly removed from redis once no single local
|
||||
subscription is look for it."
|
||||
subscription is look for it. Intended to be executed in agent."
|
||||
[nsubs cfg topic chan]
|
||||
(let [nsubs (disj nsubs chan)]
|
||||
(when (empty? nsubs)
|
||||
(l/trace :hint "close subscription" :topic topic ::l/sync? true)
|
||||
(redis-unsub! cfg topic))
|
||||
(redis-unsub cfg topic))
|
||||
nsubs))
|
||||
|
||||
(defn- subscribe-to-topics
|
||||
"Function responsible to attach local subscription to the state."
|
||||
[state cfg topics chan]
|
||||
(let [state (update state :chans assoc chan topics)]
|
||||
(reduce (fn [state topic]
|
||||
(update-in state [:topics topic] conj-subscription cfg topic chan))
|
||||
state
|
||||
topics)))
|
||||
"Function responsible to attach local subscription to the
|
||||
state. Intended to be used in agent."
|
||||
[state cfg topics chan done-ch]
|
||||
(aa/with-closing done-ch
|
||||
(let [state (update state :chans assoc chan topics)]
|
||||
(reduce (fn [state topic]
|
||||
(update-in state [:topics topic] conj-subscription cfg topic chan))
|
||||
state
|
||||
topics))))
|
||||
|
||||
(defn- unsubscribe-channel
|
||||
(defn- unsubscribe-single-channel
|
||||
"Auxiliary function responsible on removing a single local
|
||||
subscription from the state."
|
||||
[state cfg chan]
|
||||
@@ -156,113 +174,87 @@
|
||||
"Function responsible from detach from state a seq of channels,
|
||||
useful when client disconnects or in-bulk unsubscribe
|
||||
operations. Intended to be executed in agent."
|
||||
[state cfg channels]
|
||||
(reduce #(unsubscribe-channel %1 cfg %2) state channels))
|
||||
[state cfg channels done-ch]
|
||||
(aa/with-closing done-ch
|
||||
(reduce #(unsubscribe-single-channel %1 cfg %2) state channels)))
|
||||
|
||||
(defn- create-listener
|
||||
[rcv-ch]
|
||||
(rds/pubsub-listener
|
||||
(redis/pubsub-listener
|
||||
:on-message (fn [_ topic message]
|
||||
;; There are no back pressure, so we use a slidding
|
||||
;; buffer for cases when the pubsub broker sends
|
||||
;; more messages that we can process.
|
||||
(let [val {:topic topic :message (t/decode message)}]
|
||||
(when-not (sp/offer! rcv-ch val)
|
||||
(when-not (a/offer! rcv-ch val)
|
||||
(l/warn :msg "dropping message on subscription loop"))))))
|
||||
|
||||
(defn- process-input!
|
||||
[{:keys [::state ::wrk/executor] :as cfg} topic message]
|
||||
(let [chans (get-in @state [:topics topic])]
|
||||
(when-let [closed (loop [chans (seq chans)
|
||||
closed #{}]
|
||||
(if-let [ch (first chans)]
|
||||
(if (sp/put! ch message)
|
||||
(recur (rest chans) closed)
|
||||
(recur (rest chans) (conj closed ch)))
|
||||
(seq closed)))]
|
||||
(send-via executor state unsubscribe-channels cfg closed))))
|
||||
|
||||
|
||||
(defn start-io-loop!
|
||||
[{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}]
|
||||
(rds/add-listener! sconn (create-listener rcv-ch))
|
||||
(redis/add-listener! sconn (create-listener rcv-ch))
|
||||
(letfn [(send-to-topic [topic message]
|
||||
(a/go-loop [chans (seq (get-in @state [:topics topic]))
|
||||
closed #{}]
|
||||
(if-let [ch (first chans)]
|
||||
(if (a/>! ch message)
|
||||
(recur (rest chans) closed)
|
||||
(recur (rest chans) (conj closed ch)))
|
||||
(seq closed))))
|
||||
|
||||
(px/thread
|
||||
{:name "penpot/msgbus/io-loop"
|
||||
:virtual true}
|
||||
(try
|
||||
(process-incoming [{:keys [topic message]}]
|
||||
(a/go
|
||||
(when-let [closed (a/<! (send-to-topic topic message))]
|
||||
(send-via executor state unsubscribe-channels cfg closed nil))))
|
||||
]
|
||||
(px/thread
|
||||
{:name "penpot/msgbus-io-loop"}
|
||||
(loop []
|
||||
(let [timeout-ch (sp/timeout-chan 1000)
|
||||
[val port] (sp/alts! [timeout-ch pub-ch rcv-ch])]
|
||||
(let [[val port] (a/alts!! [pub-ch rcv-ch])]
|
||||
(cond
|
||||
(identical? port timeout-ch)
|
||||
(let [closed (->> (:chans @state)
|
||||
(map key)
|
||||
(filter sp/closed?))]
|
||||
(when (seq closed)
|
||||
(send-via executor state unsubscribe-channels cfg closed)
|
||||
(l/debug :hint "proactively purge channels" :count (count closed)))
|
||||
(recur))
|
||||
|
||||
(nil? val)
|
||||
(throw (InterruptedException. "internally interrupted"))
|
||||
(do
|
||||
(l/trace :hint "stopping io-loop, nil received")
|
||||
(send-via executor state (fn [state]
|
||||
(->> (vals state)
|
||||
(mapcat identity)
|
||||
(filter some?)
|
||||
(run! a/close!))
|
||||
nil)))
|
||||
|
||||
(identical? port rcv-ch)
|
||||
(let [{:keys [topic message]} val]
|
||||
(process-input! cfg topic message)
|
||||
(= port rcv-ch)
|
||||
(do
|
||||
(a/<!! (process-incoming val))
|
||||
(recur))
|
||||
|
||||
(identical? port pub-ch)
|
||||
(do
|
||||
(redis-pub! cfg val)
|
||||
(recur)))))
|
||||
(= port pub-ch)
|
||||
(let [result (a/<!! (redis-pub cfg val))]
|
||||
(when (ex/exception? result)
|
||||
(l/error :hint "unexpected error on publishing"
|
||||
:message val
|
||||
:cause result))
|
||||
(recur))))))))
|
||||
|
||||
(catch InterruptedException _
|
||||
(l/trace :hint "io-loop thread interrumpted"))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected exception on io-loop thread"
|
||||
:cause cause))
|
||||
(finally
|
||||
(l/trace :hint "clearing io-loop state")
|
||||
(when-let [chans (:chans @state)]
|
||||
(run! sp/close! (keys chans)))
|
||||
|
||||
(l/debug :hint "io-loop thread terminated")))))
|
||||
|
||||
|
||||
(defn- redis-pub!
|
||||
(defn- redis-pub
|
||||
"Publish a message to the redis server. Asynchronous operation,
|
||||
intended to be used in core.async go blocks."
|
||||
[{:keys [::pconn] :as cfg} {:keys [topic message]}]
|
||||
(try
|
||||
(p/await! (rds/publish! pconn topic (t/encode message)))
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error on publishing"
|
||||
:message message
|
||||
:cause cause))))
|
||||
(let [message (t/encode message)
|
||||
res (a/chan 1)]
|
||||
(-> (redis/publish! pconn topic message)
|
||||
(p/finally (fn [_ cause]
|
||||
(when (and cause (redis/open? pconn))
|
||||
(a/offer! res cause))
|
||||
(a/close! res))))
|
||||
res))
|
||||
|
||||
(defn- redis-sub!
|
||||
(defn redis-sub
|
||||
"Create redis subscription. Blocking operation, intended to be used
|
||||
inside an agent."
|
||||
[{:keys [::sconn] :as cfg} topic]
|
||||
(try
|
||||
(rds/subscribe! sconn topic)
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Throwable cause
|
||||
(l/trace :hint "exception on subscribing" :topic topic :cause cause))))
|
||||
(redis/subscribe! sconn topic))
|
||||
|
||||
(defn- redis-unsub!
|
||||
(defn redis-unsub
|
||||
"Removes redis subscription. Blocking operation, intended to be used
|
||||
inside an agent."
|
||||
[{:keys [::sconn] :as cfg} topic]
|
||||
(try
|
||||
(rds/unsubscribe! sconn topic)
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Throwable cause
|
||||
(l/trace :hint "exception on unsubscribing" :topic topic :cause cause))))
|
||||
|
||||
(redis/unsubscribe! sconn topic))
|
||||
|
||||
@@ -8,21 +8,17 @@
|
||||
"The msgbus abstraction implemented using redis as underlying backend."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.metrics :as mtx]
|
||||
[app.redis.script :as-alias rscript]
|
||||
[app.util.cache :as cache]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.core :as c]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px])
|
||||
[promesa.core :as p])
|
||||
(:import
|
||||
clojure.lang.IDeref
|
||||
clojure.lang.MapEntry
|
||||
@@ -91,7 +87,7 @@
|
||||
(s/def ::connect? ::us/boolean)
|
||||
(s/def ::io-threads ::us/integer)
|
||||
(s/def ::worker-threads ::us/integer)
|
||||
(s/def ::cache some?)
|
||||
(s/def ::cache #(instance? clojure.lang.Atom %))
|
||||
|
||||
(s/def ::redis
|
||||
(s/keys :req [::resources
|
||||
@@ -103,11 +99,11 @@
|
||||
|
||||
(defmethod ig/prep-key ::redis
|
||||
[_ cfg]
|
||||
(let [cpus (px/get-available-processors)
|
||||
threads (max 1 (int (* cpus 0.2)))]
|
||||
(let [runtime (Runtime/getRuntime)
|
||||
cpus (.availableProcessors ^Runtime runtime)]
|
||||
(merge {::timeout (dt/duration "10s")
|
||||
::io-threads (max 3 threads)
|
||||
::worker-threads (max 3 threads)}
|
||||
::io-threads (max 3 cpus)
|
||||
::worker-threads (max 3 cpus)}
|
||||
(d/without-nils cfg))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::redis [_]
|
||||
@@ -133,15 +129,6 @@
|
||||
(def string-codec
|
||||
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
|
||||
|
||||
(defn- create-cache
|
||||
[{:keys [::wrk/executor] :as cfg}]
|
||||
(letfn [(on-remove [key val cause]
|
||||
(l/trace :hint "evict connection (cache)" :key key :reason cause)
|
||||
(some-> val d/close!))]
|
||||
(cache/create :executor executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")))
|
||||
|
||||
(defn- initialize-resources
|
||||
"Initialize redis connection resources"
|
||||
[{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}]
|
||||
@@ -158,21 +145,19 @@
|
||||
(timer ^Timer timer)
|
||||
(build))
|
||||
|
||||
redis-uri (RedisURI/create ^String uri)
|
||||
cfg (-> cfg
|
||||
(assoc ::resources resources)
|
||||
(assoc ::timer timer)
|
||||
(assoc ::redis-uri redis-uri))]
|
||||
redis-uri (RedisURI/create ^String uri)]
|
||||
|
||||
(assoc cfg ::cache (create-cache cfg))))
|
||||
(-> cfg
|
||||
(assoc ::resources resources)
|
||||
(assoc ::timer timer)
|
||||
(assoc ::cache (atom {}))
|
||||
(assoc ::redis-uri redis-uri))))
|
||||
|
||||
(defn- shutdown-resources
|
||||
[{:keys [::resources ::cache ::timer]}]
|
||||
(cache/invalidate-all! cache)
|
||||
|
||||
(run! d/close! (vals @cache))
|
||||
(when resources
|
||||
(.shutdown ^ClientResources resources))
|
||||
|
||||
(when timer
|
||||
(.stop ^Timer timer)))
|
||||
|
||||
@@ -188,7 +173,6 @@
|
||||
:default (.connect ^RedisClient client ^RedisCodec codec)
|
||||
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
|
||||
|
||||
(l/trc :hint "connect" :hid (hash client))
|
||||
(.setTimeout ^StatefulConnection conn ^Duration timeout)
|
||||
(reify
|
||||
IDeref
|
||||
@@ -196,9 +180,8 @@
|
||||
|
||||
AutoCloseable
|
||||
(close [_]
|
||||
(ex/ignoring (.close ^StatefulConnection conn))
|
||||
(ex/ignoring (.shutdown ^RedisClient client))
|
||||
(l/trc :hint "disconnect" :hid (hash client))))))
|
||||
(.close ^StatefulConnection conn)
|
||||
(.shutdown ^RedisClient client)))))
|
||||
|
||||
(defn connect
|
||||
[state & {:as opts}]
|
||||
@@ -211,10 +194,15 @@
|
||||
(defn get-or-connect
|
||||
[{:keys [::cache] :as state} key options]
|
||||
(us/assert! ::redis state)
|
||||
(let [connection (cache/get cache key (fn [_] (connect* state options)))]
|
||||
(-> state
|
||||
(dissoc ::cache)
|
||||
(assoc ::connection connection))))
|
||||
(-> state
|
||||
(assoc ::connection
|
||||
(or (get @cache key)
|
||||
(-> (swap! cache (fn [cache]
|
||||
(when-let [prev (get cache key)]
|
||||
(d/close! prev))
|
||||
(assoc cache key (connect* state options))))
|
||||
(get key))))
|
||||
(dissoc ::cache)))
|
||||
|
||||
(defn add-listener!
|
||||
[{:keys [::connection] :as conn} listener]
|
||||
@@ -356,7 +344,7 @@
|
||||
(do
|
||||
(l/error :hint "no script found" :name sname :cause cause)
|
||||
(->> (load-script)
|
||||
(p/mcat eval-script)))
|
||||
(p/mapcat eval-script)))
|
||||
(if-let [on-error (::rscript/on-error script)]
|
||||
(on-error cause)
|
||||
(p/rejected cause))))
|
||||
@@ -387,16 +375,15 @@
|
||||
(load-script []
|
||||
(l/trace :hint "load script" :name sname)
|
||||
(->> (.scriptLoad ^RedisScriptingAsyncCommands cmd
|
||||
^String (read-script))
|
||||
(p/fmap (fn [sha]
|
||||
(swap! scripts-cache assoc sname sha)
|
||||
sha))))]
|
||||
^String (read-script))
|
||||
(p/map (fn [sha]
|
||||
(swap! scripts-cache assoc sname sha)
|
||||
sha))))]
|
||||
|
||||
(p/await!
|
||||
(if-let [sha (get @scripts-cache sname)]
|
||||
(eval-script sha)
|
||||
(->> (load-script)
|
||||
(p/mapcat eval-script)))))))
|
||||
(if-let [sha (get @scripts-cache sname)]
|
||||
(eval-script sha)
|
||||
(->> (load-script)
|
||||
(p/mapcat eval-script))))))
|
||||
|
||||
(defn timeout-exception?
|
||||
[cause]
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
@@ -19,6 +19,7 @@
|
||||
[app.http.client :as-alias http.client]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.main :as-alias main]
|
||||
[app.metrics :as mtx]
|
||||
[app.msgbus :as-alias mbus]
|
||||
@@ -34,6 +35,7 @@
|
||||
[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]))
|
||||
|
||||
@@ -45,10 +47,12 @@
|
||||
|
||||
(defn- handle-response-transformation
|
||||
[response request mdata]
|
||||
(reduce (fn [response transform-fn]
|
||||
(transform-fn request response))
|
||||
response
|
||||
(::response-transform-fns mdata)))
|
||||
(let [transform-fn (reduce (fn [res-fn transform-fn]
|
||||
(fn [request response]
|
||||
(p/then (res-fn request response) #(transform-fn request %))))
|
||||
(constantly response)
|
||||
(::response-transform-fns mdata))]
|
||||
(transform-fn request response)))
|
||||
|
||||
(defn- handle-before-comple-hook
|
||||
[response mdata]
|
||||
@@ -59,18 +63,67 @@
|
||||
(defn- handle-response
|
||||
[request result]
|
||||
(if (fn? result)
|
||||
(result request)
|
||||
(p/wrap (result request))
|
||||
(let [mdata (meta result)]
|
||||
(-> {::yrs/status (::http/status mdata 200)
|
||||
::yrs/headers (::http/headers mdata {})
|
||||
::yrs/body (rph/unwrap result)}
|
||||
(handle-response-transformation request mdata)
|
||||
(handle-before-comple-hook mdata)))))
|
||||
(p/-> (yrs/response {:status (::http/status mdata 200)
|
||||
:headers (::http/headers mdata {})
|
||||
:body (rph/unwrap result)})
|
||||
(handle-response-transformation request mdata)
|
||||
(handle-before-comple-hook mdata)))))
|
||||
|
||||
(defn- rpc-handler
|
||||
(defn- rpc-query-handler
|
||||
"Ring handler that dispatches query requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods {:keys [params path-params] :as request} respond raise]
|
||||
(let [type (keyword (:type path-params))
|
||||
profile-id (or (::session/profile-id request)
|
||||
(::actoken/profile-id request))
|
||||
|
||||
data (-> params
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::http/request request))
|
||||
data (if profile-id
|
||||
(-> data
|
||||
(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
|
||||
"Ring handler that dispatches mutation requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods {:keys [params path-params] :as request} respond raise]
|
||||
(let [type (keyword (:type path-params))
|
||||
profile-id (or (::session/profile-id request)
|
||||
(::actoken/profile-id request))
|
||||
data (-> params
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::http/request request))
|
||||
data (if profile-id
|
||||
(-> data
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc ::profile-id profile-id))
|
||||
(dissoc data :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-command-handler
|
||||
"Ring handler that dispatches cmd requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods {:keys [params path-params] :as request}]
|
||||
[methods {:keys [params path-params] :as request} respond raise]
|
||||
(let [type (keyword (:type path-params))
|
||||
etag (yrq/get-header request "if-none-match")
|
||||
profile-id (or (::session/profile-id request)
|
||||
@@ -79,103 +132,148 @@
|
||||
data (-> params
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::session/id (::session/id request))
|
||||
(assoc ::http/request request)
|
||||
(assoc ::cond/key etag)
|
||||
(cond-> (uuid? profile-id)
|
||||
(assoc ::profile-id profile-id)))
|
||||
|
||||
data (vary-meta data assoc ::http/request request)
|
||||
method (get methods type default-handler)]
|
||||
method (get methods type default-handler)]
|
||||
|
||||
(binding [cond/*enabled* true]
|
||||
(let [response (method data)]
|
||||
(handle-response request response)))))
|
||||
(->> (method data)
|
||||
(p/mcat (partial handle-response request))
|
||||
(p/fnly (fn [response cause]
|
||||
(if cause
|
||||
(raise cause)
|
||||
(respond response))))))))
|
||||
|
||||
(defn- wrap-metrics
|
||||
"Wrap service method with metrics measurement."
|
||||
[{:keys [::mtx/metrics ::metrics-id]} f mdata]
|
||||
[{:keys [metrics ::metrics-id]} f mdata]
|
||||
(let [labels (into-array String [(::sv/name mdata)])]
|
||||
(fn [cfg params]
|
||||
(let [tp (dt/tpoint)]
|
||||
(try
|
||||
(f cfg params)
|
||||
(finally
|
||||
(mtx/run! metrics
|
||||
:id metrics-id
|
||||
:val (inst-ms (tp))
|
||||
:labels labels)))))))
|
||||
(->> (f cfg params)
|
||||
(p/fnly (fn [_ _]
|
||||
(mtx/run! metrics
|
||||
:id metrics-id
|
||||
:val (inst-ms (tp))
|
||||
: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)))
|
||||
(ex/raise :type :authentication
|
||||
:code :authentication-required
|
||||
:hint "authentication required for this endpoint")
|
||||
(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
|
||||
"Wraps service method into async flow, with the ability to dispatching
|
||||
it to a preconfigured executor service."
|
||||
[{:keys [::wrk/executor] :as cfg} f mdata]
|
||||
(with-meta
|
||||
(fn [cfg params]
|
||||
(->> (px/submit! executor (px/wrap-bindings #(f cfg params)))
|
||||
(p/mapcat p/wrap)
|
||||
(p/map rph/wrap)))
|
||||
mdata))
|
||||
|
||||
(defn- wrap-audit
|
||||
[_ f mdata]
|
||||
[cfg f mdata]
|
||||
(if (or (contains? cf/flags :webhooks)
|
||||
(contains? cf/flags :audit-log))
|
||||
(if-not (::audit/skip mdata)
|
||||
(fn [cfg params]
|
||||
(let [result (f cfg params)]
|
||||
(->> (audit/prepare-event cfg mdata params result)
|
||||
(audit/submit! cfg))
|
||||
result))
|
||||
f)
|
||||
(letfn [(handle-audit [params result]
|
||||
(let [resultm (meta result)
|
||||
request (::http/request params)
|
||||
|
||||
profile-id (or (::audit/profile-id resultm)
|
||||
(:profile-id result)
|
||||
(if (= (::type cfg) "command")
|
||||
(::profile-id params)
|
||||
(:profile-id params))
|
||||
uuid/zero)
|
||||
|
||||
props (-> (or (::audit/replace-props resultm)
|
||||
(-> params
|
||||
(merge (::audit/props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
(audit/clean-props))
|
||||
|
||||
event {:type (or (::audit/type resultm)
|
||||
(::type cfg))
|
||||
:name (or (::audit/name resultm)
|
||||
(::sv/name mdata))
|
||||
:profile-id profile-id
|
||||
:ip-addr (some-> request audit/parse-client-ip)
|
||||
:props props
|
||||
|
||||
;; NOTE: for batch-key lookup we need the params as-is
|
||||
;; because the rpc api does not need to know the
|
||||
;; audit/webhook specific object layout.
|
||||
::params (dissoc params ::http/request)
|
||||
|
||||
::webhooks/batch-key
|
||||
(or (::webhooks/batch-key mdata)
|
||||
(::webhooks/batch-key resultm))
|
||||
|
||||
::webhooks/batch-timeout
|
||||
(or (::webhooks/batch-timeout mdata)
|
||||
(::webhooks/batch-timeout resultm))
|
||||
|
||||
::webhooks/event?
|
||||
(or (::webhooks/event? mdata)
|
||||
(::webhooks/event? resultm)
|
||||
false)}]
|
||||
|
||||
(audit/submit! cfg event)))
|
||||
|
||||
(handle-request [cfg params]
|
||||
(->> (f cfg params)
|
||||
(p/fnly (fn [result cause]
|
||||
(when-not cause
|
||||
(handle-audit params result))))))]
|
||||
|
||||
(if-not (::audit/skip mdata)
|
||||
(with-meta handle-request mdata)
|
||||
f))
|
||||
f))
|
||||
|
||||
(defn- wrap-spec-conform
|
||||
[_ f mdata]
|
||||
;; NOTE: skip spec conform operation on rpc methods that already
|
||||
;; uses malli validation mechanism.
|
||||
(if (contains? mdata ::sm/params)
|
||||
f
|
||||
(if-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
|
||||
(fn [cfg params]
|
||||
(f cfg (us/conform spec params)))
|
||||
f)))
|
||||
|
||||
(defn- wrap-params-validation
|
||||
[_ f mdata]
|
||||
(if-let [schema (::sm/params mdata)]
|
||||
(let [schema (sm/schema schema)
|
||||
valid? (sm/validator schema)
|
||||
explain (sm/explainer schema)
|
||||
decode (sm/decoder schema sm/default-transformer)]
|
||||
|
||||
(fn [cfg params]
|
||||
(let [params (decode params)]
|
||||
(if (valid? params)
|
||||
(f cfg params)
|
||||
(ex/raise :type :validation
|
||||
:code :params-validation
|
||||
::sm/explain (explain params))))))
|
||||
f))
|
||||
|
||||
(defn- wrap-output-validation
|
||||
[_ f mdata]
|
||||
(if (contains? cf/flags :rpc-output-validation)
|
||||
(or (when-let [schema (::sm/result mdata)]
|
||||
(let [schema (sm/schema schema)
|
||||
valid? (sm/validator schema)
|
||||
explain (sm/explainer schema)]
|
||||
(fn [cfg params]
|
||||
(let [response (f cfg params)]
|
||||
(when (map? response)
|
||||
(when-not (valid? response)
|
||||
(ex/raise :type :validation
|
||||
:code :data-validation
|
||||
::sm/explain (explain response))))
|
||||
response))))
|
||||
f)
|
||||
f))
|
||||
(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)
|
||||
@@ -183,19 +281,43 @@
|
||||
(rlimit/wrap cfg $ mdata)
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-output-validation cfg $ mdata)
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
(wrap-authentication cfg $ mdata)
|
||||
(wrap-access-token cfg $ mdata)))
|
||||
|
||||
(defn- wrap
|
||||
[cfg f mdata]
|
||||
(l/debug :hint "register method" :name (::sv/name mdata))
|
||||
(let [f (wrap-all cfg f mdata)]
|
||||
(partial f cfg)))
|
||||
(with-meta #(f cfg %) mdata)))
|
||||
|
||||
(defn- process-method
|
||||
[cfg [vfn mdata]]
|
||||
[(keyword (::sv/name mdata)) [mdata (wrap cfg vfn mdata)]])
|
||||
[cfg vfn]
|
||||
(let [mdata (meta vfn)]
|
||||
[(keyword (::sv/name mdata))
|
||||
(wrap cfg vfn mdata)]))
|
||||
|
||||
(defn- resolve-query-methods
|
||||
[cfg]
|
||||
(let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)]
|
||||
(->> (sv/scan-ns
|
||||
'app.rpc.queries.projects
|
||||
'app.rpc.queries.profile
|
||||
'app.rpc.queries.viewer
|
||||
'app.rpc.queries.fonts)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
||||
(defn- resolve-mutation-methods
|
||||
[cfg]
|
||||
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
|
||||
(->> (sv/scan-ns
|
||||
'app.rpc.mutations.media
|
||||
'app.rpc.mutations.profile
|
||||
'app.rpc.mutations.projects
|
||||
'app.rpc.mutations.fonts
|
||||
'app.rpc.mutations.share-link)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
||||
(defn- resolve-command-methods
|
||||
[cfg]
|
||||
@@ -214,7 +336,6 @@
|
||||
'app.rpc.commands.files-share
|
||||
'app.rpc.commands.files-temp
|
||||
'app.rpc.commands.files-update
|
||||
'app.rpc.commands.files-thumbnails
|
||||
'app.rpc.commands.ldap
|
||||
'app.rpc.commands.management
|
||||
'app.rpc.commands.media
|
||||
@@ -245,10 +366,23 @@
|
||||
(defmethod ig/init-key ::methods
|
||||
[_ cfg]
|
||||
(let [cfg (d/without-nils cfg)]
|
||||
(resolve-command-methods cfg)))
|
||||
{:mutations (resolve-mutation-methods cfg)
|
||||
:queries (resolve-query-methods cfg)
|
||||
:commands (resolve-command-methods cfg)}))
|
||||
|
||||
(s/def ::mutations
|
||||
(s/map-of keyword? fn?))
|
||||
|
||||
(s/def ::queries
|
||||
(s/map-of keyword? fn?))
|
||||
|
||||
(s/def ::commands
|
||||
(s/map-of keyword? fn?))
|
||||
|
||||
(s/def ::methods
|
||||
(s/map-of keyword? (s/tuple map? fn?)))
|
||||
(s/keys :req-un [::mutations
|
||||
::queries
|
||||
::commands]))
|
||||
|
||||
(s/def ::routes vector?)
|
||||
|
||||
@@ -257,11 +391,15 @@
|
||||
::db/pool
|
||||
::main/props
|
||||
::wrk/executor
|
||||
::session/manager]))
|
||||
::session/manager
|
||||
::actoken/manager]))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::methods] :as cfg}]
|
||||
(let [methods (update-vals methods peek)]
|
||||
[["/rpc" {:middleware [[session/authz cfg]
|
||||
[actoken/authz cfg]]}
|
||||
["/command/:type" {:handler (partial rpc-handler methods)}]]]))
|
||||
[["/rpc" {:middleware [[session/authz cfg]
|
||||
[actoken/authz cfg]]}
|
||||
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
|
||||
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
|
||||
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
|
||||
:allowed-methods #{:post}}]]])
|
||||
|
||||
|
||||
@@ -6,16 +6,14 @@
|
||||
|
||||
(ns app.rpc.climit
|
||||
"Concurrencly limiter for RPC."
|
||||
(:refer-clojure :exclude [run!])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit.config :as-alias config]
|
||||
[app.util.cache :as cache]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
@@ -25,200 +23,184 @@
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.bulkhead :as pbh])
|
||||
[promesa.exec.bulkhead :as pxb])
|
||||
(:import
|
||||
clojure.lang.ExceptionInfo))
|
||||
com.github.benmanes.caffeine.cache.Cache
|
||||
com.github.benmanes.caffeine.cache.CacheLoader
|
||||
com.github.benmanes.caffeine.cache.Caffeine
|
||||
com.github.benmanes.caffeine.cache.RemovalListener))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
(defn- capacity-exception?
|
||||
[o]
|
||||
(and (ex/error? o)
|
||||
(let [data (ex-data o)]
|
||||
(and (= :bulkhead-error (:type data))
|
||||
(= :capacity-limit-reached (:code data))))))
|
||||
|
||||
(defn- create-bulkhead-cache
|
||||
[{:keys [::wrk/executor]} config]
|
||||
(letfn [(load-fn [key]
|
||||
(let [config (get config (nth key 0))]
|
||||
(l/trace :hint "insert into cache" :key key)
|
||||
(pbh/create :permits (or (:permits config) (:concurrency config))
|
||||
:queue (or (:queue config) (:queue-size config))
|
||||
:timeout (:timeout config)
|
||||
:executor executor
|
||||
:type (:type config :semaphore))))
|
||||
(defn invoke!
|
||||
[limiter f]
|
||||
(->> (px/submit! limiter f)
|
||||
(p/hcat (fn [result cause]
|
||||
(cond
|
||||
(capacity-exception? cause)
|
||||
(p/rejected
|
||||
(ex/error :type :internal
|
||||
:code :concurrency-limit-reached
|
||||
:queue (-> limiter meta ::bkey name)
|
||||
:cause cause))
|
||||
|
||||
(on-remove [_ _ cause]
|
||||
(l/trace :hint "evict from cache" :key key :reason (str cause)))]
|
||||
(some? cause)
|
||||
(p/rejected cause)
|
||||
|
||||
(cache/create :executor :same-thread
|
||||
:on-remove on-remove
|
||||
:keepalive "5m"
|
||||
:load-fn load-fn)))
|
||||
:else
|
||||
(p/resolved result))))))
|
||||
|
||||
(s/def ::config/permits ::us/integer)
|
||||
(s/def ::config/queue ::us/integer)
|
||||
(s/def ::config/timeout ::us/integer)
|
||||
(defn- create-limiter
|
||||
[{:keys [::wrk/executor ::mtx/metrics ::bkey ::skey concurrency queue-size]}]
|
||||
(let [labels (into-array String [(name bkey)])
|
||||
on-queue (fn [instance]
|
||||
(l/trace :hint "enqueued"
|
||||
:key (name bkey)
|
||||
:skey (str skey)
|
||||
:queue-size (get instance ::pxb/current-queue-size)
|
||||
:concurrency (get instance ::pxb/current-concurrency))
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-queue-size
|
||||
:val (get instance ::pxb/current-queue-size)
|
||||
:labels labels)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-concurrency
|
||||
:val (get instance ::pxb/current-concurrency)
|
||||
:labels labels))
|
||||
|
||||
on-run (fn [instance task]
|
||||
(let [elapsed (- (inst-ms (dt/now))
|
||||
(inst-ms task))]
|
||||
(l/trace :hint "execute"
|
||||
:key (name bkey)
|
||||
:skey (str skey)
|
||||
:elapsed (str elapsed "ms"))
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-timing
|
||||
:val elapsed
|
||||
:labels labels)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-queue-size
|
||||
:val (get instance ::pxb/current-queue-size)
|
||||
:labels labels)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-concurrency
|
||||
:val (get instance ::pxb/current-concurrency)
|
||||
:labels labels)))
|
||||
|
||||
options {:executor executor
|
||||
:concurrency concurrency
|
||||
:queue-size (or queue-size Integer/MAX_VALUE)
|
||||
:on-queue on-queue
|
||||
:on-run on-run}]
|
||||
|
||||
(-> (pxb/create options)
|
||||
(vary-meta assoc ::bkey bkey ::skey skey))))
|
||||
|
||||
(defn- create-cache
|
||||
[{:keys [::wrk/executor] :as params} config]
|
||||
(let [listener (reify RemovalListener
|
||||
(onRemoval [_ key _val cause]
|
||||
(l/trace :hint "cache: remove" :key key :reason (str cause))))
|
||||
|
||||
loader (reify CacheLoader
|
||||
(load [_ key]
|
||||
(let [[bkey skey] key]
|
||||
(when-let [config (get config bkey)]
|
||||
(-> (merge params config)
|
||||
(assoc ::bkey bkey)
|
||||
(assoc ::skey skey)
|
||||
(create-limiter))))))]
|
||||
|
||||
(.. (Caffeine/newBuilder)
|
||||
(weakValues)
|
||||
(executor executor)
|
||||
(removalListener listener)
|
||||
(build loader))))
|
||||
|
||||
(defprotocol IConcurrencyManager)
|
||||
|
||||
(s/def ::concurrency ::us/integer)
|
||||
(s/def ::queue-size ::us/integer)
|
||||
(s/def ::config
|
||||
(s/map-of keyword?
|
||||
(s/keys :opt-un [::config/permits
|
||||
::config/queue
|
||||
::config/timeout])))
|
||||
(s/keys :req-un [::concurrency]
|
||||
:opt-un [::queue-size])))
|
||||
|
||||
(defmethod ig/prep-key ::rpc/climit
|
||||
[_ cfg]
|
||||
(assoc cfg ::path (cf/get :rpc-climit-config)))
|
||||
(merge {::path (cf/get :rpc-climit-config)}
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(s/def ::path ::fs/path)
|
||||
|
||||
(defmethod ig/pre-init-spec ::rpc/climit [_]
|
||||
(s/keys :req [::wrk/executor ::mtx/metrics ::path]))
|
||||
|
||||
(defmethod ig/init-key ::rpc/climit
|
||||
[_ {:keys [::path ::mtx/metrics ::wrk/executor] :as cfg}]
|
||||
[_ {:keys [::path] :as params}]
|
||||
(when (contains? cf/flags :rpc-climit)
|
||||
(when-let [params (some->> path slurp edn/read-string)]
|
||||
(l/info :hint "initializing concurrency limit" :config (str path))
|
||||
(us/verify! ::config params)
|
||||
{::cache (create-bulkhead-cache cfg params)
|
||||
::config params
|
||||
::wrk/executor executor
|
||||
::mtx/metrics metrics})))
|
||||
(if-let [config (some->> path slurp edn/read-string)]
|
||||
(do
|
||||
(l/info :hint "initializing concurrency limit" :config (str path))
|
||||
(us/verify! ::config config)
|
||||
|
||||
(let [cache (create-cache params config)]
|
||||
^{::cache cache}
|
||||
(reify
|
||||
IConcurrencyManager
|
||||
clojure.lang.IDeref
|
||||
(deref [_] config)
|
||||
|
||||
clojure.lang.ILookup
|
||||
(valAt [_ key]
|
||||
(let [key (if (vector? key) key [key])]
|
||||
(.get ^Cache cache key))))))
|
||||
|
||||
(l/warn :hint "unable to load configuration" :config (str path)))))
|
||||
|
||||
(s/def ::cache cache/cache?)
|
||||
(s/def ::instance
|
||||
(s/keys :req [::cache ::config ::wrk/executor]))
|
||||
|
||||
(s/def ::rpc/climit
|
||||
(s/nilable ::instance))
|
||||
(s/nilable #(satisfies? IConcurrencyManager %)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn invoke!
|
||||
[cache metrics id key f]
|
||||
(let [limiter (cache/get cache [id key])
|
||||
tpoint (dt/tpoint)
|
||||
labels (into-array String [(name id)])
|
||||
|
||||
wrapped
|
||||
(fn []
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(l/trace :hint "executed"
|
||||
:id (name id)
|
||||
:key key
|
||||
:fnh (hash f)
|
||||
:permits (:permits stats)
|
||||
:queue (:queue stats)
|
||||
:max-permits (:max-permits stats)
|
||||
:max-queue (:max-queue stats)
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-timing
|
||||
:val (inst-ms elapsed)
|
||||
:labels labels)
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
(let [elapsed (tpoint)]
|
||||
(l/trace :hint "finished"
|
||||
:id (name id)
|
||||
:key key
|
||||
:fnh (hash f)
|
||||
:permits (:permits stats)
|
||||
:queue (:queue stats)
|
||||
:max-permits (:max-permits stats)
|
||||
:max-queue (:max-queue stats)
|
||||
:elapsed (dt/format-duration elapsed)))))))
|
||||
measure!
|
||||
(fn [stats]
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-queue
|
||||
:val (:queue stats)
|
||||
:labels labels)
|
||||
(mtx/run! metrics
|
||||
:id :rpc-climit-permits
|
||||
:val (:permits stats)
|
||||
:labels labels))]
|
||||
|
||||
(try
|
||||
(let [stats (pbh/get-stats limiter)]
|
||||
(measure! stats)
|
||||
(l/trace :hint "enqueued"
|
||||
:id (name id)
|
||||
:key key
|
||||
:fnh (hash f)
|
||||
:permits (:permits stats)
|
||||
:queue (:queue stats)
|
||||
:max-permits (:max-permits stats)
|
||||
:max-queue (:max-queue stats))
|
||||
(pbh/invoke! limiter wrapped))
|
||||
(catch ExceptionInfo cause
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(if (= :bulkhead-error type)
|
||||
(ex/raise :type :concurrency-limit
|
||||
:code code
|
||||
:hint "concurrency limit reached")
|
||||
(throw cause))))
|
||||
|
||||
(finally
|
||||
(measure! (pbh/get-stats limiter))))))
|
||||
|
||||
|
||||
(defn run!
|
||||
[{:keys [::id ::cache ::mtx/metrics]} f]
|
||||
(if (and cache id)
|
||||
(invoke! cache metrics id nil f)
|
||||
(f)))
|
||||
|
||||
(defn submit!
|
||||
[{:keys [::id ::cache ::wrk/executor ::mtx/metrics]} f]
|
||||
(let [f (partial px/submit! executor (px/wrap-bindings f))]
|
||||
(if (and cache id)
|
||||
(p/await! (invoke! cache metrics id nil f))
|
||||
(p/await! (f)))))
|
||||
|
||||
(defn configure
|
||||
([{:keys [::rpc/climit]} id]
|
||||
(us/assert! ::rpc/climit climit)
|
||||
(assoc climit ::id id))
|
||||
([{:keys [::rpc/climit]} id executor]
|
||||
(us/assert! ::rpc/climit climit)
|
||||
(-> climit
|
||||
(assoc ::id id)
|
||||
(assoc ::wrk/executor executor))))
|
||||
|
||||
(defmacro with-dispatch!
|
||||
"Dispatch blocking operation to a separated thread protected with the
|
||||
specified concurrency limiter. If climit is not active, the function
|
||||
will be scheduled to execute without concurrency monitoring."
|
||||
[instance & body]
|
||||
(if (vector? instance)
|
||||
`(-> (app.rpc.climit/configure ~@instance)
|
||||
(app.rpc.climit/run! (^:once fn* [] ~@body)))
|
||||
`(run! ~instance (^:once fn* [] ~@body))))
|
||||
|
||||
(defmacro with-dispatch
|
||||
"Dispatch blocking operation to a separated thread protected with
|
||||
the specified semaphore.
|
||||
DEPRECATED"
|
||||
[& params]
|
||||
`(with-dispatch! ~@params))
|
||||
|
||||
(def noop-fn (constantly nil))
|
||||
[lim & body]
|
||||
`(if ~lim
|
||||
(invoke! ~lim (^:once fn [] (p/wrap (do ~@body))))
|
||||
(p/wrap (do ~@body))))
|
||||
|
||||
(defn wrap
|
||||
[{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
|
||||
(if (and (some? climit) (some? id))
|
||||
(if-let [config (get-in climit [::config id])]
|
||||
(let [cache (::cache climit)]
|
||||
(l/debug :hint "wrap: instrumenting method"
|
||||
:limit (name id)
|
||||
:service-name (::sv/name mdata)
|
||||
:timeout (:timeout config)
|
||||
:permits (:permits config)
|
||||
:queue (:queue config)
|
||||
:keyed? (some? key-fn))
|
||||
(fn [cfg params]
|
||||
(invoke! cache metrics id (key-fn params) (partial f cfg params))))
|
||||
|
||||
[{:keys [::rpc/climit]} f {:keys [::queue ::key-fn] :as mdata}]
|
||||
(if (and (some? climit)
|
||||
(some? queue))
|
||||
(if-let [config (get @climit queue)]
|
||||
(do
|
||||
(l/warn :hint "no config found for specified queue" :id id)
|
||||
(l/debug :hint "wrap: instrumenting method"
|
||||
:limit-name (name queue)
|
||||
:service-name (::sv/name mdata)
|
||||
:queue-size (or (:queue-size config) Integer/MAX_VALUE)
|
||||
:concurrency (:concurrency config)
|
||||
:keyed? (some? key-fn))
|
||||
(if (some? key-fn)
|
||||
(fn [cfg params]
|
||||
(let [key [queue (key-fn params)]
|
||||
lim (get climit key)]
|
||||
(invoke! lim (partial f cfg params))))
|
||||
(let [lim (get climit queue)]
|
||||
(fn [cfg params]
|
||||
(invoke! lim (partial f cfg params))))))
|
||||
(do
|
||||
(l/warn :hint "wrap: no config found"
|
||||
:queue (name queue)
|
||||
:service (::sv/name mdata))
|
||||
f))
|
||||
|
||||
f))
|
||||
|
||||
@@ -19,19 +19,18 @@
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(defn- decode-row
|
||||
[row]
|
||||
(dissoc row :perms))
|
||||
[{:keys [perms] :as row}]
|
||||
(cond-> row
|
||||
(db/pgarray? perms "text")
|
||||
(assoc :perms (db/decode-pgarray perms #{}))))
|
||||
|
||||
(defn create-access-token
|
||||
[{:keys [::db/conn ::main/props]} profile-id name expiration]
|
||||
(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})
|
||||
|
||||
expires-at (some-> expiration dt/in-future)]
|
||||
|
||||
:iat created-at})]
|
||||
(db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
@@ -39,36 +38,33 @@
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})))
|
||||
|
||||
:perms (db/create-array conn "text" perms)})))
|
||||
|
||||
(defn repl-create-access-token
|
||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||
[{:keys [::db/pool] :as system} profile-id name perms]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [props (:app.setup/props system)]
|
||||
(create-access-token {::db/conn conn ::main/props props}
|
||||
(create-access-token {::conn conn ::main/props props}
|
||||
profile-id
|
||||
name
|
||||
expiration))))
|
||||
perms))))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::expiration ::dt/duration)
|
||||
(s/def ::perms ::us/set-of-strings)
|
||||
|
||||
(s/def ::create-access-token
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::name]
|
||||
:opt-un [::expiration]))
|
||||
:req-un [::name ::perms]))
|
||||
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name perms]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::db/conn conn)]
|
||||
(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 expiration)
|
||||
(-> (create-access-token cfg profile-id name perms)
|
||||
(decode-row)))))
|
||||
|
||||
(s/def ::delete-access-token
|
||||
@@ -87,8 +83,5 @@
|
||||
(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}
|
||||
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||
:columns [:id :name :perms :created-at :updated-at :expires-at]})
|
||||
(->> (db/query pool :access-token {:profile-id profile-id})
|
||||
(mapv decode-row)))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -19,7 +19,12 @@
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.services :as sv]))
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(defn- event->row [event]
|
||||
[(uuid/next)
|
||||
@@ -37,9 +42,8 @@
|
||||
:profile-id :ip-addr :props :context])
|
||||
|
||||
(defn- handle-events
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
|
||||
(let [request (-> params meta ::http/request)
|
||||
ip-addr (audit/parse-client-ip request)
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request]}]
|
||||
(let [ip-addr (audit/parse-client-ip request)
|
||||
xform (comp
|
||||
(map #(assoc % :profile-id profile-id))
|
||||
(map #(assoc % :ip-addr ip-addr))
|
||||
@@ -50,38 +54,34 @@
|
||||
(when (seq events)
|
||||
(db/insert-multi! pool :audit-log event-columns events))))
|
||||
|
||||
(def schema:event
|
||||
[:map {:title "Event"}
|
||||
[:name [:string {:max 250}]]
|
||||
[:type [:string {:max 250}]]
|
||||
[:props
|
||||
[:map-of :keyword :any]]
|
||||
[:context {:optional true}
|
||||
[:map-of :keyword :any]]])
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::type ::us/string)
|
||||
(s/def ::props (s/map-of ::us/keyword any?))
|
||||
(s/def ::timestamp dt/instant?)
|
||||
(s/def ::context (s/map-of ::us/keyword any?))
|
||||
|
||||
(def schema:push-audit-events
|
||||
[:map {:title "push-audit-events"}
|
||||
[:events [:vector schema:event]]])
|
||||
(s/def ::event
|
||||
(s/keys :req-un [::type ::name ::props ::timestamp]
|
||||
:opt-un [::context]))
|
||||
|
||||
(s/def ::events (s/every ::event))
|
||||
|
||||
(s/def ::push-audit-events
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::events]))
|
||||
|
||||
(sv/defmethod ::push-audit-events
|
||||
{::climit/id :submit-audit-events-by-profile
|
||||
{::climit/queue :push-audit-events
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::sm/params schema:push-audit-events
|
||||
::audit/skip true
|
||||
::doc/skip true
|
||||
::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} params]
|
||||
(if (or (db/read-only? pool)
|
||||
(not (contains? cf/flags :audit-log)))
|
||||
(do
|
||||
(l/warn :hint "audit: http handler disabled or db is read-only")
|
||||
(rph/wrap nil))
|
||||
|
||||
(do
|
||||
(try
|
||||
(handle-events cfg params)
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error on persisting audit events from frontend"
|
||||
:cause cause)))
|
||||
(->> (px/submit! executor #(handle-events cfg params))
|
||||
(p/fmap (constantly nil)))))
|
||||
|
||||
(rph/wrap nil))))
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -20,6 +18,7 @@
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@@ -27,13 +26,31 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def schema:password
|
||||
[::sm/word-string {:max 500}])
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::lang ::us/string)
|
||||
(s/def ::path ::us/string)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::old-password ::us/not-empty-string)
|
||||
(s/def ::theme ::us/string)
|
||||
(s/def ::invitation-token ::us/not-empty-string)
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
|
||||
(def schema:token
|
||||
[::sm/word-string {:max 6000}])
|
||||
;; ---- HELPERS
|
||||
|
||||
(defn email-domain-in-whitelist?
|
||||
"Returns true if email's domain is in the given whitelist or if
|
||||
given whitelist is an empty string."
|
||||
[domains email]
|
||||
(if (or (empty? domains)
|
||||
(nil? domains))
|
||||
true
|
||||
(let [[_ candidate] (-> (str/lower email)
|
||||
(str/split #"@" 2))]
|
||||
(contains? domains candidate))))
|
||||
|
||||
;; ---- COMMAND: login with password
|
||||
|
||||
@@ -46,18 +63,14 @@
|
||||
:code :login-disabled
|
||||
:hint "login is disabled in this instance"))
|
||||
|
||||
(letfn [(check-password [conn profile password]
|
||||
(if (= (:password profile) "!")
|
||||
(letfn [(check-password [profile password]
|
||||
(when (= (:password profile) "!")
|
||||
(ex/raise :type :validation
|
||||
:code :account-without-password
|
||||
:hint "the current account does not have password")
|
||||
(let [result (profile/verify-password cfg password (:password profile))]
|
||||
(when (:update result)
|
||||
(l/trace :hint "updating profile password" :id (:id profile) :email (:email profile))
|
||||
(profile/update-profile-password! conn (assoc profile :password password)))
|
||||
(:valid result))))
|
||||
:hint "the current account does not have password"))
|
||||
(:valid (auth/verify-password password (:password profile))))
|
||||
|
||||
(validate-profile [conn profile]
|
||||
(validate-profile [profile]
|
||||
(when-not profile
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
@@ -67,7 +80,7 @@
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
(when-not (check-password conn profile password)
|
||||
(when-not (check-password profile password)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
(when-let [deleted-at (:deleted-at profile)]
|
||||
@@ -79,7 +92,8 @@
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (->> (profile/get-profile-by-email conn email)
|
||||
(validate-profile conn)
|
||||
(validate-profile)
|
||||
(profile/decode-row)
|
||||
(profile/strip-private-attrs))
|
||||
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
@@ -97,22 +111,23 @@
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
|
||||
(def schema:login-with-password
|
||||
[:map {:title "login-with-password"}
|
||||
[:email ::sm/email]
|
||||
[:password schema:password]
|
||||
[:invitation-token {:optional true} schema:token]])
|
||||
(s/def ::login-with-password
|
||||
(s/keys :req-un [::email ::password]
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(sv/defmethod ::login-with-password
|
||||
"Performs authentication using penpot password."
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:login-with-password}
|
||||
::climit/queue :auth
|
||||
::doc/added "1.15"}
|
||||
[cfg params]
|
||||
(login-with-password cfg params))
|
||||
|
||||
;; ---- COMMAND: Logout
|
||||
|
||||
(s/def ::logout
|
||||
(s/keys :opt [::rpc/profile-id]))
|
||||
|
||||
(sv/defmethod ::logout
|
||||
"Clears the authentication cookie and logout the current session."
|
||||
{::rpc/auth false
|
||||
@@ -129,7 +144,7 @@
|
||||
(:profile-id tdata)))
|
||||
|
||||
(update-password [conn profile-id]
|
||||
(let [pwd (profile/derive-password cfg password)]
|
||||
(let [pwd (auth/derive-password password)]
|
||||
(db/update! conn :profile {:password pwd} {:id profile-id})))]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
@@ -137,15 +152,14 @@
|
||||
(update-password conn))
|
||||
nil)))
|
||||
|
||||
(def schema:recover-profile
|
||||
[:map {:title "recover-profile"}
|
||||
[:token schema:token]
|
||||
[:password schema:password]])
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
(s/def ::recover-profile
|
||||
(s/keys :req-un [::token ::password]))
|
||||
|
||||
(sv/defmethod ::recover-profile
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:recover-profile}
|
||||
::climit/queue :auth
|
||||
::doc/added "1.15"}
|
||||
[cfg params]
|
||||
(recover-profile cfg params))
|
||||
|
||||
@@ -155,20 +169,19 @@
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
|
||||
(when-not (contains? cf/flags :registration)
|
||||
(when-not (contains? params :invitation-token)
|
||||
(if-not (contains? params :invitation-token)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled)))
|
||||
:code :registration-disabled)
|
||||
(let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
|
||||
(when-not (= (:email params) (:member-email invitation))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-does-not-match-invitation
|
||||
:hint "email should match the invitation")))))
|
||||
|
||||
(when (contains? params :invitation-token)
|
||||
(let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
|
||||
(when-not (= (:email params) (:member-email invitation))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-does-not-match-invitation
|
||||
:hint "email should match the invitation"))))
|
||||
|
||||
(when-not (auth/email-domain-in-whitelist? (:email params))
|
||||
(ex/raise :type :validation
|
||||
:code :email-domain-is-not-allowed))
|
||||
(when-let [domains (cf/get :registration-domain-whitelist)]
|
||||
(when-not (email-domain-in-whitelist? domains (:email params))
|
||||
(ex/raise :type :validation
|
||||
:code :email-domain-is-not-allowed)))
|
||||
|
||||
;; Don't allow proceed in preparing registration if the profile is
|
||||
;; already reported as spammer.
|
||||
@@ -226,16 +239,13 @@
|
||||
(with-meta {:token token}
|
||||
{::audit/profile-id uuid/zero})))
|
||||
|
||||
(def schema:prepare-register-profile
|
||||
[:map {:title "prepare-register-profile"}
|
||||
[:email ::sm/email]
|
||||
[:password schema:password]
|
||||
[:invitation-token {:optional true} schema:token]])
|
||||
(s/def ::prepare-register-profile
|
||||
(s/keys :req-un [::email ::password]
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(sv/defmethod ::prepare-register-profile
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:prepare-register-profile}
|
||||
::doc/added "1.15"}
|
||||
[cfg params]
|
||||
(prepare-register cfg params))
|
||||
|
||||
@@ -245,7 +255,7 @@
|
||||
"Create the profile entry on the database with limited set of input
|
||||
attrs (all the other attrs are filled with default values)."
|
||||
[conn {:keys [email] :as params}]
|
||||
(dm/assert! ::sm/email email)
|
||||
(us/assert! ::us/email email)
|
||||
(let [id (or (:id params) (uuid/next))
|
||||
props (-> (audit/extract-utm-params params)
|
||||
(merge (:props params))
|
||||
@@ -254,7 +264,9 @@
|
||||
:nudge {:big 10 :small 1}})
|
||||
(db/tjson))
|
||||
|
||||
password (or (:password params) "!")
|
||||
password (if-let [password (:password params)]
|
||||
(auth/derive-password password)
|
||||
"!")
|
||||
|
||||
locale (:locale params)
|
||||
locale (when (and (string? locale) (not (str/blank? locale)))
|
||||
@@ -323,20 +335,17 @@
|
||||
:extra-data ptoken})))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token] :as params}]
|
||||
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
|
||||
params (assoc claims :fullname fullname)
|
||||
params (merge params claims)
|
||||
|
||||
is-active (or (:is-active params)
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
|
||||
profile (if-let [profile-id (:profile-id claims)]
|
||||
(profile/get-profile conn profile-id)
|
||||
(let [params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password #(profile/derive-password cfg %)))]
|
||||
(->> (create-profile! conn params)
|
||||
(create-profile-rels! conn))))
|
||||
(->> (create-profile! conn (assoc params :is-active is-active))
|
||||
(create-profile-rels! conn)))
|
||||
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
|
||||
@@ -347,9 +356,9 @@
|
||||
(when-let [id (:profile-id claims)]
|
||||
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
|
||||
(audit/submit! cfg
|
||||
{::audit/type "fact"
|
||||
::audit/name "register-profile-retry"
|
||||
::audit/profile-id id}))
|
||||
{:type "fact"
|
||||
:name "register-profile-retry"
|
||||
:profile-id id}))
|
||||
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
@@ -392,16 +401,13 @@
|
||||
{::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})))))
|
||||
|
||||
|
||||
(def schema:register-profile
|
||||
[:map {:title "register-profile"}
|
||||
[:token schema:token]
|
||||
[:fullname [::sm/word-string {:max 100}]]])
|
||||
(s/def ::register-profile
|
||||
(s/keys :req-un [::token ::fullname]))
|
||||
|
||||
(sv/defmethod ::register-profile
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:register-profile}
|
||||
::climit/queue :auth
|
||||
::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(-> (assoc cfg ::db/conn conn)
|
||||
@@ -453,15 +459,12 @@
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn))))))
|
||||
|
||||
|
||||
(def schema:request-profile-recovery
|
||||
[:map {:title "request-profile-recovery"}
|
||||
[:email ::sm/email]])
|
||||
(s/def ::request-profile-recovery
|
||||
(s/keys :req-un [::email]))
|
||||
|
||||
(sv/defmethod ::request-profile-recovery
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:request-profile-recovery}
|
||||
::doc/added "1.15"}
|
||||
[cfg params]
|
||||
(request-profile-recovery cfg params))
|
||||
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
(:refer-clojure :exclude [assert])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.features :as ffeat]
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.spec :as us]
|
||||
@@ -30,6 +28,7 @@
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.tasks.file-gc]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.fressian :as fres]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
@@ -46,7 +45,8 @@
|
||||
java.io.DataInputStream
|
||||
java.io.DataOutputStream
|
||||
java.io.InputStream
|
||||
java.io.OutputStream))
|
||||
java.io.OutputStream
|
||||
java.lang.AutoCloseable))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
@@ -294,40 +294,28 @@
|
||||
[output & {:keys [level] :or {level 0}}]
|
||||
(ZstdOutputStream. ^OutputStream output (int level)))
|
||||
|
||||
(defn- get-files
|
||||
[cfg ids]
|
||||
(letfn [(get-files* [{:keys [::db/conn]}]
|
||||
(let [sql (str "SELECT id FROM file "
|
||||
" WHERE id = ANY(?) ")
|
||||
ids (db/create-array conn "uuid" ids)]
|
||||
(->> (db/exec! conn [sql ids])
|
||||
(into [] (map :id))
|
||||
(not-empty))))]
|
||||
(defn- retrieve-file
|
||||
[pool file-id]
|
||||
(with-open [^AutoCloseable conn (db/open pool)]
|
||||
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
|
||||
(some-> (db/get* conn :file {:id file-id})
|
||||
(files/decode-row)
|
||||
(update :data files/process-pointers deref)))))
|
||||
|
||||
(db/run! cfg get-files*)))
|
||||
(def ^:private sql:file-media-objects
|
||||
"SELECT * FROM file_media_object WHERE id = ANY(?)")
|
||||
|
||||
(defn- get-file
|
||||
[cfg file-id]
|
||||
(letfn [(get-file* [{:keys [::db/conn]}]
|
||||
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
|
||||
(some-> (db/get* conn :file {:id file-id} {::db/remove-deleted? false})
|
||||
(files/decode-row)
|
||||
(files/process-pointers deref))))]
|
||||
|
||||
(db/run! cfg get-file*)))
|
||||
|
||||
(defn- get-file-media
|
||||
[{:keys [::db/pool]} {:keys [data id] :as file}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(defn- retrieve-file-media
|
||||
[pool {:keys [data id] :as file}]
|
||||
(with-open [^AutoCloseable conn (db/open pool)]
|
||||
(let [ids (app.tasks.file-gc/collect-used-media data)
|
||||
ids (db/create-array conn "uuid" ids)
|
||||
sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")]
|
||||
ids (db/create-array conn "uuid" ids)]
|
||||
|
||||
;; We assoc the file-id again to the file-media-object row
|
||||
;; because there are cases that used objects refer to other
|
||||
;; files and we need to ensure in the exportation process that
|
||||
;; all ids matches
|
||||
(->> (db/exec! conn [sql ids])
|
||||
(->> (db/exec! conn [sql:file-media-objects ids])
|
||||
(mapv #(assoc % :file-id id))))))
|
||||
|
||||
(def ^:private storage-object-id-xf
|
||||
@@ -337,32 +325,35 @@
|
||||
|
||||
(def ^:private sql:file-libraries
|
||||
"WITH RECURSIVE libs AS (
|
||||
SELECT fl.id
|
||||
SELECT fl.id, fl.deleted_at
|
||||
FROM file AS fl
|
||||
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||
WHERE flr.file_id = ANY(?)
|
||||
UNION
|
||||
SELECT fl.id
|
||||
SELECT fl.id, fl.deleted_at
|
||||
FROM file AS fl
|
||||
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||
JOIN libs AS l ON (flr.file_id = l.id)
|
||||
)
|
||||
SELECT DISTINCT l.id
|
||||
FROM libs AS l")
|
||||
FROM libs AS l
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
|
||||
(defn- get-libraries
|
||||
[{:keys [::db/pool]} ids]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(defn- retrieve-libraries
|
||||
[pool ids]
|
||||
(with-open [^AutoCloseable conn (db/open pool)]
|
||||
(let [ids (db/create-array conn "uuid" ids)]
|
||||
(map :id (db/exec! pool [sql:file-libraries ids])))))
|
||||
|
||||
(defn- get-library-relations
|
||||
[cfg ids]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids (db/create-array conn "uuid" ids)
|
||||
sql (str "SELECT flr.* FROM file_library_rel AS flr "
|
||||
" WHERE flr.file_id = ANY(?)")]
|
||||
(db/exec! conn [sql ids])))))
|
||||
(def ^:private sql:file-library-rels
|
||||
"SELECT * FROM file_library_rel
|
||||
WHERE file_id = ANY(?)")
|
||||
|
||||
(defn- retrieve-library-relations
|
||||
[pool ids]
|
||||
(with-open [^AutoCloseable conn (db/open pool)]
|
||||
(db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)])))
|
||||
|
||||
|
||||
(defn- create-or-update-file
|
||||
[conn params]
|
||||
@@ -388,7 +379,7 @@
|
||||
;; --- EXPORT WRITER
|
||||
|
||||
(defn- embed-file-assets
|
||||
[data cfg file-id]
|
||||
[data conn file-id]
|
||||
(letfn [(walk-map-form [form state]
|
||||
(cond
|
||||
(uuid? (:fill-color-ref-file form))
|
||||
@@ -418,7 +409,7 @@
|
||||
;; NOTE: there is a possibility that shape refers to an
|
||||
;; non-existant file because the file was removed. In this
|
||||
;; case we just ignore the asset.
|
||||
(if-let [lib (get-file cfg lib-id)]
|
||||
(if-let [lib (retrieve-file conn lib-id)]
|
||||
(reduce (partial process-asset lib) data items)
|
||||
data))
|
||||
|
||||
@@ -486,39 +477,31 @@
|
||||
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
|
||||
|
||||
(defmethod write-section :v1/metadata
|
||||
[{:keys [::output ::file-ids ::include-libraries?] :as cfg}]
|
||||
(if-let [fids (get-files cfg file-ids)]
|
||||
(let [lids (when include-libraries?
|
||||
(get-libraries cfg file-ids))
|
||||
ids (into fids lids)]
|
||||
(write-obj! output {:version cf/version :files ids})
|
||||
(vswap! *state* assoc :files ids))
|
||||
(ex/raise :type :not-found
|
||||
:code :files-not-found
|
||||
:hint "unable to retrieve files for export")))
|
||||
[{:keys [::db/pool ::output ::file-ids ::include-libraries?]}]
|
||||
(let [libs (when include-libraries?
|
||||
(retrieve-libraries pool file-ids))
|
||||
files (into file-ids libs)]
|
||||
(write-obj! output {:version cf/version :files files})
|
||||
(vswap! *state* assoc :files files)))
|
||||
|
||||
(defmethod write-section :v1/files
|
||||
[{:keys [::output ::embed-assets?] :as cfg}]
|
||||
[{:keys [::db/pool ::output ::embed-assets?]}]
|
||||
|
||||
;; Initialize SIDS with empty vector
|
||||
(vswap! *state* assoc :sids [])
|
||||
|
||||
(doseq [file-id (-> *state* deref :files)]
|
||||
(let [file (cond-> (get-file cfg file-id)
|
||||
(let [file (cond-> (retrieve-file pool file-id)
|
||||
embed-assets?
|
||||
(update :data embed-file-assets cfg file-id))
|
||||
(update :data embed-file-assets pool file-id))
|
||||
|
||||
media (get-file-media cfg file)]
|
||||
media (retrieve-file-media pool file)]
|
||||
|
||||
(l/debug :hint "write penpot file"
|
||||
:id file-id
|
||||
:name (:name file)
|
||||
:media (count media)
|
||||
::l/sync? true)
|
||||
|
||||
(doseq [item media]
|
||||
(l/debug :hint "write penpot file media object" :id (:id item) ::l/sync? true))
|
||||
|
||||
(doto output
|
||||
(write-obj! file)
|
||||
(write-obj! media))
|
||||
@@ -526,10 +509,9 @@
|
||||
(vswap! *state* update :sids into storage-object-id-xf media))))
|
||||
|
||||
(defmethod write-section :v1/rels
|
||||
[{:keys [::output ::include-libraries?] :as cfg}]
|
||||
(let [ids (-> *state* deref :files)
|
||||
rels (when include-libraries?
|
||||
(get-library-relations cfg ids))]
|
||||
[{:keys [::db/pool ::output ::include-libraries?]}]
|
||||
(let [rels (when include-libraries?
|
||||
(retrieve-library-relations pool (-> *state* deref :files)))]
|
||||
(l/debug :hint "found rels" :total (count rels) ::l/sync? true)
|
||||
(write-obj! output rels)))
|
||||
|
||||
@@ -537,7 +519,6 @@
|
||||
[{:keys [::sto/storage ::output]}]
|
||||
(let [sids (-> *state* deref :sids)
|
||||
storage (media/configure-assets-storage storage)]
|
||||
|
||||
(l/debug :hint "found sobjects"
|
||||
:items (count sids)
|
||||
::l/sync? true)
|
||||
@@ -546,13 +527,13 @@
|
||||
(write-obj! output 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/sync? true)
|
||||
(doto output
|
||||
(write-uuid! id)
|
||||
(write-obj! (meta obj)))
|
||||
|
||||
(with-open [^InputStream stream (sto/get-object-data storage obj)]
|
||||
(with-open [^InputStream stream @(sto/get-object-data storage obj)]
|
||||
(let [written (write-stream! output stream size)]
|
||||
(when (not= written size)
|
||||
(ex/raise :type :validation
|
||||
@@ -612,7 +593,7 @@
|
||||
(let [options (-> options
|
||||
(assoc ::section section)
|
||||
(assoc ::input input)
|
||||
(assoc ::db/conn conn))]
|
||||
(assoc :conn conn))]
|
||||
(binding [*options* options]
|
||||
(read-section options))))
|
||||
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
|
||||
@@ -636,22 +617,20 @@
|
||||
(-> data
|
||||
(update :pages-index update-vals #(update % :objects omap-wrap))
|
||||
(update :pages-index update-vals pmap-wrap)
|
||||
(update :components update-vals #(d/update-when % :objects omap-wrap))
|
||||
(update :components update-vals #(update % :objects omap-wrap))
|
||||
(update :components pmap-wrap))))
|
||||
|
||||
(defmethod read-section :v1/files
|
||||
[{:keys [::db/conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
|
||||
[{:keys [conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
|
||||
(doseq [expected-file-id (-> *state* deref :files)]
|
||||
(let [file (read-obj! input)
|
||||
media' (read-obj! input)
|
||||
file-id (:id file)
|
||||
features (files/get-default-features)]
|
||||
features files/default-features]
|
||||
|
||||
(when (not= file-id expected-file-id)
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:found-id file-id
|
||||
:expected-id expected-file-id
|
||||
:hint "the penpot file seems corrupt, found unexpected uuid (file-id)"))
|
||||
|
||||
;; Update index using with media
|
||||
@@ -700,31 +679,22 @@
|
||||
(db/delete! conn :file-thumbnail {:file-id file-id'})))))))
|
||||
|
||||
(defmethod read-section :v1/rels
|
||||
[{:keys [::db/conn ::input ::timestamp]}]
|
||||
(let [rels (read-obj! input)
|
||||
ids (into #{} (-> *state* deref :files))]
|
||||
[{:keys [conn ::input ::timestamp]}]
|
||||
(let [rels (read-obj! input)]
|
||||
;; Insert all file relations
|
||||
(doseq [{:keys [library-file-id] :as rel} rels]
|
||||
(doseq [rel rels]
|
||||
(let [rel (-> rel
|
||||
(assoc :synced-at timestamp)
|
||||
(update :file-id lookup-index)
|
||||
(update :library-file-id lookup-index))]
|
||||
|
||||
(if (contains? ids library-file-id)
|
||||
(do
|
||||
(l/debug :hint "create file library link"
|
||||
:file-id (:file-id rel)
|
||||
:lib-id (:library-file-id rel)
|
||||
::l/sync? true)
|
||||
(db/insert! conn :file-library-rel rel))
|
||||
|
||||
(l/warn :hint "ignoring file library link"
|
||||
:file-id (:file-id rel)
|
||||
:lib-id (:library-file-id rel)
|
||||
::l/sync? true))))))
|
||||
(l/debug :hint "create file library link"
|
||||
:file-id (:file-id rel)
|
||||
:lib-id (:library-file-id rel)
|
||||
::l/sync? true)
|
||||
(db/insert! conn :file-library-rel rel)))))
|
||||
|
||||
(defmethod read-section :v1/sobjects
|
||||
[{:keys [::sto/storage ::db/conn ::input ::overwrite?]}]
|
||||
[{:keys [::sto/storage conn ::input ::overwrite?]}]
|
||||
(let [storage (media/configure-assets-storage storage)
|
||||
ids (read-obj! input)]
|
||||
|
||||
@@ -749,7 +719,7 @@
|
||||
(assoc ::sto/touched-at (dt/now))
|
||||
(assoc :bucket "file-media-object"))
|
||||
|
||||
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/sync? true)
|
||||
(vswap! *state* update :index assoc id (:id sobject)))))
|
||||
@@ -773,7 +743,7 @@
|
||||
(defn- lookup-index
|
||||
[id]
|
||||
(let [val (get-in @*state* [:index id])]
|
||||
(l/debug :fn "lookup-index" :id id :val val ::l/sync? true)
|
||||
(l/trace :fn "lookup-index" :id id :val val ::l/sync? true)
|
||||
(when (and (not (::ignore-index-errors? *options*)) (not val))
|
||||
(ex/raise :type :validation
|
||||
:code :incomplete-index
|
||||
@@ -786,7 +756,7 @@
|
||||
index index]
|
||||
(if-let [id (first items)]
|
||||
(let [new-id (if (::overwrite? *options*) id (uuid/next))]
|
||||
(l/debug :fn "update-index" :id id :new-id new-id ::l/sync? true)
|
||||
(l/trace :fn "update-index" :id id :new-id new-id ::l/sync? true)
|
||||
(recur (rest items)
|
||||
(assoc index id new-id)))
|
||||
index)))
|
||||
@@ -804,7 +774,8 @@
|
||||
(update-in [:metadata :id] lookup-index)
|
||||
|
||||
;; Relink paths with fill image
|
||||
(map? (:fill-image form))
|
||||
(and (map? (:fill-image form))
|
||||
(= :path (:type form)))
|
||||
(update-in [:fill-image :id] lookup-index)
|
||||
|
||||
;; This covers old shapes and the new :fills.
|
||||
@@ -864,7 +835,7 @@
|
||||
cs (volatile! nil)]
|
||||
(try
|
||||
(l/info :hint "start exportation" :export-id id)
|
||||
(dm/with-open [output (io/output-stream output)]
|
||||
(with-open [^AutoCloseable output (io/output-stream output)]
|
||||
(binding [*position* (atom 0)]
|
||||
(write-export! (assoc cfg ::output output))))
|
||||
|
||||
@@ -887,7 +858,7 @@
|
||||
(defn export-to-tmpfile!
|
||||
[cfg]
|
||||
(let [path (tmp/tempfile :prefix "penpot.export.")]
|
||||
(dm/with-open [output (io/output-stream path)]
|
||||
(with-open [^AutoCloseable output (io/output-stream path)]
|
||||
(export! cfg output)
|
||||
path)))
|
||||
|
||||
@@ -899,7 +870,7 @@
|
||||
(l/info :hint "import: started" :import-id id)
|
||||
(try
|
||||
(binding [*position* (atom 0)]
|
||||
(dm/with-open [input (io/input-stream input)]
|
||||
(with-open [^AutoCloseable input (io/input-stream input)]
|
||||
(read-import! (assoc cfg ::input input))))
|
||||
|
||||
(catch Throwable cause
|
||||
@@ -939,9 +910,7 @@
|
||||
(export! output-stream))))]
|
||||
|
||||
(fn [_]
|
||||
{::yrs/status 200
|
||||
::yrs/body body
|
||||
::yrs/headers {"content-type" "application/octet-stream"}})))
|
||||
(yrs/response 200 body {"content-type" "application/octet-stream"}))))
|
||||
|
||||
(s/def ::file ::media/upload)
|
||||
(s/def ::import-binfile
|
||||
@@ -959,10 +928,5 @@
|
||||
::input (:path file)
|
||||
::project-id project-id
|
||||
::ignore-index-errors? true))]
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
|
||||
(rph/with-meta ids
|
||||
{::audit/props {:file nil :file-ids ids}}))))
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.rpc.retry :as rtry]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.retry :as rtry]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -101,7 +101,7 @@
|
||||
(sv/defmethod ::get-comment-threads
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id)))
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
(sv/defmethod ::get-unread-comment-threads
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-unread-comment-threads conn profile-id team-id)))
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
(sv/defmethod ::get-comment-thread
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [sql (str "with threads as (" sql:comment-threads ")"
|
||||
"select * from threads where id = ?")]
|
||||
@@ -211,7 +211,7 @@
|
||||
(sv/defmethod ::get-comments
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comments conn thread-id))))
|
||||
@@ -263,7 +263,7 @@
|
||||
{::doc/added "1.15"
|
||||
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-file-comments-users conn file-id profile-id)))
|
||||
|
||||
@@ -309,8 +309,7 @@
|
||||
|
||||
(rtry/with-retry {::rtry/when rtry/conflict-exception?
|
||||
::rtry/max-retries 3
|
||||
::rtry/label "create-comment-thread"
|
||||
::db/conn conn}
|
||||
::rtry/label "create-comment-thread"}
|
||||
(create-comment-thread conn
|
||||
{:created-at request-at
|
||||
:profile-id profile-id
|
||||
@@ -468,8 +467,8 @@
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::db/for-update? true)
|
||||
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/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 ::db/for-update? true)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
@@ -49,7 +48,7 @@
|
||||
:fullname fullname
|
||||
:is-active true
|
||||
:deleted-at (dt/in-future cf/deletion-delay)
|
||||
:password (profile/derive-password cfg password)
|
||||
:password password
|
||||
:props {}}]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
|
||||
@@ -9,19 +9,19 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-js-like :as-alias smdj]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files.thumbnails :as-alias thumbs]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.cond :as-alias cond]
|
||||
@@ -38,18 +38,12 @@
|
||||
|
||||
;; --- FEATURES
|
||||
|
||||
(defn resolve-public-uri
|
||||
[media-id]
|
||||
(when media-id
|
||||
(str (cf/get :public-uri) "/assets/by-id/" media-id)))
|
||||
|
||||
(def supported-features
|
||||
#{"storage/objects-map"
|
||||
"storage/pointer-map"
|
||||
"components/v2"})
|
||||
|
||||
(defn get-default-features
|
||||
[]
|
||||
(def default-features
|
||||
(cond-> #{}
|
||||
(contains? cf/flags :fdata-storage-pointer-map)
|
||||
(conj "storage/pointer-map")
|
||||
@@ -189,15 +183,13 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn check-features-compatibility!
|
||||
"Function responsible to check if provided features are supported by
|
||||
the current backend"
|
||||
[features]
|
||||
(let [not-supported (set/difference features supported-features)]
|
||||
(when (seq not-supported)
|
||||
(ex/raise :type :restriction
|
||||
:code :features-not-supported
|
||||
:feature (first not-supported)
|
||||
:hint (format "features %s not supported" (str/join "," (map name not-supported)))))
|
||||
:hint (format "features %s not supported" (str/join "," not-supported))))
|
||||
features))
|
||||
|
||||
(defn load-pointer
|
||||
@@ -208,16 +200,6 @@
|
||||
::db/check-deleted? false})]
|
||||
(blob/decode (:content row))))
|
||||
|
||||
(defn- load-all-pointers!
|
||||
[{:keys [data] :as file}]
|
||||
(doseq [[_id page] (:pages-index data)]
|
||||
(when (pmap/pointer-map? page)
|
||||
(pmap/load! page)))
|
||||
(doseq [[_id component] (:components data)]
|
||||
(when (pmap/pointer-map? component)
|
||||
(pmap/load! component)))
|
||||
file)
|
||||
|
||||
(defn persist-pointers!
|
||||
[conn file-id]
|
||||
(doseq [[id item] @pmap/*tracked*]
|
||||
@@ -241,188 +223,129 @@
|
||||
(update-fn val)
|
||||
val)))))))
|
||||
|
||||
|
||||
(defn get-all-pointer-ids
|
||||
"Given a file, return all pointer ids used in the data."
|
||||
[fdata]
|
||||
(->> (concat (vals fdata)
|
||||
(vals (:pages-index fdata)))
|
||||
(into #{} (comp (filter pmap/pointer-map?)
|
||||
(map pmap/get-id)))))
|
||||
|
||||
;; FIXME: file locking
|
||||
(defn- process-components-v2-feature
|
||||
"A special case handling of the components/v2 feature."
|
||||
[{:keys [features data] :as file}]
|
||||
(let [data (ctf/migrate-to-components-v2 data)
|
||||
features (conj features "components/v2")]
|
||||
(-> file
|
||||
(assoc ::pmg/migrated true)
|
||||
(assoc :features features)
|
||||
(assoc :data data))))
|
||||
|
||||
(defn handle-file-features!
|
||||
[{:keys [features] :as file} client-features]
|
||||
|
||||
;; Check features compatibility between the currently supported features on
|
||||
;; the current backend instance and the file retrieved from the database
|
||||
(check-features-compatibility! features)
|
||||
|
||||
(cond-> file
|
||||
(and (contains? features "components/v2")
|
||||
(not (contains? client-features "components/v2")))
|
||||
(as-> file (ex/raise :type :restriction
|
||||
:code :feature-mismatch
|
||||
:feature "components/v2"
|
||||
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"
|
||||
:file-id (:id file)))
|
||||
|
||||
;; This operation is needed because the components migration generates a new
|
||||
;; page with random id which is returned to the client; without persisting
|
||||
;; the migration this can cause that two simultaneous clients can have a
|
||||
;; different view of the file data and end persisting two pages with main
|
||||
;; components and breaking the whole file."
|
||||
(and (contains? client-features "components/v2")
|
||||
(not (contains? features "components/v2")))
|
||||
(as-> file (process-components-v2-feature file))
|
||||
|
||||
;; This operation is needed for backward comapatibility with frontends that
|
||||
;; does not support pointer-map resolution mechanism; this just resolves the
|
||||
;; pointers on backend and return a complete file.
|
||||
(and (contains? features "storage/pointer-map")
|
||||
(not (contains? client-features "storage/pointer-map")))
|
||||
(process-pointers deref)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUERY COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn handle-file-features
|
||||
[{:keys [features] :as file} client-features]
|
||||
(when (and (contains? features "components/v2")
|
||||
(not (contains? client-features "components/v2")))
|
||||
(ex/raise :type :restriction
|
||||
:code :feature-mismatch
|
||||
:feature "components/v2"
|
||||
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"))
|
||||
|
||||
(cond-> file
|
||||
(and (contains? client-features "components/v2")
|
||||
(not (contains? features "components/v2")))
|
||||
(update :data ctf/migrate-to-components-v2)
|
||||
|
||||
(and (contains? features "storage/pointer-map")
|
||||
(not (contains? client-features "storage/pointer-map")))
|
||||
(process-pointers deref)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file (by id)
|
||||
|
||||
(sm/def! ::features
|
||||
[:schema
|
||||
{:title "FileFeatures"
|
||||
::smdj/inline true
|
||||
:gen/gen (sg/subseq supported-features)}
|
||||
::sm/set-of-strings])
|
||||
|
||||
(sm/def! ::file
|
||||
[:map {:title "File"}
|
||||
[:id ::sm/uuid]
|
||||
[:features ::features]
|
||||
[:has-media-trimmed :boolean]
|
||||
[:comment-thread-seqn {:min 0} :int]
|
||||
[:name :string]
|
||||
[:revn {:min 0} :int]
|
||||
[:modified-at ::dt/instant]
|
||||
[:is-shared :boolean]
|
||||
[:project-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:data {:optional true} :any]])
|
||||
|
||||
(sm/def! ::permissions-mixin
|
||||
[:map {:title "PermissionsMixin"}
|
||||
[:permissions ::perms/permissions]])
|
||||
|
||||
(sm/def! ::file-with-permissions
|
||||
[:merge {:title "FileWithPermissions"}
|
||||
::file
|
||||
::permissions-mixin])
|
||||
|
||||
(sm/def! ::get-file
|
||||
[:map {:title "get-file"}
|
||||
[:features {:optional true} ::features]
|
||||
[:id ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn get-file
|
||||
([conn id client-features]
|
||||
(get-file conn id client-features nil))
|
||||
([conn id client-features project-id]
|
||||
;; here we check if client requested features are supported
|
||||
(check-features-compatibility! client-features)
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)
|
||||
pmap/*tracked* (atom {})]
|
||||
|
||||
(let [params (merge {:id id}
|
||||
(when (some? project-id)
|
||||
{:project-id project-id}))
|
||||
|
||||
file (-> (db/get conn :file params)
|
||||
(decode-row)
|
||||
(pmg/migrate-file))
|
||||
|
||||
file (handle-file-features! file client-features)]
|
||||
|
||||
;; NOTE: when file is migrated, we break the rule of no perform
|
||||
;; mutations on get operations and update the file with all
|
||||
;; migrations applied
|
||||
(when (pmg/migrated? file)
|
||||
(let [features (db/create-array conn "text" (:features file))]
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode (:data file))
|
||||
:features features}
|
||||
{:id id})
|
||||
(persist-pointers! conn id)))
|
||||
|
||||
file))))
|
||||
[conn id client-features]
|
||||
;; here we check if client requested features are supported
|
||||
(check-features-compatibility! client-features)
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(-> (db/get-by-id conn :file id)
|
||||
(decode-row)
|
||||
(pmg/migrate-file)
|
||||
(handle-file-features client-features))))
|
||||
|
||||
(defn get-minimal-file
|
||||
[{:keys [::db/pool] :as cfg} id]
|
||||
(db/get pool :file {:id id} {:columns [:id :modified-at :revn]}))
|
||||
|
||||
(defn get-file-etag
|
||||
[{:keys [::rpc/profile-id]} {:keys [modified-at revn]}]
|
||||
(str profile-id (dt/format-instant modified-at :iso) revn))
|
||||
[{:keys [modified-at revn]}]
|
||||
(str (dt/format-instant modified-at :iso) "-" revn))
|
||||
|
||||
(s/def ::get-file
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::features]))
|
||||
|
||||
(sv/defmethod ::get-file
|
||||
"Retrieve a file by its ID. Only authenticated users."
|
||||
{::doc/added "1.17"
|
||||
::cond/get-object #(get-minimal-file %1 (:id %2))
|
||||
::cond/key-fn get-file-etag
|
||||
::sm/params ::get-file
|
||||
::sm/result ::file-with-permissions}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
::cond/key-fn get-file-etag}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [perms (get-permissions conn profile-id id)]
|
||||
(check-read-permissions! perms)
|
||||
(let [file (-> (get-file conn id features project-id)
|
||||
(let [file (-> (get-file conn id features)
|
||||
(assoc :permissions perms))]
|
||||
(vary-meta file assoc ::cond/key (get-file-etag params file))))))
|
||||
(vary-meta file assoc ::cond/key (get-file-etag file))))))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-fragment (by id)
|
||||
|
||||
(sm/def! ::file-fragment
|
||||
[:map {:title "FileFragment"}
|
||||
[:id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:content any?]])
|
||||
|
||||
(sm/def! ::get-file-fragment
|
||||
[:map {:title "get-file-fragment"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:fragment-id ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn- get-file-fragment
|
||||
[conn file-id fragment-id]
|
||||
(some-> (db/get conn :file-data-fragment {:file-id file-id :id fragment-id})
|
||||
(update :content blob/decode)))
|
||||
|
||||
(s/def ::share-id ::us/uuid)
|
||||
(s/def ::fragment-id ::us/uuid)
|
||||
|
||||
(s/def ::get-file-fragment
|
||||
(s/keys :req-un [::file-id ::fragment-id]
|
||||
:opt [::rpc/profile-id]
|
||||
:opt-un [::share-id]))
|
||||
|
||||
(sv/defmethod ::get-file-fragment
|
||||
"Retrieve a file by its ID. Only authenticated users."
|
||||
{::doc/added "1.17"
|
||||
::sm/params ::get-file-fragment
|
||||
::sm/result ::file-fragment}
|
||||
::rpc/:auth false}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [perms (get-permissions conn profile-id file-id share-id)]
|
||||
(check-read-permissions! perms)
|
||||
(-> (get-file-fragment conn file-id fragment-id)
|
||||
(rph/with-http-cache long-cache-duration)))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-object-thumbnails
|
||||
|
||||
(defn get-object-thumbnails
|
||||
([conn file-id]
|
||||
(let [sql (str/concat
|
||||
"select object_id, data "
|
||||
" from file_object_thumbnail"
|
||||
" where file_id=?")]
|
||||
(->> (db/exec! conn [sql file-id])
|
||||
(d/index-by :object-id :data))))
|
||||
|
||||
([conn file-id object-ids]
|
||||
(let [sql (str/concat
|
||||
"select object_id, data "
|
||||
" from file_object_thumbnail"
|
||||
" where file_id=? and object_id = ANY(?)")
|
||||
ids (db/create-array conn "text" (seq object-ids))]
|
||||
(->> (db/exec! conn [sql file-id ids])
|
||||
(d/index-by :object-id :data)))))
|
||||
|
||||
(s/def ::get-file-object-thumbnails
|
||||
(s/keys :req [::rpc/profile-id] :req-un [::file-id]))
|
||||
|
||||
(sv/defmethod ::get-file-object-thumbnails
|
||||
"Retrieve a file object thumbnails."
|
||||
{::doc/added "1.17"
|
||||
::cond/get-object #(get-minimal-file %1 (:file-id %2))
|
||||
::cond/reuse-key? true
|
||||
::cond/key-fn get-file-etag}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(get-object-thumbnails conn file-id)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-project-files
|
||||
|
||||
(def ^:private sql:project-files
|
||||
@@ -432,32 +355,24 @@
|
||||
f.modified_at,
|
||||
f.name,
|
||||
f.revn,
|
||||
f.is_shared,
|
||||
ft.media_id
|
||||
f.is_shared
|
||||
from file as f
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
|
||||
where f.project_id = ?
|
||||
and f.deleted_at is null
|
||||
order by f.modified_at desc")
|
||||
|
||||
(s/def ::get-project-files
|
||||
(s/keys :req [::rpc/profile-id] :req-un [::project-id]))
|
||||
|
||||
(defn get-project-files
|
||||
[conn project-id]
|
||||
(->> (db/exec! conn [sql:project-files project-id])
|
||||
(mapv (fn [row]
|
||||
(if-let [media-id (:media-id row)]
|
||||
(-> row
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(dissoc row :media-id))))))
|
||||
(db/exec! conn [sql:project-files project-id]))
|
||||
|
||||
(sv/defmethod ::get-project-files
|
||||
"Get all files for the specified project."
|
||||
{::doc/added "1.17"
|
||||
::sm/params [:map {:title "get-project-files"}
|
||||
[:project-id ::sm/uuid]]
|
||||
::sm/result [:vector ::file]}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(projects/check-read-permissions! conn profile-id project-id)
|
||||
(get-project-files conn project-id)))
|
||||
|
||||
@@ -466,14 +381,17 @@
|
||||
|
||||
(declare get-has-file-libraries)
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
|
||||
(s/def ::has-file-libraries
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]))
|
||||
|
||||
(sv/defmethod ::has-file-libraries
|
||||
"Checks if the file has libraries. Returns a boolean"
|
||||
{::doc/added "1.15.1"
|
||||
::sm/params [:map {:title "has-file-libraries"}
|
||||
[:file-id ::sm/uuid]]
|
||||
::sm/result :boolean}
|
||||
{::doc/added "1.15.1"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(get-has-file-libraries conn file-id)))
|
||||
|
||||
@@ -498,9 +416,8 @@
|
||||
other not needed objects removed from the `:objects` data
|
||||
structure."
|
||||
[{:keys [objects] :as page} object-id]
|
||||
(let [objects (->> (cph/get-children-with-self objects object-id)
|
||||
(filter some?))]
|
||||
(assoc page :objects (d/index-by :id objects))))
|
||||
(let [objects (cph/get-children-with-self objects object-id)]
|
||||
(assoc page :objects (d/index-by :id objects))))
|
||||
|
||||
(defn- prune-thumbnails
|
||||
"Given the page data, removes the `:thumbnail` prop from all
|
||||
@@ -510,29 +427,24 @@
|
||||
|
||||
(defn get-page
|
||||
[conn {:keys [file-id page-id object-id features]}]
|
||||
(when (and (uuid? object-id)
|
||||
(not (uuid? page-id)))
|
||||
(ex/raise :type :validation
|
||||
:code :params-validation
|
||||
:hint "page-id is required when object-id is provided"))
|
||||
|
||||
(let [file (get-file conn file-id features)
|
||||
page-id (or page-id (-> file :data :pages first))
|
||||
page (dm/get-in file [:data :pages-index page-id])
|
||||
page (if (pmap/pointer-map? page)
|
||||
(deref page)
|
||||
page)]
|
||||
page (dm/get-in file [:data :pages-index page-id])]
|
||||
(cond-> (prune-thumbnails page)
|
||||
(uuid? object-id)
|
||||
(prune-objects object-id))))
|
||||
|
||||
(sm/def! ::get-page
|
||||
[:map {:title "GetPage"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]
|
||||
[:object-id {:optional true} ::sm/uuid]
|
||||
[:features {:optional true} ::features]])
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::get-page
|
||||
(s/and
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]
|
||||
:opt-un [::page-id ::object-id ::features])
|
||||
(fn [obj]
|
||||
(if (contains? obj :object-id)
|
||||
(contains? obj :page-id)
|
||||
true))))
|
||||
|
||||
(sv/defmethod ::get-page
|
||||
"Retrieves the page data from file and returns it. If no page-id is
|
||||
@@ -544,14 +456,12 @@
|
||||
mandatory.
|
||||
|
||||
Mainly used for rendering purposes."
|
||||
{::doc/added "1.17"
|
||||
::sm/params ::get-page}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(let [perms (get-permissions conn profile-id file-id share-id)]
|
||||
(check-read-permissions! perms)
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn file-id)]
|
||||
(get-page conn params)))))
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(get-page conn params)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-team-shared-files
|
||||
|
||||
@@ -563,11 +473,9 @@
|
||||
f.created_at,
|
||||
f.modified_at,
|
||||
f.name,
|
||||
f.is_shared,
|
||||
ft.media_id
|
||||
f.is_shared
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
|
||||
where f.is_shared = true
|
||||
and f.deleted_at is null
|
||||
and p.deleted_at is null
|
||||
@@ -577,33 +485,21 @@
|
||||
(defn get-team-shared-files
|
||||
[conn team-id]
|
||||
(letfn [(assets-sample [assets limit]
|
||||
(let [sorted-assets (->> (vals assets)
|
||||
(sort-by #(str/lower (:name %))))]
|
||||
{:count (count sorted-assets)
|
||||
:sample (into [] (take limit sorted-assets))}))
|
||||
(let [sorted-assets (->> (vals assets)
|
||||
(sort-by #(str/lower (:name %))))]
|
||||
{:count (count sorted-assets)
|
||||
:sample (into [] (take limit sorted-assets))}))
|
||||
|
||||
(library-summary [{:keys [id data] :as file}]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(let [load-objects (fn [component]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(ctf/load-component-objects data component)))
|
||||
components-sample (-> (assets-sample (ctkl/components data) 4)
|
||||
(update :sample
|
||||
#(map load-objects %)))]
|
||||
{:components components-sample
|
||||
:media (assets-sample (:media data) 3)
|
||||
:colors (assets-sample (:colors data) 3)
|
||||
:typographies (assets-sample (:typographies data) 3)})))]
|
||||
{:components (assets-sample (:components data) 4)
|
||||
:media (assets-sample (:media data) 3)
|
||||
:colors (assets-sample (:colors data) 3)
|
||||
:typographies (assets-sample (:typographies data) 3)}))]
|
||||
|
||||
(->> (db/exec! conn [sql:team-shared-files team-id])
|
||||
(into #{} (comp
|
||||
(map decode-row)
|
||||
(map (fn [row]
|
||||
(if-let [media-id (:media-id row)]
|
||||
(-> row
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(dissoc row :media-id))))
|
||||
(map #(assoc % :library-summary (library-summary %)))
|
||||
(map #(dissoc % :data)))))))
|
||||
|
||||
@@ -615,14 +511,14 @@
|
||||
"Get all file (libraries) for the specified team."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-team-shared-files conn team-id)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-libraries
|
||||
|
||||
(def ^:private sql:get-file-libraries
|
||||
(def ^:private sql:file-libraries
|
||||
"WITH RECURSIVE libs AS (
|
||||
SELECT fl.*, flr.synced_at
|
||||
FROM file AS fl
|
||||
@@ -635,6 +531,7 @@
|
||||
JOIN libs AS l ON (flr.file_id = l.id)
|
||||
)
|
||||
SELECT l.id,
|
||||
l.data,
|
||||
l.features,
|
||||
l.project_id,
|
||||
l.created_at,
|
||||
@@ -647,24 +544,30 @@
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
|
||||
(defn get-file-libraries
|
||||
[conn file-id]
|
||||
(into []
|
||||
(comp
|
||||
(map #(assoc % :is-indirect false))
|
||||
(map decode-row))
|
||||
(db/exec! conn [sql:get-file-libraries file-id])))
|
||||
[conn file-id client-features]
|
||||
(check-features-compatibility! client-features)
|
||||
(->> (db/exec! conn [sql:file-libraries file-id])
|
||||
(map decode-row)
|
||||
(map #(assoc % :is-indirect false))
|
||||
(map (fn [{:keys [id] :as row}]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(-> row
|
||||
(update :data dissoc :pages-index)
|
||||
(handle-file-features client-features)))))
|
||||
(vec)))
|
||||
|
||||
(s/def ::get-file-libraries
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]))
|
||||
:req-un [::file-id]
|
||||
:opt-un [::features]))
|
||||
|
||||
(sv/defmethod ::get-file-libraries
|
||||
"Get libraries used by the specified file."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(get-file-libraries conn file-id)))
|
||||
(get-file-libraries conn file-id features)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: Files that use this File library
|
||||
@@ -688,7 +591,7 @@
|
||||
"Returns all the file references that use specified file (library) id."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(get-library-file-references conn file-id)))
|
||||
|
||||
@@ -703,11 +606,9 @@
|
||||
f.modified_at,
|
||||
f.name,
|
||||
f.is_shared,
|
||||
ft.media_id,
|
||||
row_number() over w as row_num
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
|
||||
join project as p on (p.id = f.project_id)
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null
|
||||
and f.deleted_at is null
|
||||
@@ -718,13 +619,7 @@
|
||||
|
||||
(defn get-team-recent-files
|
||||
[conn team-id]
|
||||
(->> (db/exec! conn [sql:team-recent-files team-id])
|
||||
(mapv (fn [row]
|
||||
(if-let [media-id (:media-id row)]
|
||||
(-> row
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(dissoc row :media-id))))))
|
||||
(db/exec! conn [sql:team-recent-files team-id]))
|
||||
|
||||
(s/def ::get-team-recent-files
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
@@ -733,10 +628,147 @@
|
||||
(sv/defmethod ::get-team-recent-files
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-team-recent-files conn team-id)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-thumbnail
|
||||
|
||||
(defn get-file-thumbnail
|
||||
[conn file-id revn]
|
||||
(let [sql (sql/select :file-thumbnail
|
||||
(cond-> {:file-id file-id}
|
||||
revn (assoc :revn revn))
|
||||
{:limit 1
|
||||
:order-by [[:revn :desc]]})
|
||||
row (db/exec-one! conn sql)]
|
||||
(when-not row
|
||||
(ex/raise :type :not-found
|
||||
:code :file-thumbnail-not-found))
|
||||
|
||||
{:data (:data row)
|
||||
:props (some-> (:props row) db/decode-transit-pgobject)
|
||||
:revn (:revn row)
|
||||
:file-id (:file-id row)}))
|
||||
|
||||
(s/def ::revn ::us/integer)
|
||||
|
||||
(s/def ::get-file-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]
|
||||
:opt-un [::revn]))
|
||||
|
||||
(sv/defmethod ::get-file-thumbnail
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(-> (get-file-thumbnail conn file-id revn)
|
||||
(rph/with-http-cache long-cache-duration))))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-data-for-thumbnail
|
||||
|
||||
(defn get-file-data-for-thumbnail
|
||||
[conn {:keys [data id] :as file}]
|
||||
(letfn [;; function responsible on finding the frame marked to be
|
||||
;; used as thumbnail; the returned frame always have
|
||||
;; the :page-id set to the page that it belongs.
|
||||
(get-thumbnail-frame [data]
|
||||
(d/seek :use-for-thumbnail?
|
||||
(for [page (-> data :pages-index vals)
|
||||
frame (-> page :objects ctt/get-frames)]
|
||||
(assoc frame :page-id (:id page)))))
|
||||
|
||||
;; function responsible to filter objects data structure of
|
||||
;; all unneeded shapes if a concrete frame is provided. If no
|
||||
;; frame, the objects is returned untouched.
|
||||
(filter-objects [objects frame-id]
|
||||
(d/index-by :id (cph/get-children-with-self objects frame-id)))
|
||||
|
||||
;; function responsible of assoc available thumbnails
|
||||
;; to frames and remove all children shapes from objects if
|
||||
;; thumbnails is available
|
||||
(assoc-thumbnails [objects page-id thumbnails]
|
||||
(loop [objects objects
|
||||
frames (filter cph/frame-shape? (vals objects))]
|
||||
|
||||
(if-let [frame (-> frames first)]
|
||||
(let [frame-id (:id frame)
|
||||
object-id (str page-id frame-id)
|
||||
frame (if-let [thumb (get thumbnails object-id)]
|
||||
(assoc frame :thumbnail thumb :shapes [])
|
||||
(dissoc frame :thumbnail))
|
||||
|
||||
children-ids
|
||||
(cph/get-children-ids objects frame-id)
|
||||
|
||||
bounds
|
||||
(when (:show-content frame)
|
||||
(gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects))))))
|
||||
|
||||
frame
|
||||
(cond-> frame
|
||||
(some? bounds)
|
||||
(assoc :children-bounds bounds))]
|
||||
|
||||
(if (:thumbnail frame)
|
||||
(recur (-> objects
|
||||
(assoc frame-id frame)
|
||||
(d/without-keys children-ids))
|
||||
(rest frames))
|
||||
(recur (assoc objects frame-id frame)
|
||||
(rest frames))))
|
||||
|
||||
objects)))]
|
||||
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(let [frame (get-thumbnail-frame data)
|
||||
frame-id (:id frame)
|
||||
page-id (or (:page-id frame)
|
||||
(-> data :pages first))
|
||||
|
||||
page (dm/get-in data [:pages-index page-id])
|
||||
page (cond-> page (pmap/pointer-map? page) deref)
|
||||
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
|
||||
|
||||
obj-ids (map #(str page-id %) frame-ids)
|
||||
thumbs (get-object-thumbnails conn id obj-ids)]
|
||||
|
||||
(cond-> page
|
||||
;; If we have frame, we need to specify it on the page level
|
||||
;; and remove the all other unrelated objects.
|
||||
(some? frame-id)
|
||||
(-> (assoc :thumbnail-frame-id frame-id)
|
||||
(update :objects filter-objects frame-id))
|
||||
|
||||
;; Assoc the available thumbnails and prune not visible shapes
|
||||
;; for avoid transfer unnecessary data.
|
||||
:always
|
||||
(update :objects assoc-thumbnails page-id thumbs))))))
|
||||
|
||||
(s/def ::get-file-data-for-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]
|
||||
:opt-un [::features]))
|
||||
|
||||
(sv/defmethod ::get-file-data-for-thumbnail
|
||||
"Retrieves the data for generate the thumbnail of the file. Used
|
||||
mainly for render thumbnails on dashboard."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
;; NOTE: we force here the "storage/pointer-map" feature, because
|
||||
;; it used internally only and is independent if user supports it
|
||||
;; or not.
|
||||
(let [feat (into #{"storage/pointer-map"} features)
|
||||
file (get-file conn file-id feat)]
|
||||
{:file-id file-id
|
||||
:revn (:revn file)
|
||||
:page (get-file-data-for-thumbnail conn file)})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MUTATION COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -750,30 +782,13 @@
|
||||
:modified-at (dt/now)}
|
||||
{:id id}))
|
||||
|
||||
(s/def ::rename-file
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::name ::id]))
|
||||
|
||||
(sv/defmethod ::rename-file
|
||||
{::doc/added "1.17"
|
||||
::webhooks/event? true
|
||||
|
||||
::sm/webhook
|
||||
[:map {:title "RenameFileEvent"}
|
||||
[:id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]
|
||||
|
||||
::sm/params
|
||||
[:map {:title "RenameFileParams"}
|
||||
[:name {:min 1} :string]
|
||||
[:id ::sm/uuid]]
|
||||
|
||||
::sm/result
|
||||
[:map {:title "SimplifiedFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]}
|
||||
|
||||
::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)
|
||||
@@ -796,38 +811,25 @@
|
||||
{:is-shared is-shared}
|
||||
{:id id}))
|
||||
|
||||
(def sql:get-referenced-files
|
||||
"SELECT f.id
|
||||
FROM file_library_rel AS flr
|
||||
INNER JOIN file AS f ON (f.id = flr.file_id)
|
||||
WHERE flr.library_file_id = ?
|
||||
ORDER BY f.created_at ASC;")
|
||||
|
||||
(defn absorb-library
|
||||
"Find all files using a shared library, and absorb all library assets
|
||||
into the file local libraries"
|
||||
[conn {:keys [id] :as params}]
|
||||
(let [library (db/get-by-id conn :file id)]
|
||||
(when (:is-shared library)
|
||||
(let [ldata (binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(-> library decode-row load-all-pointers! pmg/migrate-file :data))
|
||||
rows (db/exec! conn [sql:get-referenced-files id])]
|
||||
(doseq [file-id (map :id rows)]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn file-id)
|
||||
pmap/*tracked* (atom {})]
|
||||
(let [file (-> (db/get-by-id conn :file file-id
|
||||
::db/check-deleted? false
|
||||
::db/remove-deleted? false)
|
||||
(decode-row)
|
||||
(load-all-pointers!)
|
||||
(pmg/migrate-file))
|
||||
data (ctf/absorb-assets (:data file) ldata)]
|
||||
(db/update! conn :file
|
||||
{:revn (inc (:revn file))
|
||||
:data (blob/encode data)
|
||||
:modified-at (dt/now)}
|
||||
{:id file-id})
|
||||
(persist-pointers! conn file-id))))))))
|
||||
(let [ldata (-> library decode-row pmg/migrate-file :data)]
|
||||
(->> (db/query conn :file-library-rel {:library-file-id id})
|
||||
(map :file-id)
|
||||
(keep #(db/get-by-id conn :file % ::db/check-deleted? false))
|
||||
(map decode-row)
|
||||
(map pmg/migrate-file)
|
||||
(run! (fn [{:keys [id data revn] :as file}]
|
||||
(let [data (ctf/absorb-assets data ldata)]
|
||||
(db/update! conn :file
|
||||
{:revn (inc revn)
|
||||
:data (blob/encode data)
|
||||
:modified-at (dt/now)}
|
||||
{:id id})))))))))
|
||||
|
||||
(s/def ::set-file-shared
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
@@ -942,7 +944,7 @@
|
||||
;; TODO: improve naming
|
||||
|
||||
(sv/defmethod ::update-file-library-sync-status
|
||||
"Update the synchronization status of a file->library link"
|
||||
"Update the synchronization statos of a file->library link"
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
@@ -971,3 +973,65 @@
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(-> (ignore-sync conn params)
|
||||
(update :features db/decode-pgarray #{}))))
|
||||
|
||||
|
||||
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
|
||||
|
||||
(def sql:upsert-object-thumbnail
|
||||
"insert into file_object_thumbnail(file_id, object_id, data)
|
||||
values (?, ?, ?)
|
||||
on conflict(file_id, object_id) do
|
||||
update set data = ?;")
|
||||
|
||||
(defn upsert-file-object-thumbnail!
|
||||
[conn {:keys [file-id object-id data]}]
|
||||
(if data
|
||||
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
|
||||
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id})))
|
||||
|
||||
(s/def ::data (s/nilable ::us/string))
|
||||
(s/def ::thumbs/object-id ::us/string)
|
||||
(s/def ::upsert-file-object-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::thumbs/object-id]
|
||||
:opt-un [::data]))
|
||||
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
{::doc/added "1.17"
|
||||
::audit/skip true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(upsert-file-object-thumbnail! conn params)
|
||||
nil))
|
||||
|
||||
;; --- MUTATION COMMAND: upsert-file-thumbnail
|
||||
|
||||
(def sql:upsert-file-thumbnail
|
||||
"insert into file_thumbnail (file_id, revn, data, props)
|
||||
values (?, ?, ?, ?::jsonb)
|
||||
on conflict(file_id, revn) do
|
||||
update set data = ?, props=?, updated_at=now();")
|
||||
|
||||
(defn upsert-file-thumbnail
|
||||
[conn {:keys [file-id revn data props]}]
|
||||
(let [props (db/tjson (or props {}))]
|
||||
(db/exec-one! conn [sql:upsert-file-thumbnail
|
||||
file-id revn data props data props])))
|
||||
|
||||
(s/def ::revn ::us/integer)
|
||||
(s/def ::props map?)
|
||||
(s/def ::upsert-file-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::revn ::data ::props]))
|
||||
|
||||
(sv/defmethod ::upsert-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.17"
|
||||
::audit/skip true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(upsert-file-thumbnail conn params)
|
||||
nil))
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(defn create-file-role!
|
||||
@@ -34,25 +33,22 @@
|
||||
(db/insert! conn :file-profile-rel))))
|
||||
|
||||
(defn create-file
|
||||
[conn {:keys [id name project-id is-shared revn
|
||||
[conn {:keys [id name project-id is-shared data revn
|
||||
modified-at deleted-at create-page
|
||||
ignore-sync-until features]
|
||||
:or {is-shared false revn 0 create-page true}
|
||||
:as params}]
|
||||
(let [id (or id (:id data) (uuid/next))
|
||||
features (-> (into files/default-features features)
|
||||
(files/check-features-compatibility!))
|
||||
|
||||
(let [id (or id (uuid/next))
|
||||
features (->> features
|
||||
(into (files/get-default-features))
|
||||
(files/check-features-compatibility!))
|
||||
|
||||
pointers (atom {})
|
||||
data (binding [pmap/*tracked* pointers
|
||||
ffeat/*current* features
|
||||
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity)
|
||||
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)]
|
||||
(if create-page
|
||||
(ctf/make-file-data id)
|
||||
(ctf/make-file-data id nil)))
|
||||
data (or data
|
||||
(binding [ffeat/*current* features
|
||||
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity)
|
||||
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)]
|
||||
(if create-page
|
||||
(ctf/make-file-data id)
|
||||
(ctf/make-file-data id nil))))
|
||||
|
||||
features (db/create-array conn "text" features)
|
||||
file (db/insert! conn :file
|
||||
@@ -68,16 +64,9 @@
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at}))]
|
||||
|
||||
(binding [pmap/*tracked* pointers]
|
||||
(files/persist-pointers! conn id))
|
||||
|
||||
(->> (assoc params :file-id id :role :owner)
|
||||
(create-file-role! conn))
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
|
||||
(files/decode-row file)))
|
||||
|
||||
(s/def ::create-file
|
||||
@@ -90,7 +79,6 @@
|
||||
|
||||
(sv/defmethod ::create-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
|
||||
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"
|
||||
::doc/module :files}
|
||||
{::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)
|
||||
@@ -63,8 +62,7 @@
|
||||
:req-un [::us/id]))
|
||||
|
||||
(sv/defmethod ::delete-share-link
|
||||
{::doc/added "1.18"
|
||||
::doc/module ::files}
|
||||
{::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)]
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
::create-page]))
|
||||
|
||||
(sv/defmethod ::create-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
@@ -65,8 +64,7 @@
|
||||
::files/id]))
|
||||
|
||||
(sv/defmethod ::update-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(update-temp-file conn (assoc params :profile-id profile-id))
|
||||
@@ -86,16 +84,16 @@
|
||||
(ex/raise :type :validation
|
||||
:code :cant-persist-already-persisted-file))
|
||||
|
||||
|
||||
(let [data
|
||||
(->> revs
|
||||
(mapcat #(->> % :changes blob/decode))
|
||||
(cp/process-changes (blob/decode (:data file))))]
|
||||
(db/update! conn :file
|
||||
{:deleted-at nil
|
||||
:revn revn
|
||||
:data (blob/encode data)}
|
||||
{:id id}))
|
||||
(loop [revs (seq revs)
|
||||
data (blob/decode (:data file))]
|
||||
(if-let [rev (first revs)]
|
||||
(recur (rest revs)
|
||||
(->> rev :changes blob/decode (cp/process-changes data)))
|
||||
(db/update! conn :file
|
||||
{:deleted-at nil
|
||||
:revn revn
|
||||
:data (blob/encode data)}
|
||||
{:id id})))
|
||||
nil))
|
||||
|
||||
(s/def ::persist-temp-file
|
||||
@@ -103,8 +101,7 @@
|
||||
:req-un [::files/id]))
|
||||
|
||||
(sv/defmethod ::persist-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
|
||||
@@ -1,457 +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.commands.files-thumbnails
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.cond :as-alias cond]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.storage :as sto]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- FEATURES
|
||||
|
||||
(def long-cache-duration
|
||||
(dt/duration {:days 7}))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-object-thumbnails
|
||||
|
||||
(defn- get-object-thumbnails
|
||||
([conn file-id]
|
||||
(let [sql (str/concat
|
||||
"select object_id, data, media_id "
|
||||
" from file_object_thumbnail"
|
||||
" where file_id=?")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(->> res
|
||||
(d/index-by :object-id (fn [row]
|
||||
(or (some-> row :media-id files/resolve-public-uri)
|
||||
(:data row))))
|
||||
(d/without-nils))))
|
||||
|
||||
([conn file-id object-ids]
|
||||
(let [sql (str/concat
|
||||
"select object_id, data, media_id "
|
||||
" from file_object_thumbnail"
|
||||
" where file_id=? and object_id = ANY(?)")
|
||||
ids (db/create-array conn "text" (seq object-ids))
|
||||
res (db/exec! conn [sql file-id ids])]
|
||||
(d/index-by :object-id
|
||||
(fn [row]
|
||||
(or (some-> row :media-id files/resolve-public-uri)
|
||||
(:data row)))
|
||||
res))))
|
||||
|
||||
(sv/defmethod ::get-file-object-thumbnails
|
||||
"Retrieve a file object thumbnails."
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params [:map {:title "get-file-object-thumbnails"}
|
||||
[:file-id ::sm/uuid]]
|
||||
::sm/result [:map-of :string :string]
|
||||
::cond/get-object #(files/get-minimal-file %1 (:file-id %2))
|
||||
::cond/reuse-key? true
|
||||
::cond/key-fn files/get-file-etag}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(get-object-thumbnails conn file-id)))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-thumbnail
|
||||
|
||||
(defn get-file-thumbnail
|
||||
[conn file-id revn]
|
||||
(let [sql (sql/select :file-thumbnail
|
||||
(cond-> {:file-id file-id}
|
||||
revn (assoc :revn revn))
|
||||
{:limit 1
|
||||
:order-by [[:revn :desc]]})
|
||||
row (db/exec-one! conn sql)]
|
||||
|
||||
(when-not row
|
||||
(ex/raise :type :not-found
|
||||
:code :file-thumbnail-not-found))
|
||||
|
||||
(when-not (:data row)
|
||||
(ex/raise :type :not-found
|
||||
:code :file-thumbnail-not-found))
|
||||
|
||||
{:data (:data row)
|
||||
:props (some-> (:props row) db/decode-transit-pgobject)
|
||||
:revn (:revn row)
|
||||
:file-id (:file-id row)}))
|
||||
|
||||
(s/def ::revn ::us/integer)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
|
||||
(s/def ::get-file-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]
|
||||
:opt-un [::revn]))
|
||||
|
||||
(sv/defmethod ::get-file-thumbnail
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::doc/deprecated "1.19"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(-> (get-file-thumbnail conn file-id revn)
|
||||
(rph/with-http-cache long-cache-duration))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-data-for-thumbnail
|
||||
|
||||
;; We need to improve how we set frame for thumbnail in order to avoid
|
||||
;; loading all pages into memory for find the frame set for thumbnail.
|
||||
|
||||
(defn get-file-data-for-thumbnail
|
||||
[conn {:keys [data id] :as file}]
|
||||
(letfn [;; function responsible on finding the frame marked to be
|
||||
;; used as thumbnail; the returned frame always have
|
||||
;; the :page-id set to the page that it belongs.
|
||||
|
||||
(get-thumbnail-frame [data]
|
||||
;; NOTE: this is a hack for avoid perform blocking
|
||||
;; operation inside the for loop, clojure lazy-seq uses
|
||||
;; synchronized blocks that does not plays well with
|
||||
;; virtual threads, so we need to perform the load
|
||||
;; operation first. This operation forces all pointer maps
|
||||
;; load into the memory.
|
||||
(->> (-> data :pages-index vals)
|
||||
(filter pmap/pointer-map?)
|
||||
(run! pmap/load!))
|
||||
|
||||
;; Then proceed to find the frame set for thumbnail
|
||||
|
||||
(d/seek :use-for-thumbnail?
|
||||
(for [page (-> data :pages-index vals)
|
||||
frame (-> page :objects ctt/get-frames)]
|
||||
(assoc frame :page-id (:id page)))))
|
||||
|
||||
;; function responsible to filter objects data structure of
|
||||
;; all unneeded shapes if a concrete frame is provided. If no
|
||||
;; frame, the objects is returned untouched.
|
||||
(filter-objects [objects frame-id]
|
||||
(d/index-by :id (cph/get-children-with-self objects frame-id)))
|
||||
|
||||
;; function responsible of assoc available thumbnails
|
||||
;; to frames and remove all children shapes from objects if
|
||||
;; thumbnails is available
|
||||
(assoc-thumbnails [objects page-id thumbnails]
|
||||
(loop [objects objects
|
||||
frames (filter cph/frame-shape? (vals objects))]
|
||||
|
||||
(if-let [frame (-> frames first)]
|
||||
(let [frame-id (:id frame)
|
||||
object-id (str page-id frame-id)
|
||||
frame (if-let [thumb (get thumbnails object-id)]
|
||||
(assoc frame :thumbnail thumb :shapes [])
|
||||
(dissoc frame :thumbnail))
|
||||
|
||||
children-ids
|
||||
(cph/get-children-ids objects frame-id)
|
||||
|
||||
bounds
|
||||
(when (:show-content frame)
|
||||
(gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects))))))
|
||||
|
||||
frame
|
||||
(cond-> frame
|
||||
(some? bounds)
|
||||
(assoc :children-bounds bounds))]
|
||||
|
||||
(if (:thumbnail frame)
|
||||
(recur (-> objects
|
||||
(assoc frame-id frame)
|
||||
(d/without-keys children-ids))
|
||||
(rest frames))
|
||||
(recur (assoc objects frame-id frame)
|
||||
(rest frames))))
|
||||
|
||||
objects)))]
|
||||
|
||||
(binding [pmap/*load-fn* (partial files/load-pointer conn id)]
|
||||
(let [frame (get-thumbnail-frame data)
|
||||
frame-id (:id frame)
|
||||
page-id (or (:page-id frame)
|
||||
(-> data :pages first))
|
||||
|
||||
page (dm/get-in data [:pages-index page-id])
|
||||
page (cond-> page (pmap/pointer-map? page) deref)
|
||||
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
|
||||
|
||||
obj-ids (map #(str page-id %) frame-ids)
|
||||
thumbs (get-object-thumbnails conn id obj-ids)]
|
||||
|
||||
(cond-> page
|
||||
;; If we have frame, we need to specify it on the page level
|
||||
;; and remove the all other unrelated objects.
|
||||
(some? frame-id)
|
||||
(-> (assoc :thumbnail-frame-id frame-id)
|
||||
(update :objects filter-objects frame-id))
|
||||
|
||||
;; Assoc the available thumbnails and prune not visible shapes
|
||||
;; for avoid transfer unnecessary data.
|
||||
:always
|
||||
(update :objects assoc-thumbnails page-id thumbs))))))
|
||||
|
||||
(sv/defmethod ::get-file-data-for-thumbnail
|
||||
"Retrieves the data for generate the thumbnail of the file. Used
|
||||
mainly for render thumbnails on dashboard."
|
||||
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params [:map {:title "get-file-data-for-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:features {:optional true} ::files/features]]
|
||||
::sm/result [:map {:title "PartialFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} :int]
|
||||
[:page :any]]}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
;; NOTE: we force here the "storage/pointer-map" feature, because
|
||||
;; it used internally only and is independent if user supports it
|
||||
;; or not.
|
||||
(let [feat (into #{"storage/pointer-map"} features)
|
||||
file (files/get-file conn file-id feat)]
|
||||
{:file-id file-id
|
||||
:revn (:revn file)
|
||||
:page (get-file-data-for-thumbnail conn file)})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MUTATION COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
|
||||
|
||||
(def sql:upsert-object-thumbnail
|
||||
"insert into file_object_thumbnail(file_id, object_id, data)
|
||||
values (?, ?, ?)
|
||||
on conflict(file_id, object_id) do
|
||||
update set data = ?;")
|
||||
|
||||
(defn upsert-file-object-thumbnail!
|
||||
[conn {:keys [file-id object-id data]}]
|
||||
(if data
|
||||
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
|
||||
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id})))
|
||||
|
||||
(s/def ::data (s/nilable ::us/string))
|
||||
(s/def ::object-id ::us/string)
|
||||
|
||||
(s/def ::upsert-file-object-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::object-id]
|
||||
:opt-un [::data]))
|
||||
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::doc/deprecated "1.19"
|
||||
::audit/skip true}
|
||||
[{: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)
|
||||
|
||||
(when-not (db/read-only? conn)
|
||||
(upsert-file-object-thumbnail! conn params)
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- MUTATION COMMAND: create-file-object-thumbnail
|
||||
|
||||
(def ^:private sql:create-object-thumbnail
|
||||
"insert into file_object_thumbnail(file_id, object_id, media_id)
|
||||
values (?, ?, ?)
|
||||
on conflict(file_id, object_id) do
|
||||
update set media_id = ?;")
|
||||
|
||||
(defn- create-file-object-thumbnail!
|
||||
[{:keys [::db/conn ::sto/storage]} file-id object-id media]
|
||||
|
||||
(let [path (:path media)
|
||||
mtype (:mtype media)
|
||||
hash (sto/calculate-hash path)
|
||||
data (-> (sto/content path)
|
||||
(sto/wrap-with-hash hash))
|
||||
media (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/deduplicate? false
|
||||
:content-type mtype
|
||||
:bucket "file-object-thumbnail"})]
|
||||
|
||||
(db/exec-one! conn [sql:create-object-thumbnail file-id object-id
|
||||
(:id media) (:id media)])))
|
||||
|
||||
|
||||
(def schema:create-file-object-thumbnail
|
||||
[:map {:title "create-file-object-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:object-id :string]
|
||||
[:media ::media/upload]])
|
||||
|
||||
(sv/defmethod ::create-file-object-thumbnail
|
||||
{:doc/added "1.19"
|
||||
::doc/module :files
|
||||
::audit/skip true
|
||||
::sm/params schema:create-file-object-thumbnail}
|
||||
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(media/validate-media-type! media)
|
||||
(media/validate-media-size! media)
|
||||
|
||||
(when-not (db/read-only? conn)
|
||||
(-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage)
|
||||
(assoc ::db/conn conn)
|
||||
(create-file-object-thumbnail! file-id object-id media))
|
||||
nil)))
|
||||
|
||||
;; --- MUTATION COMMAND: delete-file-object-thumbnail
|
||||
|
||||
(defn- delete-file-object-thumbnail!
|
||||
[{:keys [::db/conn ::sto/storage]} file-id object-id]
|
||||
(when-let [{:keys [media-id]} (db/get* conn :file-object-thumbnail
|
||||
{:file-id file-id
|
||||
:object-id object-id}
|
||||
{::db/for-update? true})]
|
||||
(when media-id
|
||||
(sto/del-object! storage media-id))
|
||||
|
||||
(db/delete! conn :file-object-thumbnail
|
||||
{:file-id file-id
|
||||
:object-id object-id})
|
||||
nil))
|
||||
|
||||
(s/def ::delete-file-object-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::object-id]))
|
||||
|
||||
(sv/defmethod ::delete-file-object-thumbnail
|
||||
{:doc/added "1.19"
|
||||
::doc/module :files
|
||||
::audit/skip true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id]}]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
(when-not (db/read-only? conn)
|
||||
(-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage)
|
||||
(assoc ::db/conn conn)
|
||||
(delete-file-object-thumbnail! file-id object-id))
|
||||
nil)))
|
||||
|
||||
;; --- MUTATION COMMAND: upsert-file-thumbnail
|
||||
|
||||
(def ^:private sql:upsert-file-thumbnail
|
||||
"insert into file_thumbnail (file_id, revn, data, props)
|
||||
values (?, ?, ?, ?::jsonb)
|
||||
on conflict(file_id, revn) do
|
||||
update set data = ?, props=?, updated_at=now();")
|
||||
|
||||
(defn- upsert-file-thumbnail!
|
||||
[conn {:keys [file-id revn data props]}]
|
||||
(let [props (db/tjson (or props {}))]
|
||||
(db/exec-one! conn [sql:upsert-file-thumbnail
|
||||
file-id revn data props data props])))
|
||||
|
||||
(s/def ::revn ::us/integer)
|
||||
(s/def ::props map?)
|
||||
|
||||
(s/def ::upsert-file-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::revn ::props ::data]))
|
||||
|
||||
(sv/defmethod ::upsert-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::doc/deprecated "1.19"
|
||||
::audit/skip true}
|
||||
[{: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)
|
||||
(when-not (db/read-only? conn)
|
||||
(upsert-file-thumbnail! conn params)
|
||||
nil)))
|
||||
|
||||
;; --- MUTATION COMMAND: create-file-thumbnail
|
||||
|
||||
(def ^:private sql:create-file-thumbnail
|
||||
"insert into file_thumbnail (file_id, revn, media_id, props)
|
||||
values (?, ?, ?, ?::jsonb)
|
||||
on conflict(file_id, revn) do
|
||||
update set media_id=?, props=?, updated_at=now();")
|
||||
|
||||
(defn- create-file-thumbnail!
|
||||
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
|
||||
(media/validate-media-type! media)
|
||||
(media/validate-media-size! media)
|
||||
|
||||
(let [props (db/tjson (or props {}))
|
||||
path (:path media)
|
||||
mtype (:mtype media)
|
||||
hash (sto/calculate-hash path)
|
||||
data (-> (sto/content path)
|
||||
(sto/wrap-with-hash hash))
|
||||
media (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/deduplicate? false
|
||||
:content-type mtype
|
||||
:bucket "file-thumbnail"})]
|
||||
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
|
||||
(:id media) props
|
||||
(:id media) props])
|
||||
media))
|
||||
|
||||
(sv/defmethod ::create-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.19"
|
||||
::doc/module :files
|
||||
::audit/skip true
|
||||
::sm/params [:map {:title "create-file-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:media ::media/upload]]
|
||||
}
|
||||
|
||||
[{: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)
|
||||
(when-not (db/read-only? conn)
|
||||
(let [media (-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage)
|
||||
(assoc ::db/conn conn)
|
||||
(create-file-thumbnail! params))]
|
||||
|
||||
{:uri (files/resolve-public-uri (:id media))}))))
|
||||
@@ -10,10 +10,7 @@
|
||||
[app.common.files.features :as ffeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.changes :as cpc]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as smg]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -24,7 +21,7 @@
|
||||
[app.metrics :as mtx]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
@@ -63,40 +60,6 @@
|
||||
(or (contains? o :changes)
|
||||
(contains? o :changes-with-metadata)))))
|
||||
|
||||
|
||||
;; --- SCHEMA
|
||||
|
||||
(sm/def! ::changes
|
||||
[:vector ::cpc/change])
|
||||
|
||||
(sm/def! ::change-with-metadata
|
||||
[:map {:title "ChangeWithMetadata"}
|
||||
[:changes ::changes]
|
||||
[:hint-origin {:optional true} :keyword]
|
||||
[:hint-events {:optional true} [:vector :string]]])
|
||||
|
||||
(sm/def! ::update-file-params
|
||||
[:map {:title "UpdateFileParams"}
|
||||
[:id ::sm/uuid]
|
||||
[:session-id ::sm/uuid]
|
||||
[:revn {:min 0} :int]
|
||||
[:features {:optional true
|
||||
:gen/max 3
|
||||
:gen/gen (smg/subseq files/supported-features)}
|
||||
::sm/set-of-strings]
|
||||
[:changes {:optional true} ::changes]
|
||||
[:changes-with-metadata {:optional true}
|
||||
[:vector ::change-with-metadata]]])
|
||||
|
||||
(sm/def! ::update-file-result
|
||||
[:vector {:title "UpdateFileResults"}
|
||||
[:map {:title "UpdateFileResult"}
|
||||
[:changes ::changes]
|
||||
[:file-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} :int]
|
||||
[:session-id ::sm/uuid]]])
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
;; File changes that affect to the library, and must be notified
|
||||
@@ -115,7 +78,8 @@
|
||||
(defn- library-change?
|
||||
[{:keys [type] :as change}]
|
||||
(or (contains? library-change-types type)
|
||||
(contains? file-change-types type)))
|
||||
(and (contains? file-change-types type)
|
||||
(some? (:component-id change)))))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
@@ -137,7 +101,7 @@
|
||||
|
||||
(defn- wrap-with-pointer-map-context
|
||||
[f]
|
||||
(fn [{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(fn [{:keys [conn] :as cfg} {:keys [id] :as file}]
|
||||
(binding [pmap/*tracked* (atom {})
|
||||
pmap/*load-fn* (partial files/load-pointer conn id)
|
||||
ffeat/*wrap-with-pointer-map-fn* pmap/wrap]
|
||||
@@ -162,22 +126,18 @@
|
||||
;; database.
|
||||
|
||||
(sv/defmethod ::update-file
|
||||
{::climit/id :update-file-by-id
|
||||
{::climit/queue :update-file
|
||||
::climit/key-fn :id
|
||||
::webhooks/event? true
|
||||
::webhooks/batch-timeout (dt/duration "2m")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
|
||||
::sm/params ::update-file-params
|
||||
::sm/result ::update-file-result
|
||||
|
||||
::doc/module :files
|
||||
::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(db/xact-lock! conn id)
|
||||
(let [cfg (assoc cfg ::db/conn conn)
|
||||
|
||||
(let [cfg (assoc cfg :conn conn)
|
||||
params (assoc params :profile-id profile-id)
|
||||
tpoint (dt/tpoint)]
|
||||
(-> (update-file cfg params)
|
||||
@@ -185,18 +145,17 @@
|
||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
|
||||
|
||||
(defn update-file
|
||||
[{:keys [::db/conn ::mtx/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)
|
||||
features (->> (concat (:features file)
|
||||
(:features params))
|
||||
(into (files/get-default-features))
|
||||
(into files/default-features)
|
||||
(files/check-features-compatibility!))]
|
||||
|
||||
(files/check-edition-permissions! conn profile-id (:id file))
|
||||
|
||||
(binding [ffeat/*current* features
|
||||
ffeat/*previous* (:features file)]
|
||||
|
||||
(let [update-fn (cond-> update-file*
|
||||
(contains? features "storage/pointer-map")
|
||||
(wrap-with-pointer-map-context)
|
||||
@@ -238,34 +197,24 @@
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))))
|
||||
|
||||
(defn- update-file-data
|
||||
[file changes]
|
||||
(-> file
|
||||
(update :revn inc)
|
||||
(update :data (fn [data]
|
||||
(cond-> data
|
||||
:always
|
||||
(-> (blob/decode)
|
||||
(assoc :id (:id file))
|
||||
(pmg/migrate-data))
|
||||
|
||||
(and (contains? ffeat/*current* "components/v2")
|
||||
(not (contains? ffeat/*previous* "components/v2")))
|
||||
(ctf/migrate-to-components-v2)
|
||||
|
||||
:always
|
||||
(-> (cp/process-changes changes)
|
||||
(blob/encode)))))))
|
||||
|
||||
|
||||
(defn- update-file*
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
|
||||
(let [;; Process the file data in the CLIMIT context; scheduling it
|
||||
;; to be executed on a separated executor for avoid to do the
|
||||
;; CPU intensive operation on vthread.
|
||||
file (-> (climit/configure cfg :update-file)
|
||||
(climit/submit! (partial update-file-data file changes)))]
|
||||
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
|
||||
(let [file (-> file
|
||||
(update :revn inc)
|
||||
(update :data (fn [data]
|
||||
(cond-> data
|
||||
:always
|
||||
(-> (blob/decode)
|
||||
(assoc :id (:id file))
|
||||
(pmg/migrate-data))
|
||||
|
||||
(and (contains? ffeat/*current* "components/v2")
|
||||
(not (contains? ffeat/*previous* "components/v2")))
|
||||
(ctf/migrate-to-components-v2)
|
||||
|
||||
:always
|
||||
(-> (cp/process-changes changes)
|
||||
(blob/encode))))))]
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
@@ -324,10 +273,11 @@
|
||||
(vec)))
|
||||
|
||||
(defn- send-notifications!
|
||||
[{:keys [::db/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)
|
||||
msgbus (::mbus/msgbus cfg)]
|
||||
|
||||
;; Asynchronously publish message to the msgbus
|
||||
(mbus/pub! msgbus
|
||||
:topic (:id file)
|
||||
:message {:type :file-change
|
||||
@@ -340,6 +290,7 @@
|
||||
(when (and (:is-shared file) (seq lchanges))
|
||||
(let [team-id (or (:team-id file)
|
||||
(files/get-team-id conn (:project-id file)))]
|
||||
;; Asynchronously publish message to the msgbus
|
||||
(mbus/pub! msgbus
|
||||
:topic team-id
|
||||
:message {:type :library-change
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.rpc.commands.fonts
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -15,7 +15,7 @@
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
@@ -25,7 +25,10 @@
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[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"})
|
||||
@@ -36,7 +39,6 @@
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::share-id ::us/uuid)
|
||||
(s/def ::style valid-style)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::weight valid-weight)
|
||||
@@ -48,8 +50,7 @@
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:opt-un [::team-id
|
||||
::file-id
|
||||
::project-id
|
||||
::share-id])
|
||||
::project-id])
|
||||
(fn [o]
|
||||
(or (contains? o :team-id)
|
||||
(contains? o :file-id)
|
||||
@@ -57,8 +58,8 @@
|
||||
|
||||
(sv/defmethod ::get-font-variants
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id share-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
[{: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
|
||||
@@ -76,12 +77,11 @@
|
||||
|
||||
(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]})
|
||||
perms (files/get-permissions conn profile-id file-id share-id)]
|
||||
(files/check-read-permissions! perms)
|
||||
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})))))
|
||||
{:team-id (:team-id project)
|
||||
:deleted-at nil})))))
|
||||
|
||||
|
||||
(declare create-font-variant)
|
||||
@@ -107,45 +107,50 @@
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id))))
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [data] :as params}]
|
||||
(letfn [(generate-missing! [data]
|
||||
(let [data (media/run {:cmd :generate-fonts :input 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
|
||||
:hint "invalid font upload, unable to generate missing font assets"))
|
||||
data))
|
||||
[{: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})))
|
||||
|
||||
(prepare-font [data mtype]
|
||||
;; 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)]
|
||||
(let [hash (sto/calculate-hash resource)
|
||||
content (-> (sto/content resource)
|
||||
(sto/wrap-with-hash hash))]
|
||||
{::sto/content content
|
||||
::sto/touched-at (dt/now)
|
||||
::sto/deduplicate? true
|
||||
:content-type mtype
|
||||
:bucket "team-font-variant"})))
|
||||
(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-files! [data]
|
||||
(let [otf-params (prepare-font data "font/otf")
|
||||
ttf-params (prepare-font data "font/ttf")
|
||||
wf1-params (prepare-font data "font/woff")
|
||||
wf2-params (prepare-font data "font/woff2")]
|
||||
(cond-> {}
|
||||
(some? otf-params)
|
||||
(assoc :otf (sto/put-object! storage otf-params))
|
||||
(some? ttf-params)
|
||||
(assoc :ttf (sto/put-object! storage ttf-params))
|
||||
(some? wf1-params)
|
||||
(assoc :woff1 (sto/put-object! storage wf1-params))
|
||||
(some? wf2-params)
|
||||
(assoc :woff2 (sto/put-object! storage wf2-params)))))
|
||||
(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")]
|
||||
|
||||
(insert-font-variant! [{:keys [woff1 woff2 otf ttf]}]
|
||||
(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)
|
||||
@@ -159,11 +164,13 @@
|
||||
:ttf-file-id (:id ttf)}))
|
||||
]
|
||||
|
||||
(let [data (-> (climit/configure cfg :process-font)
|
||||
(climit/submit! (partial generate-missing! data)))
|
||||
assets (persist-fonts-files! data)
|
||||
result (insert-font-variant! assets)]
|
||||
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
|
||||
(->> (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
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@
|
||||
"Performs the authentication using LDAP backend. Only works if LDAP
|
||||
is properly configured and enabled with `login-with-ldap` flag."
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth}
|
||||
::doc/added "1.15"}
|
||||
[{:keys [::main/props ::ldap/provider] :as cfg} params]
|
||||
(when-not provider
|
||||
(ex/raise :type :restriction
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
@@ -21,15 +20,12 @@
|
||||
[app.rpc.commands.projects :as proj]
|
||||
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.templates :as tmpl]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.walk :as walk]
|
||||
[promesa.exec :as px]))
|
||||
[clojure.walk :as walk]))
|
||||
|
||||
;; --- COMMAND: Duplicate File
|
||||
|
||||
@@ -235,13 +231,12 @@
|
||||
;; Defer all constraints
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
|
||||
(let [project (-> (db/get-by-id conn :project project-id)
|
||||
(assoc :is-pinned false))
|
||||
(let [project (db/get-by-id conn :project project-id)
|
||||
|
||||
files (db/query conn :file
|
||||
{:project-id (:id project)
|
||||
:deleted-at nil}
|
||||
{:columns [:id]})
|
||||
{:project-id (:id project)
|
||||
:deleted-at nil}
|
||||
{:columns [:id]})
|
||||
|
||||
project (cond-> project
|
||||
(string? name)
|
||||
@@ -323,18 +318,6 @@
|
||||
;; delete possible broken relations on moved files
|
||||
(db/exec-one! conn [sql:delete-broken-relations pids])
|
||||
|
||||
;; Update the modification date of the all affected projects
|
||||
;; ensuring that the destination project is the most recent one.
|
||||
(doseq [project-id (into (list project-id) source)]
|
||||
|
||||
;; NOTE: as this is executed on virtual thread, sleeping does
|
||||
;; not causes major issues, and allows an easy way to set a
|
||||
;; trully different modification date to each file.
|
||||
(px/sleep 10)
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id}))
|
||||
|
||||
nil))
|
||||
|
||||
(s/def ::ids (s/every ::us/uuid :kind set?))
|
||||
@@ -377,6 +360,7 @@
|
||||
|
||||
nil))
|
||||
|
||||
|
||||
(s/def ::move-project
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id ::project-id]))
|
||||
@@ -391,54 +375,46 @@
|
||||
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn- clone-template!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id template-id project-id]}]
|
||||
(let [template (tmpl/get-template-stream cfg template-id)
|
||||
(declare clone-template)
|
||||
|
||||
(s/def ::template-id ::us/not-empty-string)
|
||||
(s/def ::clone-template
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::project-id ::template-id]))
|
||||
|
||||
(sv/defmethod ::clone-template
|
||||
"Clone into the specified project the template by its id."
|
||||
{::doc/added "1.16"
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(-> (assoc cfg :conn conn)
|
||||
(clone-template (assoc params :profile-id profile-id)))))
|
||||
|
||||
(defn- clone-template
|
||||
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
|
||||
(let [template (d/seek #(= (:id %) template-id) templates)
|
||||
project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
|
||||
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
||||
|
||||
(when-not template
|
||||
(ex/raise :type :not-found
|
||||
:code :template-not-found
|
||||
:hint "template not found"))
|
||||
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
||||
|
||||
(-> cfg
|
||||
;; FIXME: maybe reuse the conn instead of creating more
|
||||
;; connections in the import process?
|
||||
(dissoc ::db/conn)
|
||||
(assoc ::binfile/input template)
|
||||
(assoc ::binfile/input (:path template))
|
||||
(assoc ::binfile/project-id (:id project))
|
||||
(assoc ::binfile/ignore-index-errors? true)
|
||||
(assoc ::binfile/migrate? true)
|
||||
(binfile/import!))))
|
||||
|
||||
(def schema:clone-template
|
||||
[:map {:title "clone-template"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:template-id ::sm/word-string]])
|
||||
|
||||
(sv/defmethod ::clone-template
|
||||
"Clone into the specified project the template by its id."
|
||||
{::doc/added "1.16"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:clone-template}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(-> (assoc cfg ::db/conn conn)
|
||||
(clone-template! (assoc params :profile-id profile-id)))))
|
||||
|
||||
;; --- COMMAND: Get list of builtin templates
|
||||
;; --- COMMAND: Retrieve list of builtin templates
|
||||
|
||||
(s/def ::retrieve-list-of-builtin-templates any?)
|
||||
|
||||
(sv/defmethod ::retrieve-list-of-builtin-templates
|
||||
{::doc/added "1.10"
|
||||
::doc/deprecated "1.19"}
|
||||
[cfg _params]
|
||||
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))
|
||||
|
||||
(sv/defmethod ::get-builtin-templates
|
||||
{::doc/added "1.19"}
|
||||
[cfg _params]
|
||||
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))
|
||||
(mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg)))
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as climit]
|
||||
@@ -23,9 +22,13 @@
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]))
|
||||
[datoteka.io :as io]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
@@ -41,6 +44,15 @@
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
(defn validate-content-size!
|
||||
[content]
|
||||
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
|
||||
(ex/raise :type :restriction
|
||||
:code :media-max-file-size-reached
|
||||
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
||||
(:size content)
|
||||
default-max-file-size))))
|
||||
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(declare create-file-media-object)
|
||||
@@ -59,15 +71,9 @@
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(media/validate-media-type! content)
|
||||
(media/validate-media-size! content)
|
||||
(let [object (create-file-media-object cfg params)
|
||||
props {:name (:name params)
|
||||
:file-id file-id
|
||||
:is-local (:is-local params)
|
||||
:size (:size content)
|
||||
:mtype (:mtype content)}]
|
||||
(with-meta object
|
||||
{::audit/replace-props props}))))
|
||||
(validate-content-size! content)
|
||||
|
||||
(create-file-media-object cfg params)))
|
||||
|
||||
(defn- big-enough-for-thumbnail?
|
||||
"Checks if the provided image info is big enough for
|
||||
@@ -104,62 +110,71 @@
|
||||
;; witch holds the reference to storage object (it some kind of
|
||||
;; inverse, soft referential integrity).
|
||||
|
||||
(defn- process-main-image
|
||||
[info]
|
||||
(let [hash (sto/calculate-hash (:path info))
|
||||
data (-> (sto/content (:path info))
|
||||
(sto/wrap-with-hash hash))]
|
||||
{::sto/content data
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (:ts info)
|
||||
:content-type (:mtype info)
|
||||
:bucket "file-media-object"}))
|
||||
|
||||
(defn- process-thumb-image
|
||||
[info]
|
||||
(let [thumb (-> thumbnail-options
|
||||
(assoc :cmd :generic-thumbnail)
|
||||
(assoc :input info)
|
||||
(media/run))
|
||||
hash (sto/calculate-hash (:data thumb))
|
||||
data (-> (sto/content (:data thumb) (:size thumb))
|
||||
(sto/wrap-with-hash hash))]
|
||||
{::sto/content data
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (:ts info)
|
||||
:content-type (:mtype thumb)
|
||||
:bucket "file-media-object"}))
|
||||
|
||||
(defn- process-image
|
||||
[content]
|
||||
(let [info (media/run {:cmd :info :input content})]
|
||||
(cond-> info
|
||||
(and (not (svg-image? info))
|
||||
(big-enough-for-thumbnail? info))
|
||||
(assoc ::thumb (process-thumb-image info))
|
||||
|
||||
:always
|
||||
(assoc ::image (process-main-image info)))))
|
||||
|
||||
(defn create-file-media-object
|
||||
[{:keys [::sto/storage ::db/pool] :as cfg}
|
||||
[{:keys [::sto/storage ::db/pool climit ::wrk/executor]}
|
||||
{:keys [id file-id is-local name content]}]
|
||||
(letfn [;; Function responsible to retrieve the file information, as
|
||||
;; it is synchronous operation it should be wrapped into
|
||||
;; with-dispatch macro.
|
||||
(get-info [content]
|
||||
(climit/with-dispatch (:process-image climit)
|
||||
(media/run {:cmd :info :input content})))
|
||||
|
||||
(let [result (-> (climit/configure cfg :process-image)
|
||||
(climit/submit! (partial process-image content)))
|
||||
;; Function responsible of calculating cryptographyc hash of
|
||||
;; the provided data.
|
||||
(calculate-hash [data]
|
||||
(px/with-dispatch executor
|
||||
(sto/calculate-hash data)))
|
||||
|
||||
image (sto/put-object! storage (::image result))
|
||||
thumb (when-let [params (::thumb result)]
|
||||
(sto/put-object! storage params))]
|
||||
;; Function responsible of generating thumnail. As it is synchronous
|
||||
;; opetation, it should be wrapped into with-dispatch macro
|
||||
(generate-thumbnail [info]
|
||||
(climit/with-dispatch (:process-image climit)
|
||||
(media/run (assoc thumbnail-options
|
||||
:cmd :generic-thumbnail
|
||||
:input info))))
|
||||
|
||||
(db/exec-one! pool [sql:create-file-media-object
|
||||
(or id (uuid/next))
|
||||
file-id is-local name
|
||||
(:id image)
|
||||
(:id thumb)
|
||||
(:width result)
|
||||
(:height result)
|
||||
(:mtype result)])))
|
||||
(create-thumbnail [info]
|
||||
(when (and (not (svg-image? info))
|
||||
(big-enough-for-thumbnail? info))
|
||||
(p/let [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
|
||||
::sto/touched-at (dt/now)
|
||||
:content-type (:mtype thumb)
|
||||
:bucket "file-media-object"}))))
|
||||
|
||||
(create-image [info]
|
||||
(p/let [data (:path info)
|
||||
hash (calculate-hash data)
|
||||
content (-> (sto/content data)
|
||||
(sto/wrap-with-hash hash))]
|
||||
(sto/put-object! storage
|
||||
{::sto/content content
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (dt/now)
|
||||
:content-type (:mtype info)
|
||||
:bucket "file-media-object"})))
|
||||
|
||||
(insert-into-database [info image thumb]
|
||||
(px/with-dispatch executor
|
||||
(db/exec-one! pool [sql:create-file-media-object
|
||||
(or id (uuid/next))
|
||||
file-id is-local name
|
||||
(:id image)
|
||||
(:id thumb)
|
||||
(:width info)
|
||||
(:height info)
|
||||
(:mtype info)])))]
|
||||
|
||||
(p/let [info (get-info content)
|
||||
thumb (create-thumbnail info)
|
||||
image (create-image info)]
|
||||
(insert-into-database info image thumb))))
|
||||
|
||||
;; --- Create File Media Object (from URL)
|
||||
|
||||
@@ -171,16 +186,15 @@
|
||||
:opt-un [::id ::name]))
|
||||
|
||||
(sv/defmethod ::create-file-media-object-from-url
|
||||
{::doc/added "1.17"
|
||||
::doc/deprecated "1.19"}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(create-file-media-object-from-url cfg params)))
|
||||
|
||||
(defn- download-image
|
||||
[{:keys [::http/client]} uri]
|
||||
(letfn [(parse-and-validate [{:keys [headers] :as response}]
|
||||
(defn- create-file-media-object-from-url
|
||||
[cfg {:keys [url name] :as params}]
|
||||
(letfn [(parse-and-validate-size [headers]
|
||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||
mtype (get headers "content-type")
|
||||
format (cm/mtype->format mtype)
|
||||
@@ -203,34 +217,32 @@
|
||||
:code :media-type-not-allowed
|
||||
:hint "seems like the url points to an invalid media object"))
|
||||
|
||||
{:size size :mtype mtype :format format}))]
|
||||
{:size size
|
||||
:mtype mtype
|
||||
:format format}))
|
||||
|
||||
(let [{:keys [body] :as response} (http/req! client
|
||||
{:method :get :uri uri}
|
||||
{:response-type :input-stream :sync? true})
|
||||
{:keys [size mtype]} (parse-and-validate response)
|
||||
(download-media [uri]
|
||||
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
||||
(p/then process-response)))
|
||||
|
||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||
written (io/write-to-file! body path :size size)]
|
||||
(process-response [{:keys [body headers] :as response}]
|
||||
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||
written (io/write-to-file! body path :size size)]
|
||||
|
||||
(when (not= written size)
|
||||
(ex/raise :type :internal
|
||||
:code :mismatch-write-size
|
||||
:hint "unexpected state: unable to write to file"))
|
||||
(when (not= written size)
|
||||
(ex/raise :type :internal
|
||||
:code :mismatch-write-size
|
||||
:hint "unexpected state: unable to write to file"))
|
||||
|
||||
{:filename "tempfile"
|
||||
:size size
|
||||
:path path
|
||||
:mtype mtype})))
|
||||
{:filename "tempfile"
|
||||
:size size
|
||||
:path path
|
||||
:mtype mtype}))]
|
||||
|
||||
|
||||
(defn- create-file-media-object-from-url
|
||||
[cfg {:keys [url name] :as params}]
|
||||
(let [content (download-image cfg url)
|
||||
params (-> params
|
||||
(assoc :content content)
|
||||
(assoc :name (or name (:filename content))))]
|
||||
(create-file-media-object cfg params)))
|
||||
(p/let [content (download-media url)]
|
||||
(->> (merge params {:content content :name (or name (:filename content))})
|
||||
(create-file-media-object cfg)))))
|
||||
|
||||
;; --- Clone File Media object (Upload and create from url)
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -27,41 +26,26 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(declare check-profile-existence!)
|
||||
(declare decode-row)
|
||||
(declare derive-password)
|
||||
(declare filter-props)
|
||||
(declare get-profile)
|
||||
(declare strip-private-attrs)
|
||||
(declare verify-password)
|
||||
|
||||
(def schema:profile
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:email ::sm/email]
|
||||
[:is-active {:optional true} :boolean]
|
||||
[:is-blocked {:optional true} :boolean]
|
||||
[:is-demo {:optional true} :boolean]
|
||||
[:is-muted {:optional true} :boolean]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:default-project-id {:optional true} ::sm/uuid]
|
||||
[:default-team-id {:optional true} ::sm/uuid]
|
||||
[:props {:optional true}
|
||||
[:map-of {:title "ProfileProps"} :keyword :any]]])
|
||||
|
||||
(def profile?
|
||||
(sm/pred-fn schema:profile))
|
||||
(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"
|
||||
::sm/result schema:profile}
|
||||
::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
|
||||
@@ -79,24 +63,22 @@
|
||||
(-> (db/get-by-id conn :profile id attrs)
|
||||
(decode-row)))
|
||||
|
||||
|
||||
;; --- MUTATION: Update Profile (own)
|
||||
|
||||
(def schema:update-profile
|
||||
[:map {:title "update-profile"}
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:lang {:optional true} [:string {:max 5}]]
|
||||
[:theme {:optional true} [:string {:max 250}]]])
|
||||
(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"
|
||||
::sm/params schema:update-profile
|
||||
::sm/result schema:profile}
|
||||
{::doc/added "1.0"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid profile data"
|
||||
(profile? 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
|
||||
@@ -130,21 +112,18 @@
|
||||
(declare update-profile-password!)
|
||||
(declare invalidate-profile-session!)
|
||||
|
||||
(def schema:update-profile-password
|
||||
[:map {:title "update-profile-password"}
|
||||
[:password [::sm/word-string {:max 500}]]
|
||||
;; Social registered users don't have old-password
|
||||
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::old-password ::us/not-empty-string)
|
||||
|
||||
(s/def ::update-profile-password
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::password ::old-password]))
|
||||
|
||||
(sv/defmethod ::update-profile-password
|
||||
{:doc/added "1.0"
|
||||
::sm/params schema:update-profile-password
|
||||
::sm/result :nil}
|
||||
|
||||
{::climit/queue :auth}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::db/conn conn)
|
||||
profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
(let [profile (validate-password! conn (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
|
||||
(when (= (str/lower (:email profile))
|
||||
@@ -154,93 +133,101 @@
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
(update-profile-password! conn (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
(invalidate-profile-session! conn profile-id session-id)
|
||||
nil)))
|
||||
|
||||
(defn- invalidate-profile-session!
|
||||
"Removes all sessions except the current one."
|
||||
[{:keys [::db/conn]} profile-id session-id]
|
||||
[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!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
|
||||
[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 (verify-password cfg old-password (:password profile)))))
|
||||
(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}]
|
||||
(when-not (db/read-only? conn)
|
||||
(db/update! conn :profile
|
||||
{:password (auth/derive-password password)}
|
||||
{:id id})))
|
||||
(db/update! conn :profile
|
||||
{:password (auth/derive-password password)}
|
||||
{:id id}))
|
||||
|
||||
;; --- MUTATION: Update Photo
|
||||
|
||||
(declare upload-photo)
|
||||
(declare update-profile-photo)
|
||||
|
||||
(def schema:update-profile-photo
|
||||
[:map {:title "update-profile-photo"}
|
||||
[:file ::media/upload]])
|
||||
(s/def ::file ::media/upload)
|
||||
(s/def ::update-profile-photo
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file]))
|
||||
|
||||
(sv/defmethod ::update-profile-photo
|
||||
{:doc/added "1.1"
|
||||
::sm/params schema:update-profile-photo
|
||||
::sm/result :nil}
|
||||
[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] :as cfg} {:keys [profile-id file] :as params}]
|
||||
(let [photo (upload-photo cfg params)
|
||||
profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
|
||||
[{: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))
|
||||
;; 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})
|
||||
;; 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)}}))))
|
||||
|
||||
(defn- generate-thumbnail!
|
||||
[file]
|
||||
(let [input (media/run {:cmd :info :input file})
|
||||
thumb (media/run {:cmd :profile-thumbnail
|
||||
:format :jpeg
|
||||
:quality 85
|
||||
:width 256
|
||||
:height 256
|
||||
:input input})
|
||||
hash (sto/calculate-hash (:data thumb))
|
||||
content (-> (sto/content (:data thumb) (:size thumb))
|
||||
(sto/wrap-with-hash hash))]
|
||||
{::sto/content content
|
||||
::sto/deduplicate? true
|
||||
:bucket "profile"
|
||||
:content-type (:mtype thumb)}))
|
||||
(-> (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] :as cfg} {:keys [file]}]
|
||||
(let [params (-> (climit/configure cfg :process-image)
|
||||
(climit/submit! (partial generate-thumbnail! file)))]
|
||||
(sto/put-object! storage params)))
|
||||
[{: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
|
||||
@@ -248,13 +235,11 @@
|
||||
(declare ^:private request-email-change!)
|
||||
(declare ^:private change-email-immediately!)
|
||||
|
||||
(def schema:request-email-change
|
||||
[:map {:title "request-email-change"}
|
||||
[:email ::sm/email]])
|
||||
(s/def ::request-email-change
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::email]))
|
||||
|
||||
(sv/defmethod ::request-email-change
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema: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)
|
||||
@@ -315,13 +300,12 @@
|
||||
|
||||
;; --- MUTATION: Update Profile Props
|
||||
|
||||
(def schema:update-profile-props
|
||||
[:map {:title "update-profile-props"}
|
||||
[:props [:map-of :keyword :any]]])
|
||||
(s/def ::props map?)
|
||||
(s/def ::update-profile-props
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::props]))
|
||||
|
||||
(sv/defmethod ::update-profile-props
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema: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)
|
||||
@@ -341,12 +325,15 @@
|
||||
|
||||
(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
|
||||
{::doc/added "1.0"}
|
||||
[{: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)
|
||||
@@ -430,17 +417,6 @@
|
||||
[props]
|
||||
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
||||
|
||||
(defn derive-password
|
||||
[cfg password]
|
||||
(when password
|
||||
(-> (climit/configure cfg :derive-password)
|
||||
(climit/submit! (partial auth/derive-password password)))))
|
||||
|
||||
(defn verify-password
|
||||
[cfg password password-data]
|
||||
(-> (climit/configure cfg :derive-password)
|
||||
(climit/submit! (partial auth/verify-password password password-data))))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [props] :as row}]
|
||||
(cond-> row
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.rpc.commands.projects
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
@@ -80,7 +79,7 @@
|
||||
(sv/defmethod ::get-projects
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-projects conn profile-id team-id)))
|
||||
|
||||
@@ -115,7 +114,7 @@
|
||||
(sv/defmethod ::get-all-projects
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(get-all-projects conn profile-id)))
|
||||
|
||||
(def sql:all-projects
|
||||
@@ -158,7 +157,7 @@
|
||||
(sv/defmethod ::get-project
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [project (db/get-by-id conn :project id)]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
project)))
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
from file as f
|
||||
inner join projects as pr on (f.project_id = pr.id)
|
||||
where f.name ilike ('%' || ? || '%')
|
||||
and (f.deleted_at is null or f.deleted_at > now())
|
||||
order by f.created_at asc")
|
||||
|
||||
(defn search-files
|
||||
@@ -64,7 +63,6 @@
|
||||
:opt-un [::search-term]))
|
||||
|
||||
(sv/defmethod ::search-files
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}]
|
||||
(some->> search-term (search-files pool profile-id team-id)))
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
(ns app.rpc.commands.teams
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -29,8 +27,11 @@
|
||||
[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]))
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
@@ -61,18 +62,12 @@
|
||||
:can-edit (or is-owner is-admin can-edit)
|
||||
:can-read true})))
|
||||
|
||||
(def has-admin-permissions?
|
||||
(perms/make-admin-predicate-fn get-permissions))
|
||||
|
||||
(def has-edit-permissions?
|
||||
(perms/make-edition-predicate-fn get-permissions))
|
||||
|
||||
(def has-read-permissions?
|
||||
(perms/make-read-predicate-fn get-permissions))
|
||||
|
||||
(def check-admin-permissions!
|
||||
(perms/make-check-fn has-admin-permissions?))
|
||||
|
||||
(def check-edition-permissions!
|
||||
(perms/make-check-fn has-edit-permissions?))
|
||||
|
||||
@@ -83,15 +78,13 @@
|
||||
|
||||
(declare retrieve-teams)
|
||||
|
||||
(def counter (volatile! 0))
|
||||
|
||||
(s/def ::get-teams
|
||||
(s/keys :req [::rpc/profile-id]))
|
||||
|
||||
(sv/defmethod ::get-teams
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(retrieve-teams conn profile-id)))
|
||||
|
||||
(def sql:teams
|
||||
@@ -136,7 +129,7 @@
|
||||
(sv/defmethod ::get-team
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(retrieve-team conn profile-id id)))
|
||||
|
||||
(defn retrieve-team
|
||||
@@ -177,7 +170,7 @@
|
||||
(sv/defmethod ::get-team-members
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(retrieve-team-members conn team-id)))
|
||||
|
||||
@@ -195,7 +188,7 @@
|
||||
(sv/defmethod ::get-team-users
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(if team-id
|
||||
(do
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
@@ -253,7 +246,7 @@
|
||||
(sv/defmethod ::get-team-stats
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(retrieve-team-stats conn team-id)))
|
||||
|
||||
@@ -284,7 +277,7 @@
|
||||
(sv/defmethod ::get-team-invitations
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(get-team-invitations conn team-id)))
|
||||
|
||||
@@ -595,23 +588,23 @@
|
||||
(update-team-photo cfg (assoc params :profile-id profile-id))))
|
||||
|
||||
(defn update-team-photo
|
||||
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
(let [team (retrieve-team pool profile-id team-id)
|
||||
photo (profile/upload-photo cfg params)]
|
||||
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
(p/let [team (px/with-dispatch executor
|
||||
(retrieve-team pool profile-id team-id))
|
||||
photo (profile/upload-photo cfg params)]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(check-admin-permissions! conn profile-id team-id)
|
||||
;; Mark object as touched for make it ellegible for tentative
|
||||
;; garbage collection.
|
||||
(when-let [id (:photo-id team)]
|
||||
(sto/touch-object! storage id))
|
||||
;; Mark object as touched for make it ellegible for tentative
|
||||
;; garbage collection.
|
||||
(when-let [id (:photo-id team)]
|
||||
(sto/touch-object! storage id))
|
||||
|
||||
;; Save new photo
|
||||
(db/update! pool :team
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
;; Save new photo
|
||||
(db/update! pool :team
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
|
||||
(assoc team :photo-id (:id photo))))
|
||||
|
||||
(assoc team :photo-id (:id photo)))))
|
||||
|
||||
;; --- Mutation: Create Team Invitation
|
||||
|
||||
@@ -701,13 +694,13 @@
|
||||
(l/info :hint "invitation token" :token itoken))
|
||||
|
||||
(audit/submit! cfg
|
||||
{::audit/type "action"
|
||||
::audit/name (if updated?
|
||||
"update-team-invitation"
|
||||
"create-team-invitation")
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/props (-> (dissoc tprops :profile-id)
|
||||
(d/without-nils))})
|
||||
{:type "action"
|
||||
:name (if updated?
|
||||
"update-team-invitation"
|
||||
"create-team-invitation")
|
||||
:profile-id (:id profile)
|
||||
:props (-> (dissoc tprops :profile-id)
|
||||
(d/without-nils))})
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-team
|
||||
@@ -720,22 +713,24 @@
|
||||
|
||||
itoken))))
|
||||
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role [::sm/one-of #{:owner :admin :editor}]]
|
||||
[:emails ::sm/set-of-emails]])
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::emails ::us/set-of-valid-emails)
|
||||
(s/def ::create-team-invitations
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id ::role]
|
||||
:opt-un [::email ::emails]))
|
||||
|
||||
(sv/defmethod ::create-team-invitations
|
||||
"A rpc call that allow to send a single or multiple invitations to
|
||||
join the team."
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-invitations}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (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)]
|
||||
team (db/get-by-id conn :team team-id)
|
||||
emails (cond-> (or emails #{}) (string? email) (conj email))]
|
||||
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
@@ -758,22 +753,14 @@
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
(let [cfg (assoc cfg ::db/conn conn)
|
||||
members (->> (db/exec! conn [sql:team-members team-id])
|
||||
(into #{} (map :email)))
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send inviation to already existing members
|
||||
(remove (partial contains? members))
|
||||
(map (fn [email]
|
||||
{:email (str/lower email)
|
||||
:team team
|
||||
:profile profile
|
||||
:role role}))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
invitations (->> emails
|
||||
(map (fn [email]
|
||||
{:email (str/lower email)
|
||||
:team team
|
||||
:profile profile
|
||||
:role role}))
|
||||
(keep (partial create-invitation cfg)))]
|
||||
(with-meta (vec invitations)
|
||||
{::audit/props {:invitations (count invitations)}})))))
|
||||
|
||||
|
||||
@@ -815,13 +802,13 @@
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(audit/submit! cfg
|
||||
{::audit/type "command"
|
||||
::audit/name "create-team-invitations"
|
||||
::audit/profile-id profile-id
|
||||
::audit/props {:emails emails
|
||||
:role role
|
||||
:profile-id profile-id
|
||||
:invitations (count emails)}})
|
||||
{:type "command"
|
||||
:name "create-team-invitations"
|
||||
:profile-id profile-id
|
||||
:props {:emails emails
|
||||
:role role
|
||||
:profile-id profile-id
|
||||
:invitations (count emails)}})
|
||||
|
||||
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
|
||||
(sv/defmethod ::verify-token
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth}
|
||||
::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [claims (tokens/verify (::main/props cfg) {:token token})
|
||||
|
||||
@@ -6,16 +6,15 @@
|
||||
|
||||
(ns app.rpc.commands.viewer
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.comments :as comments]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.cond :as-alias cond]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]))
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- QUERY: View Only Bundle
|
||||
|
||||
@@ -27,18 +26,12 @@
|
||||
[conn file-id profile-id features]
|
||||
(let [file (files/get-file conn file-id features)
|
||||
project (get-project conn (:project-id file))
|
||||
libs (files/get-file-libraries conn file-id)
|
||||
libs (files/get-file-libraries conn file-id features)
|
||||
users (comments/get-file-comments-users conn file-id profile-id)
|
||||
|
||||
links (->> (db/query conn :share-link {:file-id file-id})
|
||||
(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)))))
|
||||
(update row :pages db/decode-pgarray #{}))))
|
||||
|
||||
fonts (db/query conn :team-font-variant
|
||||
{:team-id (:team-id project)
|
||||
@@ -77,18 +70,20 @@
|
||||
(update :data remove-not-allowed-pages (:pages perms))
|
||||
|
||||
:always
|
||||
(update :data select-keys [:id :options :pages :pages-index :components]))))))
|
||||
(update :data select-keys [:id :options :pages :pages-index]))))))
|
||||
|
||||
(sm/def! ::get-view-only-bundle
|
||||
[:map {:title "get-view-only-bundle"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]
|
||||
[:features {:optional true} ::files/features]])
|
||||
(s/def ::get-view-only-bundle
|
||||
(s/keys :req-un [::files/file-id]
|
||||
:opt-un [::files/share-id
|
||||
::files/features]
|
||||
:opt [::rpc/profile-id]))
|
||||
|
||||
(sv/defmethod ::get-view-only-bundle
|
||||
{::rpc/auth false
|
||||
::doc/added "1.17"
|
||||
::sm/params ::get-view-only-bundle}
|
||||
::cond/get-object #(files/get-minimal-file %1 (:file-id %2))
|
||||
::cond/key-fn files/get-file-etag
|
||||
::cond/reuse-key? true
|
||||
::doc/added "1.17"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(get-view-only-bundle conn (assoc params :profile-id profile-id))))
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.rpc.commands.webhooks
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
@@ -19,8 +18,10 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [uri] :as row}]
|
||||
@@ -47,25 +48,30 @@
|
||||
|
||||
(defn- validate-webhook!
|
||||
[cfg whook params]
|
||||
(when (not= (:uri whook) (:uri params))
|
||||
(let [response (ex/try!
|
||||
(http/req! cfg
|
||||
{:method :head
|
||||
:uri (str (:uri params))
|
||||
:timeout (dt/duration "3s")}
|
||||
{:sync? true}))]
|
||||
(if (ex/exception? response)
|
||||
(if-let [hint (webhooks/interpret-exception response)]
|
||||
(ex/raise :type :validation
|
||||
:code :webhook-validation
|
||||
:hint hint)
|
||||
(ex/raise :type :internal
|
||||
:code :webhook-validation
|
||||
:cause response))
|
||||
(when-let [hint (webhooks/interpret-response response)]
|
||||
(ex/raise :type :validation
|
||||
:code :webhook-validation
|
||||
:hint hint))))))
|
||||
(letfn [(handle-exception [exception]
|
||||
(if-let [hint (webhooks/interpret-exception exception)]
|
||||
(ex/raise :type :validation
|
||||
:code :webhook-validation
|
||||
:hint hint)
|
||||
(ex/raise :type :internal
|
||||
:code :webhook-validation
|
||||
:cause exception)))
|
||||
|
||||
(handle-response [response]
|
||||
(when-let [hint (webhooks/interpret-response response)]
|
||||
(ex/raise :type :validation
|
||||
:code :webhook-validation
|
||||
:hint hint)))]
|
||||
|
||||
(if (not= (:uri whook) (:uri params))
|
||||
(->> (http/req! cfg {:method :head
|
||||
:uri (str (:uri params))
|
||||
:timeout (dt/duration "3s")})
|
||||
(p/hmap (fn [response exception]
|
||||
(if exception
|
||||
(handle-exception exception)
|
||||
(handle-response response)))))
|
||||
(p/resolved nil))))
|
||||
|
||||
(defn- validate-quotes!
|
||||
[{:keys [::db/pool]} {:keys [team-id]}]
|
||||
@@ -100,22 +106,22 @@
|
||||
|
||||
(sv/defmethod ::create-webhook
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(check-edition-permissions! pool profile-id team-id)
|
||||
(validate-quotes! cfg params)
|
||||
(validate-webhook! cfg nil params)
|
||||
(insert-webhook! cfg params))
|
||||
(->> (validate-webhook! cfg nil params)
|
||||
(p/fmap executor (fn [_] (insert-webhook! cfg params)))))
|
||||
|
||||
(s/def ::update-webhook
|
||||
(s/keys :req-un [::id ::uri ::mtype ::is-active]))
|
||||
|
||||
(sv/defmethod ::update-webhook
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
|
||||
(check-edition-permissions! pool profile-id (:team-id whook))
|
||||
(validate-webhook! cfg whook params)
|
||||
(update-webhook! cfg whook params)))
|
||||
(->> (validate-webhook! cfg whook params)
|
||||
(p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
|
||||
|
||||
(s/def ::delete-webhook
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
@@ -143,7 +149,7 @@
|
||||
|
||||
(sv/defmethod ::get-webhooks
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(->> (db/exec! conn [sql:get-webhooks team-id])
|
||||
(mapv decode-row))))
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
[app.common.logging :as l]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.services :as-alias sv]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.hash :as bh]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[yetti.response :as yrs]))
|
||||
|
||||
(def
|
||||
@@ -36,32 +36,32 @@
|
||||
:doc "Runtime flag for enable/disable conditional processing of RPC methods."}
|
||||
*enabled* false)
|
||||
|
||||
(defn- encode
|
||||
[s]
|
||||
(-> s
|
||||
bh/blake2b-256
|
||||
bc/bytes->b64u
|
||||
bc/bytes->str))
|
||||
|
||||
(defn- fmt-key
|
||||
[s]
|
||||
(str "W/\"" (encode s) "\""))
|
||||
(when s
|
||||
(str "W/\"" s "\"")))
|
||||
|
||||
(defn wrap
|
||||
[_ f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
|
||||
[{:keys [executor]} f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
|
||||
(if (and (ifn? get-object) (ifn? key-fn))
|
||||
(do
|
||||
(l/debug :hint "instrumenting method" :service (::sv/name mdata))
|
||||
(fn [cfg {:keys [::key] :as params}]
|
||||
(if *enabled*
|
||||
(let [key' (when (or key reuse-key?)
|
||||
(some->> (get-object cfg params) (key-fn params) (fmt-key)))]
|
||||
(if (and (some? key) (= key key'))
|
||||
(fn [_] {::yrs/status 304})
|
||||
(let [result (f cfg params)
|
||||
etag (or (and reuse-key? key')
|
||||
(some-> result meta ::key fmt-key)
|
||||
(some-> result key-fn fmt-key))]
|
||||
(rph/with-header result "etag" etag))))
|
||||
(->> (if (or key reuse-key?)
|
||||
(->> (px/submit! executor (partial get-object cfg params))
|
||||
(p/map key-fn)
|
||||
(p/map fmt-key))
|
||||
(p/resolved nil))
|
||||
(p/mapcat (fn [key']
|
||||
(if (and (some? key)
|
||||
(= key key'))
|
||||
(p/resolved (fn [_] (yrs/response 304)))
|
||||
(->> (f cfg params)
|
||||
(p/map (fn [result]
|
||||
(->> (or (and reuse-key? key')
|
||||
(-> result meta ::key fmt-key)
|
||||
(-> result key-fn fmt-key))
|
||||
(rph/with-header result "etag")))))))))
|
||||
(f cfg params))))
|
||||
f))
|
||||
|
||||
@@ -8,197 +8,67 @@
|
||||
"API autogenerated documentation."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-js-like :as smdj]
|
||||
[app.common.schema.desc-native :as smdn]
|
||||
[app.common.schema.openapi :as oapi]
|
||||
[app.common.schema.registry :as sr]
|
||||
[app.config :as cf]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.util.json :as json]
|
||||
[app.util.services :as sv]
|
||||
[app.util.template :as tmpl]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[malli.transform :as mt]
|
||||
[pretty-spec.core :as ps]
|
||||
[yetti.response :as yrs]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DOC (human readable)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(defn- get-spec-str
|
||||
[k]
|
||||
(with-out-str
|
||||
(ps/pprint (s/form k)
|
||||
{:ns-aliases {"clojure.spec.alpha" "s"
|
||||
"clojure.core.specs.alpha" "score"
|
||||
"clojure.core" nil}})))
|
||||
|
||||
(defn- prepare-doc-context
|
||||
(defn- prepare-context
|
||||
[methods]
|
||||
(letfn [(fmt-spec [mdata]
|
||||
(when-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
|
||||
(with-out-str
|
||||
(ps/pprint (s/form spec)
|
||||
{:ns-aliases {"clojure.spec.alpha" "s"
|
||||
"clojure.core.specs.alpha" "score"
|
||||
"clojure.core" nil}}))))
|
||||
|
||||
(fmt-schema [type mdata key]
|
||||
(when-let [schema (get mdata key)]
|
||||
(if (= type :js)
|
||||
(smdj/describe (sm/schema schema) {::smdj/max-level 4})
|
||||
(-> (smdn/describe (sm/schema schema))
|
||||
(pp/pprint-str {:level 5 :width 70})))))
|
||||
|
||||
(get-context [mdata]
|
||||
{:name (::sv/name mdata)
|
||||
:module (or (some-> (::module mdata) d/name)
|
||||
(-> (:ns mdata) (str/split ".") last))
|
||||
:auth (::rpc/auth mdata true)
|
||||
:webhook (::webhooks/event? mdata false)
|
||||
:docs (::sv/docstring mdata)
|
||||
:deprecated (::deprecated mdata)
|
||||
:added (::added mdata)
|
||||
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
|
||||
:spec (fmt-spec mdata)
|
||||
:entrypoint (str (cf/get :public-uri) "/api/rpc/command/" (::sv/name mdata))
|
||||
|
||||
:params-schema-js (fmt-schema :js mdata ::sm/params)
|
||||
:result-schema-js (fmt-schema :js mdata ::sm/result)
|
||||
:webhook-schema-js (fmt-schema :js mdata ::sm/webhook)
|
||||
:params-schema-clj (fmt-schema :clj mdata ::sm/params)
|
||||
:result-schema-clj (fmt-schema :clj mdata ::sm/result)
|
||||
:webhook-schema-clj (fmt-schema :clj mdata ::sm/webhook)})]
|
||||
(letfn [(gen-doc [type [name f]]
|
||||
(let [mdata (meta f)]
|
||||
{:type (d/name type)
|
||||
:name (d/name name)
|
||||
:module (-> (:ns mdata) (str/split ".") last)
|
||||
:auth (:auth mdata true)
|
||||
:webhook (::webhooks/event? mdata false)
|
||||
:docs (::sv/docstring mdata)
|
||||
:deprecated (::deprecated mdata)
|
||||
:added (::added mdata)
|
||||
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
|
||||
:spec (get-spec-str (::sv/spec mdata))}))]
|
||||
|
||||
{:version (:main cf/version)
|
||||
:methods
|
||||
(->> methods
|
||||
(map val)
|
||||
(map first)
|
||||
(remove ::skip)
|
||||
(map get-context)
|
||||
:command-methods
|
||||
(->> (:commands methods)
|
||||
(map (partial gen-doc :command))
|
||||
(sort-by (juxt :module :name)))
|
||||
|
||||
:query-methods
|
||||
(->> (:queries methods)
|
||||
(map (partial gen-doc :query))
|
||||
(sort-by (juxt :module :name)))
|
||||
|
||||
:mutation-methods
|
||||
(->> (:mutations methods)
|
||||
(map (partial gen-doc :query))
|
||||
(sort-by (juxt :module :name)))}))
|
||||
|
||||
(defn- doc-handler
|
||||
[context]
|
||||
(if (contains? cf/flags :backend-api-doc)
|
||||
(fn [request]
|
||||
(let [params (:query-params request)
|
||||
pstyle (:type params "js")
|
||||
context (assoc context :param-style pstyle)]
|
||||
{::yrs/status 200
|
||||
::yrs/body (-> (io/resource "app/templates/api-doc.tmpl")
|
||||
(tmpl/render context))}))
|
||||
(fn [_]
|
||||
{::yrs/status 404})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OPENAPI / SWAGGER (v3.1)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def output-transformer
|
||||
(mt/transformer
|
||||
sm/default-transformer
|
||||
(mt/key-transformer {:encode str/camel
|
||||
:decode (comp keyword str/kebab)})))
|
||||
|
||||
(defn prepare-openapi-context
|
||||
(defn- handler
|
||||
[methods]
|
||||
(letfn [(gen-response-doc [tsx schema]
|
||||
(let [schema (sm/schema schema)
|
||||
example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:default
|
||||
{:description "A default response"
|
||||
:content
|
||||
{"application/json"
|
||||
{:schema tsx
|
||||
:example example}}}}))
|
||||
(if (contains? cf/flags :backend-api-doc)
|
||||
(let [context (prepare-context methods)]
|
||||
(fn [_ respond _]
|
||||
(respond (yrs/response 200 (-> (io/resource "app/templates/api-doc.tmpl")
|
||||
(tmpl/render context))))))
|
||||
(fn [_ respond _]
|
||||
(respond (yrs/response 404)))))
|
||||
|
||||
(gen-params-doc [tsx schema]
|
||||
(let [example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:required true
|
||||
:content
|
||||
{"application/json"
|
||||
{:schema tsx
|
||||
:example example}}}))
|
||||
|
||||
(gen-method-doc [options mdata]
|
||||
(let [pschema (::sm/params mdata)
|
||||
rschema (::sm/result mdata)
|
||||
|
||||
sparams (-> pschema (oapi/transform options) (gen-params-doc pschema))
|
||||
sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema))
|
||||
|
||||
rpost {:description (::sv/docstring mdata)
|
||||
:deprecated (::deprecated mdata false)
|
||||
:requestBody sparams}
|
||||
|
||||
rpost (cond-> rpost
|
||||
(some? sresp)
|
||||
(assoc :responses sresp))]
|
||||
|
||||
{:name (-> mdata ::sv/name d/name)
|
||||
:module (-> (:ns mdata) (str/split ".") last)
|
||||
:repr {:post rpost}}))
|
||||
]
|
||||
|
||||
(let [definitions (atom {})
|
||||
options {:registry sr/default-registry
|
||||
::oapi/definitions-path "#/components/schemas/"
|
||||
::oapi/definitions definitions}
|
||||
|
||||
paths (binding [oapi/*definitions* definitions]
|
||||
(->> methods
|
||||
(map (comp first val))
|
||||
(filter ::sm/params)
|
||||
(map (partial gen-method-doc options))
|
||||
(sort-by (juxt :module :name))
|
||||
(map (fn [doc]
|
||||
[(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
|
||||
(into {})))]
|
||||
{:openapi "3.0.0"
|
||||
:info {:version (:main cf/version)}
|
||||
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
|
||||
;; :description "penpot backend"
|
||||
}]
|
||||
:security
|
||||
{:api_key []}
|
||||
|
||||
:paths paths
|
||||
:components {:schemas @definitions}})))
|
||||
|
||||
(defn openapi-json-handler
|
||||
[context]
|
||||
(if (contains? cf/flags :backend-openapi-doc)
|
||||
(fn [_]
|
||||
{::yrs/status 200
|
||||
::yrs/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yrs/body (json/encode context)})
|
||||
|
||||
(fn [_]
|
||||
{::yrs/status 404})))
|
||||
|
||||
(defn openapi-handler
|
||||
[]
|
||||
(if (contains? cf/flags :backend-openapi-doc)
|
||||
(fn [_]
|
||||
(let [swagger-js (slurp (io/resource "app/assets/swagger-ui-4.18.3.js"))
|
||||
swagger-cs (slurp (io/resource "app/assets/swagger-ui-4.18.3.css"))
|
||||
context {:public-uri (cf/get :public-uri)
|
||||
:swagger-js swagger-js
|
||||
:swagger-css swagger-cs}]
|
||||
{::yrs/status 200
|
||||
::yrs/headers {"content-type" "text/html"}
|
||||
::yrs/body (-> (io/resource "app/templates/openapi.tmpl")
|
||||
(tmpl/render context))}))
|
||||
(fn [_]
|
||||
{::yrs/status 404})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MODULE INIT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::routes vector?)
|
||||
|
||||
@@ -207,18 +77,6 @@
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [methods] :as cfg}]
|
||||
[(let [context (prepare-doc-context methods)]
|
||||
[["/_doc"
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]
|
||||
["/doc"
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]])
|
||||
["/_doc" {:handler (handler methods)
|
||||
:allowed-methods #{:get}}])
|
||||
|
||||
(let [context (prepare-openapi-context methods)]
|
||||
[["/openapi"
|
||||
{:handler (openapi-handler)
|
||||
:allowed-methods #{:get}}]
|
||||
["/openapi.json"
|
||||
{:handler (openapi-json-handler context)
|
||||
:allowed-methods #{:get}}]])])
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.http :as-alias http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[yetti.response :as-alias yrs]))
|
||||
[app.rpc :as-alias rpc]))
|
||||
|
||||
;; A utilty wrapper object for wrap service responses that does not
|
||||
;; implements the IObj interface that make possible attach metadata to
|
||||
@@ -36,9 +35,7 @@
|
||||
o
|
||||
(MetadataWrapper. o {})))
|
||||
([o m]
|
||||
(if (instance? clojure.lang.IObj o)
|
||||
(vary-meta o merge m)
|
||||
(MetadataWrapper. o m))))
|
||||
(MetadataWrapper. o m)))
|
||||
|
||||
(defn wrapped?
|
||||
[o]
|
||||
@@ -77,4 +74,4 @@
|
||||
(fn [_ response]
|
||||
(let [exp (if (integer? max-age) max-age (inst-ms max-age))
|
||||
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
|
||||
(update response ::yrs/headers assoc "cache-control" val)))))
|
||||
(update response :headers assoc "cache-control" val)))))
|
||||
|
||||
116
backend/src/app/rpc/mutations/fonts.clj
Normal file
116
backend/src/app/rpc/mutations/fonts.clj
Normal file
@@ -0,0 +1,116 @@
|
||||
;; 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.fonts
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
[app.rpc.commands.fonts :as fonts]
|
||||
[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]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(declare create-font-variant)
|
||||
|
||||
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||
(def valid-style #{"normal" "italic"})
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::weight valid-weight)
|
||||
(s/def ::style valid-style)
|
||||
(s/def ::font-id ::us/uuid)
|
||||
(s/def ::data (s/map-of ::us/string any?))
|
||||
|
||||
(s/def ::create-font-variant
|
||||
(s/keys :req-un [::profile-id ::team-id ::data
|
||||
::font-id ::font-family ::font-weight ::font-style]))
|
||||
|
||||
(declare create-font-variant)
|
||||
|
||||
(sv/defmethod ::create-font-variant
|
||||
{::doc/added "1.3"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [team-id profile-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})
|
||||
(fonts/create-font-variant cfg params)))
|
||||
|
||||
;; --- UPDATE FONT FAMILY
|
||||
|
||||
(s/def ::update-font
|
||||
(s/keys :req-un [::profile-id ::team-id ::id ::name]))
|
||||
|
||||
(sv/defmethod ::update-font
|
||||
{::doc/added "1.3"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}]
|
||||
(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-un [::profile-id ::team-id ::id]))
|
||||
|
||||
(sv/defmethod ::delete-font
|
||||
{::doc/added "1.3"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
|
||||
(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-un [::profile-id ::team-id ::id]))
|
||||
|
||||
(sv/defmethod ::delete-font-variant
|
||||
{::doc/added "1.3"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
|
||||
(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)}}))))
|
||||
55
backend/src/app/rpc/mutations/media.clj
Normal file
55
backend/src/app/rpc/mutations/media.clj
Normal file
@@ -0,0 +1,55 @@
|
||||
;; 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.media
|
||||
(:require
|
||||
[app.db :as db]
|
||||
[app.media :as media]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.media :as cmd.media]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.storage :as-alias sto]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(s/def ::upload-file-media-object ::cmd.media/upload-file-media-object)
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::doc/added "1.2"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(media/validate-media-type! content)
|
||||
(cmd.media/validate-content-size! content)
|
||||
(cmd.media/create-file-media-object cfg params)))
|
||||
|
||||
;; --- Create File Media Object (from URL)
|
||||
|
||||
(s/def ::create-file-media-object-from-url ::cmd.media/create-file-media-object-from-url)
|
||||
|
||||
(sv/defmethod ::create-file-media-object-from-url
|
||||
{::doc/added "1.3"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(#'cmd.media/create-file-media-object-from-url cfg params)))
|
||||
|
||||
;; --- Clone File Media object (Upload and create from url)
|
||||
|
||||
(s/def ::clone-file-media-object ::cmd.media/clone-file-media-object)
|
||||
|
||||
(sv/defmethod ::clone-file-media-object
|
||||
{::doc/added "1.2"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(-> (assoc cfg :conn conn)
|
||||
(cmd.media/clone-file-media-object params))))
|
||||
193
backend/src/app/rpc/mutations/profile.clj
Normal file
193
backend/src/app/rpc/mutations/profile.clj
Normal file
@@ -0,0 +1,193 @@
|
||||
;; 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.profile
|
||||
(: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.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.media :as media]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::lang ::us/string)
|
||||
(s/def ::path ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::old-password ::us/not-empty-string)
|
||||
(s/def ::theme ::us/string)
|
||||
|
||||
;; --- MUTATION: Update Profile (own)
|
||||
|
||||
(s/def ::update-profile
|
||||
(s/keys :req-un [::fullname ::profile-id]
|
||||
:opt-un [::lang ::theme]))
|
||||
|
||||
(sv/defmethod ::update-profile
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [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)
|
||||
(profile/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
|
||||
(profile/strip-private-attrs)
|
||||
(d/without-nils)
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)})))))
|
||||
|
||||
|
||||
;; --- MUTATION: Update Password
|
||||
|
||||
(s/def ::update-profile-password
|
||||
(s/keys :req-un [::profile-id ::password ::old-password]))
|
||||
|
||||
(sv/defmethod ::update-profile-password
|
||||
{::climit/queue :auth
|
||||
::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [password] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (#'profile/validate-password! conn params)
|
||||
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"))
|
||||
(profile/update-profile-password! conn (assoc profile :password password))
|
||||
(#'profile/invalidate-profile-session! conn (:id profile) session-id)
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- MUTATION: Update Photo
|
||||
|
||||
(s/def ::file ::media/upload)
|
||||
(s/def ::update-profile-photo
|
||||
(s/keys :req-un [::profile-id ::file]))
|
||||
|
||||
(sv/defmethod ::update-profile-photo
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[cfg {:keys [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)]
|
||||
(profile/update-profile-photo cfg params)))
|
||||
|
||||
;; --- MUTATION: Request Email Change
|
||||
|
||||
(s/def ::request-email-change
|
||||
(s/keys :req-un [::email]))
|
||||
|
||||
(sv/defmethod ::request-email-change
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id email] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (db/get-by-id conn :profile profile-id)
|
||||
cfg (assoc cfg ::profile/conn conn)
|
||||
params (assoc params
|
||||
:profile profile
|
||||
:email (str/lower email))]
|
||||
|
||||
(if (contains? cf/flags :smtp)
|
||||
(#'profile/request-email-change! cfg params)
|
||||
(#'profile/change-email-immediately! cfg params)))))
|
||||
|
||||
;; --- MUTATION: Update Profile Props
|
||||
|
||||
(s/def ::props map?)
|
||||
(s/def ::update-profile-props
|
||||
(s/keys :req-un [::profile-id ::props]))
|
||||
|
||||
(sv/defmethod ::update-profile-props
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id props]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (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})
|
||||
|
||||
(profile/filter-props props))))
|
||||
|
||||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
(s/def ::delete-profile
|
||||
(s/keys :req-un [::profile-id]))
|
||||
|
||||
(sv/defmethod ::delete-profile
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [teams (#'profile/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)))))
|
||||
130
backend/src/app/rpc/mutations/projects.clj
Normal file
130
backend/src/app/rpc/mutations/projects.clj
Normal file
@@ -0,0 +1,130 @@
|
||||
;; 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.projects
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[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.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
|
||||
;; --- Mutation: Create Project
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::create-project
|
||||
(s/keys :req-un [::profile-id ::team-id ::name]
|
||||
:opt-un [::id]))
|
||||
|
||||
(sv/defmethod ::create-project
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [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 [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-un [::profile-id ::id ::team-id ::is-pinned]))
|
||||
|
||||
(sv/defmethod ::update-project-pin
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/batch-timeout (dt/duration "5s")
|
||||
::webhooks/batch-key :id
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id team-id is-pinned] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(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])
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Rename Project
|
||||
|
||||
(declare rename-project)
|
||||
|
||||
(s/def ::rename-project
|
||||
(s/keys :req-un [::profile-id ::name ::id]))
|
||||
|
||||
(sv/defmethod ::rename-project
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(projects/check-edition-permissions! conn profile-id id)
|
||||
(let [project (db/get-by-id conn :project id)]
|
||||
(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-un [::id ::profile-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.0"
|
||||
::doc/deprecated "1.18"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(projects/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)}}))))
|
||||
71
backend/src/app/rpc/mutations/share_link.clj
Normal file
71
backend/src/app/rpc/mutations/share_link.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.mutations.share-link
|
||||
"Share link related rpc mutation methods."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[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 ::id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(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-un [::profile-id ::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.5"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-share-link conn params)))
|
||||
|
||||
(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-un [::profile-id ::id]))
|
||||
|
||||
(sv/defmethod ::delete-share-link
|
||||
{::doc/added "1.5"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [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)))
|
||||
@@ -8,20 +8,9 @@
|
||||
"A permission checking helper factories."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(sm/def! ::permissions
|
||||
[:map {:title "Permissions"}
|
||||
[:type {:gen/elements [:membership :share-link]} :keyword]
|
||||
[:is-owner :boolean]
|
||||
[:is-admin :boolean]
|
||||
[:can-edit :boolean]
|
||||
[:can-read :boolean]
|
||||
[:is-logged :boolean]])
|
||||
|
||||
|
||||
(s/def ::role #{:admin :owner :editor :viewer})
|
||||
|
||||
(defn assign-role-flags
|
||||
@@ -48,14 +37,6 @@
|
||||
:is-admin 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
|
||||
"A simple factory for edition permission predicate functions."
|
||||
[qfn]
|
||||
|
||||
59
backend/src/app/rpc/queries/fonts.clj
Normal file
59
backend/src/app/rpc/queries/fonts.clj
Normal file
@@ -0,0 +1,59 @@
|
||||
;; 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.fonts
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[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.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Query: Font Variants
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::font-variants
|
||||
(s/and
|
||||
(s/keys :req-un [::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 ::font-variants
|
||||
{::doc/added "1.7"}
|
||||
[{:keys [pool] :as cfg} {:keys [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})))))
|
||||
32
backend/src/app/rpc/queries/profile.clj
Normal file
32
backend/src/app/rpc/queries/profile.clj
Normal file
@@ -0,0 +1,32 @@
|
||||
;; 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.profile
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[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]))
|
||||
|
||||
(s/def ::profile ::profile/get-profile)
|
||||
|
||||
(sv/defmethod ::profile
|
||||
{::rpc/auth false
|
||||
::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
|
||||
;; no profile-id is in session, and when db call raises not found. In all other
|
||||
;; cases we need to reraise the exception.
|
||||
(try
|
||||
(-> (profile/get-profile pool profile-id)
|
||||
(profile/strip-private-attrs)
|
||||
(update :props profile/filter-props))
|
||||
(catch Throwable _
|
||||
{:id uuid/zero :fullname "Anonymous User"})))
|
||||
59
backend/src/app/rpc/queries/projects.clj
Normal file
59
backend/src/app/rpc/queries/projects.clj
Normal file
@@ -0,0 +1,59 @@
|
||||
;; 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.projects
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Query: Projects
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::projects
|
||||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(sv/defmethod ::projects
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [pool]} {:keys [profile-id team-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(projects/get-projects conn profile-id team-id)))
|
||||
|
||||
;; --- Query: All projects
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::all-projects
|
||||
(s/keys :req-un [::profile-id]))
|
||||
|
||||
(sv/defmethod ::all-projects
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [pool]} {:keys [profile-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(projects/get-all-projects conn profile-id)))
|
||||
|
||||
;; --- Query: Project
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::project
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sv/defmethod ::project
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [pool]} {:keys [profile-id id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [project (db/get-by-id conn :project id)]
|
||||
(projects/check-read-permissions! conn profile-id id)
|
||||
project)))
|
||||
|
||||
32
backend/src/app/rpc/queries/viewer.clj
Normal file
32
backend/src/app/rpc/queries/viewer.clj
Normal file
@@ -0,0 +1,32 @@
|
||||
;; 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.viewer
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.viewer :as viewer]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::components-v2 ::us/boolean)
|
||||
(s/def ::view-only-bundle
|
||||
(s/and ::viewer/get-view-only-bundle
|
||||
(s/keys :opt-un [::components-v2])))
|
||||
|
||||
(sv/defmethod ::view-only-bundle
|
||||
{::rpc/auth false
|
||||
::doc/added "1.3"
|
||||
::doc/deprecated "1.18"}
|
||||
[{:keys [pool] :as cfg} {:keys [features components-v2] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
||||
features (cond-> (or features #{})
|
||||
components-v2 (conj "components/v2"))
|
||||
params (assoc params :features features)]
|
||||
(viewer/get-view-only-bundle conn params))))
|
||||
@@ -5,20 +5,20 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.rpc.retry
|
||||
"A fault tolerance RPC middleware. Allow retry some operations that we
|
||||
know we can retry."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.util.services :as sv])
|
||||
(:import
|
||||
org.postgresql.util.PSQLException))
|
||||
[app.util.retry :refer [conflict-exception?]]
|
||||
[app.util.services :as sv]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn conflict-exception?
|
||||
(defn conflict-db-insert?
|
||||
"Check if exception matches a insertion conflict on postgresql."
|
||||
[e]
|
||||
(and (instance? PSQLException e)
|
||||
(= "23505" (.getSQLState ^PSQLException e))))
|
||||
(conflict-exception? e))
|
||||
|
||||
(def ^:private always-false (constantly false))
|
||||
(def always-false (constantly false))
|
||||
|
||||
(defn wrap-retry
|
||||
[_ f {:keys [::matches ::sv/name] :or {matches always-false} :as mdata}]
|
||||
@@ -28,36 +28,18 @@
|
||||
|
||||
(if-let [max-retries (::max-retries mdata)]
|
||||
(fn [cfg params]
|
||||
((fn run [retry]
|
||||
(try
|
||||
(f cfg params)
|
||||
(catch Throwable cause
|
||||
(if (matches cause)
|
||||
(let [current-retry (inc retry)]
|
||||
(l/trace :hint "running retry algorithm" :retry current-retry)
|
||||
(if (<= current-retry max-retries)
|
||||
(run current-retry)
|
||||
(throw cause)))
|
||||
(throw cause))))) 1))
|
||||
(letfn [(run [retry]
|
||||
(->> (f cfg params)
|
||||
(p/merr (partial handle-error retry))))
|
||||
|
||||
(handle-error [retry cause]
|
||||
(if (matches cause)
|
||||
(let [current-retry (inc retry)]
|
||||
(l/trace :hint "running retry algorithm" :retry current-retry)
|
||||
(if (<= current-retry max-retries)
|
||||
(run current-retry)
|
||||
(throw cause)))
|
||||
(throw cause)))]
|
||||
(run 1)))
|
||||
f))
|
||||
|
||||
(defmacro with-retry
|
||||
[{:keys [::when ::max-retries ::label ::db/conn] :or {max-retries 3}} & body]
|
||||
`(let [conn# ~conn]
|
||||
(assert (or (nil? conn#) (db/connection? conn#)) "invalid database connection")
|
||||
(loop [tnum# 1]
|
||||
(let [result# (let [sp# (some-> conn# db/savepoint)]
|
||||
(try
|
||||
(let [result# (do ~@body)]
|
||||
(some->> sp# (db/release! conn#))
|
||||
result#)
|
||||
(catch Throwable cause#
|
||||
(some->> sp# (db/rollback! conn#))
|
||||
(if (and (~when cause#) (<= tnum# ~max-retries))
|
||||
::retry
|
||||
(throw cause#)))))]
|
||||
(if (= ::retry result#)
|
||||
(do
|
||||
(l/warn :hint "retrying operation" :label ~label :retry tnum#)
|
||||
(recur (inc tnum#)))
|
||||
result#)))))
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
[app.redis :as rds]
|
||||
[app.redis.script :as-alias rscript]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.rlimit.result :as-alias lresult]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.time :as dt]
|
||||
@@ -65,6 +64,7 @@
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def ^:private default-timeout
|
||||
@@ -82,7 +82,7 @@
|
||||
{::rscript/name ::window-rate-limit
|
||||
::rscript/path "app/rpc/rlimit/window.lua"})
|
||||
|
||||
(def enabled
|
||||
(def enabled?
|
||||
"Allows on runtime completely disable rate limiting."
|
||||
(atom true))
|
||||
|
||||
@@ -119,129 +119,122 @@
|
||||
(defmethod parse-limit :bucket
|
||||
[[name strategy opts :as vlimit]]
|
||||
(us/assert! ::limit-tuple vlimit)
|
||||
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
|
||||
(let [interval (dt/duration interval)
|
||||
rate (parse-long rate)
|
||||
capacity (parse-long capacity)]
|
||||
{::name name
|
||||
::strategy strategy
|
||||
::capacity capacity
|
||||
::rate rate
|
||||
::interval interval
|
||||
::opts opts
|
||||
::params [(dt/->seconds interval) rate capacity]
|
||||
::key (str "ratelimit.bucket." (d/name name))})
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-bucket-limit-opts
|
||||
:hint (str/ffmt "looks like '%' does not have a valid format" opts))))
|
||||
(merge
|
||||
{::name name
|
||||
::strategy strategy}
|
||||
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
|
||||
(let [interval (dt/duration interval)
|
||||
rate (parse-long rate)
|
||||
capacity (parse-long capacity)]
|
||||
{::capacity capacity
|
||||
::rate rate
|
||||
::interval interval
|
||||
::opts opts
|
||||
::params [(dt/->seconds interval) rate capacity]
|
||||
::key (str "ratelimit.bucket." (d/name name))})
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-bucket-limit-opts
|
||||
:hint (str/ffmt "looks like '%' does not have a valid format" opts)))))
|
||||
|
||||
(defmethod process-limit :bucket
|
||||
[redis user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
|
||||
(let [script (-> bucket-rate-limit-script
|
||||
(assoc ::rscript/keys [(str key "." service "." user-id)])
|
||||
(assoc ::rscript/vals (conj params (dt/->seconds now))))
|
||||
result (rds/eval! redis script)
|
||||
allowed? (boolean (nth result 0))
|
||||
remaining (nth result 1)
|
||||
reset (* (/ (inst-ms interval) rate)
|
||||
(- capacity remaining))]
|
||||
(l/trace :hint "limit processed"
|
||||
:service service
|
||||
:limit (name (::name limit))
|
||||
:strategy (name (::strategy limit))
|
||||
:opts (::opts limit)
|
||||
:allowed allowed?
|
||||
:remaining remaining)
|
||||
(-> limit
|
||||
(assoc ::lresult/allowed allowed?)
|
||||
(assoc ::lresult/reset (dt/plus now reset))
|
||||
(assoc ::lresult/remaining remaining))))
|
||||
(let [script (-> bucket-rate-limit-script
|
||||
(assoc ::rscript/keys [(str key "." service "." user-id)])
|
||||
(assoc ::rscript/vals (conj params (dt/->seconds now))))]
|
||||
(->> (rds/eval! redis script)
|
||||
(p/fmap (fn [result]
|
||||
(let [allowed? (boolean (nth result 0))
|
||||
remaining (nth result 1)
|
||||
reset (* (/ (inst-ms interval) rate)
|
||||
(- capacity remaining))]
|
||||
(l/trace :hint "limit processed"
|
||||
:service service
|
||||
:limit (name (::name limit))
|
||||
:strategy (name (::strategy limit))
|
||||
:opts (::opts limit)
|
||||
:allowed? allowed?
|
||||
:remaining remaining)
|
||||
(-> limit
|
||||
(assoc ::lresult/allowed? allowed?)
|
||||
(assoc ::lresult/reset (dt/plus now reset))
|
||||
(assoc ::lresult/remaining remaining))))))))
|
||||
|
||||
(defmethod process-limit :window
|
||||
[redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
|
||||
(let [ts (dt/truncate now unit)
|
||||
ttl (dt/diff now (dt/plus ts {unit 1}))
|
||||
script (-> window-rate-limit-script
|
||||
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
|
||||
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))
|
||||
result (rds/eval! redis script)
|
||||
allowed? (boolean (nth result 0))
|
||||
remaining (nth result 1)]
|
||||
(l/trace :hint "limit processed"
|
||||
:service service
|
||||
:limit (name (::name limit))
|
||||
:strategy (name (::strategy limit))
|
||||
:opts (::opts limit)
|
||||
:allowed allowed?
|
||||
:remaining remaining)
|
||||
(-> limit
|
||||
(assoc ::lresult/allowed allowed?)
|
||||
(assoc ::lresult/remaining remaining)
|
||||
(assoc ::lresult/reset (dt/plus ts {unit 1})))))
|
||||
(let [ts (dt/truncate now unit)
|
||||
ttl (dt/diff now (dt/plus ts {unit 1}))
|
||||
script (-> window-rate-limit-script
|
||||
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
|
||||
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))]
|
||||
(->> (rds/eval! redis script)
|
||||
(p/fmap (fn [result]
|
||||
(let [allowed? (boolean (nth result 0))
|
||||
remaining (nth result 1)]
|
||||
(l/trace :hint "limit processed"
|
||||
:service service
|
||||
:limit (name (::name limit))
|
||||
:strategy (name (::strategy limit))
|
||||
:opts (::opts limit)
|
||||
:allowed? allowed?
|
||||
:remaining remaining)
|
||||
(-> limit
|
||||
(assoc ::lresult/allowed? allowed?)
|
||||
(assoc ::lresult/remaining remaining)
|
||||
(assoc ::lresult/reset (dt/plus ts {unit 1})))))))))
|
||||
|
||||
(defn- process-limits!
|
||||
[redis user-id limits now]
|
||||
(let [results (into [] (map (partial process-limit redis user-id now)) limits)
|
||||
remaining (->> results
|
||||
(d/index-by ::name ::lresult/remaining)
|
||||
(uri/map->query-string))
|
||||
reset (->> results
|
||||
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
|
||||
(uri/map->query-string))
|
||||
(->> (p/all (map (partial process-limit redis user-id now) limits))
|
||||
(p/fmap (fn [results]
|
||||
(let [remaining (->> results
|
||||
(d/index-by ::name ::lresult/remaining)
|
||||
(uri/map->query-string))
|
||||
reset (->> results
|
||||
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
|
||||
(uri/map->query-string))
|
||||
rejected (->> results
|
||||
(filter (complement ::lresult/allowed?))
|
||||
(first))]
|
||||
|
||||
rejected (d/seek (complement ::lresult/allowed) results)]
|
||||
(when rejected
|
||||
(l/warn :hint "rejected rate limit"
|
||||
:user-id (str user-id)
|
||||
:limit-service (-> rejected ::service name)
|
||||
:limit-name (-> rejected ::name name)
|
||||
:limit-strategy (-> rejected ::strategy name)))
|
||||
|
||||
(when rejected
|
||||
(l/warn :hint "rejected rate limit"
|
||||
:user-id (str user-id)
|
||||
:limit-service (-> rejected ::service name)
|
||||
:limit-name (-> rejected ::name name)
|
||||
:limit-strategy (-> rejected ::strategy name)))
|
||||
{:enabled? true
|
||||
:allowed? (not (some? rejected))
|
||||
:headers {"x-rate-limit-remaining" remaining
|
||||
"x-rate-limit-reset" reset}})))))
|
||||
|
||||
{::enabled true
|
||||
::allowed (not (some? rejected))
|
||||
::remaingin remaining
|
||||
::reset reset
|
||||
::headers {"x-rate-limit-remaining" remaining
|
||||
"x-rate-limit-reset" reset}}))
|
||||
(defn- handle-response
|
||||
[f cfg params result]
|
||||
(if (:enabled? result)
|
||||
(let [headers (:headers result)]
|
||||
(if (:allowed? result)
|
||||
(->> (f cfg params)
|
||||
(p/fmap (fn [response]
|
||||
(vary-meta response update ::http/headers merge headers))))
|
||||
(p/rejected
|
||||
(ex/error :type :rate-limit
|
||||
:code :request-blocked
|
||||
:hint "rate limit reached"
|
||||
::http/headers headers))))
|
||||
(f cfg params)))
|
||||
|
||||
(defn- get-limits
|
||||
[state skey sname]
|
||||
(when-let [limits (or (get-in @state [::limits skey])
|
||||
(get-in @state [::limits :default]))]
|
||||
(into [] (map #(assoc % ::service sname)) limits)))
|
||||
(some->> (or (get-in @state [::limits skey])
|
||||
(get-in @state [::limits :default]))
|
||||
(map #(assoc % ::service sname))
|
||||
(seq)))
|
||||
|
||||
(defn- get-uid
|
||||
[{:keys [::rpc/profile-id] :as params}]
|
||||
(let [request (-> params meta ::http/request)]
|
||||
(or profile-id
|
||||
(some-> request parse-client-ip)
|
||||
uuid/zero)))
|
||||
|
||||
(defn process-request!
|
||||
[{:keys [::rpc/rlimit ::rds/redis ::skey ::sname] :as cfg} params]
|
||||
(when-let [limits (get-limits rlimit skey sname)]
|
||||
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
|
||||
uid (get-uid params)
|
||||
;; FIXME: why not clasic try/catch?
|
||||
result (ex/try! (process-limits! redis uid limits (dt/now)))]
|
||||
|
||||
(l/trc :hint "process-limits"
|
||||
:service sname
|
||||
:remaining (::remaingin result)
|
||||
:reset (::reset result))
|
||||
|
||||
(cond
|
||||
(ex/exception? result)
|
||||
(do
|
||||
(l/error :hint "error on processing rate-limit" :cause result)
|
||||
{::enabled false})
|
||||
|
||||
(contains? cf/flags :soft-rpc-rlimit)
|
||||
{::enabled false}
|
||||
|
||||
:else
|
||||
result))))
|
||||
[{:keys [::http/request] :as params}]
|
||||
(or (::rpc/profile-id params)
|
||||
(some-> request parse-client-ip)
|
||||
uuid/zero))
|
||||
|
||||
(defn wrap
|
||||
[{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata]
|
||||
@@ -250,25 +243,36 @@
|
||||
|
||||
(if rlimit
|
||||
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
|
||||
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))
|
||||
cfg (-> cfg
|
||||
(assoc ::skey skey)
|
||||
(assoc ::sname sname))]
|
||||
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))]
|
||||
|
||||
(fn [hcfg params]
|
||||
(if @enabled
|
||||
(let [result (process-request! cfg params)]
|
||||
(if (::enabled result)
|
||||
(if (::allowed result)
|
||||
(-> (f hcfg params)
|
||||
(rph/wrap)
|
||||
(vary-meta update ::http/headers merge (::headers result)))
|
||||
(ex/raise :type :rate-limit
|
||||
:code :request-blocked
|
||||
:hint "rate limit reached"
|
||||
::http/headers (::headers result)))
|
||||
(f hcfg params)))
|
||||
(f hcfg params))))
|
||||
(fn [cfg params]
|
||||
(if @enabled?
|
||||
(try
|
||||
(let [uid (get-uid params)
|
||||
rsp (when-let [limits (get-limits rlimit skey sname)]
|
||||
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
|
||||
rsp (->> (process-limits! redis uid limits (dt/now))
|
||||
(p/merr (fn [cause]
|
||||
;; If we have an error on processing the rate-limit we just skip
|
||||
;; it for do not cause service interruption because of redis
|
||||
;; downtime or similar situation.
|
||||
(l/error :hint "error on processing rate-limit" :cause cause)
|
||||
(p/resolved {:enabled? false}))))]
|
||||
|
||||
;; If soft rate are enabled, we process the rate-limit but return unprotected
|
||||
;; response.
|
||||
(if (contains? cf/flags :soft-rpc-rlimit)
|
||||
{:enabled? false}
|
||||
rsp)))]
|
||||
|
||||
(->> (p/promise rsp)
|
||||
(p/fmap #(or % {:enabled? false}))
|
||||
(p/mcat #(handle-response f cfg params %))))
|
||||
|
||||
(catch Throwable cause
|
||||
(p/rejected cause)))
|
||||
|
||||
(f cfg params))))
|
||||
f))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -348,7 +352,7 @@
|
||||
::limits limits}))))
|
||||
|
||||
(defn- refresh-config
|
||||
[{:keys [::state ::path ::wrk/executor] :as cfg}]
|
||||
[{:keys [::state ::path ::wrk/executor ::wrk/scheduled-executor] :as cfg}]
|
||||
(letfn [(update-config [{:keys [::updated-at] :as state}]
|
||||
(let [updated-at' (fs/last-modified-time path)]
|
||||
(merge state
|
||||
@@ -363,7 +367,8 @@
|
||||
state)))))
|
||||
|
||||
(schedule-next [state]
|
||||
(px/schedule! (inst-ms (::refresh state))
|
||||
(px/schedule! scheduled-executor
|
||||
(inst-ms (::refresh state))
|
||||
(partial refresh-config cfg))
|
||||
state)]
|
||||
|
||||
@@ -386,7 +391,8 @@
|
||||
(and (fs/exists? path) (fs/regular-file? path) path)))
|
||||
|
||||
(defmethod ig/pre-init-spec :app.rpc/rlimit [_]
|
||||
(s/keys :req [::wrk/executor]))
|
||||
(s/keys :req [::wrk/executor
|
||||
::wrk/scheduled-executor]))
|
||||
|
||||
(defmethod ig/init-key ::rpc/rlimit
|
||||
[_ {:keys [::wrk/executor] :as cfg}]
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.main :as-alias main]
|
||||
[app.setup.builtin-templates]
|
||||
[app.setup.keys :as keys]
|
||||
[app.setup.templates]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[clojure.spec.alpha :as s]
|
||||
|
||||
72
backend/src/app/setup/builtin_templates.clj
Normal file
72
backend/src/app/setup/builtin_templates.clj
Normal file
@@ -0,0 +1,72 @@
|
||||
;; 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.setup.builtin-templates
|
||||
"A service/module that is responsible for download, load & internally
|
||||
expose a set of builtin penpot file templates."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.http.client :as http]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare download-all!)
|
||||
|
||||
(s/def ::id ::us/not-empty-string)
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::thumbnail-uri ::us/not-empty-string)
|
||||
(s/def ::file-uri ::us/not-empty-string)
|
||||
(s/def ::path fs/path?)
|
||||
|
||||
(s/def ::template
|
||||
(s/keys :req-un [::id ::name ::thumbnail-uri ::file-uri]
|
||||
:opt-un [::path]))
|
||||
|
||||
(defmethod ig/pre-init-spec :app.setup/builtin-templates [_]
|
||||
(s/keys :req [::http/client]))
|
||||
|
||||
(defmethod ig/init-key :app.setup/builtin-templates
|
||||
[_ cfg]
|
||||
(let [presets (-> "app/onboarding.edn" io/resource slurp edn/read-string)]
|
||||
(l/info :hint "loading template files" :total (count presets))
|
||||
(let [result (download-all! cfg presets)]
|
||||
(us/conform (s/coll-of ::template) result))))
|
||||
|
||||
(defn- download-preset!
|
||||
[cfg {:keys [path file-uri] :as preset}]
|
||||
(let [response (http/req! cfg
|
||||
{:method :get
|
||||
:uri file-uri}
|
||||
{:response-type :input-stream
|
||||
:sync? true})]
|
||||
(us/verify! (= 200 (:status response)) "unexpected response found on fetching preset")
|
||||
(with-open [output (io/output-stream path)]
|
||||
(with-open [input (io/input-stream (:body response))]
|
||||
(io/copy input output)))))
|
||||
|
||||
(defn- download-all!
|
||||
"Download presets to the default directory, if preset is already
|
||||
downloaded, no action will be performed."
|
||||
[cfg presets]
|
||||
(let [dest (fs/join fs/*cwd* "builtin-templates")]
|
||||
(when-not (fs/exists? dest)
|
||||
(fs/create-dir dest))
|
||||
|
||||
(doall
|
||||
(map (fn [item]
|
||||
(let [path (fs/join dest (:id item))
|
||||
item (assoc item :path path)]
|
||||
(if (fs/exists? path)
|
||||
(l/trace :hint "template file already present" :id (:id item))
|
||||
(do
|
||||
(l/trace :hint "downloading template file" :id (:id item) :dest (str path))
|
||||
(download-preset! cfg item)))
|
||||
item))
|
||||
presets))))
|
||||
@@ -1,64 +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.setup.templates
|
||||
"A service/module that is responsible for download, load & internally
|
||||
expose a set of builtin penpot file templates."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.http.client :as http]
|
||||
[app.setup :as-alias setup]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.java.io :as io]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private schema:template
|
||||
[:map {:title "Template"}
|
||||
[:id ::sm/word-string]
|
||||
[:name ::sm/word-string]
|
||||
[:file-uri ::sm/word-string]])
|
||||
|
||||
(def ^:private schema:templates
|
||||
[:vector schema:template])
|
||||
|
||||
(defmethod ig/init-key ::setup/templates
|
||||
[_ _]
|
||||
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
|
||||
dest (fs/join fs/*cwd* "builtin-templates")]
|
||||
|
||||
(dm/verify!
|
||||
"expected a valid templates file"
|
||||
(sm/valid? schema:templates templates))
|
||||
|
||||
(doseq [{:keys [id path] :as template} templates]
|
||||
(let [path (or path (fs/join dest id))]
|
||||
(if (fs/exists? path)
|
||||
(l/debug :hint "template file" :id id :state "present" :path (dm/str path))
|
||||
(l/debug :hint "template file" :id id :state "absent"))))
|
||||
|
||||
templates))
|
||||
|
||||
(defn get-template-stream
|
||||
[cfg template-id]
|
||||
(when-let [template (d/seek #(= (:id %) template-id)
|
||||
(::setup/templates cfg))]
|
||||
(let [dest (fs/join fs/*cwd* "builtin-templates")
|
||||
path (or (:path template) (fs/join dest template-id))]
|
||||
(if (fs/exists? path)
|
||||
(io/input-stream path)
|
||||
(let [resp (http/req! cfg
|
||||
{:method :get :uri (:file-uri template)}
|
||||
{:response-type :input-stream :sync? true})]
|
||||
|
||||
(dm/verify!
|
||||
"unexpected response found on fetching template"
|
||||
(= 200 (:status resp)))
|
||||
|
||||
(io/input-stream (:body resp)))))))
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- get-current-system
|
||||
@@ -64,43 +63,9 @@
|
||||
params
|
||||
{:email email
|
||||
:deleted-at nil}
|
||||
{::db/return-keys? false})]
|
||||
{:return-keys false})]
|
||||
(pos? (:next.jdbc/update-count res))))))))
|
||||
|
||||
(defmethod run-json-cmd* :delete-profile
|
||||
[{:keys [email soft]}]
|
||||
(when-not email
|
||||
(ex/raise :type :assertion
|
||||
:code :invalid-arguments
|
||||
:hint "email should be provided"))
|
||||
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
|
||||
(let [res (if soft
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:email email :deleted-at nil}
|
||||
{::db/return-keys? false})
|
||||
(db/delete! conn :profile
|
||||
{:email email}
|
||||
{::db/return-keys? false}))]
|
||||
(pos? (:next.jdbc/update-count res))))))
|
||||
|
||||
(defmethod run-json-cmd* :search-profile
|
||||
[{:keys [email]}]
|
||||
(when-not email
|
||||
(ex/raise :type :assertion
|
||||
:code :invalid-arguments
|
||||
:hint "email should be provided"))
|
||||
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
|
||||
(let [sql (str "select email, fullname, created_at, deleted_at from profile "
|
||||
" where email similar to ? order by created_at desc limit 100")]
|
||||
(db/exec! conn [sql email])))))
|
||||
|
||||
(defmethod run-json-cmd* :derive-password
|
||||
[{:keys [password]}]
|
||||
(auth/derive-password password))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user