/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.rakam.ui; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import io.netty.handler.codec.http.HttpResponseStatus; import org.rakam.analysis.JDBCPoolDataSource; import org.rakam.server.http.HttpService; import org.rakam.server.http.annotations.ApiOperation; import org.rakam.server.http.annotations.ApiParam; import org.rakam.server.http.annotations.IgnoreApi; import org.rakam.server.http.annotations.JsonRequest; import org.rakam.ui.UIPermissionParameterProvider.Project; import org.rakam.util.AlreadyExistsException; import org.rakam.util.JsonHelper; import org.rakam.util.RakamException; import org.rakam.util.SuccessMessage; import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.tweak.TransactionHandler; import org.skife.jdbi.v2.util.IntegerMapper; import org.skife.jdbi.v2.util.LongMapper; import javax.inject.Inject; import javax.inject.Named; import javax.ws.rs.Path; import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; import static java.lang.Boolean.TRUE; // todo check permissions @Path("/ui/dashboard") @IgnoreApi public class DashboardService extends HttpService { private final DBI dbi; private final UserDefaultService userDefaultService; @Inject public DashboardService(@Named("ui.metadata.jdbc") JDBCPoolDataSource dataSource, UserDefaultService userDefaultService) { dbi = new DBI(dataSource); this.userDefaultService = userDefaultService; } @JsonRequest @ApiOperation(value = "Create dashboard") @Path("/create") @ProtectEndpoint(writeOperation = true) public Dashboard create( @Named("user_id") Project project, @ApiParam("name") String name, @ApiParam(value = "shared_everyone", required = false) Boolean sharedEveryone, @ApiParam(value = "options", required = false) Map<String, Object> options) { try (Handle handle = dbi.open()) { int id; try { id = handle.createQuery("INSERT INTO dashboard (project_id, name, user_id, options) VALUES (:project, :name, :user, :options) RETURNING id") .bind("project", project.project) .bind("user", project.userId) .bind("options", JsonHelper.encode(options)) .bind("name", name).map(IntegerMapper.FIRST).first(); } catch (Exception e) { if (handle.createQuery("SELECT 1 FROM dashboard WHERE (project_id, name) = (:project, :name)") .bind("project", project.project) .bind("name", name).first() != null) { throw new AlreadyExistsException("Dashboard", BAD_REQUEST); } throw e; } return new Dashboard(id, project.userId, name, null, options, TRUE.equals(sharedEveryone)); } } @JsonRequest @ApiOperation(value = "Get Report") @Path("/set-default") public SuccessMessage setDefault(@Named("user_id") Project project, @ApiParam("id") int id) { try (Handle handle = dbi.open()) { userDefaultService.set(handle, project, "DASHBOARD", id); return SuccessMessage.success(); } } @JsonRequest @ApiOperation(value = "Get Report") @Path("/get") public List<DashboardItem> get(@Named("user_id") Project project, @ApiParam("id") int id) { try (Handle handle = dbi.open()) { return handle.createQuery("SELECT id, name, directive, options, refresh_interval, last_updated," + "(case when refresh_interval is null or now() - last_updated > refresh_interval * INTERVAL '1 second' then null else data end)" + " FROM dashboard_items WHERE dashboard = (SELECT id FROM dashboard WHERE project_id = :project AND id = :id)") .bind("project", project.project) .bind("id", id) .map((i, r, statementContext) -> { return new DashboardItem(r.getInt(1), r.getString(2), r.getString(3), JsonHelper.read(r.getString(4), Map.class), r.getObject(5) == null ? null : Duration.ofSeconds(r.getInt(5)), r.getTimestamp(6) != null ? r.getTimestamp(6).toInstant() : null, r.getBytes(7)); }).list(); } } @JsonRequest @ApiOperation(value = "Get dashboard users") @Path("/users") public List<DashboardPermission> getUsers(@Named("user_id") Project project, @ApiParam("id") int id) { try (Handle handle = dbi.open()) { return handle.createQuery("SELECT web_user.id, permission.shared_at" + " FROM dashboard_permission permission " + " JOIN web_user ON (permission.user_id = web_user.id) " + " WHERE dashboard = (SELECT id FROM dashboard WHERE project_id = :project AND id = :id)") .bind("project", project.project) .bind("id", id) .map((i, r, statementContext) -> { return new DashboardPermission(r.getInt(1), r.getTimestamp(2).toInstant()); }).list(); } } @JsonRequest @ApiOperation(value = "Get dashboard users") @Path("/users/set") public SuccessMessage setUsers(@Named("user_id") Project project, @ApiParam("dashboard") int id, @ApiParam("user_ids") int[] users) { TransactionHandler transactionHandler = dbi.getTransactionHandler(); try (Handle handle = dbi.open()) { transactionHandler.begin(handle); Integer userId = handle.createQuery("SELECT user_id FROM dashboard where id = :id") .bind("id", id).map(IntegerMapper.FIRST).first(); if(project.userId != userId) { throw new RakamException(FORBIDDEN); } handle.createStatement("DELETE FROM dashboard_permission WHERE dashboard = :dashboard") .bind("dashboard", id).execute(); for (int user : users) { handle.createStatement("INSERT INTO dashboard_permission (dashboard, user_id) VALUES (:dashboard, :user_id) ") .bind("dashboard", id) .bind("user_id", user) .execute(); } transactionHandler.commit(handle); return SuccessMessage.success(); } } @JsonRequest @ApiOperation(value = "Cache report data") @Path("/cache-item-data") public SuccessMessage cache( @Named("user_id") Project project, @ApiParam("item_id") int item_id, @ApiParam("data") byte[] data) { try (Handle handle = dbi.open()) { handle.createStatement("UPDATE dashboard_items SET data = :data, last_updated = now() WHERE id = :id AND" + " (SELECT project_id FROM dashboard_items item JOIN dashboard ON (dashboard.id = item.dashboard) WHERE item.id = :id AND dashboard.project_id = :project) is not null") .bind("id", item_id) .bind("project", project.project) .bind("data", data).execute(); return SuccessMessage.success(); } } @JsonRequest @ApiOperation(value = "List Report") @Path("/list") public DashboardList list(@Named("user_id") Project project) { try (Handle handle = dbi.open()) { Integer defaultDashboard = userDefaultService.get(handle, project, "DASHBOARD"); List<Dashboard> dashboards = handle.createQuery("SELECT id, name, refresh_interval, options, shared_everyone FROM dashboard WHERE project_id = :project ORDER BY id") .bind("project", project.project).map((i, resultSet, statementContext) -> { Map options = JsonHelper.read(resultSet.getString(4), Map.class); return new Dashboard(resultSet.getInt(1), project.userId, resultSet.getString(2), resultSet.getObject(3) == null ? null : Duration.ofSeconds(resultSet.getInt(3)), options == null ? null : options, resultSet.getBoolean(5)); }).list(); return new DashboardList(dashboards, defaultDashboard); } } public static class DashboardList { public final List<Dashboard> dashboards; public final Integer defaultDashboard; public DashboardList(List<Dashboard> dashboards, Integer defaultDashboard) { this.dashboards = dashboards; this.defaultDashboard = defaultDashboard; } } public static class DashboardPermission { public final int id; public final Instant sharedAt; public DashboardPermission(int id, Instant sharedAt) { this.id = id; this.sharedAt = sharedAt; } } public static class Dashboard { public final int id; public final int userId; public final String name; public final Map<String, Object> options; public final Duration refresh_interval; public final boolean sharedEveryone; @JsonCreator public Dashboard(int id, int userId, String name, Duration refresh_interval, Map<String, Object> options, boolean sharedEveryone) { this.id = id; this.name = name; this.userId = userId; this.refresh_interval = refresh_interval; this.options = options; this.sharedEveryone = sharedEveryone; } } public static class DashboardItem { public final Integer id; public final Map options; public final Duration refreshInterval; public final String directive; public final String name; public final Instant lastUpdated; public final byte[] data; @JsonCreator public DashboardItem( @JsonProperty("id") Integer id, @JsonProperty("name") String name, @JsonProperty("directive") String directive, @JsonProperty("options") Map options, @JsonProperty("refreshInterval") Duration refreshInterval, @JsonProperty("data") byte[] data) { this(id, name, directive, options, refreshInterval, null, data); } public DashboardItem(Integer id, String name, String directive, Map options, Duration refreshInterval, Instant lastUpdated, byte[] data) { this.id = id; this.options = options; this.refreshInterval = refreshInterval; this.directive = directive; this.name = name; this.lastUpdated = lastUpdated; this.data = data; } } @JsonRequest @ApiOperation(value = "Add item to dashboard") @Path("/add_item") @ProtectEndpoint(writeOperation = true) public SuccessMessage addToDashboard( @Named("user_id") Project project, @ApiParam("dashboard") int dashboard, @ApiParam("name") String itemName, @ApiParam("directive") String directive, @ApiParam(value = "refresh_interval", required = false) Duration refreshInterval, @ApiParam("options") Map options) { try (Handle handle = dbi.open()) { handle.createStatement("INSERT INTO dashboard_items (dashboard, name, directive, options, refresh_interval) VALUES (:dashboard, :name, :directive, :options, :refreshInterval)") .bind("project", project.project) .bind("dashboard", dashboard) .bind("name", itemName) .bind("directive", directive) .bind("refreshInterval", Optional.ofNullable(refreshInterval).map(e -> e.getSeconds()).orElse(null)) .bind("options", JsonHelper.encode(options)).execute(); } return SuccessMessage.success(); } @JsonRequest @ApiOperation(value = "Update dashboard items") @Path("/update_dashboard_items") @ProtectEndpoint(writeOperation = true) public SuccessMessage updateDashboard( @Named("user_id") Project project, @ApiParam("dashboard") int dashboard, @ApiParam("items") List<DashboardItem> items) { dbi.inTransaction((handle, transactionStatus) -> { Long execute = handle.createQuery("SELECT id FROM dashboard WHERE id = :id AND project_id = :project") .bind("id", dashboard) .bind("project", project.project) .map(LongMapper.FIRST).first(); if (execute == null) { throw new RakamException(HttpResponseStatus.NOT_FOUND); } for (DashboardItem item : items) { // TODO: verify dashboard is in project handle.createStatement("UPDATE dashboard_items SET name = :name, directive = :directive, options = :options WHERE id = :id") .bind("id", item.id) .bind("name", item.name) .bind("directive", item.directive) .bind("options", JsonHelper.encode(item.options)) .execute(); } return null; }); return SuccessMessage.success(); } @JsonRequest @ApiOperation(value = "Update dashboard options") @Path("/update_dashboard_options") @ProtectEndpoint(writeOperation = true) public SuccessMessage updateDashboardOptions( @Named("user_id") Project project, @ApiParam("dashboard") int dashboard, @ApiParam("name") String name, @ApiParam("refresh_interval") Duration refreshDuration, @ApiParam(value = "sharedEveryone", required = false) Boolean sharedEveryone, @ApiParam("options") Map<String, Object> options) { dbi.inTransaction((handle, transactionStatus) -> { handle.createStatement("UPDATE dashboard SET options = :options, refresh_interval = :refreshDuration, name = :name," + " shared_everyone = (case when :shared is null then shared_everyone else :shared end)" + " WHERE id = :id AND project_id = :project") .bind("id", dashboard) .bind("name", name) .bind("shared", TRUE.equals(sharedEveryone)) .bind("refreshDuration", refreshDuration.getSeconds()) .bind("options", JsonHelper.encode(options)) .bind("project", project.project) .execute(); return null; }); return SuccessMessage.success(); } @JsonRequest @ApiOperation(value = "Rename dashboard item") @Path("/rename_item") @ProtectEndpoint(writeOperation = true) public SuccessMessage renameDashboardItem( @Named("user_id") Project project, @ApiParam("dashboard") int dashboard, @ApiParam("id") int id, @ApiParam("name") String name) { try (Handle handle = dbi.open()) { // todo: check project handle.createStatement("UPDATE dashboard_items SET name = :name WHERE id = :id") .bind("id", id) .bind("name", name).execute(); } return SuccessMessage.success(); } @JsonRequest @ApiOperation(value = "Delete dashboard item") @Path("/delete_item") @ProtectEndpoint(writeOperation = true) public SuccessMessage removeFromDashboard( @Named("user_id") Project project, @ApiParam("dashboard") int dashboard, @ApiParam("id") int id) { try (Handle handle = dbi.open()) { handle.createStatement("DELETE FROM dashboard_items " + "WHERE dashboard = :dashboard AND id = :id") .bind("project", project.project) .bind("dashboard", dashboard) .bind("id", id).execute(); } return SuccessMessage.success(); } @JsonRequest @ApiOperation(value = "Delete dashboard item") @Path("/delete") @ProtectEndpoint(writeOperation = true) public SuccessMessage delete(@Named("user_id") Project project, @ApiParam("id") int dashboard) { try (Handle handle = dbi.open()) { int execute = handle.createStatement("DELETE FROM dashboard WHERE id = :id and project_id = :project AND " + "(select count(*) FROM dashboard WHERE project_id = :project) > 1") .bind("id", dashboard).bind("project", project.project).execute(); if (execute == 0) { throw new RakamException("You cannot remove the single dashboard.", BAD_REQUEST); } } return SuccessMessage.success(); } }