getProperties() {
17 | if (properties == null) {
18 | properties = new EnumMap<>(OptionProperty.class);
19 | }
20 | return properties;
21 | }
22 |
23 | @Override
24 | public int compareTo(@NonNull Option o) {
25 | if (this.equals(o)) return 0;
26 | byte i = (byte) this.properties.get(OptionProperty.SEQUENCE);
27 | byte j = (byte) o.properties.get(OptionProperty.SEQUENCE);
28 | return i - j;
29 | }
30 |
31 | @Override
32 | public boolean equals(Object obj) {
33 | return super.equals(obj);
34 | }
35 |
36 | @Override
37 | public int hashCode() {
38 | return super.hashCode();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/PublicCommand.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto;
2 |
3 | public class PublicCommand extends BaseCommand {
4 | private boolean isDeletable = false;
5 |
6 | public boolean isDeletable() {
7 | return isDeletable;
8 | }
9 |
10 | public void setDeletable(boolean deletable) {
11 | isDeletable = deletable;
12 | }
13 |
14 | @Override
15 | public String toString() {
16 | return "PublicCommand{" + "isDeletable=" + isDeletable + "} " + super.toString();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/User.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto;
2 |
3 | public class User {
4 |
5 | private String name;
6 | private String email;
7 | private boolean emailVerified;
8 | private String provider;
9 | private String providerId;
10 | private String imageUrl;
11 |
12 | public String getName() {
13 | return name;
14 | }
15 |
16 | public void setName(String name) {
17 | this.name = name;
18 | }
19 |
20 | public String getEmail() {
21 | return email;
22 | }
23 |
24 | public void setEmail(String email) {
25 | this.email = email;
26 | }
27 |
28 | public boolean isEmailVerified() {
29 | return emailVerified;
30 | }
31 |
32 | public void setEmailVerified(boolean emailVerified) {
33 | this.emailVerified = emailVerified;
34 | }
35 |
36 | public String getProvider() {
37 | return provider;
38 | }
39 |
40 | public void setProvider(String provider) {
41 | this.provider = provider;
42 | }
43 |
44 | public String getProviderId() {
45 | return providerId;
46 | }
47 |
48 | public void setProviderId(String providerId) {
49 | this.providerId = providerId;
50 | }
51 |
52 | public String getImageUrl() {
53 | return imageUrl;
54 | }
55 |
56 | public void setImageUrl(String imageUrl) {
57 | this.imageUrl = imageUrl;
58 | }
59 |
60 | @Override
61 | public String toString() {
62 | return "User{"
63 | + "name='"
64 | + name
65 | + '\''
66 | + ", email='"
67 | + email
68 | + '\''
69 | + ", emailVerified="
70 | + emailVerified
71 | + ", provider='"
72 | + provider
73 | + '\''
74 | + ", providerId='"
75 | + providerId
76 | + '\''
77 | + ", imageUrl='"
78 | + imageUrl
79 | + '\''
80 | + '}';
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/UserCommand.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto;
2 |
3 | import java.sql.Timestamp;
4 |
5 | public class UserCommand extends BaseCommand {
6 |
7 | private Timestamp modifiedOn;
8 | private Timestamp operatedOn;
9 |
10 | public Timestamp getModifiedOn() {
11 | return modifiedOn;
12 | }
13 |
14 | public void setModifiedOn(Timestamp modifiedOn) {
15 | this.modifiedOn = modifiedOn;
16 | }
17 |
18 | public Timestamp getOperatedOn() {
19 | return operatedOn;
20 | }
21 |
22 | public void setOperatedOn(Timestamp operatedOn) {
23 | this.operatedOn = operatedOn;
24 | }
25 |
26 | @Override
27 | public String toString() {
28 | return "UserCommand{"
29 | + "modifiedOn="
30 | + modifiedOn
31 | + ", operatedOn="
32 | + operatedOn
33 | + "} "
34 | + super.toString();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/auth/Login.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto.auth;
2 |
3 | import javax.validation.constraints.Email;
4 | import javax.validation.constraints.NotBlank;
5 |
6 | public class Login {
7 |
8 | @NotBlank @Email private String email;
9 |
10 | @NotBlank private String password;
11 |
12 | public String getEmail() {
13 | return email;
14 | }
15 |
16 | public void setEmail(String email) {
17 | this.email = email;
18 | }
19 |
20 | public String getPassword() {
21 | return password;
22 | }
23 |
24 | public void setPassword(String password) {
25 | this.password = password;
26 | }
27 |
28 | @Override
29 | public String toString() {
30 | return "Login{" + "email='" + email + '\'' + '}';
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/auth/SignUp.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto.auth;
2 |
3 | import javax.validation.constraints.Email;
4 | import javax.validation.constraints.NotBlank;
5 |
6 | public class SignUp {
7 |
8 | @NotBlank private String name;
9 |
10 | @NotBlank @Email private String email;
11 |
12 | @NotBlank private String password;
13 |
14 | public String getName() {
15 | return name;
16 | }
17 |
18 | public void setName(String name) {
19 | this.name = name;
20 | }
21 |
22 | public String getEmail() {
23 | return email;
24 | }
25 |
26 | public void setEmail(String email) {
27 | this.email = email;
28 | }
29 |
30 | public String getPassword() {
31 | return password;
32 | }
33 |
34 | public void setPassword(String password) {
35 | this.password = password;
36 | }
37 |
38 | @Override
39 | public String toString() {
40 | return "SignUp{" + "name='" + name + '\'' + ", email='" + email + '\'' + '}';
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/filter/Condition.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto.filter;
2 |
3 | /**
4 | * This class is used to get the filter conditions for filtering entity based on the matching
5 | * criteria. The key belongs to a field in the entity, operator specifies the type of match. The
6 | * value will be matched against the field's value of the entity stored in database.
7 | */
8 | public class Condition {
9 |
10 | private String key;
11 | private String value;
12 | private Operator operator;
13 |
14 | public String getKey() {
15 | return key;
16 | }
17 |
18 | public void setKey(String key) {
19 | this.key = key;
20 | }
21 |
22 | public String getValue() {
23 | return value;
24 | }
25 |
26 | public void setValue(String value) {
27 | this.value = value;
28 | }
29 |
30 | public Operator getOperator() {
31 | return operator;
32 | }
33 |
34 | public void setOperator(Operator operator) {
35 | this.operator = operator;
36 | }
37 |
38 | @Override
39 | public String toString() {
40 | return "Condition{"
41 | + "key='"
42 | + key
43 | + '\''
44 | + ", value='"
45 | + value
46 | + '\''
47 | + ", operator="
48 | + operator
49 | + '}';
50 | }
51 |
52 | public enum Operator {
53 | EQUALS,
54 | CONTAINS,
55 | STARTS_WITH,
56 | ENDS_WITH,
57 | LESS_THAN,
58 | GREATER_THAN
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/filter/Filter.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto.filter;
2 |
3 | import javax.validation.Valid;
4 | import javax.validation.constraints.NotNull;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | /**
9 | * This class is used to receive the filter criteria from the client. It contains the pagination,
10 | * sort and conditions filters.
11 | *
12 | * Business logic should validate the condition list and decide how the conditions should be
13 | * applied. e.g. for a specific entity, business logic should decide which operator (AND/OR) will be
14 | * applied between conditions.
15 | */
16 | public class Filter {
17 |
18 | private List conditions = new ArrayList<>();
19 |
20 | @Valid @NotNull private Pagination pagination;
21 |
22 | public List getConditions() {
23 | return conditions;
24 | }
25 |
26 | public void setConditions(List conditions) {
27 | this.conditions = conditions;
28 | }
29 |
30 | public Pagination getPagination() {
31 | return pagination;
32 | }
33 |
34 | public void setPagination(Pagination pagination) {
35 | this.pagination = pagination;
36 | }
37 |
38 | @Override
39 | public String toString() {
40 | return "Filter{" + "conditions=" + conditions + ", pagination=" + pagination + '}';
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/filter/PageResponse.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto.filter;
2 |
3 | import java.util.List;
4 |
5 | public class PageResponse {
6 |
7 | private long pageNumber = 1L;
8 | private long totalSize = 0L;
9 | private long pageSize = 10L;
10 | private List records;
11 |
12 | public PageResponse(long pageNumber, long pageSize, long totalSize, List records) {
13 | this.pageNumber = pageNumber;
14 | this.totalSize = totalSize;
15 | this.pageSize = pageSize;
16 | this.records = records;
17 | }
18 |
19 | public long getPageNumber() {
20 | return pageNumber;
21 | }
22 |
23 | public void setPageNumber(long pageNumber) {
24 | this.pageNumber = pageNumber;
25 | }
26 |
27 | public long getTotalSize() {
28 | return totalSize;
29 | }
30 |
31 | public void setTotalSize(long totalSize) {
32 | this.totalSize = totalSize;
33 | }
34 |
35 | public long getPageSize() {
36 | return pageSize;
37 | }
38 |
39 | public void setPageSize(long pageSize) {
40 | this.pageSize = pageSize;
41 | }
42 |
43 | public long getTotalPages() {
44 | return (long) Math.ceil(this.totalSize / ((double) this.pageSize));
45 | }
46 |
47 | public List getRecords() {
48 | return records;
49 | }
50 |
51 | public void setRecords(List records) {
52 | this.records = records;
53 | }
54 |
55 | public void addRecord(T newRecord) {
56 | this.records.add(newRecord);
57 | }
58 |
59 | @Override
60 | public String toString() {
61 | return "PageResponse{"
62 | + "pageNumber="
63 | + pageNumber
64 | + ", totalSize="
65 | + totalSize
66 | + ", pageSize="
67 | + pageSize
68 | + ", records="
69 | + records
70 | + '}';
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/dto/filter/Pagination.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.dto.filter;
2 |
3 | import com.fasterxml.jackson.annotation.JsonValue;
4 |
5 | import javax.validation.Valid;
6 | import javax.validation.constraints.Min;
7 | import javax.validation.constraints.NotEmpty;
8 | import javax.validation.constraints.NotNull;
9 | import java.util.Arrays;
10 |
11 | public class Pagination {
12 |
13 | @Min(1)
14 | @NotNull
15 | private int pageNumber;
16 |
17 | @Min(5)
18 | @NotNull
19 | private int pageSize;
20 |
21 | @Valid @NotNull private Sort sort;
22 |
23 | public int getOffset() {
24 | return (this.pageNumber - 1) * this.pageSize;
25 | }
26 |
27 | public int getPageNumber() {
28 | return pageNumber;
29 | }
30 |
31 | public void setPageNumber(int pageNumber) {
32 | this.pageNumber = pageNumber;
33 | }
34 |
35 | public int getPageSize() {
36 | return pageSize;
37 | }
38 |
39 | public void setPageSize(int pageSize) {
40 | this.pageSize = pageSize;
41 | }
42 |
43 | public Sort getSort() {
44 | return sort;
45 | }
46 |
47 | public void setSort(Sort sort) {
48 | this.sort = sort;
49 | }
50 |
51 | @Override
52 | public String toString() {
53 | return "Pagination{"
54 | + "pageNumber="
55 | + pageNumber
56 | + ", pageSize="
57 | + pageSize
58 | + ", sort="
59 | + sort
60 | + '}';
61 | }
62 |
63 | public static class Sort {
64 |
65 | @NotEmpty private String[] by;
66 |
67 | private SortOrder order = SortOrder.ASC;
68 |
69 | public String[] getBy() {
70 | return by;
71 | }
72 |
73 | public void setBy(String[] by) {
74 | this.by = by;
75 | }
76 |
77 | public SortOrder getOrder() {
78 | return order;
79 | }
80 |
81 | public void setOrder(SortOrder order) {
82 | this.order = order;
83 | }
84 |
85 | @Override
86 | public String toString() {
87 | return "Sort{" + "by='" + Arrays.toString(by) + '\'' + ", order=" + order + '}';
88 | }
89 |
90 | public enum SortOrder {
91 | ASC,
92 | DESC;
93 |
94 | @JsonValue
95 | public String toLowerCase() {
96 | return name().toLowerCase();
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/exception/BadRequestException.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.exception;
2 |
3 | public class BadRequestException extends RuntimeException {
4 |
5 | public BadRequestException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/exception/GlobalExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.exception;
2 |
3 | import com.wirehall.commandhunt.backend.dto.ErrorResponse;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.core.Ordered;
7 | import org.springframework.core.annotation.Order;
8 | import org.springframework.http.HttpStatus;
9 | import org.springframework.security.authentication.BadCredentialsException;
10 | import org.springframework.web.bind.MethodArgumentNotValidException;
11 | import org.springframework.web.bind.annotation.ControllerAdvice;
12 | import org.springframework.web.bind.annotation.ExceptionHandler;
13 | import org.springframework.web.bind.annotation.ResponseBody;
14 | import org.springframework.web.bind.annotation.ResponseStatus;
15 |
16 | import java.util.List;
17 | import java.util.stream.Collectors;
18 |
19 | @Order(Ordered.HIGHEST_PRECEDENCE)
20 | @ControllerAdvice
21 | public class GlobalExceptionHandler {
22 | private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
23 |
24 | @ExceptionHandler({BadCredentialsException.class})
25 | @ResponseStatus(HttpStatus.UNAUTHORIZED)
26 | @ResponseBody
27 | private ErrorResponse handleUnauthorizedExceptions(Exception ex) {
28 | return new ErrorResponse(ex.getMessage());
29 | }
30 |
31 | @ExceptionHandler(MethodArgumentNotValidException.class)
32 | @ResponseStatus(HttpStatus.BAD_REQUEST)
33 | @ResponseBody
34 | private ErrorResponse handleValidationExceptions(MethodArgumentNotValidException ex) {
35 | // Get all the field validation error messages
36 | List errorMessages =
37 | ex.getBindingResult().getFieldErrors().stream()
38 | .map(f -> f.getField() + " : " + f.getDefaultMessage())
39 | .collect(Collectors.toList());
40 |
41 | ErrorResponse errorResponse = new ErrorResponse("Validation Failed");
42 | errorResponse.setDetails(errorMessages);
43 | return errorResponse;
44 | }
45 |
46 | @ExceptionHandler(BadRequestException.class)
47 | @ResponseStatus(HttpStatus.BAD_REQUEST)
48 | @ResponseBody
49 | private ErrorResponse handleBadRequestException(Exception ex) {
50 | return new ErrorResponse(ex.getMessage());
51 | }
52 |
53 | @ExceptionHandler(OAuthException.class)
54 | @ResponseStatus(HttpStatus.UNAUTHORIZED)
55 | @ResponseBody
56 | private ErrorResponse handleOAuthException(Exception ex) {
57 | return new ErrorResponse(ex.getMessage());
58 | }
59 |
60 | @ExceptionHandler(Exception.class)
61 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
62 | @ResponseBody
63 | private ErrorResponse handleAll(Exception ex) {
64 | // Never pass error details of this unknown exception to client
65 | // Since these exceptions are unknown, sending the details to client may leak sensitive info
66 |
67 | // Logging the exception here, since this is an unknown exception & we are suppressing the
68 | // details
69 | LOGGER.error("Unknown error occurred,", ex);
70 |
71 | return new ErrorResponse("Something went wrong. Contact Support.");
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/exception/OAuthException.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.exception;
2 |
3 | import org.springframework.security.core.AuthenticationException;
4 |
5 | public class OAuthException extends AuthenticationException {
6 | public OAuthException(String msg) {
7 | super(msg);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/graph/DatabaseSeed.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.graph;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.boot.ApplicationArguments;
7 | import org.springframework.boot.ApplicationRunner;
8 | import org.springframework.stereotype.Component;
9 |
10 | /** This component is used to seed the database on application startup. */
11 | @Component
12 | public class DatabaseSeed implements ApplicationRunner {
13 |
14 | private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseSeed.class);
15 | private final MetadataManager metadataManager;
16 |
17 | @Autowired
18 | public DatabaseSeed(MetadataManager metadataManager) {
19 | this.metadataManager = metadataManager;
20 | }
21 |
22 | @Override
23 | public void run(ApplicationArguments args) {
24 | try {
25 | metadataManager.load();
26 | } catch (Exception e) {
27 | LOGGER.error(e.getMessage(), e);
28 |
29 | // Restore interrupted state...
30 | Thread.currentThread().interrupt();
31 |
32 | // Exit with error status code
33 | System.exit(1);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/graph/GraphConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.graph;
2 |
3 | import org.apache.commons.configuration.ConfigurationException;
4 | import org.apache.commons.configuration.PropertiesConfiguration;
5 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
6 | import org.apache.tinkerpop.gremlin.structure.util.GraphFactory;
7 | import org.janusgraph.core.JanusGraph;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.context.annotation.Bean;
10 | import org.springframework.context.annotation.Configuration;
11 |
12 | @Configuration
13 | public class GraphConfiguration {
14 |
15 | @Value("${app.graph.config}")
16 | private String appGraphConfigPath;
17 |
18 | @Bean
19 | JanusGraph janusGraph() throws ConfigurationException {
20 | return (JanusGraph) GraphFactory.open(new PropertiesConfiguration(appGraphConfigPath));
21 | }
22 |
23 | @Bean
24 | GraphTraversalSource graphTraversalSource(JanusGraph janusGraph) {
25 | return janusGraph.traversal();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/graph/GraphIO.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.graph;
2 |
3 | import org.apache.tinkerpop.gremlin.process.traversal.IO;
4 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
5 | import org.janusgraph.core.JanusGraph;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.beans.factory.annotation.Value;
10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11 | import org.springframework.context.annotation.Configuration;
12 | import org.springframework.scheduling.annotation.EnableScheduling;
13 | import org.springframework.scheduling.annotation.Scheduled;
14 |
15 | import java.io.File;
16 | import java.io.IOException;
17 | import java.nio.file.Files;
18 | import java.nio.file.Paths;
19 |
20 | @Configuration
21 | @EnableScheduling
22 | @ConditionalOnProperty(name = "app.graph.export.enable")
23 | public class GraphIO {
24 |
25 | private static final Logger LOGGER = LoggerFactory.getLogger(GraphIO.class);
26 |
27 | private final JanusGraph janusGraph;
28 | private final GraphTraversalSource gt;
29 |
30 | @Value("${app.graph.export.path}")
31 | private String exportPath;
32 |
33 | @Value("${app.graph.import.path}")
34 | private String importPath;
35 |
36 | @Autowired
37 | public GraphIO(JanusGraph janusGraph, GraphTraversalSource gt) {
38 | this.janusGraph = janusGraph;
39 | this.gt = gt;
40 | }
41 |
42 | /** Export the database in graphml format. */
43 | @Scheduled(cron = "${app.graph.export.cronExpression}")
44 | public void exportGraphMl() throws IOException {
45 | Files.createDirectories(Paths.get(exportPath));
46 | String outputFilePath = exportPath + File.separator + System.currentTimeMillis() + ".graphml";
47 | try {
48 | LOGGER.info("Exporting graph to file {}", exportPath);
49 | gt.io(outputFilePath).with(IO.writer, IO.graphml).write().iterate();
50 | janusGraph.tx().commit();
51 | LOGGER.info("Graph export successful!");
52 | } catch (Exception e) {
53 | janusGraph.tx().rollback();
54 | LOGGER.info("Failed to export graph", e);
55 | }
56 | }
57 |
58 | /**
59 | * Import the database from graphml file.
60 | *
61 | * This method can only import graph file from disk path. It will not support loading graph
62 | * file from resources. Since the GraphTraversal.io() api does not support importing from
63 | * classpath resources.
64 | */
65 | public void importGraphMl() {
66 | try {
67 | LOGGER.info("Importing graph from file {}", importPath);
68 | gt.io(importPath).with(IO.reader, IO.graphml).read().iterate();
69 | janusGraph.tx().commit();
70 | LOGGER.info("Graph import successful!");
71 | } catch (Exception e) {
72 | janusGraph.tx().rollback();
73 | LOGGER.info("Failed to import graph", e);
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/graph/MetadataManager.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.graph;
2 |
3 | import org.apache.tinkerpop.gremlin.structure.io.IoCore;
4 | import org.janusgraph.core.JanusGraph;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 |
13 | @Component
14 | public class MetadataManager {
15 |
16 | public static final String GRAPH_FILE = "db/db.graphml";
17 | private static final Logger LOGGER = LoggerFactory.getLogger(MetadataManager.class);
18 | private final JanusGraph graph;
19 |
20 | /**
21 | * Performs graph db initialization operations.
22 | *
23 | * @param graph Graph instance.
24 | */
25 | @Autowired
26 | public MetadataManager(JanusGraph graph) {
27 | this.graph = graph;
28 | }
29 |
30 | /**
31 | * Import the database from graphml file.
32 | *
33 | *
This method supports importing graph file from resources path.
34 | */
35 | public void load() throws InterruptedException, IOException {
36 | SchemaBuilder.load(graph);
37 | LOGGER.info("Importing graph from file {}", GRAPH_FILE);
38 | InputStream stream = MetadataManager.class.getClassLoader().getResourceAsStream(GRAPH_FILE);
39 | // Using deprecated api since there are no alternative APIs for loading graphml form jar file
40 | graph.io(IoCore.graphml()).reader().create().readGraph(stream, graph); // NOSONAR
41 | graph.tx().commit();
42 | LOGGER.info("Graph import successful!");
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/mapper/PaginationMapper.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.mapper;
2 |
3 | import com.wirehall.commandhunt.backend.dto.filter.Filter;
4 | import org.springframework.data.domain.PageRequest;
5 | import org.springframework.data.domain.Pageable;
6 | import org.springframework.data.domain.Sort;
7 |
8 | public class PaginationMapper {
9 | public Pageable mapToPageable(Filter filter) {
10 | return PageRequest.of(
11 | filter.getPagination().getPageNumber() - 1,
12 | filter.getPagination().getPageSize(),
13 | Sort.by(
14 | Sort.Direction.fromString(filter.getPagination().getSort().getOrder().toString()),
15 | filter.getPagination().getSort().getBy()));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/mapper/PublicCommandMapper.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.mapper;
2 |
3 | import com.wirehall.commandhunt.backend.dto.PublicCommand;
4 | import com.wirehall.commandhunt.backend.model.PublicCommandEntity;
5 |
6 | import java.util.Map;
7 | import java.util.Set;
8 | import java.util.stream.Collectors;
9 |
10 | public class PublicCommandMapper {
11 |
12 | /**
13 | * Maps public-command entity to public-command dto
14 | *
15 | * @param publicCommandEntity To be mapped to public-command dto.
16 | * @param mapAssociations This flag should be set to false if entity's associations are not
17 | * required. Since un-necessarily mapping this will invoke lazy loading of these associations.
18 | * @return The public-command dto.
19 | */
20 | public PublicCommand mapToPublicCommand(
21 | PublicCommandEntity publicCommandEntity, boolean mapAssociations) {
22 | PublicCommand publicCommand = new PublicCommand();
23 | publicCommand.setId(publicCommandEntity.getId());
24 |
25 | publicCommand.setCommandName(publicCommandEntity.getCommandName());
26 | publicCommand.setCommandText(publicCommandEntity.getCommandText());
27 | publicCommand.setCreatedOn(publicCommandEntity.getCreatedOn());
28 |
29 | if (mapAssociations) {
30 | publicCommand.setFlags(
31 | publicCommandEntity.getFlags().stream().collect(Collectors.toMap(f -> f, f -> true)));
32 | publicCommand.setOptions(publicCommandEntity.getOptions());
33 | }
34 | return publicCommand;
35 | }
36 |
37 | /**
38 | * Maps public-command dto to public-command entity
39 | *
40 | * @param publicCommand The public-command dto.
41 | * @param userEmail Logged in user's email-id.
42 | * @return The public-command entity.
43 | */
44 | public PublicCommandEntity mapToPublicCommandEntity(
45 | PublicCommand publicCommand, String userEmail) {
46 | PublicCommandEntity publicCommandEntity = new PublicCommandEntity();
47 | publicCommandEntity.setId(publicCommand.getId());
48 | publicCommandEntity.setUserEmail(userEmail);
49 | publicCommandEntity.setCommandName(publicCommand.getCommandName());
50 | publicCommandEntity.setCommandText(publicCommand.getCommandText());
51 | publicCommandEntity.setCreatedOn(publicCommand.getCreatedOn());
52 |
53 | Set flags =
54 | publicCommand.getFlags().entrySet().stream()
55 | .filter(Map.Entry::getValue)
56 | .map(Map.Entry::getKey)
57 | .collect(Collectors.toSet());
58 | publicCommandEntity.setFlags(flags);
59 | publicCommandEntity.setOptions(publicCommand.getOptions());
60 | return publicCommandEntity;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/mapper/UserCommandMapper.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.mapper;
2 |
3 | import com.wirehall.commandhunt.backend.dto.UserCommand;
4 | import com.wirehall.commandhunt.backend.model.UserCommandEntity;
5 |
6 | import java.util.Map;
7 | import java.util.Set;
8 | import java.util.stream.Collectors;
9 |
10 | public final class UserCommandMapper {
11 |
12 | /**
13 | * Maps user-command entity to user-command dto
14 | *
15 | * @param userCommandEntity To be mapped to user-command dto.
16 | * @param mapAssociations This flag should be set to false if entity's associations are not
17 | * required. Since un-necessarily mapping this will invoke lazy loading of these associations.
18 | * @return The user-command dto.
19 | */
20 | public UserCommand mapToUserCommand(
21 | UserCommandEntity userCommandEntity, boolean mapAssociations) {
22 | UserCommand userCommand = new UserCommand();
23 | userCommand.setId(userCommandEntity.getId());
24 |
25 | userCommand.setCommandName(userCommandEntity.getCommandName());
26 | userCommand.setCommandText(userCommandEntity.getCommandText());
27 | userCommand.setCreatedOn(userCommandEntity.getCreatedOn());
28 | userCommand.setModifiedOn(userCommandEntity.getModifiedOn());
29 | userCommand.setOperatedOn(userCommandEntity.getOperatedOn());
30 |
31 | if (mapAssociations) {
32 | userCommand.setFlags(
33 | userCommandEntity.getFlags().stream().collect(Collectors.toMap(f -> f, f -> true)));
34 | userCommand.setOptions(userCommandEntity.getOptions());
35 | }
36 | return userCommand;
37 | }
38 |
39 | /**
40 | * Maps user-command dto to user-command entity
41 | *
42 | * @param userCommand The user-command dto.
43 | * @param userEmail Logged in user's email id.
44 | * @return The user-command entity.
45 | */
46 | public UserCommandEntity mapToUserCommandEntity(UserCommand userCommand, String userEmail) {
47 | UserCommandEntity userCommandEntity = new UserCommandEntity();
48 | userCommandEntity.setId(userCommand.getId());
49 | userCommandEntity.setUserEmail(userEmail);
50 | userCommandEntity.setCommandName(userCommand.getCommandName());
51 | userCommandEntity.setCommandText(userCommand.getCommandText());
52 | userCommandEntity.setCreatedOn(userCommand.getCreatedOn());
53 | userCommandEntity.setModifiedOn(userCommand.getModifiedOn());
54 | userCommandEntity.setOperatedOn(userCommand.getOperatedOn());
55 |
56 | Set flags =
57 | userCommand.getFlags().entrySet().stream()
58 | .filter(Map.Entry::getValue)
59 | .map(Map.Entry::getKey)
60 | .collect(Collectors.toSet());
61 | userCommandEntity.setFlags(flags);
62 | userCommandEntity.setOptions(userCommand.getOptions());
63 | return userCommandEntity;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/mapper/UserMapper.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.mapper;
2 |
3 | import com.wirehall.commandhunt.backend.dto.User;
4 | import com.wirehall.commandhunt.backend.dto.auth.SignUp;
5 | import com.wirehall.commandhunt.backend.model.UserEntity;
6 | import com.wirehall.commandhunt.backend.model.UserEntity.OAuthProvider;
7 | import org.springframework.security.crypto.password.PasswordEncoder;
8 |
9 | public class UserMapper {
10 |
11 | /**
12 | * Maps the sign up request to user entity.
13 | *
14 | * @param signUpRequest SignUp request used to create user dto.
15 | * @return User entity for jpa operations.
16 | */
17 | public UserEntity mapToUserEntity(SignUp signUpRequest, PasswordEncoder passwordEncoder) {
18 | UserEntity userEntity = new UserEntity();
19 |
20 | userEntity.setName(signUpRequest.getName());
21 | userEntity.setEmail(signUpRequest.getEmail());
22 | userEntity.setEmailVerified(false);
23 | userEntity.setProvider(OAuthProvider.LOCAL);
24 | userEntity.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));
25 | return userEntity;
26 | }
27 |
28 | /**
29 | * Maps the user entity object to user dto object.
30 | *
31 | * @param userEntity User entity from jpa operations.
32 | * @return User dto object for sending response.
33 | */
34 | public User mapToUser(UserEntity userEntity) {
35 | User user = new User();
36 |
37 | user.setName(userEntity.getName());
38 | user.setEmail(userEntity.getEmail());
39 | user.setEmailVerified(userEntity.isEmailVerified());
40 | user.setProvider(userEntity.getProvider().toString());
41 | user.setProviderId(userEntity.getProviderId());
42 | user.setImageUrl(userEntity.getImageUrl());
43 | return user;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/PublicCommandEntity.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model;
2 |
3 | import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
4 | import org.hibernate.annotations.Type;
5 | import org.hibernate.annotations.TypeDef;
6 |
7 | import javax.persistence.*;
8 | import java.sql.Timestamp;
9 | import java.util.*;
10 |
11 | @Entity
12 | @Table(name = "public_command")
13 | @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
14 | public class PublicCommandEntity {
15 | @Type(type = "jsonb")
16 | @Column(columnDefinition = "jsonb")
17 | Set flags = new HashSet<>();
18 |
19 | @Type(type = "jsonb")
20 | @Column(columnDefinition = "jsonb")
21 | Map> options = new HashMap<>();
22 |
23 | @Id
24 | @GeneratedValue(strategy = GenerationType.AUTO)
25 | private Long id;
26 |
27 | @Column(nullable = false, updatable = false)
28 | private String commandName;
29 |
30 | @Column(nullable = false)
31 | private String commandText;
32 |
33 | @Column(nullable = false, updatable = false)
34 | private String userEmail;
35 |
36 | @Column(nullable = false, updatable = false)
37 | private Timestamp createdOn;
38 |
39 | public Long getId() {
40 | return id;
41 | }
42 |
43 | public void setId(Long id) {
44 | this.id = id;
45 | }
46 |
47 | public Set getFlags() {
48 | return flags;
49 | }
50 |
51 | public void setFlags(Set flags) {
52 | this.flags = flags;
53 | }
54 |
55 | public Map> getOptions() {
56 | return options;
57 | }
58 |
59 | public void setOptions(Map> options) {
60 | this.options = options;
61 | }
62 |
63 | public String getCommandName() {
64 | return commandName;
65 | }
66 |
67 | public void setCommandName(String commandName) {
68 | this.commandName = commandName;
69 | }
70 |
71 | public String getCommandText() {
72 | return commandText;
73 | }
74 |
75 | public void setCommandText(String commandText) {
76 | this.commandText = commandText;
77 | }
78 |
79 | public String getUserEmail() {
80 | return userEmail;
81 | }
82 |
83 | public void setUserEmail(String userEmail) {
84 | this.userEmail = userEmail;
85 | }
86 |
87 | public Timestamp getCreatedOn() {
88 | return createdOn;
89 | }
90 |
91 | public void setCreatedOn(Timestamp createdOn) {
92 | this.createdOn = createdOn;
93 | }
94 |
95 | @Override
96 | public String toString() {
97 | return "PublicCommandEntity{"
98 | + "id="
99 | + id
100 | + ", flags="
101 | + flags
102 | + ", options="
103 | + options
104 | + ", commandName='"
105 | + commandName
106 | + '\''
107 | + ", commandText='"
108 | + commandText
109 | + '\''
110 | + ", userEmail='"
111 | + userEmail
112 | + '\''
113 | + ", createdOn="
114 | + createdOn
115 | + '}';
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/UserCommandEntity.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model;
2 |
3 | import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
4 | import org.hibernate.annotations.Type;
5 | import org.hibernate.annotations.TypeDef;
6 |
7 | import javax.persistence.*;
8 | import java.sql.Timestamp;
9 | import java.util.*;
10 |
11 | @Entity
12 | @Table(name = "user_command")
13 | @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
14 | public class UserCommandEntity {
15 | @Type(type = "jsonb")
16 | @Column(columnDefinition = "jsonb")
17 | Set flags = new HashSet<>();
18 |
19 | @Type(type = "jsonb")
20 | @Column(columnDefinition = "jsonb")
21 | Map> options = new HashMap<>();
22 |
23 | @Id
24 | @GeneratedValue(strategy = GenerationType.AUTO)
25 | private Long id;
26 |
27 | @Column(nullable = false, updatable = false)
28 | private String commandName;
29 |
30 | @Column(nullable = false)
31 | private String commandText;
32 |
33 | @Column(nullable = false, updatable = false)
34 | private String userEmail;
35 |
36 | @Column(nullable = false, updatable = false)
37 | private Timestamp createdOn;
38 |
39 | private Timestamp modifiedOn;
40 |
41 | @Column(nullable = false)
42 | private Timestamp operatedOn;
43 |
44 | public Long getId() {
45 | return id;
46 | }
47 |
48 | public void setId(Long id) {
49 | this.id = id;
50 | }
51 |
52 | public String getCommandName() {
53 | return commandName;
54 | }
55 |
56 | public void setCommandName(String commandName) {
57 | this.commandName = commandName;
58 | }
59 |
60 | public String getCommandText() {
61 | return commandText;
62 | }
63 |
64 | public void setCommandText(String commandText) {
65 | this.commandText = commandText;
66 | }
67 |
68 | public String getUserEmail() {
69 | return userEmail;
70 | }
71 |
72 | public void setUserEmail(String userEmail) {
73 | this.userEmail = userEmail;
74 | }
75 |
76 | public Timestamp getCreatedOn() {
77 | return createdOn;
78 | }
79 |
80 | public void setCreatedOn(Timestamp createdOn) {
81 | this.createdOn = createdOn;
82 | }
83 |
84 | public Timestamp getModifiedOn() {
85 | return modifiedOn;
86 | }
87 |
88 | public void setModifiedOn(Timestamp modifiedOn) {
89 | this.modifiedOn = modifiedOn;
90 | }
91 |
92 | public Timestamp getOperatedOn() {
93 | return operatedOn;
94 | }
95 |
96 | public void setOperatedOn(Timestamp operatedOn) {
97 | this.operatedOn = operatedOn;
98 | }
99 |
100 | public Set getFlags() {
101 | return flags;
102 | }
103 |
104 | public void setFlags(Set flags) {
105 | this.flags = flags;
106 | }
107 |
108 | public Map> getOptions() {
109 | return options;
110 | }
111 |
112 | public void setOptions(Map> options) {
113 | this.options = options;
114 | }
115 |
116 | @Override
117 | public String toString() {
118 | return "UserCommand{"
119 | + "id="
120 | + id
121 | + ", commandName='"
122 | + commandName
123 | + '\''
124 | + ", commandText='"
125 | + commandText
126 | + '\''
127 | + ", userEmail='"
128 | + userEmail
129 | + '\''
130 | + ", createdOn="
131 | + createdOn
132 | + ", modifiedOn="
133 | + modifiedOn
134 | + ", operatedOn="
135 | + operatedOn
136 | + ", flags="
137 | + flags
138 | + ", options="
139 | + options
140 | + '}';
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/UserEntity.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model;
2 |
3 | import javax.persistence.*;
4 | import java.sql.Timestamp;
5 |
6 | @Entity
7 | @Table(name = "app_user") // 'User' is a reserved keyword in postgres
8 | public class UserEntity {
9 |
10 | @Id private String email;
11 |
12 | @Column(nullable = false)
13 | private String name;
14 |
15 | private boolean emailVerified;
16 |
17 | @Column(nullable = false)
18 | @Enumerated(EnumType.STRING)
19 | private OAuthProvider provider;
20 |
21 | private String providerId;
22 | private String imageUrl;
23 | private String password;
24 |
25 | @Column(nullable = false)
26 | private Timestamp joinedOn;
27 |
28 | public String getEmail() {
29 | return email;
30 | }
31 |
32 | public void setEmail(String email) {
33 | this.email = email;
34 | }
35 |
36 | public String getName() {
37 | return name;
38 | }
39 |
40 | public void setName(String name) {
41 | this.name = name;
42 | }
43 |
44 | public boolean isEmailVerified() {
45 | return emailVerified;
46 | }
47 |
48 | public void setEmailVerified(boolean emailVerified) {
49 | this.emailVerified = emailVerified;
50 | }
51 |
52 | public OAuthProvider getProvider() {
53 | return provider;
54 | }
55 |
56 | public void setProvider(OAuthProvider provider) {
57 | this.provider = provider;
58 | }
59 |
60 | public String getProviderId() {
61 | return providerId;
62 | }
63 |
64 | public void setProviderId(String providerId) {
65 | this.providerId = providerId;
66 | }
67 |
68 | public String getImageUrl() {
69 | return imageUrl;
70 | }
71 |
72 | public void setImageUrl(String imageUrl) {
73 | this.imageUrl = imageUrl;
74 | }
75 |
76 | public String getPassword() {
77 | return password;
78 | }
79 |
80 | public void setPassword(String password) {
81 | this.password = password;
82 | }
83 |
84 | public Timestamp getJoinedOn() {
85 | return joinedOn;
86 | }
87 |
88 | public void setJoinedOn(Timestamp joinedOn) {
89 | this.joinedOn = joinedOn;
90 | }
91 |
92 | @Override
93 | public String toString() {
94 | return "UserEntity{"
95 | + "email='"
96 | + email
97 | + '\''
98 | + ", name='"
99 | + name
100 | + '\''
101 | + ", emailVerified="
102 | + emailVerified
103 | + ", provider="
104 | + provider
105 | + ", providerId='"
106 | + providerId
107 | + '\''
108 | + ", imageUrl='"
109 | + imageUrl
110 | + '\''
111 | + ", password='"
112 | + password
113 | + '\''
114 | + ", joinedOn="
115 | + joinedOn
116 | + '}';
117 | }
118 |
119 | public enum OAuthProvider {
120 | LOCAL,
121 | FACEBOOK,
122 | GOOGLE,
123 | GITHUB
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/graph/DataType.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model.graph;
2 |
3 | public enum DataType {
4 | PATH,
5 | FILE_NAME,
6 | NUMERIC_PERMISSION,
7 | NUMBER,
8 | STRING
9 | }
10 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/graph/EdgeType.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model.graph;
2 |
3 | import com.fasterxml.jackson.annotation.JsonValue;
4 |
5 | public enum EdgeType {
6 | BELONGS_TO,
7 | HAS_FLAG,
8 | HAS_FLAG_VALUE,
9 | HAS_OPTION,
10 | HAS_OPTION_VALUE,
11 | OVERRIDES;
12 |
13 | @JsonValue
14 | public String toLowerCase() {
15 | return name().toLowerCase();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/graph/VertexType.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model.graph;
2 |
3 | import com.fasterxml.jackson.annotation.JsonValue;
4 |
5 | public enum VertexType {
6 | METACOMMAND,
7 | FLAG,
8 | OPTION,
9 | USER;
10 |
11 | @JsonValue
12 | public String toLowerCase() {
13 | return name().toLowerCase();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/graph/props/FlagProperty.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model.graph.props;
2 |
3 | import com.fasterxml.jackson.annotation.JsonValue;
4 |
5 | public enum FlagProperty {
6 | NAME("V", true),
7 | ALIAS("V", false),
8 | PREFIX("V", true),
9 | DESC("V", true),
10 | LONG_DESC("V", false),
11 | SEQUENCE("E", true),
12 | IS_GROUPING_ALLOWED("V", false),
13 | IS_SOLITARY("V", false);
14 |
15 | private final String propertyOf;
16 | private final boolean isMandatory;
17 |
18 | FlagProperty(String propertyOf, boolean isMandatory) {
19 | this.propertyOf = propertyOf;
20 | this.isMandatory = isMandatory;
21 | }
22 |
23 | /**
24 | * Used to identify if the property belongs to vertex or edge.
25 | *
26 | * @return "V" if the property belongs to Vertex, "E" for Edge.
27 | */
28 | public String propertyOf() {
29 | return propertyOf;
30 | }
31 |
32 | /**
33 | * Used to check if the property is mandatory or optional.
34 | *
35 | * @return true if the property is mandatory, false otherwise.
36 | */
37 | public boolean isMandatory() {
38 | return isMandatory;
39 | }
40 |
41 | @JsonValue
42 | public String toLowerCase() {
43 | return name().toLowerCase();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/graph/props/MetaCommandProperty.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model.graph.props;
2 |
3 | import com.fasterxml.jackson.annotation.JsonValue;
4 |
5 | public enum MetaCommandProperty {
6 | NAME("V", true),
7 | SYNTAX("V", true),
8 | DESC("V", true),
9 | LONG_DESC("V", false),
10 | MAN_PAGE_URL("V", false);
11 |
12 | private final String propertyOf;
13 | private final boolean isMandatory;
14 |
15 | MetaCommandProperty(String propertyOf, boolean isMandatory) {
16 | this.propertyOf = propertyOf;
17 | this.isMandatory = isMandatory;
18 | }
19 |
20 | /**
21 | * Used to identify if the property belongs to vertex or edge.
22 | *
23 | * @return "V" if the property belongs to Vertex, "E" for Edge.
24 | */
25 | public String propertyOf() {
26 | return propertyOf;
27 | }
28 |
29 | /**
30 | * Used to identify if the property is mandatory or optional.
31 | *
32 | * @return true if the property is mandatory, false otherwise.
33 | */
34 | public boolean isMandatory() {
35 | return isMandatory;
36 | }
37 |
38 | @JsonValue
39 | public String toLowerCase() {
40 | return name().toLowerCase();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/model/graph/props/OptionProperty.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.model.graph.props;
2 |
3 | import com.fasterxml.jackson.annotation.JsonValue;
4 |
5 | public enum OptionProperty {
6 | NAME("V", true),
7 | ALIAS("V", false),
8 | PREFIX("V", false),
9 | DESC("V", true),
10 | LONG_DESC("V", false),
11 | DATA_TYPE("V", true),
12 | IS_MANDATORY("E", true),
13 | IS_REPEATABLE("E", true),
14 | SEQUENCE("E", true);
15 |
16 | private final String propertyOf;
17 | private final boolean isMandatory;
18 |
19 | OptionProperty(String propertyOf, boolean isMandatory) {
20 | this.propertyOf = propertyOf;
21 | this.isMandatory = isMandatory;
22 | }
23 |
24 | /**
25 | * Used to identify if the property belongs to vertex or edge.
26 | *
27 | * @return "V" if the property belongs to Vertex, "E" for Edge.
28 | */
29 | public String propertyOf() {
30 | return propertyOf;
31 | }
32 |
33 | /**
34 | * Used to check if the property is mandatory or optional.
35 | *
36 | * @return true if the property is mandatory, false otherwise.
37 | */
38 | public boolean isMandatory() {
39 | return isMandatory;
40 | }
41 |
42 | @JsonValue
43 | public String toLowerCase() {
44 | return name().toLowerCase();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/repository/PublicCommandRepository.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.repository;
2 |
3 | import com.wirehall.commandhunt.backend.model.PublicCommandEntity;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
6 | import org.springframework.stereotype.Repository;
7 |
8 | @Repository
9 | public interface PublicCommandRepository
10 | extends JpaRepository,
11 | JpaSpecificationExecutor {
12 |
13 | void deleteByIdAndUserEmail(Long id, String userEmail);
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/repository/UserCommandRepository.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.repository;
2 |
3 | import com.wirehall.commandhunt.backend.model.UserCommandEntity;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
6 | import org.springframework.stereotype.Repository;
7 |
8 | @Repository
9 | public interface UserCommandRepository
10 | extends JpaRepository, JpaSpecificationExecutor {
11 |
12 | UserCommandEntity findOneByIdAndUserEmail(Long userCommandId, String userEmail);
13 |
14 | void deleteByIdAndUserEmail(Long userCommandId, String userEmail);
15 | }
16 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/repository/UserCommandSpecification.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.repository;
2 |
3 | import com.wirehall.commandhunt.backend.model.UserCommandEntity;
4 | import org.springframework.data.jpa.domain.Specification;
5 | import org.springframework.util.StringUtils;
6 |
7 | public class UserCommandSpecification {
8 |
9 | private UserCommandSpecification() {}
10 |
11 | public static Specification equalsCommandName(String commandName) {
12 | if (!StringUtils.hasLength(commandName)) {
13 | return null;
14 | }
15 | return (root, query, cb) -> cb.equal(root.get("commandName"), commandName);
16 | }
17 |
18 | public static Specification likeCommandText(String commandText) {
19 | if (!StringUtils.hasLength(commandText)) {
20 | return null;
21 | }
22 | return (root, query, cb) -> cb.like(root.get("commandText"), "%" + commandText + "%");
23 | }
24 |
25 | public static Specification equalsUserEmail(String userEmail) {
26 | return (root, query, cb) -> cb.equal(root.get("userEmail"), userEmail);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.repository;
2 |
3 | import com.wirehall.commandhunt.backend.model.UserEntity;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | @Repository
8 | public interface UserRepository extends JpaRepository {}
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/security/CurrentUser.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.security;
2 |
3 | import org.springframework.security.core.annotation.AuthenticationPrincipal;
4 |
5 | import java.lang.annotation.*;
6 |
7 | @Target({ElementType.PARAMETER, ElementType.TYPE})
8 | @Retention(RetentionPolicy.RUNTIME)
9 | @Documented
10 | @AuthenticationPrincipal
11 | public @interface CurrentUser {}
12 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/security/CustomJwtAuthFilter.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.security;
2 |
3 | import com.wirehall.commandhunt.backend.service.auth.CustomUserDetailsService;
4 | import com.wirehall.commandhunt.backend.util.JwtUtil;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
9 | import org.springframework.security.core.context.SecurityContextHolder;
10 | import org.springframework.security.core.userdetails.UserDetails;
11 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
12 | import org.springframework.util.StringUtils;
13 | import org.springframework.web.filter.OncePerRequestFilter;
14 |
15 | import javax.servlet.FilterChain;
16 | import javax.servlet.ServletException;
17 | import javax.servlet.http.HttpServletRequest;
18 | import javax.servlet.http.HttpServletResponse;
19 | import java.io.IOException;
20 |
21 | public class CustomJwtAuthFilter extends OncePerRequestFilter {
22 |
23 | private static final Logger LOGGER = LoggerFactory.getLogger(CustomJwtAuthFilter.class);
24 |
25 | @Autowired private CustomUserDetailsService customUserDetailsService;
26 |
27 | @Autowired private JwtUtil jwtUtil;
28 |
29 | @Override
30 | protected void doFilterInternal(
31 | HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
32 | throws ServletException, IOException {
33 |
34 | try {
35 | String jwt = getJwtFromRequest(request);
36 |
37 | if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt)) {
38 | String email = jwtUtil.getUserEmailFromToken(jwt);
39 |
40 | UserDetails userDetails = customUserDetailsService.loadUserByUsername(email);
41 | UsernamePasswordAuthenticationToken auth =
42 | new UsernamePasswordAuthenticationToken(
43 | userDetails, null, userDetails.getAuthorities());
44 | auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
45 |
46 | SecurityContextHolder.getContext().setAuthentication(auth);
47 | }
48 | } catch (Exception ex) {
49 | LOGGER.error("Could not set user authentication in security context", ex);
50 | }
51 |
52 | filterChain.doFilter(request, response);
53 | }
54 |
55 | private String getJwtFromRequest(HttpServletRequest request) {
56 | String bearerToken = request.getHeader("Authorization");
57 | if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
58 | return bearerToken.substring(7);
59 | }
60 | return null;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/security/CustomOAuthRequestRepository.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.security;
2 |
3 | import com.nimbusds.oauth2.sdk.util.StringUtils;
4 | import com.wirehall.commandhunt.backend.util.CookieUtil;
5 | import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
6 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
7 | import org.springframework.stereotype.Component;
8 |
9 | import javax.servlet.http.HttpServletRequest;
10 | import javax.servlet.http.HttpServletResponse;
11 |
12 | @Component
13 | public class CustomOAuthRequestRepository
14 | implements AuthorizationRequestRepository {
15 |
16 | public static final String OAUTH_REQUEST_COOKIE_NAME = "oauth_request";
17 | public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
18 | private static final int COOKIE_EXPIRE_SECONDS = 180;
19 |
20 | @Override
21 | public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
22 | return CookieUtil.getCookie(request, OAUTH_REQUEST_COOKIE_NAME)
23 | .map(cookie -> CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class))
24 | .orElse(null);
25 | }
26 |
27 | @Override
28 | public void saveAuthorizationRequest(
29 | OAuth2AuthorizationRequest authorizationRequest,
30 | HttpServletRequest request,
31 | HttpServletResponse response) {
32 | if (authorizationRequest == null) {
33 | CookieUtil.deleteCookie(request, response, OAUTH_REQUEST_COOKIE_NAME);
34 | CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
35 | return;
36 | }
37 |
38 | CookieUtil.addCookie(
39 | response,
40 | OAUTH_REQUEST_COOKIE_NAME,
41 | CookieUtil.serialize(authorizationRequest),
42 | COOKIE_EXPIRE_SECONDS);
43 | String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
44 | if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
45 | CookieUtil.addCookie(
46 | response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, COOKIE_EXPIRE_SECONDS);
47 | }
48 | }
49 |
50 | @Override
51 | public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
52 | return this.loadAuthorizationRequest(request);
53 | }
54 |
55 | public void removeAuthorizationRequestCookies(
56 | HttpServletRequest request, HttpServletResponse response) {
57 | CookieUtil.deleteCookie(request, response, OAUTH_REQUEST_COOKIE_NAME);
58 | CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/security/OAuthFailureHandler.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.security;
2 |
3 | import com.wirehall.commandhunt.backend.util.CookieUtil;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.security.core.AuthenticationException;
6 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
7 | import org.springframework.stereotype.Component;
8 | import org.springframework.web.util.UriComponentsBuilder;
9 |
10 | import javax.servlet.http.Cookie;
11 | import javax.servlet.http.HttpServletRequest;
12 | import javax.servlet.http.HttpServletResponse;
13 | import java.io.IOException;
14 |
15 | @Component
16 | public class OAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
17 |
18 | @Autowired CustomOAuthRequestRepository customOAuthRequestRepository;
19 |
20 | @Override
21 | public void onAuthenticationFailure(
22 | HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
23 | throws IOException {
24 |
25 | String targetUrl =
26 | CookieUtil.getCookie(request, CustomOAuthRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME)
27 | .map(Cookie::getValue)
28 | .orElse(("/"));
29 |
30 | targetUrl =
31 | UriComponentsBuilder.fromUriString(targetUrl)
32 | .queryParam("error", exception.getLocalizedMessage())
33 | .build()
34 | .toUriString();
35 |
36 | customOAuthRequestRepository.removeAuthorizationRequestCookies(request, response);
37 |
38 | getRedirectStrategy().sendRedirect(request, response, targetUrl);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/security/OAuthSuccessHandler.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.security;
2 |
3 | import com.wirehall.commandhunt.backend.exception.BadRequestException;
4 | import com.wirehall.commandhunt.backend.util.AuthUtil;
5 | import com.wirehall.commandhunt.backend.util.CookieUtil;
6 | import com.wirehall.commandhunt.backend.util.JwtUtil;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.beans.factory.annotation.Autowired;
10 | import org.springframework.beans.factory.annotation.Value;
11 | import org.springframework.security.core.Authentication;
12 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
13 | import org.springframework.stereotype.Component;
14 | import org.springframework.web.util.UriComponentsBuilder;
15 |
16 | import javax.servlet.http.Cookie;
17 | import javax.servlet.http.HttpServletRequest;
18 | import javax.servlet.http.HttpServletResponse;
19 | import java.io.IOException;
20 | import java.util.Optional;
21 |
22 | @Component
23 | public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
24 |
25 | private static final Logger LOGGER = LoggerFactory.getLogger(OAuthSuccessHandler.class);
26 |
27 | private final CustomOAuthRequestRepository customOAuthRequestRepository;
28 |
29 | @Value("${app.oauth2.authorizedRedirectUris}")
30 | private String[] authorizedRedirectUris;
31 |
32 | @Autowired private JwtUtil jwtUtil;
33 |
34 | @Autowired
35 | OAuthSuccessHandler(CustomOAuthRequestRepository customOAuthRequestRepository) {
36 | this.customOAuthRequestRepository = customOAuthRequestRepository;
37 | }
38 |
39 | @Override
40 | public void onAuthenticationSuccess(
41 | HttpServletRequest request, HttpServletResponse response, Authentication authentication)
42 | throws IOException {
43 | String targetUrl = determineTargetUrl(request, response, authentication);
44 |
45 | if (response.isCommitted()) {
46 | LOGGER.warn("Response has already been committed. Unable to redirect to url: {}", targetUrl);
47 | return;
48 | }
49 |
50 | clearAuthenticationAttributes(request, response);
51 | getRedirectStrategy().sendRedirect(request, response, targetUrl);
52 | }
53 |
54 | @Override
55 | protected String determineTargetUrl(
56 | HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
57 | Optional redirectUri =
58 | CookieUtil.getCookie(request, CustomOAuthRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME)
59 | .map(Cookie::getValue);
60 |
61 | if (redirectUri.isPresent()
62 | && !AuthUtil.isAuthorizedRedirectUri(redirectUri.get(), authorizedRedirectUris)) {
63 | throw new BadRequestException(
64 | "Unauthorized Redirect URI, Can't proceed with the authentication");
65 | }
66 |
67 | String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
68 |
69 | String token = jwtUtil.createToken(authentication, 864000000L);
70 |
71 | return UriComponentsBuilder.fromUriString(targetUrl)
72 | .queryParam("token", token)
73 | .build()
74 | .toUriString();
75 | }
76 |
77 | protected void clearAuthenticationAttributes(
78 | HttpServletRequest request, HttpServletResponse response) {
79 | super.clearAuthenticationAttributes(request);
80 | customOAuthRequestRepository.removeAuthorizationRequestCookies(request, response);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/security/OAuthUserFactory.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.security;
2 |
3 | import com.wirehall.commandhunt.backend.model.UserEntity;
4 | import com.wirehall.commandhunt.backend.model.UserEntity.OAuthProvider;
5 |
6 | import java.util.Map;
7 |
8 | public class OAuthUserFactory {
9 | private static final String PICTURE_ATTRIBUTE_KEY = "picture";
10 |
11 | private OAuthUserFactory() {
12 | // Utility classes should not have public constructors.
13 | }
14 |
15 | /**
16 | * Factory used to get the user entity with the info received from OAuth provider.
17 | *
18 | * @param oAuthProvider The user's oAuth provider.
19 | * @param attributes User attributes received from provider.
20 | * @return User entity.
21 | */
22 | public static UserEntity getOAuth2UserInfo(
23 | OAuthProvider oAuthProvider, Map attributes) {
24 | UserEntity userEntity = new UserEntity();
25 | userEntity.setProvider(oAuthProvider);
26 | userEntity.setName((String) attributes.get("name"));
27 | userEntity.setEmail((String) attributes.get("email"));
28 |
29 | if (oAuthProvider.equals(OAuthProvider.GOOGLE)) {
30 | userEntity.setProviderId(String.valueOf(attributes.get("sub")));
31 | userEntity.setImageUrl((String) attributes.get(PICTURE_ATTRIBUTE_KEY));
32 | userEntity.setEmailVerified((boolean) attributes.get("email_verified"));
33 | } else if (oAuthProvider.equals(OAuthProvider.FACEBOOK)) {
34 | userEntity.setProviderId(String.valueOf(attributes.get("id")));
35 | userEntity.setImageUrl(getFacebookImageUrl(attributes));
36 | } else if (oAuthProvider.equals(OAuthProvider.GITHUB)) {
37 | userEntity.setProviderId(String.valueOf(attributes.get("id")));
38 | userEntity.setImageUrl((String) attributes.get("avatar_url"));
39 | }
40 | return userEntity;
41 | }
42 |
43 | private static String getFacebookImageUrl(Map attributes) {
44 | if (attributes.containsKey(PICTURE_ATTRIBUTE_KEY)) {
45 | Map pictureObj = (Map) attributes.get(PICTURE_ATTRIBUTE_KEY);
46 | if (pictureObj.containsKey("data")) {
47 | Map dataObj = (Map) pictureObj.get("data");
48 | if (dataObj.containsKey("url")) {
49 | return (String) dataObj.get("url");
50 | }
51 | }
52 | }
53 | return null;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/security/RestAuthenticationEntryPoint.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.security;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.security.core.AuthenticationException;
6 | import org.springframework.security.web.AuthenticationEntryPoint;
7 |
8 | import javax.servlet.http.HttpServletRequest;
9 | import javax.servlet.http.HttpServletResponse;
10 | import java.io.IOException;
11 |
12 | public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
13 |
14 | private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class);
15 |
16 | @Override
17 | public void commence(
18 | HttpServletRequest httpServletRequest,
19 | HttpServletResponse httpServletResponse,
20 | AuthenticationException e)
21 | throws IOException {
22 |
23 | LOGGER.error("Responding with unauthorized error. Message - {}", e.getMessage());
24 | httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getLocalizedMessage());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/service/MetaCommandService.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.service;
2 |
3 | import com.wirehall.commandhunt.backend.dto.MetaCommand;
4 | import com.wirehall.commandhunt.backend.dto.filter.Filter;
5 | import com.wirehall.commandhunt.backend.dto.filter.PageResponse;
6 | import com.wirehall.commandhunt.backend.repository.MetaCommandRepository;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.stereotype.Service;
9 |
10 | import java.util.List;
11 |
12 | @Service
13 | public class MetaCommandService {
14 |
15 | private final MetaCommandRepository metaCommandRepository;
16 |
17 | @Autowired
18 | public MetaCommandService(MetaCommandRepository metaCommandRepository) {
19 | this.metaCommandRepository = metaCommandRepository;
20 | }
21 |
22 | public List getAllMetaCommands() {
23 | return metaCommandRepository.getAllMetaCommands();
24 | }
25 |
26 | public PageResponse getAllMetaCommands(Filter filter) {
27 | return metaCommandRepository.getAllMetaCommands(filter);
28 | }
29 |
30 | public MetaCommand getMetaCommandById(String id) {
31 | return metaCommandRepository.getMetaCommandById(id);
32 | }
33 |
34 | public MetaCommand getMetaCommandByName(String name) {
35 | return metaCommandRepository.getMetaCommandByName(name);
36 | }
37 |
38 | public List getMatchingMetaCommands(String query) {
39 | return metaCommandRepository.getMatchingMetaCommands(query);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/service/PublicCommandService.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.service;
2 |
3 | import com.wirehall.commandhunt.backend.dto.PublicCommand;
4 | import com.wirehall.commandhunt.backend.exception.BadRequestException;
5 | import com.wirehall.commandhunt.backend.mapper.PublicCommandMapper;
6 | import com.wirehall.commandhunt.backend.model.PublicCommandEntity;
7 | import com.wirehall.commandhunt.backend.repository.PublicCommandRepository;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.springframework.beans.factory.annotation.Autowired;
11 | import org.springframework.stereotype.Service;
12 |
13 | import javax.transaction.Transactional;
14 | import java.sql.Timestamp;
15 |
16 | @Service
17 | @Transactional
18 | public class PublicCommandService {
19 | private static final Logger LOGGER = LoggerFactory.getLogger(PublicCommandService.class);
20 |
21 | private final PublicCommandRepository publicCommandRepository;
22 | private final PublicCommandMapper mapper = new PublicCommandMapper();
23 |
24 | @Autowired
25 | public PublicCommandService(PublicCommandRepository publicCommandRepository) {
26 | this.publicCommandRepository = publicCommandRepository;
27 | }
28 |
29 | /**
30 | * Retrieves the public-command using id.
31 | *
32 | * @param publicCommandId The id of the requested public-command.
33 | * @param userEmail The email of logged in user if any, otherwise null.
34 | * @return The public-command with deletable indicator.
35 | */
36 | public PublicCommand getPublicCommandById(Long publicCommandId, String userEmail) {
37 | PublicCommandEntity entity = publicCommandRepository.getOne(publicCommandId);
38 | LOGGER.debug("Retrieved public-command entity: {}", entity);
39 | PublicCommand publicCommand = mapper.mapToPublicCommand(entity, true);
40 | publicCommand.setDeletable(entity.getUserEmail().equals(userEmail));
41 | return publicCommand;
42 | }
43 |
44 | /**
45 | * Adds the new public-command.
46 | *
47 | * @param publicCommand The public-command dto to be added.
48 | * @param userEmail Logged-in user's email id.
49 | * @return The newly added public-command.
50 | */
51 | public PublicCommand addPublicCommand(PublicCommand publicCommand, String userEmail) {
52 | if (publicCommand.getId() != null) {
53 | throw new BadRequestException("Invalid save operation. Not a new public-command.");
54 | }
55 | Timestamp timestamp = new Timestamp(System.currentTimeMillis());
56 | publicCommand.setCreatedOn(timestamp);
57 | PublicCommandEntity entity = mapper.mapToPublicCommandEntity(publicCommand, userEmail);
58 | LOGGER.info("Inserting public-command entity: {}", entity);
59 | publicCommandRepository.save(entity);
60 | LOGGER.info("Inserted public-command entity with id: {}", entity.getId());
61 | return mapper.mapToPublicCommand(entity, false);
62 | }
63 |
64 | /**
65 | * Deletes the public-command using id.
66 | *
67 | * @param publicCommandId The id of public-command to be deleted.
68 | * @param userEmail Logged-in user's email id.
69 | */
70 | public void deletePublicCommand(Long publicCommandId, String userEmail) {
71 | publicCommandRepository.deleteByIdAndUserEmail(publicCommandId, userEmail);
72 | LOGGER.info("Deleted public-command entity having id: {}", publicCommandId);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/service/UserService.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.service;
2 |
3 | import com.wirehall.commandhunt.backend.dto.User;
4 | import com.wirehall.commandhunt.backend.dto.auth.SignUp;
5 | import com.wirehall.commandhunt.backend.exception.BadRequestException;
6 | import com.wirehall.commandhunt.backend.mapper.UserMapper;
7 | import com.wirehall.commandhunt.backend.model.UserEntity;
8 | import com.wirehall.commandhunt.backend.repository.UserRepository;
9 | import com.wirehall.commandhunt.backend.util.SecurityUtil;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.security.crypto.password.PasswordEncoder;
14 | import org.springframework.stereotype.Service;
15 |
16 | import javax.transaction.Transactional;
17 | import java.sql.Timestamp;
18 |
19 | @Service
20 | @Transactional
21 | public class UserService {
22 |
23 | private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
24 |
25 | private final UserRepository userRepository;
26 | private final PasswordEncoder passwordEncoder;
27 | private final UserMapper mapper = new UserMapper();
28 |
29 | @Autowired
30 | public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
31 | this.userRepository = userRepository;
32 | this.passwordEncoder = passwordEncoder;
33 | }
34 |
35 | /**
36 | * Adds the user created as a result of sign up request.
37 | *
38 | * @param signUpRequest Sign up request payload.
39 | * @return User DTO.
40 | */
41 | public User registerUser(SignUp signUpRequest) {
42 | if (userRepository.findById(signUpRequest.getEmail()).isPresent()) {
43 | if (LOGGER.isErrorEnabled()) {
44 | LOGGER.error(
45 | "Email: {} is already in use",
46 | SecurityUtil.sanitizeForLogging(signUpRequest.getEmail()));
47 | }
48 | throw new BadRequestException("Email address already in use.");
49 | }
50 |
51 | UserEntity userEntity = mapper.mapToUserEntity(signUpRequest, passwordEncoder);
52 | userEntity.setJoinedOn(new Timestamp(System.currentTimeMillis()));
53 | userRepository.save(userEntity);
54 | return mapper.mapToUser(userEntity);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/service/auth/CustomUserDetailsService.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.service.auth;
2 |
3 | import com.wirehall.commandhunt.backend.model.UserEntity;
4 | import com.wirehall.commandhunt.backend.model.auth.CustomUserPrincipal;
5 | import com.wirehall.commandhunt.backend.repository.UserRepository;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.security.authentication.BadCredentialsException;
8 | import org.springframework.security.core.userdetails.UserDetails;
9 | import org.springframework.security.core.userdetails.UserDetailsService;
10 | import org.springframework.stereotype.Service;
11 |
12 | import java.util.Optional;
13 |
14 | /**
15 | * The principal is always the result of the UserDetailsService that returned the object
16 | *
17 | * NOTE: There is often some confusion about UserDetailsService. It is purely a DAO for user data
18 | * and performs no other function other than to supply that data to other components within the
19 | * framework. In particular, it does not authenticate the user, which is done by the
20 | * AuthenticationManager. In many cases it makes more sense to implement AuthenticationProvider
21 | * directly if you require a custom authentication process.
22 | */
23 | @Service
24 | public class CustomUserDetailsService implements UserDetailsService {
25 |
26 | private final UserRepository userRepository;
27 |
28 | @Autowired
29 | public CustomUserDetailsService(UserRepository userRepository) {
30 | this.userRepository = userRepository;
31 | }
32 |
33 | /**
34 | * Returns the user instance by username (i.e. email). Email is used as the primary key for user
35 | * entity so username will always refer to the email.
36 | *
37 | * @param email The email address of user.
38 | * @return Returns the user.
39 | */
40 | @Override
41 | public UserDetails loadUserByUsername(String email) {
42 | Optional userEntity = userRepository.findById(email);
43 | if (!userEntity.isPresent()) {
44 | throw new BadCredentialsException("No user exists with email : " + email);
45 | }
46 |
47 | return CustomUserPrincipal.create(userEntity.get());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/util/AuthUtil.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.util;
2 |
3 | import java.net.URI;
4 | import java.util.Arrays;
5 |
6 | public class AuthUtil {
7 | private AuthUtil() {}
8 |
9 | public static boolean isAuthorizedRedirectUri(String uri, String[] authorizedRedirectUris) {
10 | URI clientRedirectUri = URI.create(uri);
11 |
12 | return Arrays.stream(authorizedRedirectUris)
13 | .anyMatch(
14 | authorizedRedirectUri -> {
15 | // Only validate host and port. Let the clients use different paths if they want to
16 | URI authorizedUri = URI.create(authorizedRedirectUri);
17 | return authorizedUri.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
18 | && authorizedUri.getPort() == clientRedirectUri.getPort();
19 | });
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/util/CookieUtil.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.util;
2 |
3 | import org.springframework.util.SerializationUtils;
4 |
5 | import javax.servlet.http.Cookie;
6 | import javax.servlet.http.HttpServletRequest;
7 | import javax.servlet.http.HttpServletResponse;
8 | import java.util.Base64;
9 | import java.util.Optional;
10 |
11 | public class CookieUtil {
12 |
13 | private CookieUtil() {
14 | // Utility classes should not have public constructors
15 | }
16 |
17 | /**
18 | * Returns the matching cookie by name.
19 | *
20 | * @param request The request instance.
21 | * @param name The name of the cookie to be retrieved.
22 | * @return The matched cookie.
23 | */
24 | public static Optional getCookie(HttpServletRequest request, String name) {
25 | Cookie[] cookies = request.getCookies();
26 |
27 | if (cookies != null && cookies.length > 0) {
28 | for (Cookie cookie : cookies) {
29 | if (cookie.getName().equals(name)) {
30 | return Optional.of(cookie);
31 | }
32 | }
33 | }
34 |
35 | return Optional.empty();
36 | }
37 |
38 | /**
39 | * Adds the cookie to response instance.
40 | *
41 | * @param response The response instance.
42 | * @param name The name of the cookie.
43 | * @param value The value of the cookie.
44 | * @param maxAge Validity time of the cookie.
45 | */
46 | public static void addCookie(
47 | HttpServletResponse response, String name, String value, int maxAge) {
48 | Cookie cookie = new Cookie(name, value);
49 | cookie.setPath("/");
50 | cookie.setHttpOnly(true);
51 | cookie.setMaxAge(maxAge);
52 | response.addCookie(cookie);
53 | }
54 |
55 | /**
56 | * Deletes the cookie with matching name.
57 | *
58 | * @param request The request instance.
59 | * @param response The response instance.
60 | * @param name The name of the cookie to be deleted.
61 | */
62 | public static void deleteCookie(
63 | HttpServletRequest request, HttpServletResponse response, String name) {
64 | Cookie[] cookies = request.getCookies();
65 | if (cookies != null && cookies.length > 0) {
66 | for (Cookie cookie : cookies) {
67 | if (cookie.getName().equals(name)) {
68 | cookie.setValue("");
69 | cookie.setPath("/");
70 | cookie.setMaxAge(0);
71 | response.addCookie(cookie);
72 | }
73 | }
74 | }
75 | }
76 |
77 | /**
78 | * Serializes the object and encodes using Base64 encoding.
79 | *
80 | * @param object The object to be serialized.
81 | * @return The serialized string in base64 format.
82 | */
83 | public static String serialize(Object object) {
84 | return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object));
85 | }
86 |
87 | /**
88 | * Deserialize the cookie value and cast it to the specified class type.
89 | *
90 | * @param cookie The cookie to deserialize.
91 | * @param cls The type of class to cast the deserialize the result.
92 | * @return Object created out of deserialization.
93 | */
94 | public static T deserialize(Cookie cookie, Class cls) {
95 | return cls.cast(
96 | SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/util/JwtUtil.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.util;
2 |
3 | import com.wirehall.commandhunt.backend.model.auth.CustomUserPrincipal;
4 | import io.jsonwebtoken.*;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.security.core.Authentication;
9 | import org.springframework.stereotype.Component;
10 |
11 | import java.util.Date;
12 |
13 | @Component
14 | public class JwtUtil {
15 |
16 | private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtil.class);
17 |
18 | @Value("${app.jwt.secret}")
19 | private String appJwtSecret;
20 |
21 | /**
22 | * Creates a JWT token with specified information.
23 | *
24 | * @param authentication Used to get the principal required in token creation.
25 | * @param expiry Token expiry duration.
26 | * @return JWT token.
27 | */
28 | public String createToken(Authentication authentication, Long expiry) {
29 | CustomUserPrincipal customUserPrincipal = (CustomUserPrincipal) authentication.getPrincipal();
30 |
31 | Date now = new Date();
32 | Date expiryDate = new Date(now.getTime() + expiry);
33 |
34 | return Jwts.builder()
35 | .setSubject(customUserPrincipal.getEmail())
36 | .setIssuedAt(new Date())
37 | .setExpiration(expiryDate)
38 | .signWith(SignatureAlgorithm.HS512, appJwtSecret)
39 | .compact();
40 | }
41 |
42 | /**
43 | * Retrieve user's email from token.
44 | *
45 | * @param token JWT token.
46 | * @return User's email address.
47 | */
48 | public String getUserEmailFromToken(String token) {
49 | Claims claims = Jwts.parser().setSigningKey(appJwtSecret).parseClaimsJws(token).getBody();
50 |
51 | return claims.getSubject();
52 | }
53 |
54 | /**
55 | * Validates the specified token.
56 | *
57 | * @param authToken JWT token.
58 | * @return true if token is valid, false otherwise.
59 | */
60 | public boolean validateToken(String authToken) {
61 | try {
62 | Jwts.parser().setSigningKey(appJwtSecret).parseClaimsJws(authToken);
63 | return true;
64 | } catch (SignatureException ex) {
65 | LOGGER.error("Invalid JWT signature");
66 | } catch (MalformedJwtException ex) {
67 | LOGGER.error("Invalid JWT token");
68 | } catch (ExpiredJwtException ex) {
69 | LOGGER.error("Expired JWT token");
70 | } catch (UnsupportedJwtException ex) {
71 | LOGGER.error("Unsupported JWT token");
72 | } catch (IllegalArgumentException ex) {
73 | LOGGER.error("JWT claims string is empty.");
74 | }
75 | return false;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/wirehall/commandhunt/backend/util/SecurityUtil.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.util;
2 |
3 | public class SecurityUtil {
4 | private SecurityUtil() {
5 | // Utility classes should not have public constructors
6 | }
7 |
8 | /**
9 | * This method is helps to sanitize a string and make it safe for logging.
10 | *
11 | * As per the sonar javasecurity:S5145 rule - User provided data, such as URL parameters, POST
12 | * data payloads or cookies, should always be considered untrusted and tainted. Applications
13 | * logging tainted data could enable an attacker to inject characters that would break the log
14 | * file pattern. This could be used to block monitors and SIEM (Security Information and Event
15 | * Management) systems from detecting other malicious events.
16 | *
17 | * @param string The string which needs to be sanitized
18 | * @return Sanitized string which is safe for logging
19 | */
20 | public static String sanitizeForLogging(String string) {
21 | if (string == null) return null;
22 | // javasecurity:S5145
23 | // This is to avoid logging user controlled data directly
24 | return string.replaceAll("[\n\r\t]", "_");
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/backend/src/main/resources/META-INF/spring-configuration-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": [
3 | {
4 | "name": "app.graph.config",
5 | "type": "java.lang.String"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/backend/src/main/resources/application-dev.yml:
--------------------------------------------------------------------------------
1 | # This property file values will override values from application.yml file when using dev profile
2 |
3 | app:
4 | jwt:
5 | secret: test
6 | oauth2:
7 | authorizedRedirectUris: http://localhost:3000/login
8 | spring:
9 | datasource:
10 | url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
11 | username: sa
12 | password: sa
13 | h2:
14 | console:
15 | # http://localhost:8080/h2-console
16 | enabled: true
17 | settings:
18 | web-allow-others: true
19 | jpa:
20 | generate-ddl: false
21 | show-sql: true
22 | hibernate:
23 | ddl-auto: none
--------------------------------------------------------------------------------
/backend/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | app:
2 | graph:
3 | config: jg-inmemory.properties
4 | export:
5 | cronExpression: 0 0 12 * * ?
6 | enable: false
7 | path: ./graph
8 | import:
9 | enable: false
10 | path: ./graph/app.graphml
11 |
12 | isManualAuthAllowed: false # Enable/disable manual sign-up and login. If disabled, only OAuth login will be allowed
13 | jwt:
14 | secret: ${APP_JWT_SECRET}
15 | oauth2:
16 | # After successfully authenticating with the OAuth2 Provider,
17 | # we'll be generating an auth token for the user and sending the token to the
18 | # redirectUri mentioned by the client in the /oauth2/authorize request.
19 | # We're not using cookies because they won't work well in mobile clients.
20 | authorizedRedirectUris: https://commandhunt.com/login
21 |
22 | # Enable actuator's health probes for using them in kubernetes for readiness and liveness
23 | management:
24 | health:
25 | probes:
26 | enabled: true
27 |
28 | spring:
29 | liquibase:
30 | enabled: true
31 | changeLog: classpath:/db/changelog/db.changelog-master.xml
32 | datasource:
33 | url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?currentSchema=commandhunt
34 | username: ${POSTGRES_USER}
35 | password: ${POSTGRES_PASSWORD}
36 | jpa:
37 | generate-ddl: false
38 | show-sql: true
39 | hibernate:
40 | ddl-auto: none
41 | jackson:
42 | mapper:
43 | accept-case-insensitive-enums: true
44 | security:
45 | oauth2:
46 | client:
47 | registration:
48 | google:
49 | clientId: ${GOOGLE_CLIENT_ID}
50 | clientSecret: ${GOOGLE_CLIENT_SECRET}
51 | redirectUri: "{baseUrl}/api/oauth2/callback/{registrationId}"
52 | scope:
53 | - email
54 | - profile
55 | facebook:
56 | clientId: ${FACEBOOK_CLIENT_ID}
57 | clientSecret: ${FACEBOOK_CLIENT_SECRET}
58 | redirectUri: "{baseUrl}/api/oauth2/callback/{registrationId}"
59 | scope:
60 | - email
61 | - public_profile
62 | github:
63 | clientId: ${GITHUB_CLIENT_ID}
64 | clientSecret: ${GITHUB_CLIENT_SECRET}
65 | redirectUri: "{baseUrl}/api/oauth2/callback/{registrationId}"
66 | scope:
67 | - user:email
68 | - read:user
69 | provider:
70 | facebook:
71 | authorizationUri: https://www.facebook.com/v3.0/dialog/oauth
72 | tokenUri: https://graph.facebook.com/v3.0/oauth/access_token
73 | userInfoUri: https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250)
74 |
--------------------------------------------------------------------------------
/backend/src/main/resources/db/changelog/db.changelog-master.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/backend/src/main/resources/jg-cql-dev.properties:
--------------------------------------------------------------------------------
1 | gremlin.graph=org.janusgraph.core.JanusGraphFactory
2 | storage.backend=cql
3 | storage.cql.keyspace=commandhunt
4 | storage.hostname=localhost
5 | cache.db-cache=true
6 | cache.db-cache-clean-wait=20
7 | cache.db-cache-time=180000
8 | cache.db-cache-size=0.5
9 | index.search.backend=elasticsearch
10 | index.search.hostname=localhost
--------------------------------------------------------------------------------
/backend/src/main/resources/jg-cql.properties:
--------------------------------------------------------------------------------
1 | gremlin.graph=org.janusgraph.core.JanusGraphFactory
2 | storage.backend=cql
3 | storage.cql.keyspace=commandhunt
4 | storage.hostname=${env:STORAGE_HOSTNAME}
5 | cache.db-cache=true
6 | cache.db-cache-clean-wait=20
7 | cache.db-cache-time=180000
8 | cache.db-cache-size=0.5
9 | index.search.backend=elasticsearch
10 | index.search.hostname=${env:INDEX_SEARCH_HOSTNAME}
--------------------------------------------------------------------------------
/backend/src/main/resources/jg-inmemory.properties:
--------------------------------------------------------------------------------
1 | gremlin.graph=org.janusgraph.core.JanusGraphFactory
2 | storage.backend=inmemory
--------------------------------------------------------------------------------
/backend/src/main/resources/logback-spring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/wirehall/commandhunt/backend/service/UserServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.service;
2 |
3 | import com.wirehall.commandhunt.backend.dto.User;
4 | import com.wirehall.commandhunt.backend.dto.auth.SignUp;
5 | import com.wirehall.commandhunt.backend.exception.BadRequestException;
6 | import com.wirehall.commandhunt.backend.model.UserEntity;
7 | import com.wirehall.commandhunt.backend.repository.UserRepository;
8 | import org.junit.jupiter.api.Assertions;
9 | import org.junit.jupiter.api.BeforeEach;
10 | import org.junit.jupiter.api.Test;
11 | import org.junit.jupiter.api.extension.ExtendWith;
12 | import org.junit.jupiter.api.function.Executable;
13 | import org.mockito.ArgumentCaptor;
14 | import org.mockito.ArgumentMatchers;
15 | import org.mockito.Mock;
16 | import org.mockito.junit.jupiter.MockitoExtension;
17 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
18 |
19 | import java.util.Optional;
20 |
21 | import static org.junit.jupiter.api.Assertions.assertThrows;
22 | import static org.mockito.ArgumentMatchers.anyString;
23 | import static org.mockito.Mockito.verify;
24 | import static org.mockito.Mockito.when;
25 |
26 | @ExtendWith(MockitoExtension.class)
27 | class UserServiceTest {
28 | @Mock UserRepository userRepository;
29 |
30 | UserService userService;
31 |
32 | @BeforeEach
33 | void setUp() {
34 | userService = new UserService(userRepository, new BCryptPasswordEncoder());
35 | }
36 |
37 | @Test
38 | void should_AddUser_When_SignUpRequested() {
39 | // Given
40 | SignUp signUp = new SignUp();
41 | signUp.setName("John Doe");
42 | signUp.setEmail("abc@xyz.com");
43 | signUp.setPassword("secret");
44 | when(userRepository.save(ArgumentMatchers.isA(UserEntity.class))).thenReturn(new UserEntity());
45 |
46 | // When
47 | User user = userService.registerUser(signUp);
48 |
49 | // Then
50 | ArgumentCaptor arg = ArgumentCaptor.forClass(UserEntity.class);
51 | verify(userRepository).save(arg.capture());
52 | UserEntity ue = arg.getValue();
53 | Assertions.assertEquals(signUp.getName(), ue.getName());
54 | Assertions.assertEquals(signUp.getEmail(), ue.getEmail());
55 | Assertions.assertNotNull(ue.getPassword());
56 | Assertions.assertNotEquals(signUp.getPassword(), ue.getPassword());
57 | Assertions.assertNotNull(ue.getJoinedOn());
58 | Assertions.assertEquals(UserEntity.OAuthProvider.LOCAL, ue.getProvider());
59 |
60 | Assertions.assertEquals(signUp.getName(), user.getName());
61 | Assertions.assertEquals(signUp.getEmail(), user.getEmail());
62 | Assertions.assertEquals(UserEntity.OAuthProvider.LOCAL.toString(), user.getProvider());
63 | }
64 |
65 | @Test
66 | void should_ThrowException_When_UserWithEmailExist() {
67 | // Given
68 | SignUp signUp = new SignUp();
69 | signUp.setName("John Doe");
70 | signUp.setEmail("abc@xyz.com");
71 | signUp.setPassword("secret");
72 | when(userRepository.findById(anyString())).thenReturn(Optional.of(new UserEntity()));
73 |
74 | // When
75 | Executable e = () -> userService.registerUser(signUp);
76 |
77 | // Then
78 | assertThrows(BadRequestException.class, e);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/wirehall/commandhunt/backend/service/auth/CustomUserDetailsServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.wirehall.commandhunt.backend.service.auth;
2 |
3 | import com.wirehall.commandhunt.backend.model.UserEntity;
4 | import com.wirehall.commandhunt.backend.repository.UserRepository;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.extension.ExtendWith;
8 | import org.junit.jupiter.api.function.Executable;
9 | import org.mockito.Mock;
10 | import org.mockito.junit.jupiter.MockitoExtension;
11 | import org.springframework.security.authentication.BadCredentialsException;
12 |
13 | import java.util.Optional;
14 |
15 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
16 | import static org.junit.jupiter.api.Assertions.assertThrows;
17 | import static org.mockito.ArgumentMatchers.anyString;
18 | import static org.mockito.Mockito.when;
19 |
20 | @ExtendWith(MockitoExtension.class)
21 | class CustomUserDetailsServiceTest {
22 | @Mock UserRepository userRepository;
23 |
24 | CustomUserDetailsService customUserDetailsService;
25 |
26 | @BeforeEach
27 | void setUp() {
28 | customUserDetailsService = new CustomUserDetailsService(userRepository);
29 | }
30 |
31 | @Test
32 | void should_LoadUser_When_ValidEmailProvided() {
33 | when(userRepository.findById(anyString())).thenReturn(Optional.of(new UserEntity()));
34 | Executable e = () -> customUserDetailsService.loadUserByUsername(anyString());
35 | assertDoesNotThrow(e);
36 | }
37 |
38 | @Test
39 | void should_ThrowException_When_NoUserWithEmailFound() {
40 | when(userRepository.findById(anyString())).thenReturn(Optional.empty());
41 | Executable e = () -> customUserDetailsService.loadUserByUsername(anyString());
42 | assertThrows(BadCredentialsException.class, e);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/backend/src/test/resources/sql/InsertSingleUser.sql:
--------------------------------------------------------------------------------
1 | insert into app_user (email, email_verified, image_url, joined_on, name, password, provider, provider_id) values('abc@xyz.com', true, 'https://lh4.googleusercontent.com/-AsZlmSkxhq4/AAAAAAAAAAI/AAAAAAAAVvM/AMZuucmdk6SkJkBWkmiU5byqPkbMWGxlmA/s96-c/photo.jpg', CURRENT_TIMESTAMP, 'John Doe', null, 'GOOGLE', 435600468853482916416);
--------------------------------------------------------------------------------
/backend/src/test/resources/sql/InsertSingleUserCommand.sql:
--------------------------------------------------------------------------------
1 | insert into user_command (id, user_email, command_name, command_text, created_on, modified_on, operated_on) values(99, 'abc@xyz.com', 'basename', 'basename -az --suffix=test /abc/xyz', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
--------------------------------------------------------------------------------
/frontend/.env.production:
--------------------------------------------------------------------------------
1 | # This file will be called when creating the build using 'npm run build' command
2 | GENERATE_SOURCEMAP=false
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /target
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | ## CommandHunt Frontend Module
2 | This is the frontend module of commandhunt application.
3 |
4 | ### Build the frontend module
5 |
6 | The following commands are used to build this frontend module.
7 | ```shell
8 | npm install
9 | npm run build
10 | ```
11 |
12 | ### Running the frontend module locally
13 | The following command is used to run this frontend module.
14 | ```shell
15 | npm start
16 | ```
17 | This will start the react app in the development mode.
18 |
19 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
20 |
21 | ### Analyzing the bundle size
22 | Source map explorer analyzes JavaScript bundles using the source maps. This helps you understand where code bloat is coming from.
23 |
24 | Run below command to generate the report.
25 | ```shell
26 | npm run analyze
27 | ```
28 |
29 | ### Create production build
30 | ```shell
31 | npm run build
32 | ```
33 |
34 | This will create the production build of application in the `build` folder.
35 | It correctly bundles application in production mode and optimizes the build for the best performance.
36 |
37 | ### Running the production build of frontend module
38 | When you do `npm start` the app is started in development mode.
39 |
40 | If you have built the app and you want to run the app in production mode (i.e.from build directory),
41 | Install `serve` and start the app using below commands
42 | ```shell
43 | npm install serve -g
44 | npm run build
45 | serve -s build
46 | ```
47 |
48 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
49 |
--------------------------------------------------------------------------------
/frontend/assembly.xml:
--------------------------------------------------------------------------------
1 |
4 | distribution
5 |
6 | zip
7 |
8 |
9 |
10 | build
11 |
12 | **
13 |
14 | /
15 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:stable-alpine
2 | EXPOSE 3000
3 | COPY ./app/nginx/default.conf /etc/nginx/conf.d/default.conf
4 | COPY ./app/webapp /usr/share/nginx/html
--------------------------------------------------------------------------------
/frontend/docker/nginx/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 3000;
3 |
4 | location / {
5 | root /usr/share/nginx/html;
6 | index index.html index.htm;
7 | try_files $uri $uri/ /index.html;
8 | }
9 | }
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.1",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^1.2.32",
7 | "@fortawesome/free-solid-svg-icons": "^5.15.1",
8 | "@fortawesome/react-fontawesome": "^0.1.13",
9 | "latest-version": "^5.1.0",
10 | "node-sass": "^4.14.1",
11 | "react": "^16.14.0",
12 | "react-app-polyfill": "^1.0.6",
13 | "react-copy-to-clipboard": "^5.0.2",
14 | "react-dom": "^16.14.0",
15 | "react-redux": "^7.2.2",
16 | "react-router-dom": "^5.2.0",
17 | "react-scripts": "3.4.1",
18 | "redux": "^4.0.5",
19 | "redux-thunk": "^2.3.0",
20 | "tinydate": "^1.3.0",
21 | "toastmaker": "^1.0.9"
22 | },
23 | "scripts": {
24 | "analyze": "cross-env GENERATE_SOURCEMAP=true react-scripts build && source-map-explorer 'build/static/js/*.js'",
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "jestSonar": {
31 | "reportPath": "coverage",
32 | "reportFile": "test-reporter.xml",
33 | "indent": 4
34 | },
35 | "eslintConfig": {
36 | "extends": "react-app"
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "ie 11",
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | },
51 | "devDependencies": {
52 | "cross-env": "^7.0.3",
53 | "jest-sonar-reporter": "^2.0.0",
54 | "source-map-explorer": "^2.5.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vivekweb2013/commandhunt/10acc2e13a28df808ad4d0900c7f539fc8db4e22/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
26 | CommandHunt
27 |
28 |
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/frontend/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vivekweb2013/commandhunt/10acc2e13a28df808ad4d0900c7f539fc8db4e22/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vivekweb2013/commandhunt/10acc2e13a28df808ad4d0900c7f539fc8db4e22/frontend/public/logo240.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vivekweb2013/commandhunt/10acc2e13a28df808ad4d0900c7f539fc8db4e22/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "CommandHunt",
3 | "name": "CommandHunt",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Header from "./components/Header";
3 | import Content from "./components/Content";
4 | import Footer from "./components/Footer";
5 | import Spinner from "./components/common/Spinner";
6 | import * as API from "./api/API";
7 | import { connect } from "react-redux";
8 | import { withRouter } from "react-router";
9 | import { userLogin, userLogout } from "./actions";
10 | import "./App.scss";
11 |
12 | import { library } from "@fortawesome/fontawesome-svg-core";
13 | import { faHome, faSignInAlt, faSignOutAlt, faUserCog, faEye, faCog, faClipboard, faEdit, faTrashAlt, faSortUp, faSortDown, faCircleNotch, faBug, faHeart, faCodeBranch, faQuestion } from "@fortawesome/free-solid-svg-icons";
14 |
15 | library.add(faHome, faSignInAlt, faSignOutAlt, faUserCog, faEye, faCog, faClipboard, faEdit, faTrashAlt, faSortUp, faSortDown, faCircleNotch, faBug, faHeart, faCodeBranch, faQuestion);
16 |
17 | class App extends Component {
18 | state = {
19 | loading: true
20 | }
21 |
22 | componentDidMount() {
23 | this._isMounted = true;
24 | this.props.getUserProfile().then(() => this._isMounted && this.setState({ loading: false }));
25 | }
26 |
27 | render() {
28 | return (
29 | this.state.loading ?
LOADING
:
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | componentWillUnmount() {
41 | this._isMounted = false;
42 | }
43 | }
44 |
45 | const mapStateToProps = (state) => {
46 | return {
47 | user: state.authReducer.user
48 | };
49 | };
50 |
51 | const mapDispatchToProps = (dispatch) => {
52 | return {
53 | getUserProfile: () => {
54 | return API.getUserProfile().then((user) => {
55 | dispatch(userLogin(user));
56 | });
57 | },
58 | userLogout: () => {
59 | dispatch(userLogout());
60 | },
61 | };
62 | };
63 |
64 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
65 |
--------------------------------------------------------------------------------
/frontend/src/App.scss:
--------------------------------------------------------------------------------
1 | @import "styles/shared";
2 |
3 | .app.loading {
4 | align-items: center;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | position: relative;
9 | top: 30%;
10 | }
11 |
12 | .main-container {
13 | background: none;
14 | border: 0;
15 | display: flex;
16 | flex: 1 0 auto;
17 | height: inherit;
18 | justify-content: space-between;
19 | margin: 0 auto;
20 | padding: 0;
21 | position: relative;
22 | text-align: left;
23 | width: 100%;
24 |
25 | @media screen and (max-width: 700px) {
26 | flex-direction: column;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter } from "react-router-dom";
4 |
5 | import { createStore, applyMiddleware, compose } from "redux";
6 | import { Provider } from "react-redux";
7 | import thunk from "redux-thunk";
8 | import reducer from "./reducers/index";
9 | import App from "./App";
10 |
11 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
12 | const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
13 |
14 | it("renders without crashing", () => {
15 | const div = document.createElement("div");
16 | ReactDOM.render(, div);
17 | ReactDOM.unmountComponentAtNode(div);
18 | });
19 |
--------------------------------------------------------------------------------
/frontend/src/Utils.js:
--------------------------------------------------------------------------------
1 | import tinydate from "tinydate";
2 |
3 | export const getValidationRegex = (dataType) => {
4 | // Note that HTML5 engines wraps the whole pattern inside ^(?: and )$ constructs
5 | switch (dataType) {
6 | case "PATH":
7 | return "((\\.{0,2}/(?!/))(\\.?[a-zA-Z0-9_*-])*)+|\"((\\.{0,2}/(?!/))(\\.?[ ]*[a-zA-Z0-9_*-])*)+\"";
8 | case "NUMBER":
9 | return "[0-9]*";
10 | case "PERMISSION":
11 | return "[0-9]{3}";
12 | default:
13 | return "[\\s\\S]*"; // match anything
14 | }
15 | };
16 |
17 | export const getQueryParamByName = (name, url) => {
18 | if (!url) { url = window.location.href; }
19 | name = name.replace(/[[\]]/g, "\\$&");
20 | const regex = new RegExp("[?&]" + name + "(=([^]*)|&|#|$)");
21 | const results = regex.exec(url);
22 | if (!results) { return null; }
23 | if (!results[2]) { return ""; }
24 | return decodeURIComponent(results[2].replace(/\+/g, " "));
25 | };
26 |
27 | export const getArrayQueryParamByName = (name, url) => {
28 | if (!url) { url = decodeURIComponent(window.location.search); }
29 | name = name.replace(/[[\]]/g, "\\$&");
30 | const regex = new RegExp(`[?|&](${name})\\[(\\d+)\\]\\.(\\w+)=(\\w+)`, "gm");
31 |
32 | let match, result = [];
33 | while ((match = regex.exec(url)) !== null) {
34 | result[match[2]] = result[match[2]] || {};
35 | result[match[2]][match[3]] = match[4];
36 | }
37 | return result;
38 | };
39 |
40 | export const getQueryParamsFromFilter = (filter) => {
41 | const queryParamStr = `?pagination.pageNumber=${filter.pagination.pageNumber}`
42 | + `&pagination.pageSize=${filter.pagination.pageSize}`
43 | + `&pagination.sort.by=${filter.pagination.sort.by}`
44 | + `&pagination.sort.order=${filter.pagination.sort.order}&`
45 | + filter.conditions.reduce((a, c, i) => a +
46 | `conditions%5B${i}%5D.key=${c.key}&conditions%5B${i}%5D.operator=${c.operator}&conditions%5B${i}%5D.value=${c.value}`, "");
47 |
48 | return queryParamStr;
49 | };
50 |
51 | export const formatDate = (date) => {
52 | const stamp = tinydate("{DD} {MMMM} {YYYY}", {
53 | MMMM: (d) => date.toLocaleString("default", { month: "long" }),
54 | DD: (d) => date.getDate()
55 | });
56 |
57 | return stamp(date);
58 | };
59 |
60 | export const formatTime = (date) => {
61 | const stamp = tinydate("{HH}:{mm} {A}", {
62 | A: (d1) => (d1.getHours() >= 12) ? "PM" : "AM",
63 | HH: (d2) => {
64 | const h = d2.getHours();
65 | return (h > 12) ? h - 12 : h;
66 | }
67 | });
68 |
69 | return stamp(date);
70 | };
71 |
72 | export const arrayEquals = (array1, array2) => {
73 | const array2Sorted = array2.slice().sort();
74 | return array1.length === array2.length && array1.slice().sort().every(function (value, index) {
75 | return value === array2Sorted[index];
76 | });
77 | };
78 |
--------------------------------------------------------------------------------
/frontend/src/actions/AuthActions.js:
--------------------------------------------------------------------------------
1 | export const USER_LOGIN = "USER_LOGIN";
2 | export const USER_LOGOUT = "USER_LOGOUT";
3 | export const IS_MANUAL_AUTH_ALLOWED = "IS_MANUAL_AUTH_ALLOWED";
4 |
5 | export function userLogin(user) {
6 | return {
7 | type: USER_LOGIN,
8 | user
9 | };
10 | }
11 |
12 | export function userLogout() {
13 | return {
14 | type: USER_LOGOUT
15 | };
16 | }
17 |
18 | export function isManualAuthAllowed(manualAuthAllowed) {
19 | return {
20 | type: IS_MANUAL_AUTH_ALLOWED,
21 | manualAuthAllowed
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/actions/CommandActions.js:
--------------------------------------------------------------------------------
1 | export const GET_META_COMMANDS = "GET_META_COMMANDS";
2 | export const GET_META_COMMAND = "GET_META_COMMAND";
3 |
4 | export function getMetaCommands(metaCommands) {
5 | return {
6 | type: GET_META_COMMANDS,
7 | metaCommands
8 | };
9 | }
10 |
11 | export function getMetaCommand(metaCommand) {
12 | return {
13 | type: GET_META_COMMAND,
14 | metaCommand
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/actions/UserCommandActions.js:
--------------------------------------------------------------------------------
1 | export const GET_USER_COMMANDS = "GET_USER_COMMANDS";
2 |
3 | export function getUserCommands(userCommands) {
4 | return {
5 | type: GET_USER_COMMANDS,
6 | userCommands
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from "./CommandActions";
2 | export * from "./UserCommandActions";
3 | export * from "./AuthActions";
4 |
--------------------------------------------------------------------------------
/frontend/src/components/Content.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route, Switch } from "react-router-dom";
3 | import Finder from "./Finder";
4 | import Builder from "./Builder";
5 | import Login from "./Login";
6 | import UserCommands from "./UserCommands";
7 | import "./Content.scss";
8 | import { Redirect, withRouter } from "react-router";
9 | import SignUp from "./SignUp";
10 | import { connect } from "react-redux";
11 | import PageNotFound from "./common/PageNotFound";
12 |
13 | const ProtectedRoute = ({ component: Component, isLoggedIn, path, ...rest }) =>
14 | isLoggedIn ?
15 | : } />
16 |
17 | class Content extends Component {
18 | render() {
19 | const { user, location } = this.props;
20 | const relativeUrl = location.pathname + location.search;
21 | return
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
;
34 | }
35 | }
36 |
37 | const mapStateToProps = (state) => {
38 | return {
39 | user: state.authReducer.user
40 | };
41 | };
42 |
43 | export default withRouter(connect(mapStateToProps)(Content));
44 |
--------------------------------------------------------------------------------
/frontend/src/components/Content.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/shared";
2 |
3 | div.main-content {
4 | background-color: lighten($color-primary, 50%);
5 | border: 0;
6 | clear: both;
7 | flex-grow: 1;
8 | width: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/components/Finder.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/shared";
2 |
3 | $table-bg-color: white;
4 | $table-row-hover-bg-color: lighten($color-primary, 60%);
5 |
6 | .finder {
7 | padding: 24px;
8 |
9 | .search-input {
10 | margin-bottom: 75px;
11 | width: 60%;
12 | }
13 |
14 | .toolbar {
15 | padding: 6px 0;
16 | overflow: hidden;
17 |
18 | .items-per-page {
19 | float: right;
20 | }
21 | }
22 |
23 | table {
24 | background: $table-bg-color;
25 | border-collapse: collapse;
26 | border-radius: 6px;
27 | margin: 0 auto;
28 | overflow: hidden;
29 | position: relative;
30 | table-layout: fixed;
31 | user-select: none;
32 | width: 100%;
33 |
34 | td,
35 | th {
36 | overflow: hidden;
37 | padding: 6px 16px;
38 | text-align: left;
39 | text-overflow: ellipsis;
40 | white-space: pre;
41 | }
42 |
43 | thead tr {
44 | background: lighten($color-primary, 15%);
45 | color: lighten($color-primary, 60%);
46 | cursor: default;
47 | font-size: 1.2rem;
48 | height: 60px;
49 | }
50 |
51 | tbody tr {
52 | border-bottom: 1px solid lighten($color-primary, 60%);
53 | cursor: pointer;
54 | font-size: 1.1rem;
55 | height: 60px;
56 |
57 | td:first-child {
58 | font-weight: 500;
59 | }
60 |
61 | &:last-child {
62 | border: 0;
63 | }
64 |
65 | &:hover {
66 | background-color: $table-row-hover-bg-color;
67 | }
68 | }
69 |
70 | th.name-column {
71 | width: 15%;
72 | }
73 |
74 | th.syntax-column {
75 | width: 30%;
76 | }
77 |
78 | /* Responsive layout - when the screen is less than 800px wide,
79 | hide the syntax-column and make name-column a fixed width */
80 | @media screen and (max-width: 800px) {
81 | th.syntax-column {
82 | width: 0;
83 | }
84 |
85 | th.name-column {
86 | width: 160px;
87 | }
88 | }
89 | }
90 |
91 | .no-data-msg {
92 | text-align: center;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "./Footer.scss";
3 |
4 | class Footer extends Component {
5 | render() {
6 | return (
7 |
10 | );
11 | }
12 | }
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/shared";
2 |
3 | .site-footer {
4 | background-color: $color-primary;
5 | border: 0;
6 | color: lighten($color-primary, 25%);
7 | display: block;
8 | margin: 0;
9 | padding: 0;
10 | text-align: center;
11 | user-select: none;
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/shared";
2 | $avatar-size: 40px;
3 |
4 | .site-header {
5 | background-color: $color-primary;
6 | height: $header-height;
7 | left: 0;
8 | min-width: auto;
9 | position: fixed;
10 | top: 0;
11 | user-select: none;
12 | width: 100%;
13 | z-index: 5050;
14 |
15 | .logo > a {
16 | border-radius: 4px;
17 | color: lighten($color-primary, 50%);
18 | float: left;
19 | font-size: 1.6rem;
20 | font-weight: bold;
21 | height: inherit;
22 | line-height: 25px;
23 | padding: 12px;
24 | text-align: center;
25 | text-decoration: none;
26 |
27 | &:hover {
28 | color: white;
29 | }
30 | }
31 |
32 | .nav-icon {
33 | background-color: lighten($color-primary, 15%);
34 | border: none;
35 | border-left: 1px solid lighten($color-primary, 50%);
36 | color: lighten($color-primary, 50%);
37 | cursor: pointer;
38 | float: right;
39 | font-size: 1rem;
40 | height: inherit;
41 | min-width: 50px;
42 | text-align: center;
43 | outline: none;
44 |
45 | &:not([disabled]):hover {
46 | filter: contrast(180%);
47 | }
48 | }
49 |
50 | button.nav-icon {
51 | padding: 5px;
52 | }
53 |
54 | a.nav-icon {
55 | padding: 9px;
56 | }
57 |
58 | .dropdown {
59 | font-size: 1rem;
60 | float: right;
61 | height: 100%;
62 | position: relative;
63 |
64 | .dropdown-btn {
65 | background-color: lighten($color-primary, 15%);
66 | border: none;
67 | border-left: 1px solid lighten($color-primary, 50%);
68 | color: lighten($color-primary, 50%);
69 | cursor: pointer;
70 | font-size: 1rem;
71 | height: inherit;
72 | outline: none;
73 | padding: 0 6px;
74 | width: 100%;
75 |
76 | .avatar {
77 | height: $avatar-size;
78 | width: $avatar-size;
79 | }
80 | }
81 |
82 | .dropdown-content {
83 | background-color: #f1f1f1;
84 | box-shadow: 0 8px 16px 0 rgba(240, 208, 208, 0.2);
85 | display: none;
86 | min-width: fit-content;
87 | white-space: nowrap;
88 | z-index: 1;
89 |
90 | button {
91 | background-color: transparent;
92 | border: none;
93 | cursor: pointer;
94 | font: inherit;
95 | margin: 0;
96 | padding: 0;
97 | width: inherit;
98 | }
99 |
100 | a,
101 | button {
102 | color: black;
103 | display: block;
104 | padding: 10px;
105 | text-align: left;
106 | text-decoration: none;
107 |
108 | &:hover {
109 | background-color: #ddd;
110 | }
111 | }
112 | }
113 |
114 | &:hover {
115 | .dropdown-btn {
116 | filter: contrast(180%);
117 | }
118 |
119 | .dropdown-content {
120 | display: block;
121 | width: 100%;
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/frontend/src/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 | import "./Sidebar.scss";
4 |
5 | class Sidebar extends Component {
6 | render() {
7 | return (
8 |
9 |
10 | Home
11 | News
12 | Contact
13 | About
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | export default Sidebar;
21 |
--------------------------------------------------------------------------------
/frontend/src/components/Sidebar.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/shared";
2 |
3 | .left-sidebar {
4 | background-color: $color-secondary;
5 | border: 0;
6 | display: flex;
7 | flex-direction: column;
8 | flex-shrink: 0;
9 | margin: 0;
10 | padding: 0;
11 | position: relative;
12 | width: $sidebar-width;
13 | z-index: 1000;
14 |
15 | .sticky-sidebar {
16 | border: 0;
17 | margin: 0;
18 | padding: 0;
19 | position: sticky;
20 | top: $header-height;
21 | width: auto;
22 |
23 | a {
24 | color: lighten($color-secondary, 45%);
25 | display: block;
26 | padding: 16px;
27 | text-decoration: none;
28 |
29 | &.active {
30 | background-color: lighten($color-secondary, 10%);
31 | color: lighten($color-secondary, 60%);
32 | }
33 |
34 | &:hover {
35 | background-color: darken($color-secondary, 10%);
36 | color: lighten($color-secondary, 45%);
37 | }
38 |
39 | @media screen and (max-width: 400px) {
40 | float: none;
41 | text-align: center;
42 | }
43 | }
44 | }
45 |
46 | @media screen and (max-width: 700px) {
47 | align-self: flex-start;
48 | height: auto;
49 | width: 100%;
50 |
51 | a {
52 | float: left;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/frontend/src/components/SignUp.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { withRouter } from "react-router";
4 | import Spinner from "./common/Spinner";
5 | import * as API from "../api/API";
6 | import "./SignUp.scss";
7 |
8 | class SignUp extends Component {
9 | state = {
10 | signupInProgress: false,
11 | signUpRequest: {}
12 | }
13 |
14 | handleInputChange(event) {
15 | const { name, value } = event.target;
16 | const { signUpRequest } = this.state;
17 |
18 | this.setState({ signUpRequest: { ...signUpRequest, [name]: value } });
19 | }
20 |
21 | handleSignUp(e) {
22 | e.preventDefault();
23 | const { signUpRequest } = this.state;
24 | this.props.userSignUp(signUpRequest).then(() => {
25 | this.props.history.push("/login");
26 | });
27 | }
28 | render() {
29 | const { user } = this.props;
30 | if (user) { this.props.history.goBack(); }
31 |
32 | const { signupInProgress } = this.state;
33 | return (
34 | signupInProgress ?
LOADING
:
35 |
55 | );
56 | }
57 | }
58 |
59 | const mapStateToProps = (state) => {
60 | return {
61 | user: state.authReducer.user
62 | };
63 | };
64 |
65 | const mapDispatchToProps = () => {
66 | return {
67 | userSignUp: (signUpRequest) => {
68 | return API.userSignUp(signUpRequest);
69 | }
70 | };
71 | };
72 |
73 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SignUp));
74 |
--------------------------------------------------------------------------------
/frontend/src/components/SignUp.scss:
--------------------------------------------------------------------------------
1 | .signup {
2 | .container {
3 | padding: 16px;
4 | background-color: white;
5 | }
6 |
7 | /* Full-width input fields */
8 | input[type="text"],
9 | input[type="password"] {
10 | width: 100%;
11 | padding: 15px;
12 | margin: 5px 0 22px 0;
13 | display: inline-block;
14 | border: none;
15 | background: #f1f1f1;
16 | }
17 |
18 | input[type="text"]:focus,
19 | input[type="password"]:focus {
20 | background-color: #ddd;
21 | outline: none;
22 | }
23 |
24 | /* Overwrite default styles of hr */
25 | hr {
26 | border: 1px solid #f1f1f1;
27 | margin-bottom: 25px;
28 | }
29 |
30 | /* Set a style for the submit button */
31 | .registerbtn {
32 | background-color: #4caf50;
33 | color: white;
34 | padding: 16px 20px;
35 | margin: 8px 0;
36 | border: none;
37 | cursor: pointer;
38 | width: 100%;
39 | opacity: 0.9;
40 | }
41 |
42 | .registerbtn:hover {
43 | opacity: 1;
44 | }
45 |
46 | /* Add a blue text color to links */
47 | a {
48 | color: dodgerblue;
49 | }
50 |
51 | /* Set a grey background color and center the text of the "sign in" section */
52 | .signin {
53 | background-color: #f1f1f1;
54 | text-align: center;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/src/components/UserCommands.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/shared";
2 |
3 | $table-bg-color: white;
4 | $table-row-hover-bg-color: lighten($color-primary, 60%);
5 |
6 | $search-input-height: 25px;
7 | $search-field-font-size: ($search-input-height/30px * 1rem);
8 |
9 | .user-commands {
10 | padding: 1%;
11 |
12 | .no-data-msg {
13 | text-align: center;
14 | }
15 |
16 | .heading {
17 | font-size: 1.5rem;
18 | }
19 |
20 | .toolbar {
21 | padding: 6px 0;
22 | overflow: hidden;
23 |
24 | .search-input {
25 | display: inline-block;
26 | width: 18rem;
27 | height: $search-input-height;
28 |
29 | .field {
30 | padding: 5px $search-input-height 5px 10px;
31 | font-size: $search-field-font-size;
32 | }
33 |
34 | .icons-container {
35 | width: $search-input-height;
36 |
37 | .icon-search {
38 | border-width: 2px;
39 |
40 | &::after {
41 | bottom: -50%;
42 | width: 2px;
43 | }
44 | }
45 |
46 | .icon-close,
47 | .icon-search {
48 | left: 4px;
49 | }
50 | }
51 | }
52 |
53 | .items-per-page {
54 | float: right;
55 | }
56 | }
57 |
58 | table {
59 | background: $table-bg-color;
60 | border-collapse: collapse;
61 | border-radius: 6px;
62 | margin: 0 auto;
63 | overflow: hidden;
64 | position: relative;
65 | table-layout: fixed;
66 | user-select: none;
67 | width: 100%;
68 |
69 | td.actions > span {
70 | cursor: pointer;
71 | padding: 6px;
72 |
73 | &.view-icon,
74 | &.copy-icon,
75 | &.edit-icon {
76 | svg:hover {
77 | color: blue;
78 | }
79 | }
80 |
81 | &.delete-icon svg:hover {
82 | color: red;
83 | }
84 | }
85 |
86 | td,
87 | th {
88 | overflow: hidden;
89 | padding: 6px 16px;
90 | text-align: left;
91 | text-overflow: ellipsis;
92 | white-space: pre;
93 | }
94 |
95 | thead tr {
96 | background: lighten($color-primary, 15%);
97 | color: lighten($color-primary, 60%);
98 | cursor: default;
99 | font-size: 1.2rem;
100 | height: 60px;
101 | }
102 |
103 | tbody tr {
104 | border-bottom: 1px solid lighten($color-primary, 60%);
105 | font-size: 1.1rem;
106 | height: 60px;
107 |
108 | td:first-child {
109 | font-weight: 500;
110 | }
111 |
112 | &:last-child {
113 | border: 0;
114 | }
115 |
116 | &:hover {
117 | background-color: $table-row-hover-bg-color;
118 | }
119 | }
120 |
121 | th.command-column {
122 | width: 50%;
123 | }
124 |
125 | th.actions-column {
126 | width: 175px;
127 | }
128 |
129 | th.date-column {
130 | width: 135px;
131 | }
132 |
133 | /* Responsive layout - when the screen is less than 600px wide,
134 | make the two columns stack on top of each other instead of next to each other */
135 | @media screen and (max-width: 600px) {
136 | th.date-column,
137 | th.type-column {
138 | width: 0;
139 | }
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/frontend/src/components/common/DynamicTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | class DynamicTextInput extends Component {
4 |
5 | handleChange(i, event) {
6 | let values = this.props.values;
7 | values[i] = event.target.value;
8 | this.props.handleChange(event.target.name, values);
9 | }
10 |
11 | addClick() {
12 | const values = [...this.props.values, ""];
13 | this.props.handleChange(this.props.name, values);
14 | }
15 |
16 | removeClick(i) {
17 | let values = this.props.values;
18 | values.splice(i, 1);
19 | this.props.handleChange(this.props.name, values);
20 | }
21 |
22 | render() {
23 | const { id, name, pattern, required, disabled, values, isRepeatable } = this.props;
24 |
25 | return ({values.map((val, i) =>
26 |
27 |
29 |
30 | {isRepeatable && !disabled && }
32 | )}
33 |
);
34 | }
35 |
36 | }
37 |
38 | export default DynamicTextInput;
--------------------------------------------------------------------------------
/frontend/src/components/common/ItemsPerPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "./ItemsPerPage.scss";
3 |
4 | class ItemsPerPage extends Component {
5 | render() {
6 | const pageSizeOptions = [5, 10, 25];
7 |
8 | if (pageSizeOptions.indexOf(this.props.pageSize) === -1) {
9 | //In case custom pageSize mentioned in url query param
10 | pageSizeOptions.push(this.props.pageSize);
11 | }
12 |
13 | return (
14 |
15 |
16 |
19 |
20 | );
21 | }
22 | }
23 |
24 | export default ItemsPerPage;
--------------------------------------------------------------------------------
/frontend/src/components/common/ItemsPerPage.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/shared";
2 |
3 | .items-per-page {
4 | position: relative;
5 | display: flex;
6 | width: 5rem;
7 | line-height: 1.48rem;
8 | overflow: hidden;
9 |
10 | span.icon-pagesize {
11 | align-self: center;
12 | background: url(../../styles/icons/pagesize.svg) no-repeat center left;
13 | height: 25px;
14 | width: 25px;
15 | float: right;
16 | }
17 |
18 | &::after {
19 | align-self: center;
20 | border-radius: 0 0.25rem 0.25rem 0;
21 | content: "\25BC";
22 | position: absolute;
23 | right: 0;
24 | padding: 0 0.5rem;
25 | background: lighten($color-primary, 10%);
26 | cursor: pointer;
27 | pointer-events: none;
28 | transition: 0.25s all ease;
29 | }
30 |
31 | select {
32 | appearance: none;
33 | border-radius: 0.25rem;
34 | outline: 0;
35 | box-shadow: none;
36 | border: 0 !important;
37 | background: lighten($color-primary, 15%);
38 | background-image: none;
39 | flex: 1;
40 | padding: 0 0.5rem;
41 | color: #fff;
42 | cursor: pointer;
43 | line-height: inherit;
44 |
45 | /* Remove IE arrow */
46 | &::-ms-expand {
47 | display: none;
48 | }
49 |
50 | option {
51 | background: #f1f1f1;
52 | color: black;
53 | }
54 | }
55 |
56 | /* Transition */
57 | &:hover::after {
58 | color: white;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { createPortal } from "react-dom";
3 | import "./Modal.scss";
4 |
5 |
6 | class Modal extends Component {
7 | constructor(props) {
8 | super(props);
9 | // Create a div that we"ll render the modal into. Because each
10 | // Modal component has its own element, we can render multiple
11 | // modal components into the modal container.
12 | this.el = document.createElement("div");
13 | this.el.setAttribute("class", "modal-wrapper");
14 | }
15 |
16 | componentDidMount() {
17 | // Append the element into the DOM on mount. We"ll render
18 | // into the modal container element (see the HTML tab).
19 | document.getElementById("modal-root").appendChild(this.el);
20 | }
21 |
22 | componentWillUnmount() {
23 | // Remove the element from the DOM when we unmount
24 | document.getElementById("modal-root").removeChild(this.el);
25 | }
26 |
27 | /**
28 | * These "type" values are supported - info, warn, error, confirm
29 | * If "type" prop is not specified, info will be default value
30 | */
31 | render() {
32 | const { title, children, style, onClose, onConfirm } = this.props;
33 | const type = this.props.type || "info";
34 | // Use a portal to render the children into the element
35 | return createPortal(
36 | // Any valid React child: JSX, strings, arrays, etc.
37 |
38 |
39 |
onClose()}>✕
40 |
41 | {title}
42 |
43 |
44 | {children}
45 |
46 |
47 | {type === "confirm" && }
48 |
49 |
50 |
51 |
,
52 | // A DOM element
53 | this.el
54 | );
55 | }
56 | }
57 |
58 | export default Modal;
59 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Modal.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/shared";
2 |
3 | /* The Modal (background) */
4 | .modal {
5 | position: fixed;
6 | padding-top: 100px;
7 | top: 0;
8 | left: 0;
9 | width: 100%;
10 | height: 100%;
11 | z-index: 9000;
12 | overflow: auto;
13 | background: rgba(0, 0, 0, 0.5);
14 | }
15 |
16 | /* Modal Content */
17 | .modal-content {
18 | width: 350px;
19 | margin: auto;
20 | background: #fff;
21 | border-radius: 5px;
22 | border: 1px solid #b8c8cc;
23 | overflow: hidden;
24 | z-index: 1001;
25 |
26 | & > .top-close-btn {
27 | overflow: hidden;
28 | position: relative;
29 | display: block;
30 | float: right;
31 | right: 20px;
32 | top: 15px;
33 | cursor: pointer;
34 | color: #fff;
35 |
36 | &:hover {
37 | font-weight: bold;
38 | }
39 | }
40 |
41 | & > .modal-header {
42 | padding: 15px 20px;
43 | background: lighten($color-primary, 10%);
44 | color: #fff;
45 |
46 | &.error {
47 | background: lighten(red, 20%);
48 | }
49 | }
50 |
51 | & > .modal-body {
52 | padding: 20px;
53 | line-height: 1.4;
54 | font-size: 14px;
55 | color: #454b4d;
56 | background: #fff;
57 | position: relative;
58 | z-index: 2;
59 | }
60 |
61 | & > .modal-footer {
62 | overflow: hidden;
63 | padding: 20px;
64 |
65 | & > .footer-btn {
66 | box-sizing: border-box;
67 | padding: 0 10px;
68 | margin: 0;
69 | line-height: 32px;
70 | height: 32px;
71 | border: 1px solid #666;
72 | text-align: center;
73 | font-size: 12px;
74 | font-weight: 400;
75 | color: #333;
76 | background: transparent;
77 | outline: none;
78 | text-decoration: none;
79 | cursor: pointer;
80 | float: right;
81 | border-radius: 3px;
82 |
83 | &:hover {
84 | filter: brightness(-80%);
85 | }
86 |
87 | &.confirm {
88 | margin-left: 10px;
89 | background-color: lighten($color-primary, 10%);
90 | color: white;
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/frontend/src/components/common/PageNotFound.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import "./PageNotFound.scss";
4 |
5 | function PageNotFound() {
6 | return (
7 |
8 |
9 |
404 Page Not Found
10 |
11 |
12 |
13 |
14 |
20 |
21 |
27 |
28 |
The page you are looking for does not exist. How you got here is a mystery.
29 |
But you can click the button below to go back to the homepage.
30 |
Go to Home
31 |
32 |
33 | );
34 | }
35 |
36 | export default PageNotFound;
37 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Pagination.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "./Pagination.scss";
3 |
4 | class Pagination extends Component {
5 |
6 | render() {
7 | const { totalSize, totalPages, pageNumber, maxPagesToShow } = this.props;
8 |
9 | const visiblePages = totalPages > maxPagesToShow ? maxPagesToShow : totalPages;
10 | const visiblePageOffset = Math.trunc(visiblePages / 2);
11 | const startOfVisiblePages = (pageNumber + visiblePageOffset) <= totalPages ?
12 | ((pageNumber - visiblePageOffset) > 0 ? (pageNumber - visiblePageOffset) : 1) :
13 | totalPages - visiblePages + 1;
14 |
15 | return totalSize > 0 ? (
16 |
17 |
18 |
19 | {Array.from(Array(visiblePages), (e, i) =>
20 |
24 | )}
25 |
26 |
27 |
28 | ) : "";
29 | }
30 | }
31 |
32 | export default Pagination;
33 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Pagination.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/shared";
2 | $border-radius: 5px;
3 |
4 | .pagination {
5 | display: block;
6 | float: right;
7 | margin: 20px 0;
8 |
9 | button {
10 | background-color: lighten($color-primary, 40%);
11 | border: 1px solid lighten($color-primary, 20%);
12 | border-width: 1px 0 1px 1px;
13 | color: black;
14 | float: left;
15 | margin: 0;
16 | outline: none;
17 | padding: 8px 16px;
18 | text-decoration: none;
19 |
20 | &[disabled] {
21 | background-color: lighten($color-primary, 15%);
22 | color: white;
23 | }
24 |
25 | &:first-child {
26 | border-radius: $border-radius 0 0 $border-radius;
27 | }
28 |
29 | &:last-child {
30 | border-radius: 0 $border-radius $border-radius 0;
31 | border-width: 1px;
32 | }
33 |
34 | &:hover:not([disabled]) {
35 | background-color: lighten($color-primary, 30%);
36 | cursor: pointer;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/src/components/common/PermissionInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | class PermissionInput extends Component {
4 |
5 | handleChange(e, assigneeIndex, permissionIndex) {
6 | const binaryPermissions = [4, 2, 1];
7 | let value = this.props.value[0];
8 | const valArray = value.split("").map((v) => +v);
9 | valArray[assigneeIndex] = valArray[assigneeIndex] + (e.target.checked ? 1 : -1) * binaryPermissions[permissionIndex];
10 | this.props.handleChange(e.target.name, [valArray.join("")]);
11 | }
12 |
13 | handleTextChange(e) {
14 | e.stopPropagation();
15 | const value = e.target.value;
16 | if (e.target.value === "" || e.target.value.match(/^([0-9]{3})$/g)) {
17 | this.props.handleChange(e.target.name, [value]);
18 | }
19 | }
20 |
21 | toggle(e) {
22 | e.stopPropagation();
23 | const { name, value } = this.props;
24 | const valArray = value[0].split("").map((v) => +v);
25 | valArray.length === 0 ? this.props.handleChange(name, ["000"]) : this.props.handleChange(name, [""]);
26 | }
27 |
28 | render() {
29 | const { id, name, pattern, required, disabled, value } = this.props;
30 | const binaryPermissions = [4, 2, 1];
31 | const valArray = value[0].split("").map((v) => +v); // get array of permission numbers
32 |
33 | return ();
56 | }
57 |
58 | }
59 |
60 | export default PermissionInput;
--------------------------------------------------------------------------------
/frontend/src/components/common/SearchInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "./SearchInput.scss";
3 |
4 | class SearchInput extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.delayedOnChange = this.debounce(this.delayedOnChange.bind(this), 1000);
8 | }
9 |
10 | debounce = (fn, time) => {
11 | let timeout;
12 | return (...args) => {
13 | const functionCall = () => fn.apply(this, args);
14 | clearTimeout(timeout);
15 | timeout = setTimeout(functionCall, time);
16 | };
17 | }
18 |
19 | handleOnChange = (e) => {
20 | e.preventDefault();
21 | this.delayedOnChange(e.target.value);
22 | }
23 |
24 | delayedOnChange(value) {
25 | this.props.onChange(value);
26 | }
27 |
28 | handleInputReset = (e) => {
29 | e.preventDefault();
30 | this.props.onChange("");
31 | }
32 |
33 | render() {
34 | const { defaultValue } = this.props;
35 | return (
36 |
44 | );
45 | }
46 | }
47 |
48 | export default SearchInput;
--------------------------------------------------------------------------------
/frontend/src/components/common/SearchInput.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/shared";
2 |
3 | $cornflower-blue: #6078ea;
4 | $cubic-bezier: cubic-bezier(0.694, 0.048, 0.335, 1);
5 |
6 | $search-input-height: 50px;
7 | $search-field-font-size: ($search-input-height/30px * 1rem);
8 |
9 | .search-input {
10 | border: 0;
11 | height: $search-input-height;
12 | margin: 0 auto;
13 | padding: 0;
14 | position: relative;
15 |
16 | .field {
17 | background: white;
18 | border: 0;
19 | border-radius: 3px;
20 | box-shadow: 0 8px 15px rgba(#4b4848, 0.1);
21 | font-size: $search-field-font-size;
22 | height: 100%;
23 | padding: 10px $search-input-height 10px 20px;
24 | transition: all 0.4s ease;
25 | width: 100%;
26 |
27 | .icon-close,
28 | .icon-search {
29 | left: 8px;
30 | }
31 |
32 | .icon-search {
33 | border: 3px solid mix($cornflower-blue, white, 35%);
34 | border-radius: 50%;
35 | bottom: -22%;
36 | height: 50%;
37 | opacity: 1;
38 | position: relative;
39 | transition: opacity 0.25s ease, transform 0.43s $cubic-bezier;
40 | width: 50%;
41 |
42 | &::after {
43 | background-color: mix($cornflower-blue, white, 35%);
44 | border-radius: 3px;
45 | bottom: -30%;
46 | content: "";
47 | height: 50%;
48 | position: absolute;
49 | right: -2px;
50 | transform: rotate(-45deg);
51 | width: 4px;
52 | }
53 | }
54 |
55 | .icons-container {
56 | height: inherit;
57 | overflow: hidden;
58 | position: absolute;
59 | right: 0;
60 | top: 0;
61 | width: $search-input-height;
62 |
63 | .icon-close {
64 | cursor: pointer;
65 | opacity: 0;
66 | position: absolute;
67 | padding-top: 50%;
68 | padding-bottom: 50%;
69 | top: 0;
70 | transform: translateX(-200%);
71 | transition: opacity 0.25s ease, transform 0.43s $cubic-bezier;
72 | width: 60%;
73 |
74 | &::before,
75 | &::after {
76 | background-color: $cornflower-blue;
77 | content: "";
78 | height: 2px;
79 | left: 0;
80 | position: absolute;
81 | transform: rotate(45deg);
82 | width: 100%;
83 | }
84 |
85 | &::after {
86 | transform: rotate(-45deg);
87 | }
88 | }
89 | }
90 |
91 | + .icons-container-flip {
92 | .icon-close {
93 | opacity: 1;
94 | transform: translateX(0);
95 | }
96 |
97 | .icon-search {
98 | opacity: 0;
99 | transform: translateX(200%);
100 | }
101 | }
102 |
103 | &:focus {
104 | box-shadow: 0 9px 20px rgba(#4b4848, 0.3);
105 | outline: none;
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Spinner.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "./Spinner.scss";
3 |
4 | class Spinner extends Component {
5 | render() {
6 | const size = this.props.size || 20;
7 | const style = { height: size + "px", width: size + "px", borderWidth: size / 10 + "px" };
8 | return (
9 |
10 | );
11 | }
12 | }
13 |
14 | export default Spinner;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Spinner.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/shared";
2 |
3 | .spinner {
4 | animation: spin 1s linear infinite;
5 | border-color: #f3f3f3;
6 | border-radius: 50%;
7 | border-style: solid;
8 | border-top-color: $color-primary;
9 | }
10 |
11 | @keyframes spin {
12 | 0% {
13 | transform: rotate(0deg);
14 | }
15 |
16 | 100% {
17 | transform: rotate(360deg);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import "react-app-polyfill/ie11";
2 | import "react-app-polyfill/stable";
3 | import React from "react";
4 | import ReactDOM from "react-dom";
5 | import { BrowserRouter } from "react-router-dom";
6 |
7 | import { createStore, applyMiddleware, compose } from "redux";
8 | import { Provider } from "react-redux";
9 | import thunk from "redux-thunk";
10 | import reducer from "./reducers/index";
11 |
12 | import "./index.scss";
13 | import App from "./App";
14 |
15 | import * as serviceWorker from "./serviceWorker";
16 |
17 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
18 | const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
19 |
20 | ReactDOM.render(
21 |
22 |
23 |
24 |
25 | ,
26 | document.getElementById("root")
27 | );
28 |
29 | // If you want your app to work offline and load faster, you can change
30 | // unregister() to register() below. Note this comes with some pitfalls.
31 | // Learn more about service workers: https://bit.ly/CRA-PWA
32 | serviceWorker.unregister();
33 |
--------------------------------------------------------------------------------
/frontend/src/index.scss:
--------------------------------------------------------------------------------
1 | @import "styles/shared";
2 | @import "styles/global";
3 |
4 | html {
5 | height: 100%;
6 | font-size: medium;
7 | }
8 |
9 | body {
10 | -moz-osx-font-smoothing: grayscale;
11 | -webkit-font-smoothing: antialiased;
12 | border: 0;
13 | box-sizing: border-box; // because everyone is moving to `box-sizing: border-box` https://stackoverflow.com/questions/18854259/why-did-bootstrap-3-switch-to-box-sizing-border-box
14 | display: flex;
15 | font-family: $font-family-primary;
16 | margin: 0;
17 | min-height: 100%;
18 | min-width: auto;
19 | padding: $header-height 0 0 0;
20 |
21 | & > #root {
22 | display: flex;
23 | flex-direction: column;
24 | flex: 1 0 auto;
25 | height: inherit;
26 | min-height: 100%;
27 | width: 100%;
28 | }
29 | }
30 |
31 | body *,
32 | body *::before,
33 | body *::after {
34 | box-sizing: inherit;
35 | }
36 |
37 | code {
38 | font-family: $font-family-code;
39 | }
40 |
41 | @media print {
42 | body {
43 | padding: 0;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import {
3 | GET_META_COMMANDS, GET_META_COMMAND,
4 | GET_USER_COMMANDS,
5 | USER_LOGIN, USER_LOGOUT, IS_MANUAL_AUTH_ALLOWED
6 | } from "../actions";
7 |
8 | const metaCommandReducer = (state = {}, action = {}) => {
9 | const { metaCommands, metaCommand } = action;
10 | switch (action.type) {
11 | case GET_META_COMMANDS:
12 | return {
13 | ...state,
14 | metaCommands
15 | };
16 | case GET_META_COMMAND:
17 | return {
18 | ...state,
19 | metaCommand
20 | };
21 | default:
22 | return state;
23 | }
24 | };
25 |
26 | const userCommandReducer = (state = {}, action = {}) => {
27 | const { userCommands } = action;
28 | switch (action.type) {
29 | case GET_USER_COMMANDS:
30 | return {
31 | ...state,
32 | userCommands
33 | };
34 | default:
35 | return state;
36 | }
37 | };
38 |
39 | const authReducer = (state = {}, action = {}) => {
40 | const { user, manualAuthAllowed } = action;
41 | switch (action.type) {
42 | case USER_LOGIN:
43 | return {
44 | ...state,
45 | user
46 | };
47 | case USER_LOGOUT:
48 | return {
49 | ...state,
50 | user // user is set to null
51 | };
52 | case IS_MANUAL_AUTH_ALLOWED:
53 | return {
54 | ...state,
55 | manualAuthAllowed
56 | };
57 | default:
58 | return state;
59 | }
60 | };
61 |
62 | export default combineReducers({ authReducer, metaCommandReducer, userCommandReducer });
63 |
--------------------------------------------------------------------------------
/frontend/src/styles/_color.scss:
--------------------------------------------------------------------------------
1 | // https://colorhunt.co/palette/2763
2 |
3 | $color-primary: #474052;
4 | $color-secondary: lightslategray;
5 |
6 | $color-disabled: #aaa;
7 |
--------------------------------------------------------------------------------
/frontend/src/styles/_font.scss:
--------------------------------------------------------------------------------
1 | $font-family-code: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
2 | $font-family-primary: -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
3 | "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
4 |
--------------------------------------------------------------------------------
/frontend/src/styles/_global.scss:
--------------------------------------------------------------------------------
1 | /* only import this file in index.scss to avoid duplication */
2 |
3 | @import "shared";
4 | @import "modals";
5 |
6 | [data-tooltip] {
7 | position: relative;
8 |
9 | &::before,
10 | &::after {
11 | display: block;
12 | opacity: 0;
13 | pointer-events: none;
14 | position: absolute;
15 | transform: translate3d(0, -10px, 0);
16 | transition: all 0.15s ease-in-out;
17 | }
18 |
19 | &::after {
20 | content: "";
21 | height: 0;
22 | width: 0;
23 | }
24 |
25 | &::before {
26 | background-color: black;
27 | border-radius: 2px;
28 | color: #fff;
29 | content: attr(data-tooltip);
30 | padding: 6px 10px;
31 | text-transform: none;
32 | white-space: nowrap;
33 | }
34 |
35 | &:hover::after,
36 | &:hover::before {
37 | opacity: 1;
38 | transform: translate3d(0, 0, 0);
39 | z-index: 9999;
40 | }
41 |
42 | &.tooltip-t {
43 | &::after {
44 | border-top: 6px solid black;
45 | bottom: 100%;
46 | }
47 |
48 | &::before {
49 | bottom: 100%;
50 | left: 0;
51 | margin-bottom: 6px;
52 | }
53 | }
54 |
55 | &.tooltip-b {
56 | &::after {
57 | border-bottom: 6px solid black;
58 | top: 100%;
59 | }
60 |
61 | &::before {
62 | left: 0;
63 | margin-top: 6px;
64 | top: 100%;
65 | }
66 | }
67 |
68 | &.tooltip-r {
69 | &::after {
70 | border-right: 6px solid black;
71 | left: 100%;
72 | top: 2px;
73 | }
74 |
75 | &::before {
76 | left: 100%;
77 | margin-left: 6px;
78 | top: -5px;
79 | }
80 | }
81 |
82 | &.tooltip-l {
83 | &::after {
84 | border-left: 6px solid black;
85 | right: 100%;
86 | top: 2px;
87 | }
88 |
89 | &::before {
90 | margin-right: 6px;
91 | right: 100%;
92 | top: -5px;
93 | }
94 | }
95 |
96 | &.tooltip-t::after,
97 | &.tooltip-b::after {
98 | border-left: 6px solid transparent;
99 | border-right: 6px solid transparent;
100 | left: 20px;
101 | }
102 |
103 | &.tooltip-r::after,
104 | &.tooltip-l::after {
105 | border-bottom: 6px solid transparent;
106 | border-top: 6px solid transparent;
107 | }
108 | }
109 |
110 | button[data-tooltip] {
111 | &.tooltip-l,
112 | &.tooltip-r {
113 | &::after {
114 | margin-top: -6px;
115 | top: 50%;
116 | }
117 |
118 | &::before {
119 | top: auto;
120 | }
121 | }
122 | }
123 |
124 | .txt-btn-input-wrapper {
125 | display: inline-flex;
126 | width: 100%;
127 |
128 | input[type="text"] {
129 | width: 100%;
130 | padding: 6px 10px;
131 | border: 1px solid #ccc;
132 | border-radius: 3px;
133 | resize: vertical;
134 | background-color: lighten($color-primary, 66%);
135 | outline: none;
136 | margin: 0; // required for safari
137 | }
138 |
139 | input[type="button"],
140 | button {
141 | border: 1px solid #767676;
142 | border-radius: 2px;
143 | background-color: #efefef;
144 | outline: none;
145 | cursor: pointer;
146 | margin: 0; // required for safari
147 |
148 | &:hover {
149 | filter: brightness(80%);
150 | }
151 | }
152 | }
153 |
154 | .avatar {
155 | background-size: cover;
156 | border-radius: 50%;
157 | display: inline-block;
158 | vertical-align: middle;
159 | }
160 |
161 | @media print {
162 | .no-print,
163 | .no-print * {
164 | display: none !important;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/frontend/src/styles/_layout.scss:
--------------------------------------------------------------------------------
1 | $header-height: 50px;
2 | $sidebar-width: 200px;
3 |
--------------------------------------------------------------------------------
/frontend/src/styles/_modals.scss:
--------------------------------------------------------------------------------
1 | // Since modal is added to the top level of the dom,
2 | // Define all the modal stylings here
3 |
4 | #modal-root .modal .modal-body {
5 | .profile-modal {
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 |
10 | .avatar {
11 | height: 100px;
12 | width: 100px;
13 | }
14 |
15 | .name,
16 | .email {
17 | font-size: 1.2rem;
18 | font-weight: 600;
19 | margin-top: 5px;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/styles/_shared.scss:
--------------------------------------------------------------------------------
1 | @import "color";
2 | @import "font";
3 | @import "layout";
4 |
--------------------------------------------------------------------------------
/frontend/src/styles/icons/pagesize.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/styles/icons/social/fbk.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/styles/icons/social/ggl.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/styles/icons/social/gh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/styles/icons/social/social.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/styles/icons/social/ttr.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/styles/icons/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/helm/.gitignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .bzr/
8 | .bzrignore
9 | .hg/
10 | .hgignore
11 | .svn/
12 | # Common backup files
13 | *.swp
14 | *.bak
15 | *.tmp
16 | *.orig
17 | *~
18 | # Various IDEs
19 | .project
20 | .idea/
21 | *.tmproj
22 | .vscode/
23 | /target
24 |
--------------------------------------------------------------------------------
/helm/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
25 | /target
26 | pom.xml
27 |
--------------------------------------------------------------------------------
/helm/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: commandhunt
3 | description: A Helm chart for installing CommandHunt application.
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 1.0.1
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | appVersion: 1.0.1-SNAPSHOT
24 |
--------------------------------------------------------------------------------
/helm/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.wirehall
8 | commandhunt
9 | ${revision}
10 |
11 |
12 | helm
13 |
14 | com.wirehall.commandhunt
15 | commandhunt-helm
16 |
17 | commandhunt-helm
18 | CommandHunt Helm
19 |
20 |
21 |
22 |
23 | com.kiwigrid
24 | helm-maven-plugin
25 | 5.7
26 |
27 | true
28 |
29 | ${project.basedir}
30 | ${project.version}
31 | true
32 |
33 |
34 |
35 | com.google.code.maven-replacer-plugin
36 | replacer
37 |
38 | ${project.basedir}/Chart.yaml
39 |
40 |
41 |
42 | (^version:)\s(.*)
43 |
44 |
45 |
46 |
47 |
48 |
49 | (^appVersion:)\s(.*)
50 |
51 |
52 |
53 |
54 | MULTILINE
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/helm/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | 1. Get the application URL by running these commands:
2 | {{- if .Values.ingress.enabled }}
3 | {{- range $host := .Values.ingress.hosts }}
4 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}/
5 | {{- end }}
6 | {{- else if contains "NodePort" .Values.service.type }}
7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "commandhunt.fullname" . }})
8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
9 | echo http://$NODE_IP:$NODE_PORT
10 | {{- else if contains "LoadBalancer" .Values.service.type }}
11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available.
12 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "commandhunt.fullname" . }}'
13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "commandhunt.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
14 | echo http://$SERVICE_IP:{{ .Values.service.port }}
15 | {{- else if contains "ClusterIP" .Values.service.type }}
16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "commandhunt.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
17 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
18 | echo "Visit http://127.0.0.1:8080 to use your application"
19 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
20 | {{- end }}
21 |
--------------------------------------------------------------------------------
/helm/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "commandhunt.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "commandhunt.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "commandhunt.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "commandhunt.labels" -}}
37 | helm.sh/chart: {{ include "commandhunt.chart" . }}
38 | {{ include "commandhunt.selectorLabels" . }}
39 | {{- if .Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ .Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "commandhunt.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "commandhunt.name" . }}
50 | app.kubernetes.io/instance: {{ .Release.Name }}
51 | {{- end }}
52 |
53 | {{/*
54 | Create the name of the service account to use
55 | */}}
56 | {{- define "commandhunt.serviceAccountName" -}}
57 | {{- if .Values.serviceAccount.create }}
58 | {{- default (include "commandhunt.fullname" .) .Values.serviceAccount.name }}
59 | {{- else }}
60 | {{- default "default" .Values.serviceAccount.name }}
61 | {{- end }}
62 | {{- end }}
63 |
--------------------------------------------------------------------------------
/helm/templates/backend-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "commandhunt.fullname" . }}-backend
5 | labels:
6 | {{- include "commandhunt.labels" . | nindent 4 }}
7 | spec:
8 | replicas: {{ .Values.replicaCount }}
9 | selector:
10 | matchLabels:
11 | {{- include "commandhunt.selectorLabels" . | nindent 6 }}-backend
12 | template:
13 | metadata:
14 | labels:
15 | {{- include "commandhunt.selectorLabels" . | nindent 8 }}-backend
16 | spec:
17 | {{- with .Values.imagePullSecrets }}
18 | imagePullSecrets:
19 | {{- toYaml . | nindent 8 }}
20 | {{- end }}
21 | containers:
22 | - name: {{ .Chart.Name }}-backend
23 | image: "{{ .Values.image.repository }}/{{ .Chart.Name }}-backend:{{ .Values.image.tag | default .Chart.AppVersion }}"
24 | imagePullPolicy: {{ .Values.image.pullPolicy }}
25 | ports:
26 | - name: http
27 | containerPort: 8080
28 | protocol: TCP
29 | envFrom:
30 | - secretRef:
31 | name: postgres-secret
32 | - secretRef:
33 | name: oauth-api-secret
34 | livenessProbe:
35 | httpGet:
36 | path: /actuator/health/liveness
37 | port: 8080
38 | initialDelaySeconds: 60
39 | periodSeconds: 30
--------------------------------------------------------------------------------
/helm/templates/backend-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "commandhunt.fullname" . }}-backend
5 | labels:
6 | {{- include "commandhunt.labels" . | nindent 4 }}
7 | spec:
8 | type: {{ .Values.service.backend.type }}
9 | ports:
10 | - port: {{ .Values.service.backend.port }}
11 | targetPort: 8080
12 | protocol: TCP
13 | name: http
14 | selector:
15 | {{- include "commandhunt.selectorLabels" . | nindent 4 }}-backend
16 |
--------------------------------------------------------------------------------
/helm/templates/frontend-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "commandhunt.fullname" . }}-frontend
5 | labels:
6 | {{- include "commandhunt.labels" . | nindent 4 }}
7 | spec:
8 | replicas: {{ .Values.replicaCount }}
9 | selector:
10 | matchLabels:
11 | {{- include "commandhunt.selectorLabels" . | nindent 6 }}-frontend
12 | template:
13 | metadata:
14 | labels:
15 | {{- include "commandhunt.selectorLabels" . | nindent 8 }}-frontend
16 | spec:
17 | {{- with .Values.imagePullSecrets }}
18 | imagePullSecrets:
19 | {{- toYaml . | nindent 8 }}
20 | {{- end }}
21 | containers:
22 | - name: {{ .Chart.Name }}-frontend
23 | image: "{{ .Values.image.repository }}/{{ .Chart.Name }}-frontend:{{ .Values.image.tag | default .Chart.AppVersion }}"
24 | imagePullPolicy: {{ .Values.image.pullPolicy }}
25 | ports:
26 | - name: http
27 | containerPort: 3000
28 | protocol: TCP
29 |
--------------------------------------------------------------------------------
/helm/templates/frontend-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "commandhunt.fullname" . }}-frontend
5 | labels:
6 | {{- include "commandhunt.labels" . | nindent 4 }}
7 | spec:
8 | type: {{ .Values.service.frontend.type }}
9 | ports:
10 | - port: {{ .Values.service.frontend.port }}
11 | targetPort: 3000
12 | protocol: TCP
13 | name: http
14 | selector:
15 | {{- include "commandhunt.selectorLabels" . | nindent 4 }}-frontend
16 |
--------------------------------------------------------------------------------
/helm/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | {{- $fullName := include "commandhunt.fullname" . -}}
3 | apiVersion: networking.k8s.io/v1
4 | kind: Ingress
5 | metadata:
6 | name: {{ $fullName }}
7 | labels:
8 | {{- include "commandhunt.labels" . | nindent 4 }}
9 | {{- with .Values.ingress.annotations }}
10 | annotations:
11 | {{- toYaml . | nindent 4 }}
12 | {{- end }}
13 | spec:
14 | {{- if .Values.ingress.tls }}
15 | tls:
16 | {{- range .Values.ingress.tls }}
17 | - hosts:
18 | {{- range .hosts }}
19 | - {{ . | quote }}
20 | {{- end }}
21 | secretName: {{ .secretName }}
22 | {{- end }}
23 | {{- end }}
24 | rules:
25 | {{- range .Values.ingress.hosts }}
26 | - host: {{ .host | quote }}
27 | http:
28 | paths:
29 | - pathType: Prefix
30 | path: /
31 | backend:
32 | service:
33 | name: {{ $fullName }}-frontend
34 | port:
35 | number: 3000
36 | - pathType: Prefix
37 | path: /api/
38 | backend:
39 | service:
40 | name: {{ $fullName }}-backend
41 | port:
42 | number: 8080
43 | {{- end }}
44 | {{- end }}
45 |
--------------------------------------------------------------------------------
/helm/templates/initdb-configmap.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: {{ include "commandhunt.fullname" . }}-initdb-config
5 | labels:
6 | {{- include "commandhunt.labels" . | nindent 4 }}
7 | data:
8 | initdb.groovy: |
9 | :remote connect tinkerpop.server conf/remote.yaml session
10 | :remote console
11 | :remote config timeout 300000
12 | println "Start creating schema..."
13 | com.wirehall.commandhunt.initdb.SchemaBuilder.load(graph)
14 | println "Schema creation successful!"
15 | println "Start importing metadata..."
16 | com.wirehall.commandhunt.initdb.MetadataManager.load(graph)
17 | println "Metadata import successful!"
18 | :exit
19 |
--------------------------------------------------------------------------------
/helm/templates/letsencrypt-prod-issuer.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: cert-manager.io/v1
2 | kind: ClusterIssuer
3 | metadata:
4 | name: letsencrypt-prod
5 | spec:
6 | acme:
7 | # Email address used for ACME registration
8 | email: {{ .Values.email }}
9 | server: https://acme-v02.api.letsencrypt.org/directory
10 | privateKeySecretRef:
11 | # Name of a secret used to store the ACME account private key
12 | name: letsencrypt-prod-private-key
13 | # Add a single challenge solver, HTTP01 using nginx
14 | solvers:
15 | - http01:
16 | ingress:
17 | class: nginx
--------------------------------------------------------------------------------
/helm/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "commandhunt.fullname" . }}-test-connection"
5 | labels:
6 | {{- include "commandhunt.labels" . | nindent 4 }}
7 | annotations:
8 | "helm.sh/hook": test
9 | spec:
10 | containers:
11 | - name: wget
12 | image: busybox
13 | command: ['wget']
14 | args: ['{{ include "commandhunt.fullname" . }}:{{ .Values.service.port }}']
15 | restartPolicy: Never
16 |
--------------------------------------------------------------------------------
/helm/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for CommandHunt chart deployment.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 |
5 | email: support@commandhunt.com
6 |
7 | replicaCount: 2
8 |
9 | image:
10 | repository: ghcr.io/vivekweb2013/commandhunt
11 | pullPolicy: Always
12 | # Overrides the image tag whose default is the chart appVersion.
13 | tag: ""
14 |
15 | imagePullSecrets:
16 | - name: regcred
17 |
18 | nameOverride: ""
19 | fullnameOverride: ""
20 |
21 | serviceAccount:
22 | # Specifies whether a service account should be created
23 | create: true
24 | # Annotations to add to the service account
25 | annotations: {}
26 | # The name of the service account to use.
27 | # If not set and create is true, a name is generated using the fullname template
28 | name: ""
29 |
30 | podAnnotations: {}
31 |
32 | podSecurityContext:
33 | {}
34 | # fsGroup: 2000
35 |
36 | securityContext:
37 | {}
38 | # capabilities:
39 | # drop:
40 | # - ALL
41 | # readOnlyRootFilesystem: true
42 | # runAsNonRoot: true
43 | # runAsUser: 1000
44 |
45 | service:
46 | frontend:
47 | type: ClusterIP
48 | port: 3000
49 | backend:
50 | type: ClusterIP
51 | port: 8080
52 |
53 | ingress:
54 | enabled: true
55 | annotations:
56 | kubernetes.io/ingress.class: nginx
57 | cert-manager.io/cluster-issuer: letsencrypt-prod
58 | hosts:
59 | - host: commandhunt.com
60 | tls:
61 | - secretName: commandhunt-tls
62 | hosts:
63 | - commandhunt.com
64 |
65 | resources:
66 | {}
67 | # We usually recommend not to specify default resources and to leave this as a conscious
68 | # choice for the user. This also increases chances charts run on environments with little
69 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
70 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
71 | # limits:
72 | # cpu: 100m
73 | # memory: 128Mi
74 | # requests:
75 | # cpu: 100m
76 | # memory: 128Mi
77 |
78 | nodeSelector: {}
79 |
80 | tolerations: []
81 |
82 | affinity: {}
83 |
--------------------------------------------------------------------------------