From 6169f5c2e820de8f51b4a1abd11aea095026ef20 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 27 Jun 2024 11:00:31 +0200 Subject: [PATCH] :tada: New oops page with login and request access --- backend/resources/app/email/join-team/en.html | 518 ++++++++++++++++ backend/resources/app/email/join-team/en.subj | 1 + backend/resources/app/email/join-team/en.txt | 10 + .../en.html | 528 ++++++++++++++++ .../en.subj | 1 + .../en.txt | 17 + .../request-file-access-yourpenpot/en.html | 551 +++++++++++++++++ .../request-file-access-yourpenpot/en.subj | 1 + .../request-file-access-yourpenpot/en.txt | 30 + .../app/email/request-file-access/en.html | 568 ++++++++++++++++++ .../app/email/request-file-access/en.subj | 1 + .../app/email/request-file-access/en.txt | 34 ++ .../app/email/request-team-access/en.html | 526 ++++++++++++++++ .../app/email/request-team-access/en.subj | 1 + .../app/email/request-team-access/en.txt | 14 + backend/src/app/email.clj | 75 +++ backend/src/app/migrations.clj | 5 +- .../0126-add-team-access-request-table.sql | 10 + backend/src/app/rpc/commands/files.clj | 17 + backend/src/app/rpc/commands/teams.clj | 268 ++++++++- backend/src/app/rpc/commands/verify_token.clj | 4 + backend/test/backend_tests/rpc_team_test.clj | 143 +++++ frontend/src/app/main/data/common.cljs | 15 + frontend/src/app/main/data/users.cljs | 10 +- frontend/src/app/main/errors.cljs | 22 +- frontend/src/app/main/ui.cljs | 5 +- frontend/src/app/main/ui/auth.cljs | 23 +- frontend/src/app/main/ui/auth.scss | 20 - frontend/src/app/main/ui/auth/login.cljs | 13 +- .../app/main/ui/auth/recovery_request.cljs | 13 + .../app/main/ui/auth/recovery_request.scss | 7 + frontend/src/app/main/ui/auth/register.cljs | 37 +- frontend/src/app/main/ui/auth/register.scss | 20 + .../src/app/main/ui/components/forms.cljs | 8 +- frontend/src/app/main/ui/dashboard.cljs | 8 +- frontend/src/app/main/ui/dashboard/team.cljs | 21 +- frontend/src/app/main/ui/static.cljs | 283 ++++++++- frontend/src/app/main/ui/static.scss | 223 +++++++ frontend/src/app/main/ui/viewer.cljs | 5 +- frontend/src/app/main/ui/viewer/header.cljs | 13 +- frontend/src/app/main/ui/viewer/login.cljs | 12 +- frontend/src/app/main/ui/viewer/login.scss | 5 +- frontend/src/app/util/router.cljs | 6 + frontend/src/app/util/storage.cljs | 1 + frontend/translations/en.po | 78 +++ frontend/translations/es.po | 80 +++ 46 files changed, 4117 insertions(+), 134 deletions(-) create mode 100644 backend/resources/app/email/join-team/en.html create mode 100644 backend/resources/app/email/join-team/en.subj create mode 100644 backend/resources/app/email/join-team/en.txt create mode 100644 backend/resources/app/email/request-file-access-yourpenpot-view/en.html create mode 100644 backend/resources/app/email/request-file-access-yourpenpot-view/en.subj create mode 100644 backend/resources/app/email/request-file-access-yourpenpot-view/en.txt create mode 100644 backend/resources/app/email/request-file-access-yourpenpot/en.html create mode 100644 backend/resources/app/email/request-file-access-yourpenpot/en.subj create mode 100644 backend/resources/app/email/request-file-access-yourpenpot/en.txt create mode 100644 backend/resources/app/email/request-file-access/en.html create mode 100644 backend/resources/app/email/request-file-access/en.subj create mode 100644 backend/resources/app/email/request-file-access/en.txt create mode 100644 backend/resources/app/email/request-team-access/en.html create mode 100644 backend/resources/app/email/request-team-access/en.subj create mode 100644 backend/resources/app/email/request-team-access/en.txt create mode 100644 backend/src/app/migrations/sql/0126-add-team-access-request-table.sql diff --git a/backend/resources/app/email/join-team/en.html b/backend/resources/app/email/join-team/en.html new file mode 100644 index 000000000..7668ee0fb --- /dev/null +++ b/backend/resources/app/email/join-team/en.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+ As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{ + team|abbreviate:25}}”.
+
+ + + + +
+ Go to the Team +
+
+
+ Enjoy!
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/join-team/en.subj b/backend/resources/app/email/join-team/en.subj new file mode 100644 index 000000000..296ce140f --- /dev/null +++ b/backend/resources/app/email/join-team/en.subj @@ -0,0 +1 @@ +You have joined {{team}} diff --git a/backend/resources/app/email/join-team/en.txt b/backend/resources/app/email/join-team/en.txt new file mode 100644 index 000000000..78cba680e --- /dev/null +++ b/backend/resources/app/email/join-team/en.txt @@ -0,0 +1,10 @@ +Hello! + +As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{ team|abbreviate:25}}”. + +Go to the team with this link: + +{{ public-uri }}/#/dashboard/team/{{team-id}} + +Enjoy! +The Penpot team. diff --git a/backend/resources/app/email/request-file-access-yourpenpot-view/en.html b/backend/resources/app/email/request-file-access-yourpenpot-view/en.html new file mode 100644 index 000000000..53e217cf7 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot-view/en.html @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the + file named “{{file-name|abbreviate:25}}”. +

+

+ Since this file is in your Penpot team, you can provide access by sending a view-only link. + This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. +

+

To proceed, please click the button below to generate and send the view-only link:

