From b87cd6f4ab12848909ffec5298672d111a2f84e3 Mon Sep 17 00:00:00 2001 From: juliangojani Date: Sun, 10 Nov 2024 03:55:25 +0100 Subject: [PATCH] Adding workspaces --- .../controller/domains/DomainsController.java | 6 +- .../links/ShortenLinksController.java | 86 +++++++++- .../WorkspaceDomainsController.java | 108 ++++++++++++ .../workspaces/WorkspacesController.java | 162 ++++++++++++++++++ .../WorkspaceSlugInvalidException.java | 4 + .../exceptions/WorkspaceTakenException.java | 4 + .../punyshort/helper/RequestHelper.java | 31 +++- .../punyshort/model/database/AccessToken.java | 2 +- .../punyshort/model/database/ShortenLink.java | 28 ++- .../model/database/ShortenLinkTag.java | 21 +++ .../punyshort/model/database/User.java | 8 + .../model/database/domains/Domain.java | 20 +++ .../model/database/workspaces/Workspace.java | 102 +++++++++++ .../database/workspaces/WorkspaceDomain.java | 40 +++++ .../database/workspaces/WorkspaceUser.java | 53 ++++++ .../requests/links/ShortenLinkRequest.java | 7 + .../requests/workspaces/AddDomainRequest.java | 5 + .../CreateWorkspaceInvitationRequest.java | 11 ++ .../workspaces/CreateWorkspaceRequest.java | 11 ++ .../responses/links/ShortenLinkResponse.java | 9 +- .../workspaces/WorkspaceResponse.java | 49 ++++++ .../workspaces/WorkspaceUserResponse.java | 33 ++++ 22 files changed, 781 insertions(+), 19 deletions(-) create mode 100644 src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspaceDomainsController.java create mode 100644 src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspacesController.java create mode 100644 src/main/java/de/interaapps/punyshort/exceptions/WorkspaceSlugInvalidException.java create mode 100644 src/main/java/de/interaapps/punyshort/exceptions/WorkspaceTakenException.java create mode 100644 src/main/java/de/interaapps/punyshort/model/database/ShortenLinkTag.java create mode 100644 src/main/java/de/interaapps/punyshort/model/database/workspaces/Workspace.java create mode 100644 src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceDomain.java create mode 100644 src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceUser.java create mode 100644 src/main/java/de/interaapps/punyshort/model/requests/workspaces/AddDomainRequest.java create mode 100644 src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceInvitationRequest.java create mode 100644 src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceRequest.java create mode 100644 src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceResponse.java create mode 100644 src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceUserResponse.java diff --git a/src/main/java/de/interaapps/punyshort/controller/domains/DomainsController.java b/src/main/java/de/interaapps/punyshort/controller/domains/DomainsController.java index 8ea56ca..e807f06 100644 --- a/src/main/java/de/interaapps/punyshort/controller/domains/DomainsController.java +++ b/src/main/java/de/interaapps/punyshort/controller/domains/DomainsController.java @@ -72,7 +72,8 @@ public PaginatedResponse getAll(Exchange exchange, @Attrib("user @Post @With("auth") - public DomainResponse create(@Body CreateDomainRequest request, @Attrib("user") User user) { + public DomainResponse create(@Body CreateDomainRequest request, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { + accessToken.checkPermission("domains:write"); Domain domain = new Domain(); request.name = request.name.toLowerCase(); @@ -167,6 +168,8 @@ public ActionResponse delete(@Path("id") String id, @Attrib("token") AccessToken @Post("/{id}/dns-check") @With("auth") public ActionResponse triggerDomainCheck(@Path("id") String id, @Attrib("token") AccessToken accessToken, @Attrib("user") User user) { + accessToken.checkPermission("domains:write"); + Domain domain = Domain.get(id, false); if (domain == null) throw new NotFoundException(); @@ -175,7 +178,6 @@ public ActionResponse triggerDomainCheck(@Path("id") String id, @Attrib("token") if (domainUser == null || domainUser.role != DomainUser.Role.ADMIN) throw new PermissionsDeniedException(); - accessToken.checkPermission("domains:write"); domain.updateStatus(); diff --git a/src/main/java/de/interaapps/punyshort/controller/links/ShortenLinksController.java b/src/main/java/de/interaapps/punyshort/controller/links/ShortenLinksController.java index e6c8330..e68832c 100644 --- a/src/main/java/de/interaapps/punyshort/controller/links/ShortenLinksController.java +++ b/src/main/java/de/interaapps/punyshort/controller/links/ShortenLinksController.java @@ -7,10 +7,14 @@ import de.interaapps.punyshort.helper.RequestHelper; import de.interaapps.punyshort.model.database.AccessToken; import de.interaapps.punyshort.model.database.ShortenLink; +import de.interaapps.punyshort.model.database.ShortenLinkTag; import de.interaapps.punyshort.model.database.User; import de.interaapps.punyshort.model.database.domains.Domain; import de.interaapps.punyshort.model.database.stats.ShortenLinkClickPerDateStats; import de.interaapps.punyshort.model.database.stats.ShortenLinkCountriesStats; +import de.interaapps.punyshort.model.database.workspaces.Workspace; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceDomain; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceUser; import de.interaapps.punyshort.model.requests.links.ShortenLinkRequest; import de.interaapps.punyshort.model.responses.ActionResponse; import de.interaapps.punyshort.model.responses.PaginatedResponse; @@ -53,7 +57,7 @@ public ShortenLinkResponse create(@Body ShortenLinkRequest request, @Attrib("use checkLongLink(request.longLink); - Domain domain = getAndCheckDomainAccess(request.domain, user); + Domain domain = getAndCheckDomainAccess(request.domain, user, request.workspaceId); shortenLink.domain = domain.id; String path = ""; @@ -65,7 +69,7 @@ public ShortenLinkResponse create(@Body ShortenLinkRequest request, @Attrib("use } while (ShortenLink.get(domain, path) != null); } - if (path.equals("")) { + if (path.isEmpty()) { if (!domain.isPublic) { if (user == null) { throw new AuthenticationException(); @@ -82,12 +86,37 @@ public ShortenLinkResponse create(@Body ShortenLinkRequest request, @Attrib("use shortenLink.userId = user.id; } + + if (request.workspaceId != null) { + if (user == null) throw new AuthenticationException(); + + accessToken.checkPermission("workspaces:write"); + + Workspace workspace = Workspace.getById(request.workspaceId); + + if (workspace == null) throw new NotFoundException(); + + WorkspaceUser workspaceUser = workspace.getUser(user.id); + if (workspaceUser == null) throw new PermissionsDeniedException(); + + shortenLink.workspaceId = workspace.id; + } + shortenLink.saveAndUpdateLinkCache(domain); + if (request.tags != null) { + for (String tag : request.tags) { + ShortenLinkTag sTag = new ShortenLinkTag(); + sTag.linkId = shortenLink.id; + sTag.tag = tag; + sTag.save(); + } + } + return new ShortenLinkResponse(shortenLink, domain); } - public Domain getAndCheckDomainAccess(String domainId, User user) { + public Domain getAndCheckDomainAccess(String domainId, User user, String workspaceId) { Domain domain = Domain.get(domainId); if (domain == null) { @@ -99,6 +128,19 @@ public Domain getAndCheckDomainAccess(String domainId, User user) { } } + if (workspaceId != null) { + Workspace workspace = Workspace.getById(workspaceId); + WorkspaceDomain workspaceDomain = workspace.getDomain(domainId); + + if (workspace.getUser(user.id) == null) + throw new PermissionsDeniedException(); + + if (workspaceDomain == null && !domain.isPublic) + throw new PermissionsDeniedException(); + else + return domain; + } + if (!domain.isPublic) { if (user == null) { throw new AuthenticationException(); @@ -141,14 +183,31 @@ public String getAndCheckPath(String path, User user, Domain domain) { public PaginatedResponse getAll(Exchange exchange, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { accessToken.checkPermission("shorten_links:read"); - Query userLinks = Repo.get(ShortenLink.class).where("userId", user.id); + Query userLinks = Repo.get(ShortenLink.class).query(); + + userLinks.and(q -> { + q.where("userId", user.id); + q.orWhere(orQuery -> orQuery + .whereNotNull("workspaceId") + .whereExists(Workspace.class, w -> w + .where(Workspace.class, "id", "=", ShortenLink.class, "workspaceId") + .whereExists(WorkspaceUser.class, u -> + u.where(WorkspaceUser.class, "workspaceId", "=", Workspace.class, "id") + .where("userId", user.id) + .where("state", WorkspaceUser.State.ACCEPTED) + ) + )); + return q; + }); userLinks.whereExists(Domain.class, d -> d.where(ShortenLink.class, "domain", "=", Domain.class, "id")); RequestHelper.defaultNavigation(exchange, userLinks); + RequestHelper.filterTags(userLinks, exchange.getQueryParameters()); RequestHelper.orderBy(userLinks, exchange, "created_at", true); PaginationData pagination = RequestHelper.pagination(userLinks, exchange); + return new PaginatedResponse<>(userLinks.all().stream().map(r -> { ShortenLinkResponse shortenLinkResponse = new ShortenLinkResponse(r); @@ -181,7 +240,9 @@ public ActionResponse delete(@Path("id") String id, @Attrib("token") AccessToken ShortenLink shortenLink = ShortenLink.get(id); if (shortenLink == null) throw new NotFoundException(); + accessToken.checkPermission("shorten_links:delete"); + shortenLink.checkUserAccess(user); shortenLink.delete(); return new ActionResponse(true); @@ -193,7 +254,9 @@ public ActionResponse edit(@Body ShortenLinkRequest request, @Path("id") String ShortenLink shortenLink = ShortenLink.get(id); if (shortenLink == null) throw new NotFoundException(); + accessToken.checkPermission("shorten_links:write"); + shortenLink.checkUserAccess(user); boolean checkPath = false; if (request.path != null && !request.path.equals(shortenLink.path)) { @@ -202,7 +265,7 @@ public ActionResponse edit(@Body ShortenLinkRequest request, @Path("id") String } if (request.domain != null && !request.domain.equals(shortenLink.domain)) { - shortenLink.domain = getAndCheckDomainAccess(request.domain, user).id; + shortenLink.domain = getAndCheckDomainAccess(request.domain, user, shortenLink.workspaceId).id; checkPath = true; } @@ -214,6 +277,19 @@ public ActionResponse edit(@Body ShortenLinkRequest request, @Path("id") String shortenLink.longLink = request.longLink; } + if (request.tags != null) { + List tags = shortenLink.getTags(); + request.tags.stream().filter(t -> !tags.contains(t)).forEach(tag -> { + ShortenLinkTag pTag = new ShortenLinkTag(); + pTag.linkId = shortenLink.id; + pTag.tag = tag; + pTag.save(); + }); + tags.stream().filter(t -> !request.tags.contains(t)).forEach(t -> { + Repo.get(ShortenLinkTag.class).where("linkId", shortenLink.id).where("tag", t).delete(); + }); + } + shortenLink.saveAndUpdateLinkCache(); return new ActionResponse(true); diff --git a/src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspaceDomainsController.java b/src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspaceDomainsController.java new file mode 100644 index 0000000..fcdbf12 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspaceDomainsController.java @@ -0,0 +1,108 @@ +package de.interaapps.punyshort.controller.workspaces; + +import de.interaapps.punyshort.controller.HttpController; +import de.interaapps.punyshort.exceptions.NotFoundException; +import de.interaapps.punyshort.exceptions.PermissionsDeniedException; +import de.interaapps.punyshort.exceptions.WorkspaceSlugInvalidException; +import de.interaapps.punyshort.exceptions.WorkspaceTakenException; +import de.interaapps.punyshort.helper.RequestHelper; +import de.interaapps.punyshort.model.database.AccessToken; +import de.interaapps.punyshort.model.database.ShortenLink; +import de.interaapps.punyshort.model.database.User; +import de.interaapps.punyshort.model.database.domains.Domain; +import de.interaapps.punyshort.model.database.workspaces.Workspace; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceDomain; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceUser; +import de.interaapps.punyshort.model.requests.workspaces.AddDomainRequest; +import de.interaapps.punyshort.model.requests.workspaces.CreateWorkspaceInvitationRequest; +import de.interaapps.punyshort.model.requests.workspaces.CreateWorkspaceRequest; +import de.interaapps.punyshort.model.responses.ActionResponse; +import de.interaapps.punyshort.model.responses.PaginatedResponse; +import de.interaapps.punyshort.model.responses.PaginationData; +import de.interaapps.punyshort.model.responses.domains.DomainResponse; +import de.interaapps.punyshort.model.responses.workspaces.WorkspaceResponse; +import org.javawebstack.httpserver.Exchange; +import org.javawebstack.httpserver.router.annotation.PathPrefix; +import org.javawebstack.httpserver.router.annotation.With; +import org.javawebstack.httpserver.router.annotation.params.Attrib; +import org.javawebstack.httpserver.router.annotation.params.Body; +import org.javawebstack.httpserver.router.annotation.params.Path; +import org.javawebstack.httpserver.router.annotation.verbs.Delete; +import org.javawebstack.httpserver.router.annotation.verbs.Get; +import org.javawebstack.httpserver.router.annotation.verbs.Post; +import org.javawebstack.orm.Repo; +import org.javawebstack.orm.query.Query; + +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@PathPrefix("/v1/workspaces/{id}/domains") +public class WorkspaceDomainsController extends HttpController { + @Get + @With("auth") + public PaginatedResponse getAll(Exchange exchange, @Path("id") String id, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { + accessToken.checkPermission("workspaces.domains:read"); + + Workspace workspace = Workspace.getById(id); + + if (workspace == null) throw new NotFoundException(); + + WorkspaceUser workspaceUser = workspace.getUser(user.id); + + if (workspaceUser == null || workspaceUser.role != WorkspaceUser.Role.ADMIN) + throw new PermissionsDeniedException(); + + Query workspaceDomainsQuery = Domain.getByWorkspace(workspace.id, user); + + RequestHelper.defaultNavigation(exchange, workspaceDomainsQuery); + RequestHelper.orderBy(workspaceDomainsQuery, exchange, "created_at", false); + + PaginationData pagination = RequestHelper.pagination(workspaceDomainsQuery, exchange); + return new PaginatedResponse<>(workspaceDomainsQuery.all().stream().map(d -> new DomainResponse(d, false)).collect(Collectors.toList()), pagination); + } + + @Post + @With("auth") + public DomainResponse add(@Body AddDomainRequest request, @Path("id") String id, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { + accessToken.checkPermission("workspaces.domains:write"); + + Workspace workspace = Workspace.getById(id); + if (workspace == null) throw new NotFoundException(); + + WorkspaceUser workspaceUser = workspace.getUser(user.id); + + if (workspaceUser == null || workspaceUser.role != WorkspaceUser.Role.ADMIN) + throw new PermissionsDeniedException(); + + Domain domain = Domain.get(request.domainId); + if (domain == null) throw new NotFoundException(); + + if (!domain.isPublic && domain.getUser(user.id) == null) + throw new PermissionsDeniedException(); + + workspace.addDomain(domain); + + return new DomainResponse(domain, false); + } + + @Delete("/{domainId}") + @With("auth") + public ActionResponse delete(@Path("id") String id, @Path("domainId") String domainId, @Attrib("token") AccessToken accessToken, @Attrib("user") User user) { + accessToken.checkPermission("workspaces.domains:delete"); + + Workspace workspace = Workspace.getById(id); + if (workspace == null) throw new NotFoundException(); + + WorkspaceUser workspaceUser = workspace.getUser(user.id); + + if (workspaceUser == null || workspaceUser.role != WorkspaceUser.Role.ADMIN) + throw new PermissionsDeniedException(); + + Domain domain = Domain.get(domainId); + if (domain == null) throw new NotFoundException(); + + workspace.removeDomain(domain); + + return new ActionResponse(true); + } +} diff --git a/src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspacesController.java b/src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspacesController.java new file mode 100644 index 0000000..f1e579e --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/controller/workspaces/WorkspacesController.java @@ -0,0 +1,162 @@ +package de.interaapps.punyshort.controller.workspaces; + +import de.interaapps.punyshort.controller.HttpController; +import de.interaapps.punyshort.exceptions.*; +import de.interaapps.punyshort.helper.RequestHelper; +import de.interaapps.punyshort.model.database.AccessToken; +import de.interaapps.punyshort.model.database.ShortenLink; +import de.interaapps.punyshort.model.database.User; +import de.interaapps.punyshort.model.database.workspaces.Workspace; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceDomain; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceUser; +import de.interaapps.punyshort.model.requests.workspaces.CreateWorkspaceInvitationRequest; +import de.interaapps.punyshort.model.requests.workspaces.CreateWorkspaceRequest; +import de.interaapps.punyshort.model.responses.ActionResponse; +import de.interaapps.punyshort.model.responses.PaginatedResponse; +import de.interaapps.punyshort.model.responses.PaginationData; +import de.interaapps.punyshort.model.responses.workspaces.WorkspaceResponse; +import org.javawebstack.httpserver.Exchange; +import org.javawebstack.httpserver.router.annotation.PathPrefix; +import org.javawebstack.httpserver.router.annotation.With; +import org.javawebstack.httpserver.router.annotation.params.Attrib; +import org.javawebstack.httpserver.router.annotation.params.Body; +import org.javawebstack.httpserver.router.annotation.params.Path; +import org.javawebstack.httpserver.router.annotation.verbs.Delete; +import org.javawebstack.httpserver.router.annotation.verbs.Get; +import org.javawebstack.httpserver.router.annotation.verbs.Post; +import org.javawebstack.orm.Repo; +import org.javawebstack.orm.query.Query; + +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@PathPrefix("/v1/workspaces") +public class WorkspacesController extends HttpController { + @Get + @With("auth") + public PaginatedResponse getAll(Exchange exchange, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { + accessToken.checkPermission("workspaces:read"); + Query workspaceQuery = Repo.get(Workspace.class) + .query(); + workspaceQuery.whereExists(WorkspaceUser.class, u -> + u.where(WorkspaceUser.class, "workspaceId", "=", Workspace.class, "id") + .where("userId", user.id) + .where("state", WorkspaceUser.State.ACCEPTED) + ); + + RequestHelper.defaultNavigation(exchange, workspaceQuery); + RequestHelper.orderBy(workspaceQuery, exchange, "created_at", false); + + PaginationData pagination = RequestHelper.pagination(workspaceQuery, exchange); + return new PaginatedResponse<>(workspaceQuery.all().stream().map(WorkspaceResponse::new).collect(Collectors.toList()), pagination); + } + + @Post + @With("auth") + public WorkspaceResponse create(@Body CreateWorkspaceRequest request, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { + accessToken.checkPermission("workspaces:write"); + + request.slug = request.slug.toLowerCase(); + if (!Pattern.matches("^[A-Za-z0-9._-]*$", request.slug)) + throw new WorkspaceSlugInvalidException(); + + if (Workspace.bySlug(request.name) != null) + throw new WorkspaceTakenException(); + + Workspace workspace = new Workspace(); + + workspace.name = request.name; + workspace.slug = request.slug; + + workspace.save(); + workspace.addUser(user, WorkspaceUser.Role.ADMIN, WorkspaceUser.State.ACCEPTED); + + return new WorkspaceResponse(workspace); + } + + @Get("/{id}") + public WorkspaceResponse get(@Path("id") String id, @Attrib("token") AccessToken accessToken, @Attrib("user") User user) { + Workspace workspace = Workspace.getById(id); + if (workspace == null) + workspace = Workspace.bySlug(id); + + if (workspace == null) + throw new NotFoundException(); + + if (workspace.getUser(user.id) == null) + throw new PermissionsDeniedException(); + + accessToken.checkPermission("workspaces:read"); + + return new WorkspaceResponse(workspace); + } + + + @Delete("/{id}") + @With("auth") + public ActionResponse delete(@Path("id") String id, @Attrib("token") AccessToken accessToken, @Attrib("user") User user) { + Workspace workspace = Workspace.getById(id); + + if (workspace == null) + throw new NotFoundException(); + + accessToken.checkPermission("workspaces:delete"); + + WorkspaceUser workspaceUser = workspace.getUser(user.id); + + if (workspaceUser == null || workspaceUser.role != WorkspaceUser.Role.ADMIN) + throw new PermissionsDeniedException(); + + workspace.delete(); + + Repo.get(ShortenLink.class).where("workspaceId", workspace.id).get().forEach(s -> { + s.workspaceId = null; + s.save(); + }); + Repo.get(WorkspaceUser.class).where("workspaceId", workspace.id).delete(); + Repo.get(WorkspaceDomain.class).where("workspaceId", workspace.id).delete(); + + return new ActionResponse(true); + } + + + @Post("/{id}/invite") + @With("auth") + public ActionResponse inviteUser(@Path("id") String id, @Body CreateWorkspaceInvitationRequest request, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { + Workspace workspace = Workspace.getById(id); + accessToken.checkPermission("workspaces.users:write"); + + if (workspace.getUser(user.id).role != WorkspaceUser.Role.ADMIN) + throw new PermissionsDeniedException(); + + User requestUser = User.getByMail(request.email); + + if (requestUser == null) + return new ActionResponse(true); + + workspace.addUser(requestUser, request.role, WorkspaceUser.State.INVITED); + + return new ActionResponse(true); + } + + @Delete("/{id}/users/{userId}") + @With("auth") + public ActionResponse removeUser(@Path("id") String id, @Path("userId") String userId, @Attrib("user") User user, @Attrib("token") AccessToken accessToken) { + accessToken.checkPermission("workspaces.users:write"); + + Workspace workspace = Workspace.getById(id); + + User requestUser = User.getById(userId); + + if (workspace == null) throw new NotFoundException(); + + if (workspace.getUser(user.id).role != WorkspaceUser.Role.ADMIN && !requestUser.id.equals(user.id)) + throw new PermissionsDeniedException(); + + if (requestUser == null) throw new NotFoundException(); + + workspace.removeUser(user); + + return new ActionResponse(true); + } +} diff --git a/src/main/java/de/interaapps/punyshort/exceptions/WorkspaceSlugInvalidException.java b/src/main/java/de/interaapps/punyshort/exceptions/WorkspaceSlugInvalidException.java new file mode 100644 index 0000000..34f6dcc --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/exceptions/WorkspaceSlugInvalidException.java @@ -0,0 +1,4 @@ +package de.interaapps.punyshort.exceptions; + +public class WorkspaceSlugInvalidException extends BadRequestException { +} diff --git a/src/main/java/de/interaapps/punyshort/exceptions/WorkspaceTakenException.java b/src/main/java/de/interaapps/punyshort/exceptions/WorkspaceTakenException.java new file mode 100644 index 0000000..10c6e30 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/exceptions/WorkspaceTakenException.java @@ -0,0 +1,4 @@ +package de.interaapps.punyshort.exceptions; + +public class WorkspaceTakenException extends BadRequestException { +} diff --git a/src/main/java/de/interaapps/punyshort/helper/RequestHelper.java b/src/main/java/de/interaapps/punyshort/helper/RequestHelper.java index a0e28db..67e032e 100644 --- a/src/main/java/de/interaapps/punyshort/helper/RequestHelper.java +++ b/src/main/java/de/interaapps/punyshort/helper/RequestHelper.java @@ -1,8 +1,7 @@ package de.interaapps.punyshort.helper; -import de.interaapps.punyshort.Punyshort; -import de.interaapps.punyshort.exceptions.PermissionsDeniedException; -import de.interaapps.punyshort.model.database.User; +import de.interaapps.punyshort.model.database.ShortenLink; +import de.interaapps.punyshort.model.database.ShortenLinkTag; import de.interaapps.punyshort.model.responses.PaginationData; import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.httpserver.Exchange; @@ -36,20 +35,28 @@ public static PaginationData pagination(Query query, Exchange exchange) { return paginationData; } - public static void queryFilter(Query query, AbstractObject params) { + public static Map getQueryFilter(AbstractObject params) { Map filters = new HashMap<>(); params.forEach((key, value) -> { - if ((key.startsWith("filter_") && key.endsWith("]")) || key.startsWith("filter%5B") && key.endsWith("%5D")) { + if ((key.startsWith("filter_")) || key.startsWith("filter%5B") && key.endsWith("%5D") || key.startsWith("filter[") && key.endsWith("]")) { filters.put(key .replace("filter[", "") - .replace("]", "") .replace("filter%5B", "") + .replace("%5D", "") + .replace("]", "") + .replace("filter_", "") .replace("%5D", ""), value.string() ); } }); - if (filters.size() > 0) + return filters; + } + + public static void queryFilter(Query query, AbstractObject params) { + Map filters = getQueryFilter(params); + + if (!filters.isEmpty()) query.filter(filters); } @@ -62,4 +69,14 @@ public static void defaultNavigation(Exchange exchange, Query query) { query.search(exchange.query("search")); queryFilter(query, exchange.getQueryParameters()); } + + public static void filterTags(Query query, AbstractObject params) { + if (params.has("filter_tags")) { + String[] filterTags = params.get("filter_tags").string().split(","); + + for (String filterTag : filterTags) { + query.whereExists(ShortenLinkTag.class, (pasteTagQuery) -> pasteTagQuery.where("tag", filterTag).where(ShortenLinkTag.class, "linkId", "=", ShortenLink.class, "id")); + } + } + } } diff --git a/src/main/java/de/interaapps/punyshort/model/database/AccessToken.java b/src/main/java/de/interaapps/punyshort/model/database/AccessToken.java index 8c81548..4d7fd42 100644 --- a/src/main/java/de/interaapps/punyshort/model/database/AccessToken.java +++ b/src/main/java/de/interaapps/punyshort/model/database/AccessToken.java @@ -60,7 +60,7 @@ public boolean hasPermission(String permission) { /** * Standard: group.permission:action - * or punyshort.ga|link:read (Will passthrough the request but adds a user-id header) + * or punyshort|link:read (Will passthrough the request but adds a user-id header, InteraApps AccountsDocumentation) */ public void checkPermission(String... permissions) { if (type == Type.ADMIN_REDIRECT_PROXY_INSTANCE) diff --git a/src/main/java/de/interaapps/punyshort/model/database/ShortenLink.java b/src/main/java/de/interaapps/punyshort/model/database/ShortenLink.java index 03d65af..d172b03 100644 --- a/src/main/java/de/interaapps/punyshort/model/database/ShortenLink.java +++ b/src/main/java/de/interaapps/punyshort/model/database/ShortenLink.java @@ -5,12 +5,15 @@ import de.interaapps.punyshort.exceptions.PermissionsDeniedException; import de.interaapps.punyshort.model.database.domains.Domain; import de.interaapps.punyshort.model.database.stats.*; +import de.interaapps.punyshort.model.database.workspaces.Workspace; import org.apache.commons.lang3.RandomStringUtils; import org.javawebstack.orm.Model; import org.javawebstack.orm.Repo; import org.javawebstack.orm.annotation.*; import java.sql.Timestamp; +import java.util.List; +import java.util.stream.Collectors; @Dates @SoftDelete @@ -21,6 +24,7 @@ public class ShortenLink extends Model { public String id; @Column(size = 8) + @Filterable public String userId; @Column @@ -29,6 +33,7 @@ public class ShortenLink extends Model { @Column @Searchable + @Filterable public String domain; @Column @@ -42,8 +47,14 @@ public class ShortenLink extends Model { @Column @Searchable + @Filterable public Type type = Type.SHORTEN_LINK; + + @Column(size = 8) + @Filterable + public String workspaceId = null; + @Column public Timestamp createdAt; @@ -74,8 +85,17 @@ public static ShortenLink get(String domain, String path) { } public void checkUserAccess(User user) { - if (!userId.equals(user.id)) - throw new PermissionsDeniedException(); + if (workspaceId != null) { + Workspace workspace = Workspace.getById(workspaceId); + + if (workspace != null && workspace.getUser(user) != null) + return; + } + + if (userId.equals(user.id)) + return; + + throw new PermissionsDeniedException(); } public void saveAndUpdateLinkCache(Domain domain) { @@ -87,6 +107,10 @@ public void saveAndUpdateLinkCache() { saveAndUpdateLinkCache(Domain.get(domain)); } + public List getTags() { + return Repo.get(ShortenLinkTag.class).where("linkId", id).get().stream().map(t -> t.tag).collect(Collectors.toList()); + } + @Override public void delete() { Repo.get(ShortenLinkBrowserStats.class).query().where("linkId", id).delete(); diff --git a/src/main/java/de/interaapps/punyshort/model/database/ShortenLinkTag.java b/src/main/java/de/interaapps/punyshort/model/database/ShortenLinkTag.java new file mode 100644 index 0000000..e400cd8 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/database/ShortenLinkTag.java @@ -0,0 +1,21 @@ +package de.interaapps.punyshort.model.database; + +import org.javawebstack.orm.Model; +import org.javawebstack.orm.annotation.Column; +import org.javawebstack.orm.annotation.Filterable; +import org.javawebstack.orm.annotation.Table; + +@Table("shorten_link_tags") +public class ShortenLinkTag extends Model { + @Column + private int id; + + + @Column(size = 8) + @Filterable + public String linkId; + + @Column + @Filterable + public String tag; +} diff --git a/src/main/java/de/interaapps/punyshort/model/database/User.java b/src/main/java/de/interaapps/punyshort/model/database/User.java index ec0851c..3b7b296 100644 --- a/src/main/java/de/interaapps/punyshort/model/database/User.java +++ b/src/main/java/de/interaapps/punyshort/model/database/User.java @@ -103,6 +103,14 @@ public enum Type { BLOCKED } + public static User getById(String id) { + return Repo.get(User.class).where("id", id).first(); + } + + public static User getByMail(String mail) { + return Repo.get(User.class).where("eMail", mail).first(); + } + public String getId() { return id; } diff --git a/src/main/java/de/interaapps/punyshort/model/database/domains/Domain.java b/src/main/java/de/interaapps/punyshort/model/database/domains/Domain.java index bf50277..af56bf2 100644 --- a/src/main/java/de/interaapps/punyshort/model/database/domains/Domain.java +++ b/src/main/java/de/interaapps/punyshort/model/database/domains/Domain.java @@ -6,6 +6,8 @@ import de.interaapps.punyshort.helper.DNSHelper; import de.interaapps.punyshort.model.database.AccessToken; import de.interaapps.punyshort.model.database.User; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceDomain; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceUser; import org.apache.commons.lang3.RandomStringUtils; import org.javawebstack.abstractdata.AbstractArray; import org.javawebstack.abstractdata.AbstractObject; @@ -184,4 +186,22 @@ public void updateCloudflare() { throw new InternalErrorException(); } } + + public static Query getByWorkspace(String workspaceId, User user) { + return Repo.get(Domain.class).query() + .whereExists(WorkspaceDomain.class, q -> { + if (user != null) { + q.whereExists(WorkspaceUser.class, w -> + w.where(WorkspaceUser.class, "workspaceId" ,"=", WorkspaceDomain.class, "workspaceId") + .where("userId", user.id) + ); + } + return q.where("workspaceId", workspaceId).where(Domain.class, "id", "=", WorkspaceDomain.class, "domainId"); + } + ); + } + + public static Query getByWorkspace(String workspaceId) { + return getByWorkspace(workspaceId, null); + } } diff --git a/src/main/java/de/interaapps/punyshort/model/database/workspaces/Workspace.java b/src/main/java/de/interaapps/punyshort/model/database/workspaces/Workspace.java new file mode 100644 index 0000000..341a680 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/database/workspaces/Workspace.java @@ -0,0 +1,102 @@ +package de.interaapps.punyshort.model.database.workspaces; + +import de.interaapps.punyshort.model.database.User; +import de.interaapps.punyshort.model.database.domains.Domain; +import de.interaapps.punyshort.model.database.domains.DomainUser; +import org.apache.commons.lang3.RandomStringUtils; +import org.javawebstack.orm.Model; +import org.javawebstack.orm.Repo; +import org.javawebstack.orm.annotation.Column; +import org.javawebstack.orm.annotation.Filterable; +import org.javawebstack.orm.annotation.Searchable; +import org.javawebstack.orm.annotation.Table; + +import java.sql.Timestamp; +import java.util.List; + +@Table("workspaces") +public class Workspace extends Model { + @Column(id = true, size = 8) + public String id; + + @Column(size = 20) + @Searchable + @Filterable + public String slug; + + @Column + @Searchable + @Filterable + public String name; + + @Column + public Timestamp createdAt; + + @Column + public Timestamp updatedAt; + + public Workspace() { + id = RandomStringUtils.random(8, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"); + } + + public List getUsers() { + return Repo.get(WorkspaceUser.class).where("workspaceId", id).where("state", WorkspaceUser.State.ACCEPTED).all(); + } + + public List getInvitedUsers() { + return Repo.get(WorkspaceUser.class).where("workspaceId", id).where("state", WorkspaceUser.State.INVITED).all(); + } + + public List getDomains() { + return Repo.get(WorkspaceDomain.class).where("workspaceId", id).all(); + } + + public WorkspaceDomain getDomain(String domainId) { + return Repo.get(WorkspaceDomain.class).where("workspaceId", id).where("domainId", domainId).first(); + } + + public WorkspaceDomain getDomain(Domain domain) { + return getDomain(domain.id); + } + + public static Workspace getById(String id) { + return Repo.get(Workspace.class).where("id", id).first(); + } + + public static Workspace bySlug(String slug) { + return Repo.get(Workspace.class).where("slug", slug).first(); + } + + + public void removeUser(User user) { + Repo.get(WorkspaceUser.class).where("workspaceId", id).where("userId", user.getId()).delete(); + } + + public void removeDomain(Domain domain) { + Repo.get(WorkspaceDomain.class).where("workspaceId", id).where("domainId", domain.id).delete(); + } + + public void addUser(User user, WorkspaceUser.Role role, WorkspaceUser.State state) { + WorkspaceUser domainUser = new WorkspaceUser(); + domainUser.userId = user.id; + domainUser.role = role; + domainUser.workspaceId = id; + domainUser.state = state; + domainUser.save(); + } + + public void addDomain(Domain domain) { + WorkspaceDomain workspaceDomain = new WorkspaceDomain(); + workspaceDomain.workspaceId = id; + workspaceDomain.domainId = domain.id; + workspaceDomain.save(); + } + + public WorkspaceUser getUser(String userId) { + return Repo.get(WorkspaceUser.class).where("workspaceId", id).where("userId", userId).first(); + } + + public WorkspaceUser getUser(User user) { + return getUser(user.getId()); + } +} diff --git a/src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceDomain.java b/src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceDomain.java new file mode 100644 index 0000000..ec0471e --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceDomain.java @@ -0,0 +1,40 @@ +package de.interaapps.punyshort.model.database.workspaces; + +import de.interaapps.punyshort.model.database.domains.Domain; +import org.javawebstack.orm.Model; +import org.javawebstack.orm.annotation.Column; +import org.javawebstack.orm.annotation.Dates; +import org.javawebstack.orm.annotation.Table; + +import java.sql.Timestamp; + +@Dates +@Table("workspace_domains") +public class WorkspaceDomain extends Model { + @Column(id = true) + public int id; + + @Column(size = 8) + public String workspaceId; + + @Column(size = 8) + public String domainId; + + @Column + public boolean active = true; + + @Column + public Timestamp createdAt; + + @Column + public Timestamp updatedAt; + + + public Domain getDomain() { + return Domain.get(domainId); + } + + public Workspace getWorkspace() { + return Workspace.getById(workspaceId); + } +} diff --git a/src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceUser.java b/src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceUser.java new file mode 100644 index 0000000..279fd09 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/database/workspaces/WorkspaceUser.java @@ -0,0 +1,53 @@ +package de.interaapps.punyshort.model.database.workspaces; + +import de.interaapps.punyshort.model.database.User; +import org.javawebstack.orm.Model; +import org.javawebstack.orm.Repo; +import org.javawebstack.orm.annotation.Column; +import org.javawebstack.orm.annotation.Dates; +import org.javawebstack.orm.annotation.Table; + +import java.sql.Timestamp; + +@Dates +@Table("workspace_users") +public class WorkspaceUser extends Model { + @Column(id = true) + public int id; + + @Column(size = 8) + public String workspaceId; + + @Column(size = 8) + public String userId; + + @Column + public Role role = Role.MEMBER; + + @Column + public State state = State.INVITED; + + @Column + public Timestamp createdAt; + + @Column + public Timestamp updatedAt; + + public enum Role { + MEMBER, + ADMIN; + } + + public enum State { + INVITED, + ACCEPTED + } + + public User getUser() { + return User.getById(userId); + } + + public Workspace getWorkspace() { + return Workspace.getById(workspaceId); + } +} diff --git a/src/main/java/de/interaapps/punyshort/model/requests/links/ShortenLinkRequest.java b/src/main/java/de/interaapps/punyshort/model/requests/links/ShortenLinkRequest.java index 9855909..bf27b7d 100644 --- a/src/main/java/de/interaapps/punyshort/model/requests/links/ShortenLinkRequest.java +++ b/src/main/java/de/interaapps/punyshort/model/requests/links/ShortenLinkRequest.java @@ -2,6 +2,8 @@ import org.javawebstack.validator.Rule; +import java.util.List; + public class ShortenLinkRequest { @Rule("string") public String domain; @@ -13,4 +15,9 @@ public class ShortenLinkRequest { public String longLink; public String type; + + @Rule("string") + public String workspaceId; + + public List tags = null; } diff --git a/src/main/java/de/interaapps/punyshort/model/requests/workspaces/AddDomainRequest.java b/src/main/java/de/interaapps/punyshort/model/requests/workspaces/AddDomainRequest.java new file mode 100644 index 0000000..e2064f6 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/requests/workspaces/AddDomainRequest.java @@ -0,0 +1,5 @@ +package de.interaapps.punyshort.model.requests.workspaces; + +public class AddDomainRequest { + public String domainId; +} diff --git a/src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceInvitationRequest.java b/src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceInvitationRequest.java new file mode 100644 index 0000000..f4b610f --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceInvitationRequest.java @@ -0,0 +1,11 @@ +package de.interaapps.punyshort.model.requests.workspaces; + +import de.interaapps.punyshort.model.database.workspaces.WorkspaceUser; +import org.javawebstack.validator.Rule; + +public class CreateWorkspaceInvitationRequest { + @Rule({"required"}) + public String email; + + public WorkspaceUser.Role role; +} diff --git a/src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceRequest.java b/src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceRequest.java new file mode 100644 index 0000000..b8a6967 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/requests/workspaces/CreateWorkspaceRequest.java @@ -0,0 +1,11 @@ +package de.interaapps.punyshort.model.requests.workspaces; + +import org.javawebstack.validator.Rule; + +public class CreateWorkspaceRequest { + @Rule({"string(2)", "required"}) + public String name; + + @Rule({"string(2)", "required"}) + public String slug; +} diff --git a/src/main/java/de/interaapps/punyshort/model/responses/links/ShortenLinkResponse.java b/src/main/java/de/interaapps/punyshort/model/responses/links/ShortenLinkResponse.java index 2a387c3..eb46fe9 100644 --- a/src/main/java/de/interaapps/punyshort/model/responses/links/ShortenLinkResponse.java +++ b/src/main/java/de/interaapps/punyshort/model/responses/links/ShortenLinkResponse.java @@ -2,23 +2,28 @@ import de.interaapps.punyshort.model.database.ShortenLink; import de.interaapps.punyshort.model.database.domains.Domain; +import de.interaapps.punyshort.model.responses.domains.DomainResponse; + +import java.util.List; public class ShortenLinkResponse { public String id; public String longLink; - public Domain domain; + public DomainResponse domain; public String path; public String fullLink; public ShortenLink.Type type; public CompactStats compactStats; + public List tags; public ShortenLinkResponse(ShortenLink shortenLink, Domain domain) { - this.domain = domain; + this.domain = new DomainResponse(domain, false); this.id = shortenLink.id; this.path = shortenLink.path; this.type = shortenLink.type; this.longLink = shortenLink.longLink; this.fullLink = "https://" + domain.name + "/" + this.path; + this.tags = shortenLink.getTags(); } public ShortenLinkResponse(ShortenLink shortenLink) { diff --git a/src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceResponse.java b/src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceResponse.java new file mode 100644 index 0000000..ae9c174 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceResponse.java @@ -0,0 +1,49 @@ +package de.interaapps.punyshort.model.responses.workspaces; + +import de.interaapps.punyshort.model.database.User; +import de.interaapps.punyshort.model.database.domains.Domain; +import de.interaapps.punyshort.model.database.domains.DomainUser; +import de.interaapps.punyshort.model.database.workspaces.Workspace; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceDomain; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceUser; +import de.interaapps.punyshort.model.responses.domains.DomainResponse; +import org.javawebstack.orm.Repo; +import org.javawebstack.orm.query.Query; + +import java.sql.Timestamp; +import java.util.List; +import java.util.stream.Collectors; + +public class WorkspaceResponse { + public String id; + public String name; + public String slug; + public Timestamp createdAt; + + public List users; + public List domains; + + public WorkspaceResponse(Workspace workspace) { + id = workspace.id; + name = workspace.name; + slug = workspace.slug; + createdAt = workspace.createdAt; + + + users = Repo.get(WorkspaceUser.class).query() + .where("workspaceId", workspace.id) + .whereExists(Workspace.class, q -> + q.where(Workspace.class, "id", "=", WorkspaceUser.class, "workspaceId") + ) + .all() + .stream() + .map(WorkspaceUserResponse::new) + .collect(Collectors.toList()); + + domains = Domain.getByWorkspace(workspace.id) + .all() + .stream() + .map(d -> new DomainResponse(d, false)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceUserResponse.java b/src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceUserResponse.java new file mode 100644 index 0000000..a2999d0 --- /dev/null +++ b/src/main/java/de/interaapps/punyshort/model/responses/workspaces/WorkspaceUserResponse.java @@ -0,0 +1,33 @@ +package de.interaapps.punyshort.model.responses.workspaces; + +import de.interaapps.punyshort.model.database.User; +import de.interaapps.punyshort.model.database.domains.Domain; +import de.interaapps.punyshort.model.database.workspaces.Workspace; +import de.interaapps.punyshort.model.database.workspaces.WorkspaceUser; +import de.interaapps.punyshort.model.responses.domains.DomainResponse; + +import java.sql.Timestamp; +import java.util.List; +import java.util.stream.Collectors; + +public class WorkspaceUserResponse { + public String id; + public String name; + public String uniqueName; + public String email; + public String avatar; + public WorkspaceUser.Role role; + public WorkspaceUser.State state; + + + public WorkspaceUserResponse(WorkspaceUser workspaceUser) { + User user = workspaceUser.getUser(); + id = user.getId(); + name = user.getName(); + uniqueName = user.getUniqueName(); + email = user.eMail; + avatar = user.avatar; + role = workspaceUser.role; + state = workspaceUser.state; + } +}