+
+
+ + + + +
+ Send a View-Only link +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/request-file-access-yourpenpot-view/en.subj b/backend/resources/app/email/request-file-access-yourpenpot-view/en.subj new file mode 100644 index 000000000..2e577c3e0 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot-view/en.subj @@ -0,0 +1 @@ +Request View-Only Access to “{{file-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-file-access-yourpenpot-view/en.txt b/backend/resources/app/email/request-file-access-yourpenpot-view/en.txt new file mode 100644 index 000000000..67eb6cedf --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot-view/en.txt @@ -0,0 +1,17 @@ +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the file named “{{file-name|abbreviate:25}}”. + +Since this file is in your Penpot team, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. + +To proceed, please click the link below to generate and send the view-only link: + +{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true + + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/resources/app/email/request-file-access-yourpenpot/en.html b/backend/resources/app/email/request-file-access-yourpenpot/en.html new file mode 100644 index 000000000..f168168ba --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot/en.html @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named + “{{file-name|abbreviate:25}}”. +

+

+ Please note that the file is currently in Your Penpot 's team, so direct access cannot be + granted. However, you have two options to provide the requested access: +

+
    +
  • +

    Move the File to Another Team:

    +

    You can move the file to another team and then give access to that team, inviting + {{requested-by|abbreviate:25}}.

    +
  • +
+

+
+
+
+
    +
  • +

    Send a View-Only Link:

    +

    Alternatively, you can create and share a view-only link to the file. This will allow + {{requested-by|abbreviate:25}} to view the content without making any changes.

    +

    Click the button below to generate and send the link:

    +
  • +
+

+
+
+ + + + +
+ Send a View-Only link +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/request-file-access-yourpenpot/en.subj b/backend/resources/app/email/request-file-access-yourpenpot/en.subj new file mode 100644 index 000000000..d4a90980b --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot/en.subj @@ -0,0 +1 @@ +Request Access to “{{file-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-file-access-yourpenpot/en.txt b/backend/resources/app/email/request-file-access-yourpenpot/en.txt new file mode 100644 index 000000000..140cb0445 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot/en.txt @@ -0,0 +1,30 @@ +Hello! + + +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”. + +Please note that the file is currently in Your Penpot 's team, so direct access cannot be granted. However, you have two options to provide the requested access: + +- Move the File to Another Team: + +You can move the file to another team and then give access to that team, inviting {{requested-by|abbreviate:25}}. + + + +- Send a View-Only Link: + +Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. + +Click the link below to generate and send the link: + +{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true + + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/resources/app/email/request-file-access/en.html b/backend/resources/app/email/request-file-access/en.html new file mode 100644 index 000000000..23254524b --- /dev/null +++ b/backend/resources/app/email/request-file-access/en.html @@ -0,0 +1,568 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named + “{{file-name|abbreviate:25}}”. +

+

+ To provide this access, you have the following options: +

+
    +
  • +

    Give Access to the “{{team-name|abbreviate:25}}” Team:

    +

    This will automatically include {{requested-by|abbreviate:25}} in the team, so the user + can see all the projects and files in it.

    +

    Click the button below to provide team access:

    +
  • +
+

+
+
+ + + + +
+ Give access to “{{team-name|abbreviate:25}}” Team +
+
+
+
    +
  • +

    Send a View-Only Link:

    +

    Alternatively, you can create and share a view-only link to the file. This will allow + {{requested-by|abbreviate:25}} to view the content without making any changes.

    +

    Click the button below to generate and send the link:

    +
  • +
+

+
+
+ + + + +
+ Send a View-Only link +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/request-file-access/en.subj b/backend/resources/app/email/request-file-access/en.subj new file mode 100644 index 000000000..d4a90980b --- /dev/null +++ b/backend/resources/app/email/request-file-access/en.subj @@ -0,0 +1 @@ +Request Access to “{{file-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-file-access/en.txt b/backend/resources/app/email/request-file-access/en.txt new file mode 100644 index 000000000..d327e4780 --- /dev/null +++ b/backend/resources/app/email/request-file-access/en.txt @@ -0,0 +1,34 @@ +Hello! + + +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”. + +To provide this access, you have the following options: + +- Give Access to the “{{team-name|abbreviate:25}}” Team: + +This will automatically include {{requested-by|abbreviate:25}} in the team, so the user can see all the projects and files in it. + +Click the link below to provide team access: + +{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}} + + + +- Send a View-Only Link: + +Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. + +Click the link below to generate and send the link: + +{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true + + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/resources/app/email/request-team-access/en.html b/backend/resources/app/email/request-team-access/en.html new file mode 100644 index 000000000..103b0870b --- /dev/null +++ b/backend/resources/app/email/request-team-access/en.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the + “{{team-name|abbreviate:25}}” Team. +

+

+ To provide access, please click the button below: +

+
+
+ + + + +
+ Give access to “{{team-name|abbreviate:25}}” +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/request-team-access/en.subj b/backend/resources/app/email/request-team-access/en.subj new file mode 100644 index 000000000..d455c082b --- /dev/null +++ b/backend/resources/app/email/request-team-access/en.subj @@ -0,0 +1 @@ +Request Access to “{{team-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-team-access/en.txt b/backend/resources/app/email/request-team-access/en.txt new file mode 100644 index 000000000..225bc1e26 --- /dev/null +++ b/backend/resources/app/email/request-team-access/en.txt @@ -0,0 +1,14 @@ +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the “{{team-name|abbreviate:25}}” Team. + +To provide access, please click the link below: + +{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}} + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 102b6fea8..4329b8452 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -17,6 +17,8 @@ [app.db :as db] [app.db.sql :as sql] [app.email.invite-to-team :as-alias email.invite-to-team] + [app.email.join-team :as-alias email.join-team] + [app.email.request-team-access :as-alias email.request-team-access] [app.metrics :as mtx] [app.util.template :as tmpl] [app.worker :as wrk] @@ -399,6 +401,79 @@ "Teams member invitation email." (template-factory ::invite-to-team)) + +(s/def ::email.join-team/invited-by ::us/string) +(s/def ::email.join-team/team ::us/string) +(s/def ::email.join-team/team-id ::us/uuid) + +(s/def ::join-team + (s/keys :req-un [::email.join-team/invited-by + ::email.join-team/team-id + ::email.join-team/team])) + +(def join-team + "Teams member joined after request email." + (template-factory ::join-team)) + +(s/def ::email.request-team-access/requested-by ::us/string) +(s/def ::email.request-team-access/requested-by-email ::us/string) +(s/def ::email.request-team-access/team-name ::us/string) +(s/def ::email.request-team-access/team-id ::us/uuid) +(s/def ::email.request-team-access/file-name ::us/string) +(s/def ::email.request-team-access/file-id ::us/uuid) +(s/def ::email.request-team-access/page-id ::us/uuid) + +(s/def ::request-file-access + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id + ::email.request-team-access/file-name + ::email.request-team-access/file-id + ::email.request-team-access/page-id])) + +(def request-file-access + "File access request email." + (template-factory ::request-file-access)) + + +(s/def ::request-file-access-yourpenpot + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id + ::email.request-team-access/file-name + ::email.request-team-access/file-id + ::email.request-team-access/page-id])) + +(def request-file-access-yourpenpot + "File access on Your Penpot request email." + (template-factory ::request-file-access-yourpenpot)) + +(s/def ::request-file-access-yourpenpot-view + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id + ::email.request-team-access/file-name + ::email.request-team-access/file-id + ::email.request-team-access/page-id])) + +(def request-file-access-yourpenpot-view + "File access on Your Penpot view mode request email." + (template-factory ::request-file-access-yourpenpot-view)) + +(s/def ::request-team-access + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id])) + +(def request-team-access + "Team access request email." + (template-factory ::request-team-access)) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; BOUNCE/COMPLAINS HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 0efd24613..a66110fb5 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -397,7 +397,10 @@ :fn (mg/resource "app/migrations/sql/0124-mod-profile-table.sql")} {:name "0125-mod-file-table" - :fn (mg/resource "app/migrations/sql/0125-mod-file-table.sql")}]) + :fn (mg/resource "app/migrations/sql/0125-mod-file-table.sql")} + + {:name "0126-add-team-access-request-table" + :fn (mg/resource "app/migrations/sql/0126-add-team-access-request-table.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0126-add-team-access-request-table.sql b/backend/src/app/migrations/sql/0126-add-team-access-request-table.sql new file mode 100644 index 000000000..548003adb --- /dev/null +++ b/backend/src/app/migrations/sql/0126-add-team-access-request-table.sql @@ -0,0 +1,10 @@ +CREATE TABLE team_access_request ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE, + requester_id uuid NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE, + valid_until timestamptz NOT NULL, + auto_join_until timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (team_id, requester_id) +); diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 336c4aeb2..721068d05 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -729,6 +729,23 @@ [cfg {:keys [::rpc/profile-id] :as params}] (db/tx-run! cfg get-file-summary (assoc params :profile-id profile-id))) + +;; --- COMMAND QUERY: get-file-info + +(defn- get-file-info + [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] + (db/get* conn :file + {:id id} + {::sql/columns [:id]})) + +(sv/defmethod ::get-file-info + "Retrieve minimal file info by its ID." + {::rpc/auth false + ::doc/added "2.2.0" + ::sm/params schema:get-file} + [cfg params] + (db/tx-run! cfg get-file-info params)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MUTATION COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 553257560..2760e73be 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -15,6 +15,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.db.sql :as sql] [app.email :as eml] [app.loggers.audit :as audit] [app.main :as-alias main] @@ -28,6 +29,7 @@ [app.setup :as-alias setup] [app.storage :as sto] [app.tokens :as tokens] + [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] [app.worker :as wrk] @@ -80,6 +82,37 @@ (cond-> row (some? features) (assoc :features (db/decode-pgarray features #{})))) + + +(defn- check-valid-email-muted + "Check if the member's email is part of the global bounce report." + [conn member] + (let [email (profile/clean-email (:email member))] + (when (and member (not (eml/allow-send-emails? conn member))) + (ex/raise :type :validation + :code :member-is-muted + :email email + :hint "the profile has reported repeatedly as spam or has bounces")))) + +(defn- check-valid-email-bounce + "Check if the email is part of the global complain report" + [conn email show?] + (when (eml/has-bounce-reports? conn email) + (ex/raise :type :restriction + :code :email-has-permanent-bounces + :email (if show? email "private") + :hint "this email has been repeatedly reported as bounce"))) + +(defn- check-valid-email-spam + "Check if the member email is part of the global complain report" + [conn email show?] + (when (eml/has-complaint-reports? conn email) + (ex/raise :type :restriction + :code :email-has-complaints + :email (if show? email "private") + :hint "this email has been repeatedly reported as spam"))) + + ;; --- Query: Teams (declare get-teams) @@ -333,6 +366,24 @@ (check-read-permissions! conn profile-id team-id) (get-team-invitations conn team-id))) + +;; --- COMMAND QUERY: get-team-info + +(defn- get-team-info + [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] + (db/get* conn :team + {:id id} + {::sql/columns [:id :is-default]})) + +(sv/defmethod ::get-team-info + "Retrieve minimal team info by its ID." + {::rpc/auth false + ::doc/added "2.2.0" + ::sm/params schema:get-team} + [cfg params] + (db/tx-run! cfg get-team-info params)) + + ;; --- Mutation: Create Team (declare create-team) @@ -727,25 +778,10 @@ (let [email (profile/clean-email email) member (profile/get-profile-by-email conn email)] - (when (and member (not (eml/allow-send-emails? conn member))) - (ex/raise :type :validation - :code :member-is-muted - :email email - :hint "the profile has reported repeatedly as spam or has bounces")) + (check-valid-email-muted conn member) + (check-valid-email-bounce conn email true) + (check-valid-email-spam conn email true) - ;; Secondly check if the invited member email is part of the global bounce report. - (when (eml/has-bounce-reports? conn email) - (ex/raise :type :restriction - :code :email-has-permanent-bounces - :email email - :hint "the email you invite has been repeatedly reported as bounce")) - - ;; Secondly check if the invited member email is part of the global complain report. - (when (eml/has-complaint-reports? conn email) - (ex/raise :type :restriction - :code :email-has-complaints - :email email - :hint "the email you invite has been repeatedly reported as spam")) ;; When we have email verification disabled and invitation user is ;; already present in the database, we proceed to add it to the @@ -814,6 +850,58 @@ itoken)))) +(defn- add-user-to-team + [conn profile team email role] + + (let [team-id (:id team) + member (db/get* conn :profile + {:email (str/lower email)} + {::sql/columns [:id :email]}) + params (merge + {:team-id team-id + :profile-id (:id member)} + (role->params role))] + + ;; Do not allow blocked users to join teams. + (when (:is-blocked member) + (ex/raise :type :restriction + :code :profile-blocked)) + + (quotes/check-quote! conn + {::quotes/id ::quotes/profiles-per-team + ::quotes/profile-id (:id member) + ::quotes/team-id team-id}) + + ;; Insert the member to the team + (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) + + ;; Delete any request + (db/delete! conn :team-access-request + {:team-id team-id :requester-id (:id member)}) + + ;; Delete any invitation + (db/delete! conn :team-invitation + {:team-id team-id :email-to (:email member)}) + + (eml/send! {::eml/conn conn + ::eml/factory eml/join-team + :public-uri (cf/get :public-uri) + :to email + :invited-by (:fullname profile) + :team (:name team) + :team-id (:id team)}))) + +(def sql:valid-requests-email + "SELECT p.email + FROM team_access_request AS tr + JOIN profile AS p ON (tr.requester_id = p.id) + WHERE tr.team_id = ? + AND tr.auto_join_until > now()") + +(defn- get-valid-requests-email + [conn team-id] + (db/exec! conn [sql:valid-requests-email team-id])) + (def ^:private schema:create-team-invitations [:map {:title "create-team-invitations"} [:team-id ::sm/uuid] @@ -846,13 +934,14 @@ (ex/raise :type :validation :code :insufficient-permissions)) - ;; First check if the current profile is allowed to send emails. - (when-not (eml/allow-send-emails? conn profile) - (ex/raise :type :validation - :code :profile-is-muted - :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) + ;; Check if the current profile is allowed to send emails. + (check-valid-email-muted conn profile) - (let [cfg (assoc cfg ::db/conn conn) + + (let [requested (into #{} (map :email) (get-valid-requests-email conn team-id)) + emails-to-add (filter #(contains? requested %) emails) + emails (remove #(contains? requested %) emails) + cfg (assoc cfg ::db/conn conn) members (->> (db/exec! conn [sql:team-members team-id]) (into #{} (map :email))) @@ -868,6 +957,10 @@ (assoc :role role)))) (keep (partial create-invitation cfg))) emails)] + ;; For requested invitations, do not send invitation emails, add the user directly to the team + (doseq [email emails-to-add] + (add-user-to-team conn profile team email role)) + (with-meta {:total (count invitations) :invitations invitations} {::audit/props {:invitations (count invitations)}}))))) @@ -1006,3 +1099,130 @@ :email-to (profile/clean-email email)} {::db/return-keys true})] (rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))) + + + + +;; --- Mutation: Request Team Invitation + +(def sql:upsert-team-access-request + "INSERT INTO team_access_request (id, team_id, requester_id, valid_until, auto_join_until) + VALUES (?, ?, ?, ?, ?) + ON conflict(id) + DO UPDATE SET valid_until = ?, auto_join_until = ?, updated_at = now() + RETURNING *") + + +(def sql:team-access-request + "SELECT id, (valid_until < now()) AS expired + FROM team_access_request + WHERE team_id = ? + AND requester_id = ?") + +(def sql:team-owner + "SELECT profile_id + FROM team_profile_rel + WHERE team_id = ? + AND is_owner = true") + + +(defn- create-team-access-request + [{:keys [::db/conn] :as cfg} {:keys [team requester team-owner file is-viewer] :as params}] + (let [old-request (->> (db/exec-one! conn [sql:team-access-request (:id team) (:id requester)]) + (decode-row))] + (when (false? (:expired old-request)) + (ex/raise :type :validation + :code :request-already-sent + :hint "you have already made a request to join this team less than 24 hours ago")) + + (let [id (or (:id old-request) (uuid/next)) + valid_until (dt/in-future "24h") + auto_join_until (dt/in-future "168h") ;; 7 days + request (db/exec-one! conn [sql:upsert-team-access-request + id (:id team) (:id requester) valid_until auto_join_until + valid_until auto_join_until]) + factory (cond + (and (some? file) (:is-default team) is-viewer) + eml/request-file-access-yourpenpot-view + (and (some? file) (:is-default team)) + eml/request-file-access-yourpenpot + (some? file) + eml/request-file-access + :else + eml/request-team-access) + page-id (when (some? file) + (-> file :data :pages first))] + + ;; TODO needs audit? + + (eml/send! {::eml/conn conn + ::eml/factory factory + :public-uri (cf/get :public-uri) + :to (:email team-owner) + :requested-by (:fullname requester) + :requested-by-email (:email requester) + :team-name (:name team) + :team-id (:id team) + :file-name (:name file) + :file-id (:id file) + :page-id page-id}) + + request))) + + +(def ^:private schema:create-team-access-request + [:and + [:map {:title "create-team-access-request"} + [:file-id {:optional true} ::sm/uuid] + [:team-id {:optional true} ::sm/uuid] + [:is-viewer {:optional true} :boolean]] + + [:fn (fn [params] + (or (contains? params :file-id) + (contains? params :team-id)))]]) + + +(sv/defmethod ::create-team-access-request + "A rpc call that allow to request for an invitations to join the team." + {::doc/added "2.2.0" + ::sm/params schema:create-team-access-request} + [cfg {:keys [::rpc/profile-id file-id team-id is-viewer] :as params}] + + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + + (let [requester (db/get-by-id conn :profile profile-id) + team-id (if (some? team-id) + team-id + (:id (get-team-for-file conn file-id))) + team (db/get-by-id conn :team team-id) + owner-id (->> (db/exec! conn [sql:team-owner (:id team)]) + (map decode-row) + (first) + :profile-id) + team-owner (db/get-by-id conn :profile owner-id) + file (when (some? file-id) + (db/get* conn :file + {:id file-id} + {::sql/columns [:id :name :data]})) + file (when (some? file) + (assoc file :data (blob/decode (:data file))))] + + ;;TODO needs quotes? + + (when (or (nil? requester) (nil? team) (nil? team-owner) (and (some? file-id) (nil? file))) + (ex/raise :type :validation + :code :invalid-parameters)) + + ;; Check that the requester is not muted + (check-valid-email-muted conn requester) + + ;; Check that the owner is not marked as bounce nor spam + (check-valid-email-bounce conn (:email team-owner) false) + (check-valid-email-spam conn (:email team-owner) true) + + (let [request (create-team-access-request + cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})] + (when request + (with-meta {:request request} + {::audit/props {:request 1}}))))))) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 0e4f3c89f..95f266d6e 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -127,6 +127,10 @@ (db/delete! conn :team-invitation {:team-id team-id :email-to member-email}) + ;; Delete any request + (db/delete! conn :team-access-request + {:team-id team-id :requester-id (:id member)}) + (assoc member :is-active true))) (def schema:team-invitation-claims diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 8b4ccda3f..0cdce4995 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -467,3 +467,146 @@ (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 5 (:processed result)))))) + +(t/deftest create-team-access-request + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 3 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + ;; request success + (let [out (th/command! data) + ;; retrieve the value from the database and check its content + request (db/exec-one! + th/*pool* + ["select count(*) as num from team_access_request where team_id = ? and requester_id = ?" + (:id team) (:id requester)])] + + (t/is (th/success? out)) + (t/is (= 1 (:call-count @mock))) + (t/is (= 1 (:num request)))) + + ;; request again fails + (th/reset-mock! mock) + (let [out (th/command! data) + edata (-> out :error ex-data)] + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + + (t/is (= :validation (:type edata))) + (t/is (= :request-already-sent (:code edata)))) + + + ;; request again when is expired success + (th/reset-mock! mock) + + (db/exec-one! + th/*pool* + ["update team_access_request set valid_until = ? where team_id = ? and requester_id = ?" + (dt/in-past "1h") (:id team) (:id requester)]) + + (t/is (th/success? (th/command! data))) + (t/is (= 1 (:call-count @mock)))))) + + +(t/deftest create-team-access-request-owner-muted + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :is-muted true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + ;; request to team with owner muted should success + (t/is (th/success? (th/command! data))) + (t/is (= 1 (:call-count @mock)))))) + + +(t/deftest create-team-access-request-requester-muted + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :is-muted true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)} + + out (th/command! data) + edata (-> out :error ex-data)] + + ;; request with requester muted should fail + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + + (t/is (= :validation (:type edata))) + (t/is (= :member-is-muted (:code edata))) + (t/is (= (:email requester) (:email edata)))))) + + +(t/deftest create-team-access-request-owner-bounce + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + pool (:app.db/pool th/*system*) + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + + (th/create-global-complaint-for pool {:type :bounce :email "owner@bar.com"}) + (let [out (th/command! data) + edata (-> out :error ex-data)] + + ;; request with owner bounce should fail + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + + (t/is (= :restriction (:type edata))) + (t/is (= :email-has-permanent-bounces (:code edata))) + (t/is (= "private" (:email edata))))))) + +(t/deftest create-team-access-request-requester-bounce + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + pool (:app.db/pool th/*system*) + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + ;; request with requester bounce should success + (th/create-global-complaint-for pool {:type :bounce :email "requester@bar.com"}) + (t/is (th/success? (th/command! data))) + (t/is (= 1 (:call-count @mock)))))) + diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index 201193b70..a9b219f78 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -155,3 +155,18 @@ :files files :binary? binary?})))))))) +;;;;;;;;;;;;;;;;;;;;;; +;; Team Request +;;;;;;;;;;;;;;;;;;;;;; + +(defn create-team-access-request + [params] + (ptk/reify ::create-team-access-request + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [on-success on-error] + :or {on-success identity + on-error rx/throw}} (meta params)] + (->> (rp/cmd! :create-team-access-request params) + (rx/tap on-success) + (rx/catch on-error)))))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index ac49a8fbd..1a61a0644 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -153,13 +153,8 @@ accepting invitation, or third party auth signup or singin." [profile] (letfn [(get-redirect-event [] - (let [team-id (get-current-team-id profile) - redirect-url (:redirect-url @storage)] - (if (some? redirect-url) - (do - (swap! storage dissoc :redirect-url) - (.replace js/location redirect-url)) - (rt/nav' :dashboard-projects {:team-id team-id}))))] + (let [team-id (get-current-team-id profile)] + (rt/nav' :dashboard-projects {:team-id team-id})))] (ptk/reify ::logged-in ev/Event @@ -316,7 +311,6 @@ ptk/EffectEvent (effect [_ _ _] ;; We prefer to keek some stuff in the storage like the current-team-id and the profile - (swap! storage dissoc :redirect-url) (set-current-team! nil))))) (defn logout diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 998d81c48..38fd01c22 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -17,7 +17,6 @@ [app.util.globals :as glob] [app.util.i18n :refer [tr]] [app.util.router :as rt] - [app.util.storage :refer [storage]] [app.util.timers :as ts] [cuerdas.core :as str] [potok.v2.core :as ptk])) @@ -96,16 +95,23 @@ (print-trace! error) (print-data! error)))) -;; We receive a explicit authentication error; this explicitly clears +;; We receive a explicit authentication error; +;; If the uri is for workspace, dashboard or view assign the +;; exception for the 'Oops' page. Otherwise this explicitly clears ;; all profile data and redirect the user to the login page. This is ;; here and not in app.main.errors because of circular dependency. (defmethod ptk/handle-error :authentication - [_] - (let [msg (tr "errors.auth.unable-to-login") - uri (. (. js/document -location) -href)] - (st/emit! (du/logout {:capture-redirect true})) - (ts/schedule 500 #(st/emit! (ntf/warn msg))) - (ts/schedule 1000 #(swap! storage assoc :redirect-url uri)))) + [e] + (let [msg (tr "errors.auth.unable-to-login") + uri (.-href glob/location) + show-oops? (or (str/includes? uri "workspace") + (str/includes? uri "dashboard") + (str/includes? uri "view"))] + (if show-oops? + (st/async-emit! (rt/assign-exception e)) + (do + (st/emit! (du/logout {:capture-redirect true})) + (ts/schedule 500 #(st/emit! (ntf/warn msg))))))) ;; Error that happens on an active business model validation does not ;; passes an validation (example: profile can't leave a team). From diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 2e18214e5..30668501f 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -133,7 +133,7 @@ [:& dashboard-page {:route route :profile profile}]] :viewer (let [{:keys [query-params path-params]} route - {:keys [index share-id section page-id interactions-mode frame-id] + {:keys [index share-id section page-id interactions-mode frame-id share] :or {section :interactions interactions-mode :show-on-click}} query-params {:keys [file-id]} path-params] [:? {} @@ -154,7 +154,8 @@ :hide false :show true :show-on-click false) - :frame-id frame-id}])]) + :frame-id frame-id + :share share}])]) :workspace (let [project-id (some-> params :path :project-id uuid) diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 4d24070cc..42db83794 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -14,31 +14,12 @@ [app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.recovery :refer [recovery-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]] - [app.main.ui.auth.register :refer [register-page register-success-page register-validate-page]] + [app.main.ui.auth.register :refer [register-page register-success-page register-validate-page terms-register]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) -(mf/defc terms-login - [] - (let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri) - show-terms? (some? cf/terms-of-service-uri) - show-privacy? (some? cf/privacy-policy-uri)] - - (when show-all? - [:div {:class (stl/css :terms-login)} - (when show-terms? - [:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)} - (tr "auth.terms-of-service")]) - - (when show-all? - [:span {:class (stl/css :and-text)} - (dm/str " " (tr "labels.and") " ")]) - - (when show-privacy? - [:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)} - (tr "auth.privacy-policy")])]))) (mf/defc auth {::mf/props :obj} @@ -90,4 +71,4 @@ [:& recovery-page {:params params}]) (when (= section :auth-register) - [:& terms-login])]])) + [:& terms-register])]])) diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss index 390d4083b..edcb34520 100644 --- a/frontend/src/app/main/ui/auth.scss +++ b/frontend/src/app/main/ui/auth.scss @@ -103,23 +103,3 @@ fill: var(--main-icon-foreground); } } - -.terms-login { - @include bodySmallTypography; - display: flex; - gap: $s-4; - justify-content: center; - width: 100%; -} - -.and-text { - border-bottom: $s-1 solid transparent; - color: var(--title-foreground-color); -} - -.auth-link { - color: var(--link-foreground-color); - &:hover { - text-decoration: underline; - } -} diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 2881458d8..0d0b587a9 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -71,7 +71,7 @@ [:string {:min 1}]]]) (mf/defc login-form - [{:keys [params on-success-callback origin] :as props}] + [{:keys [params on-success-callback on-recovery-request origin] :as props}] (let [initial (mf/with-memo [params] params) error (mf/use-state false) form (fm/use-form :schema schema:login-form @@ -139,9 +139,12 @@ :on-success on-success})] (st/emit! (du/login-with-ldap params))))) - on-recovery-request + default-recovery-req (mf/use-fn - #(st/emit! (rt/nav :auth-recovery-request)))] + #(st/emit! (rt/nav :auth-recovery-request))) + + on-recovery-request (or on-recovery-request + default-recovery-req)] [:* (when-let [message @error] @@ -243,7 +246,7 @@ (tr "auth.login-with-oidc-submit")]))) (mf/defc login-methods - [{:keys [params on-success-callback origin] :as props}] + [{:keys [params on-success-callback on-recovery-request origin] :as props}] [:* (when show-alt-login-buttons? [:* @@ -257,7 +260,7 @@ (when (or (contains? cf/flags :login) (contains? cf/flags :login-with-password) (contains? cf/flags :login-with-ldap)) - [:& login-form {:params params :on-success-callback on-success-callback :origin origin}])]) + [:& login-form {:params params :on-success-callback on-success-callback :on-recovery-request on-recovery-request :origin origin}])]) (mf/defc login-page [{:keys [params] :as props}] diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index 55b6fd28f..afb240647 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -102,3 +102,16 @@ :class (stl/css :go-back-link) :data-testid "go-back-link"} (tr "labels.go-back")]]])) + + +(mf/defc recovery-sent-page + {::mf/props :obj} + [{:keys [email]}] + [:div {:class (stl/css :auth-form-wrapper :register-success)} + [:div {:class (stl/css :auth-title-wrapper)} + [:h2 {:class (stl/css :auth-title)} + (tr "auth.check-mail")] + [:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]] + [:div {:class (stl/css :notification-text-email)} email] + [:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]]) + diff --git a/frontend/src/app/main/ui/auth/recovery_request.scss b/frontend/src/app/main/ui/auth/recovery_request.scss index e78e21b6d..8b384e59d 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.scss +++ b/frontend/src/app/main/ui/auth/recovery_request.scss @@ -10,3 +10,10 @@ .fields-row { margin-bottom: $s-8; } + +.notification-text-email { + @include medTitleTipography; + font-size: $fs-20; + color: var(--register-confirmation-color); + margin-inline: $s-36; +} diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 0d0ed0ecb..f4788b628 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.auth.register (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.common.schema :as sm] [app.config :as cf] [app.main.data.notifications :as ntf] @@ -103,11 +104,12 @@ (mf/defc register-methods {::mf/props :obj} - [{:keys [params on-success-callback]}] + [{:keys [params hide-separator on-success-callback]}] [:* (when login/show-alt-login-buttons? [:& login/login-buttons {:params params}]) - [:hr {:class (stl/css :separator)}] + (when (or login/show-alt-login-buttons? (false? hide-separator)) + [:hr {:class (stl/css :separator)}]) [:& register-form {:params params :on-success-callback on-success-callback}]]) (mf/defc register-page @@ -251,14 +253,37 @@ (mf/defc register-success-page {::mf/props :obj} - [] - (let [email (::email @sto/storage)] + [{:keys [params]}] + (let [email (or (:email params) (::email @sto/storage))] [:div {:class (stl/css :auth-form-wrapper :register-success)} - [:h1 {:class (stl/css :logo-container)} - [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]] + (when-not (:hide-logo params) + [:h1 {:class (stl/css :logo-container)} + [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]) [:div {:class (stl/css :auth-title-wrapper)} [:h2 {:class (stl/css :auth-title)} (tr "auth.check-mail")] [:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]] [:div {:class (stl/css :notification-text-email)} email] [:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])) + + +(mf/defc terms-register + [] + (let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri) + show-terms? (some? cf/terms-of-service-uri) + show-privacy? (some? cf/privacy-policy-uri)] + + (when show-all? + [:div {:class (stl/css :terms-register)} + (when show-terms? + [:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)} + (tr "auth.terms-of-service")]) + + (when show-all? + [:span {:class (stl/css :and-text)} + (dm/str " " (tr "labels.and") " ")]) + + (when show-privacy? + [:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)} + (tr "auth.privacy-policy")])]))) + diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss index 0f0497442..0309cd44a 100644 --- a/frontend/src/app/main/ui/auth/register.scss +++ b/frontend/src/app/main/ui/auth/register.scss @@ -66,3 +66,23 @@ width: $s-120; margin-block-end: $s-24; } + +.terms-register { + @include bodySmallTypography; + display: flex; + gap: $s-4; + justify-content: center; + width: 100%; +} + +.and-text { + border-bottom: $s-1 solid transparent; + color: var(--title-foreground-color); +} + +.auth-link { + color: var(--link-foreground-color); + &:hover { + text-decoration: underline; + } +} diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index eef34a8cf..75f6ee25a 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -420,7 +420,7 @@ (into [] (distinct) (conj coll item))) (mf/defc multi-input - [{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}] + [{:keys [form label class name trim valid-item-fn caution-item-fn on-submit invite-email] :as props}] (let [form (or form (mf/use-ctx form-ctx)) input-name (get props :name) touched? (get-in @form [:touched input-name]) @@ -528,6 +528,12 @@ values (filterv #(:valid %) values)] (update-form! values))) + (mf/with-effect [] + (when invite-email + (swap! items conj-dedup {:text (str/trim invite-email) + :valid (valid-item-fn invite-email) + :caution (caution-item-fn invite-email)}))) + [:div {:class klass} [:input {:id (name input-name) :class in-klass diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 4adec8d15..78c9902a9 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -52,7 +52,7 @@ (assoc :project-id (uuid project-id))))) (mf/defc dashboard-content - [{:keys [team projects project section search-term profile] :as props}] + [{:keys [team projects project section search-term profile invite-email] :as props}] (let [container (mf/use-ref) content-width (mf/use-state 0) project-id (:id project) @@ -129,7 +129,7 @@ [:& libraries-page {:team team}] :dashboard-team-members - [:& team-members-page {:team team :profile profile}] + [:& team-members-page {:team team :profile profile :invite-email invite-email}] :dashboard-team-invitations [:& team-invitations-page {:team team}] @@ -153,6 +153,7 @@ project-id (:project-id params) team-id (:team-id params) search-term (:search-term params) + invite-email (-> route :query-params :invite-email) teams (mf/deref refs/teams) team (get teams team-id) @@ -204,5 +205,6 @@ :project project :section section :search-term search-term - :team team}])])]])) + :team team + :invite-email invite-email}])])]])) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index af8b3e4d5..c19f6f495 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -61,7 +61,7 @@ (mf/defc header {::mf/wrap [mf/memo] ::mf/wrap-props false} - [{:keys [section team]}] + [{:keys [section team invite-email]}] (let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members))) on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings))) on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations))) @@ -79,7 +79,12 @@ (fn [] (st/emit! (modal/show {:type :invite-members :team team - :origin :team}))))] + :origin :team + :invite-email invite-email}))))] + + (mf/with-effect [] + (when invite-email + (on-invite-member))) [:header {:class (stl/css :dashboard-header :team) :data-testid "dashboard-header"} [:div {:class (stl/css :dashboard-title)} @@ -141,7 +146,7 @@ {::mf/register modal/components ::mf/register-as :invite-members ::mf/wrap-props false} - [{:keys [team origin]}] + [{:keys [team origin invite-email]}] (let [members-map (mf/deref refs/dashboard-team-members) perms (:permissions team) @@ -192,7 +197,8 @@ :on-error (partial on-error form)}] (st/emit! (-> (dd/invite-team-members (with-meta params mdata)) (with-meta {::ev/origin origin})) - (dd/fetch-team-invitations))))] + (dd/fetch-team-invitations) + (dd/fetch-team-members (:id team)))))] [:div {:class (stl/css-case :modal-team-container true @@ -223,7 +229,8 @@ :valid-item-fn us/parse-email :caution-item-fn current-members-emails :label (tr "modals.invite-member.emails") - :on-submit on-submit}]] + :on-submit on-submit + :invite-email invite-email}]] [:div {:class (stl/css :action-buttons)} [:> fm/submit-button* @@ -497,7 +504,7 @@ (mf/defc team-members-page {::mf/wrap-props false} - [{:keys [team profile]}] + [{:keys [team profile invite-email]}] (let [members-map (mf/deref refs/dashboard-team-members)] (mf/with-effect [team] @@ -511,7 +518,7 @@ (st/emit! (dd/fetch-team-members (:id team)))) [:* - [:& header {:section :dashboard-team-members :team team}] + [:& header {:section :dashboard-team-members :team team :invite-email invite-email}] [:section {:class (stl/css :dashboard-container :dashboard-team-members)} [:& team-members {:profile profile diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 5a0b0a11a..dea1570f6 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -10,32 +10,50 @@ [app.common.data :as d] [app.common.pprint :as pp] [app.common.uri :as u] + [app.main.data.common :as dc] [app.main.data.events :as ev] + [app.main.repo :as rp] [app.main.store :as st] + [app.main.ui.auth.login :refer [login-methods]] + [app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]] + [app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]] + [app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.icons :as i] + [app.main.ui.viewer.header :as header] [app.util.dom :as dom] - [app.util.globals :as globals] [app.util.i18n :refer [tr]] [app.util.router :as rt] [app.util.webapi :as wapi] + [beicon.v2.core :as rx] + [cuerdas.core :as str] [potok.v2.core :as ptk] [rumext.v2 :as mf])) (mf/defc error-container {::mf/wrap-props false} [{:keys [children]}] - (let [on-click (mf/use-callback #(set! (.-href globals/location) "/"))] + (let [profile-id (:profile-id @st/state)] [:section {:class (stl/css :exception-layout)} [:button {:class (stl/css :exception-header) - :on-click on-click} - i/logo-icon] + :on-click rt/nav-root} + i/logo-icon + (when profile-id + (str "< " + (tr "not-found.no-permission.go-dashboard")))] [:div {:class (stl/css :deco-before)} i/logo-error-screen] + (when-not profile-id + [:button {:class (stl/css :login-header) + :on-click rt/nav-root} + (tr "labels.login")]) [:div {:class (stl/css :exception-content)} [:div {:class (stl/css :container)} children]] - [:div {:class (stl/css :deco-after)} i/logo-error-screen]])) + [:div {:class (stl/css :deco-after2)} + [:span (tr "labels.copyright")] + i/logo-error-screen + [:span (tr "not-found.made-with-love")]]])) (mf/defc invalid-token [] @@ -43,16 +61,221 @@ [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")] [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]]) + + +(mf/defc login-dialog + {::mf/props :obj} + [{:keys [show-dialog]}] + (let [current-section (mf/use-state :login) + user-email (mf/use-state "") + register-token (mf/use-state "") + + set-section + (mf/use-fn + (fn [event] + (let [section (-> (dom/get-current-target event) + (dom/get-data "section") + (keyword))] + (reset! current-section section)))) + + set-section-recovery + (mf/use-fn + #(reset! current-section :recovery-request)) + + set-section-login + (mf/use-fn + #(reset! current-section :login)) + + success-login + (fn [] + (reset! show-dialog false) + (.reload js/window.location true)) + + success-register + (fn [data] + (reset! register-token (:token data)) + (reset! current-section :register-validate)) + + register-email-sent + (fn [email] + (reset! user-email email) + (reset! current-section :register-email-sent)) + + recovery-email-sent + (fn [email] + (reset! user-email email) + (reset! current-section :recovery-email-sent))] + + [:div {:class (stl/css :overlay)} + [:div {:class (stl/css :dialog-login)} + [:div {:class (stl/css :modal-close)} + [:button {:class (stl/css :modal-close-button) :on-click rt/nav-root} + i/close]] + [:div {:class (stl/css :login)} + [:div {:class (stl/css :logo)} i/logo] + + (case @current-section + :login + [:* + [:div {:class (stl/css :logo-title)} (tr "labels.login")] + [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")] + [:& login-methods {:on-recovery-request set-section-recovery + :on-success-callback success-login}] + [:hr {:class (stl/css :separator)}] + [:div {:class (stl/css :change-section)} + (tr "auth.register") + " " + [:a {:data-section "register" + :on-click set-section} (tr "auth.register-submit")]]] + + :register + [:* + [:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")] + [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")] + [:& register-methods {:on-success-callback success-register :hide-separator true}] + #_[:hr {:class (stl/css :separator)}] + [:div {:class (stl/css :separator)}] + [:div {:class (stl/css :change-section)} + (tr "auth.already-have-account") + " " + [:a {:data-section "login" + :on-click set-section} (tr "auth.login-here")]] + [:div {:class (stl/css :links)} + [:hr {:class (stl/css :separator)}] + [:& terms-register]]] + + :register-validate + [:div {:class (stl/css :form-container)} + [:& register-validate-form {:params {:token @register-token} + :on-success-callback register-email-sent}] + [:div {:class (stl/css :links)} + [:div {:class (stl/css :register)} + [:a {:data-section "register" + :on-click set-section} + (tr "labels.go-back")]]]] + + :register-email-sent + [:div {:class (stl/css :form-container)} + [:& register-success-page {:params {:email @user-email :hide-logo true}}]] + + :recovery-request + [:& recovery-request-page {:go-back-callback set-section-login + :on-success-callback recovery-email-sent}] + + :recovery-email-sent + [:div {:class (stl/css :form-container)} + [:& recovery-sent-page {:email @user-email}]])]]])) + +(mf/defc request-dialog + {::mf/props :obj} + [{:keys [title content button-text on-button-click cancel-text]}] + (let [on-click (or on-button-click rt/nav-root)] + [:div {:class (stl/css :overlay)} + [:div {:class (stl/css :dialog)} + [:div {:class (stl/css :modal-close)} + [:button {:class (stl/css :modal-close-button) :on-click rt/nav-root} + i/close]] + [:div {:class (stl/css :dialog-title)} title] + (for [txt content] + [:div txt]) + [:div {:class (stl/css :sign-info)} + (when cancel-text + [:button {:class (stl/css :cancel-button) :on-click rt/nav-root} cancel-text]) + [:button {:on-click on-click} button-text]]]])) + + +(mf/defc request-access + {::mf/props :obj} + [{:keys [file-id team-id is-default workspace?]}] + (let [profile (:profile @st/state) + requested* (mf/use-state {:sent false :already-requested false}) + requested (deref requested*) + show-dialog (mf/use-state true) + on-success + (mf/use-fn + #(reset! requested* {:sent true :already-requested false})) + on-error + (mf/use-fn + #(reset! requested* {:sent true :already-requested true})) + on-request-access + (mf/use-fn + (mf/deps file-id team-id workspace?) + (fn [] + (let [params (if (some? file-id) {:file-id file-id :is-viewer (not workspace?)} {:team-id team-id}) + mdata {:on-success on-success :on-error on-error}] + (st/emit! (dc/create-team-access-request (with-meta params mdata))))))] + + + [:* + (if (some? file-id) + (if workspace? + [:div {:class (stl/css :workspace)} + [:div {:class (stl/css :workspace-left)} + i/logo-icon + [:div + [:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")] + [:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]] + [:div {:class (stl/css :workspace-right)}]] + [:div {:class (stl/css :viewer)} + [:& header/header {:project {:name (tr "not-found.no-permission.project-name")} + :index 0 + :file {:name (tr "not-found.no-permission.penpot-file")} + :page nil + :frame nil + :permissions {:is-logged true} + :zoom 1 + :section :interactions + :shown-thumbnails false + :interactions-mode nil}]]) + + [:div {:class (stl/css :dashboard)} + [:div {:class (stl/css :dashboard-sidebar)} + [:& sidebar + {:team nil + :projects [] + :project (:default-project-id profile) + :profile profile + :section :dashboard-projects + :search-term ""}]]]) + + (when @show-dialog + (cond + (nil? profile) + [:& login-dialog {:show-dialog show-dialog}] + + is-default + [:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}] + + (and (some? file-id) (:already-requested requested)) + [:& request-dialog {:title (tr "not-found.no-permission.already-requested.file") :content [(tr "not-found.no-permission.already-requested.or-others.file")] :button-text (tr "not-found.no-permission.go-dashboard")}] + + (:already-requested requested) + [:& request-dialog {:title (tr "not-found.no-permission.already-requested.project") :content [(tr "not-found.no-permission.already-requested.or-others.project")] :button-text (tr "not-found.no-permission.go-dashboard")}] + + (:sent requested) + [:& request-dialog {:title (tr "not-found.no-permission.done.success") :content [(tr "not-found.no-permission.done.remember")] :button-text (tr "not-found.no-permission.go-dashboard")}] + + (some? file-id) + [:& request-dialog {:title (tr "not-found.no-permission.file") :content [(tr "not-found.no-permission.you-can-ask.file") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}] + + (some? team-id) + [:& request-dialog {:title (tr "not-found.no-permission.project") :content [(tr "not-found.no-permission.you-can-ask.project") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}]))])) + + + (mf/defc not-found [] [:> error-container {} [:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")] - [:div {:class (stl/css :desc-message)} (tr "labels.not-found.desc-message")]]) + [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.error")] + [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.doesnt-exist")]]) + + (mf/defc bad-gateway [] (let [handle-retry - (mf/use-callback + (mf/use-fn (fn [] (st/emit! (rt/assign-exception nil))))] [:> error-container {} [:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")] @@ -150,13 +373,49 @@ (mf/defc exception-page {::mf/props :obj} [{:keys [data route] :as props}] - (let [type (:type data) - path (:path route) - query-params (u/map->query-string (:query-params route))] - (st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params})) + (let [file-info (mf/use-state {:pending true}) + team-info (mf/use-state {:pending true}) + type (:type data) + path (:path route) + + workspace? (str/includes? path "workspace") + dashboard? (str/includes? path "dashboard") + view? (str/includes? path "view") + + request-access? (and + (or workspace? dashboard? view?) + (or (not (str/empty? (:file-id @file-info))) (not (str/empty? (:team-id @team-info))))) + + query-params (u/map->query-string (:query-params route)) + pparams (:path-params route) + on-file-info (mf/use-fn + (fn [info] + (reset! file-info {:file-id (:id info)}))) + on-team-info (mf/use-fn + (fn [info] + (reset! team-info {:team-id (:id info) :is-default (:is-default info)})))] + + (mf/with-effect [type path query-params pparams @file-info @team-info] + (st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params})) + + (when (and (:file-id pparams) (:pending @file-info)) + (->> (rp/cmd! :get-file-info {:id (:file-id pparams)}) + (rx/subs! on-file-info))) + + (when (and (:team-id pparams) (:pending @team-info)) + (->> (rp/cmd! :get-team-info {:id (:team-id pparams)}) + (rx/subs! on-team-info)))) + (case (:type data) :not-found - [:& not-found] + (if request-access? + [:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}] + [:& not-found]) + + :authentication + (if request-access? + [:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}] + [:& not-found]) :bad-gateway [:& bad-gateway] diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss index 1d2ea861a..601bd780c 100644 --- a/frontend/src/app/main/ui/static.scss +++ b/frontend/src/app/main/ui/static.scss @@ -28,6 +28,7 @@ .deco-before { height: 34vh; top: 0; + svg { bottom: 0; } @@ -36,17 +37,52 @@ .deco-after { height: 34vh; bottom: 0; + svg { top: 0; } } +.deco-after2 { + display: flex; + justify-content: center; + gap: $s-8; + width: 100%; + height: 34vh; + position: absolute; + left: 0; + bottom: 0; + color: var(--color-foreground-primary); + + svg { + fill: var(--color-foreground-secondary); + height: 1537px; + width: $s-80; + } + + span { + display: flex; + flex-direction: column; + justify-content: flex-end; + height: 100%; + width: 25%; + + &:first-child { + text-align: right; + } + } +} + .exception-header { + color: var(--color-foreground-secondary); padding: $s-24 $s-32; position: fixed; background: none; border: none; cursor: pointer; + display: flex; + align-items: center; + svg { fill: var(--color-foreground-primary); width: $s-48; @@ -54,6 +90,15 @@ } } +.login-header { + @extend .button-primary; + padding: $s-8 $s-16; + font-size: $fs-11; + position: fixed; + right: 0; + margin: $s-40 $s-32; +} + .exception-content { display: flex; height: 100%; @@ -85,6 +130,7 @@ .sign-info { text-align: center; + button { @extend .button-primary; text-transform: uppercase; @@ -98,3 +144,180 @@ fill: var(--color-foreground-primary); } } + +.workspace { + width: 100%; + height: 100%; + display: flex; + justify-content: space-between; + background-color: var(--color-canvas); + position: relative; + + .workspace-left, + .workspace-right { + padding: $s-12; + width: $s-276; + height: 100%; + background-color: var(--color-background-primary); + display: flex; + gap: $s-4; + + svg { + width: 2rem; + height: 2rem; + fill: var(--icon-foreground-hover); + } + + .project-name { + @include uppercaseTitleTipography; + color: var(--title-foreground-color); + } + + .file-name { + @include smallTitleTipography; + text-transform: none; + color: var(--title-foreground-color-hover); + } + } +} + +.dashboard { + width: 100%; + height: 100%; + + .dashboard-sidebar { + width: $s-300; + height: 100%; + } +} + +.viewer { + width: 100%; + height: 100%; +} + +.overlay { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 100; + background-color: rgba(0, 0, 0, 0.65); + display: flex; + justify-content: center; + align-items: center; + + .dialog, + .dialog-login { + width: 556px; + background-color: var(--color-background-primary); + border-radius: $s-8; + display: flex; + flex-direction: column; + align-content: stretch; + padding: $s-36; + color: var(--modal-text-foreground-color); + + .modal-close { + text-align: right; + + .modal-close-button { + background: none; + border: none; + cursor: pointer; + + svg { + cursor: pointer; + width: $s-24; + height: $s-24; + fill: var(--modal-text-foreground-color); + stroke: var(--modal-text-foreground-color); + } + } + } + + .dialog-title { + font-size: $fs-20; + } + + .sign-info { + display: flex; + justify-content: flex-end; + margin-top: $s-32; + + button { + @extend .button-primary; + text-transform: uppercase; + padding: $s-8 $s-16; + font-size: $fs-11; + } + + .cancel-button { + @extend .button-secondary; + text-transform: uppercase; + padding: $s-8 $s-16; + font-size: $fs-11; + margin-right: $s-16; + } + } + } + + .dialog { + gap: $s-12; + } + + .login { + gap: 0; + padding: 0 $s-36 $s-72 $s-36; + + .logo { + margin-bottom: $s-40; + + svg { + fill: var(--color-foreground-primary); + width: $s-120; + height: $s-40; + } + } + + .logo-title { + font-size: $fs-20; + color: var(--title-foreground-color-hover); + margin-bottom: $s-4; + } + + .logo-subtitle { + font-size: $fs-14; + color: var(--title-foreground-color-hover); + margin-bottom: $s-24; + } + + .change-section { + width: 100%; + text-align: center; + + a { + color: var(--link-foreground-color); + } + } + + hr { + margin: $s-20 0; + border-top: solid 1px var(--modal-separator-backogrund-color); + } + + .separator { + margin: $s-20 0; + } + + form div { + margin-bottom: $s-8; + } + } +} + +.login-container { + width: 100%; + background-color: red; +} diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 2fe373491..0410bd1df 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -276,7 +276,7 @@ (mf/defc viewer-content {::mf/wrap-props false} - [{:keys [data page-id share-id section index interactions-mode] :as props}] + [{:keys [data page-id share-id section index interactions-mode share] :as props}] (let [{:keys [file users project permissions]} data allowed (or (= section :interactions) @@ -615,7 +615,8 @@ :zoom zoom :section section :shown-thumbnails (:show-thumbnails local) - :interactions-mode interactions-mode}]])) + :interactions-mode interactions-mode + :share share}]])) ;; --- Component: Viewer diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 603c1cba1..deeaced4d 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -120,7 +120,7 @@ :key (dm/str "zoom-fullscreen-" sc)} sc])]]]]])) (mf/defc header-options - [{:keys [section zoom page file index permissions interactions-mode]}] + [{:keys [section zoom page file index permissions interactions-mode share]}] (let [fullscreen? (mf/deref fullscreen-ref) toggle-fullscreen @@ -159,6 +159,12 @@ handle-zoom-fit (mf/use-fn #(st/emit! dv/zoom-to-fit))] + (mf/with-effect [permissions share] + (when (and + (:in-team permissions) + (:is-admin permissions) + share) + (open-share-dialog))) [:div {:class (stl/css :options-zone)} [:& export-progress-widget] @@ -261,7 +267,7 @@ (mf/defc header - [{:keys [project file page frame zoom section permissions index interactions-mode shown-thumbnails]}] + [{:keys [project file page frame zoom section permissions index interactions-mode shown-thumbnails share]}] (let [go-to-dashboard (mf/use-fn #(st/emit! (dv/go-to-dashboard))) @@ -351,4 +357,5 @@ :file file :index index :zoom zoom - :interactions-mode interactions-mode}]])) + :interactions-mode interactions-mode + :share share}]])) diff --git a/frontend/src/app/main/ui/viewer/login.cljs b/frontend/src/app/main/ui/viewer/login.cljs index 1a1e692dc..c8b02b2f1 100644 --- a/frontend/src/app/main/ui/viewer/login.cljs +++ b/frontend/src/app/main/ui/viewer/login.cljs @@ -10,14 +10,12 @@ [app.common.logging :as log] [app.main.data.modal :as modal] [app.main.store :as st] - [app.main.ui.auth :refer [terms-login]] [app.main.ui.auth.login :refer [login-methods]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]] - [app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page]] + [app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.storage :refer [storage]] [rumext.v2 :as mf])) (log/set-level! :warn) @@ -26,8 +24,7 @@ {::mf/register modal/components ::mf/register-as :login-register} [_] - (let [uri (. (. js/document -location) -href) - user-email (mf/use-state "") + (let [user-email (mf/use-state "") register-token (mf/use-state "") current-section* (mf/use-state :login) @@ -66,9 +63,6 @@ (reset! register-token (:token data)) (set-current-section :register-validate))] - (mf/with-effect [] - (swap! storage assoc :redirect-url uri)) - [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-header)} @@ -125,4 +119,4 @@ (when main-section [:div {:class (stl/css :links)} - [:& terms-login]])]]])) + [:& terms-register]])]]])) diff --git a/frontend/src/app/main/ui/viewer/login.scss b/frontend/src/app/main/ui/viewer/login.scss index 74dc3eb6e..614bfa701 100644 --- a/frontend/src/app/main/ui/viewer/login.scss +++ b/frontend/src/app/main/ui/viewer/login.scss @@ -12,6 +12,7 @@ .modal-container { @extend .modal-container-base; + width: $s-368; } .modal-header { @@ -32,8 +33,8 @@ @include bodySmallTypography; gap: $s-24; max-height: $s-400; - width: $s-368; overflow: hidden auto; + form { display: flex; flex-direction: column; @@ -46,7 +47,6 @@ display: flex; justify-content: center; flex-direction: column; - max-width: $s-368; } .links { @@ -64,6 +64,7 @@ color: var(--modal-text-foreground-color); margin-top: $s-12; } + a { @extend .button-secondary; height: $s-40; diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs index c4d541cfd..6374d8822 100644 --- a/frontend/src/app/util/router.cljs +++ b/frontend/src/app/util/router.cljs @@ -13,6 +13,7 @@ [app.main.data.events :as ev] [app.util.browser-history :as bhistory] [app.util.dom :as dom] + [app.util.globals :as globals] [app.util.timers :as ts] [beicon.v2.core :as rx] [goog.events :as e] @@ -143,6 +144,11 @@ (= (.-hostname location) (:host referrer))) (nav-back)))) +(defn nav-root + "Navigate to the root page." + [] + (set! (.-href globals/location) "/")) + ;; --- History API (defn initialize-history diff --git a/frontend/src/app/util/storage.cljs b/frontend/src/app/util/storage.cljs index a29049740..cd9303edd 100644 --- a/frontend/src/app/util/storage.cljs +++ b/frontend/src/app/util/storage.cljs @@ -44,3 +44,4 @@ (defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage"))))) (add-watch storage :persistence #(persist js/localStorage %3 %4)) + diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 7d968ff1b..fe0da0f66 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2627,6 +2627,15 @@ msgstr "Start" msgid "labels.student-teacher" msgstr "Student or teacher" +msgid "labels.login" +msgstr "Login" + +msgid "labels.ok" +msgstr "Ok" + +msgid "labels.copyright" +msgstr "Kaleidos @2024" + #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, msgid "settings.detach" msgstr "Detach" @@ -5322,3 +5331,72 @@ msgstr "Read and modify your libraries and assets." msgid "workspace.plugins.discover" msgstr "" "Discover [more plugins](%s)" + +msgid "not-found.desc-message.error" +msgstr "404 error" + +msgid "not-found.desc-message.doesnt-exist" +msgstr "This page doesn't exist" + +msgid "not-found.made-with-love" +msgstr "Made with LOVE and Open Source" + +msgid "not-found.no-permission.file" +msgstr "You don't have access to this file." + +msgid "not-found.no-permission.project" +msgstr "You don't have access to this project." + +msgid "not-found.no-permission.you-can-ask.file" +msgstr "To access this file, you can ask the team owner." + +msgid "not-found.no-permission.you-can-ask.project" +msgstr "To access this project, you can ask the team owner." + +msgid "not-found.no-permission.if-approves" +msgstr "If the owner allows it, you're going to be invited to the team." + +msgid "not-found.no-permission.ask" +msgstr "REQUEST ACCESS" + +msgid "not-found.no-permission.already-requested.file" +msgstr "You have already requested access to this file." + +msgid "not-found.no-permission.already-requested.project" +msgstr "You have already requested access to this project." + +msgid "not-found.no-permission.already-requested.or-others.file" +msgstr "You have already requested access to this file or other files or projects of this team." + +msgid "not-found.no-permission.already-requested.or-others.project" +msgstr "You have already requested access to this project or other projects or files of this team." + +msgid "not-found.no-permission.done.success" +msgstr "Your request has been sent correctly!" + +msgid "not-found.no-permission.done.remember" +msgstr "Remember that, if the owner allows it, you're going to be invited to the team." + +msgid "not-found.no-permission.go-dashboard" +msgstr "Go to your Penpot" + +msgid "not-found.no-permission.project-name" +msgstr "PROJECT" + +msgid "not-found.no-permission.penpot-file" +msgstr "Penpot file" + +msgid "not-found.login.free" +msgstr "Penpot is the free and open-source design tool for collaboration between Design and Code" + +msgid "not-found.login.signup-free" +msgstr "Signup for free" + +msgid "not-found.login.start-using" +msgstr "And start using Penpot in seconds!" + +msgid "not-found.login.sent-recovery" +msgstr "We have sent a recovery email to" + +msgid "not-found.login.sent-recovery-check" +msgstr "Check your email and click on the link to create a new password." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 72d891e8a..57fff1984 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2680,6 +2680,15 @@ msgstr "Comenzar" msgid "labels.student-teacher" msgstr "Estudiante o profesorado" +msgid "labels.login" +msgstr "Entrar" + +msgid "labels.ok" +msgstr "Ok" + +msgid "labels.copyright" +msgstr "Kaleidos @2024" + #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, msgid "settings.detach" msgstr "Desacoplar" @@ -5426,3 +5435,74 @@ msgstr "Leer y modificar la información de sus bibliotecas y recursos." msgid "workspace.plugins.discover" msgstr "" "Descubre [más extensiones](%s)" + +msgid "not-found.desc-message.error" +msgstr "Error 404" + +msgid "not-found.desc-message.doesnt-exist" +msgstr "Esta página no existe" + +msgid "not-found.made-with-love" +msgstr "Hecho con AMOR y Software Libre" + +msgid "not-found.no-permission.file" +msgstr "No tienes permiso para acceder a este archivo." + +msgid "not-found.no-permission.project" +msgstr "No tienes permiso para acceder a este proyecto." + +msgid "not-found.no-permission.you-can-ask.file" +msgstr "Para acceder a este archivo, puedes pedir permiso al propietario del equipo." + +msgid "not-found.no-permission.you-can-ask.project" +msgstr "Para acceder a este proyecto, puedes pedir permiso al propietario del equipo." + +msgid "not-found.no-permission.if-approves" +msgstr "Si el propietario lo aprueba, te invitará al equipo." + +msgid "not-found.no-permission.ask" +msgstr "SOLICITAR ACCESO" + +msgid "not-found.no-permission.already-requested.file" +msgstr "Ya has solicitado acceso a este archivo." + +msgid "not-found.no-permission.already-requested.project" +msgstr "Ya has solicitado acceso a este proyecto." + +msgid "not-found.no-permission.already-requested.or-others.file" +msgstr "Ya has solicitado acceso a este archivo o a otros archivos o proyectos del mismo equipo." + +msgid "not-found.no-permission.already-requested.or-others.project" +msgstr "Ya has solicitado acceso a este proyecto o a otros proyectos o archivos del mismo equipo." + +msgid "not-found.no-permission.done.success" +msgstr "¡Tu solicitud se ha enviado correctamente!" + +msgid "not-found.no-permission.done.remember" +msgstr "Recuerda que, si el propietario la aprueba, entrarás en el equipo." + +msgid "not-found.no-permission.go-dashboard" +msgstr "Ir a tu Penpot" + +msgid "not-found.no-permission.project-name" +msgstr "PROYECTO" + +msgid "not-found.no-permission.penpot-file" +msgstr "Archivo de Penpot" + +msgid "not-found.login.free" +msgstr "Penpot es la herramienta de diseño libre y de código abierto para la colaboración entre diseño y código" + +msgid "not-found.login.signup-free" +msgstr "Registrate gratis" + +msgid "not-found.login.start-using" +msgstr "¡Y comienza a usar Penpot en segundos!" + +msgid "not-found.login.sent-recovery" +msgstr "Hemos enviado un email de recuperación a" + +msgid "not-found.login.sent-recovery-check" +msgstr "Revisa tu correo y haz clic en el enlace para crear una nueva contraseña." + +