diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..3af097f65bfcafc89a87ab69bb927d1694df3f38 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +PORT=5555 + +MYSQL_USER=podcastify-soap +MYSQL_PASSWORD=podcastify-soap +MYSQL_ROOT_PASSWORD=podcastify-soap +MYSQL_DATABASE=podcastify-soap +MYSQL_PORT=3306 +MYSQL_HOST=podcastify-soap-service-db + +REST_API_KEY=55183fd4-9238-42ed-8b98-e18d5bac7075 +APP_API_KEY=139cfa2f-3742-4b43-815e-f7bc6ecb8af5 + +SENDER_EMAIL= +SENDER_PASSWORD= + +RECEIVER_EMAIL= + +SMTP_HOST=smtp-mail.outlook.com +SMTP_PORT=587 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fc0106e8b5137f97425845ec423dea058e20327e --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +mysql +.idea +target +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3734793d5ad3a78a79dfbf02a33b2d202e4a5085 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3-amazoncorretto-8 as build + +WORKDIR /app + +COPY . . + +RUN --mount=type=cache,target=/root/.m2 mvn clean install assembly:single + +FROM amazoncorretto:8 + +WORKDIR /target + +COPY --from=build /app/target . + +EXPOSE 5555 + +CMD java -cp podcastify-soap-service-jar-with-dependencies.jar com.podcastify.main.Main \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..df9765de21327b33f69e09720ed1e52741b8d278 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +start: + docker compose up -d --build + +stop: + docker compose down + +stop-clean: + docker compose down + if exist mysql rmdir /s /q mysql + +build: + docker build -t podcastify-soap-service-app . \ No newline at end of file diff --git a/OWASP.md b/OWASP.md new file mode 100644 index 0000000000000000000000000000000000000000..7b838f362fe4c001cfdb981451de9afa90dfabc8 --- /dev/null +++ b/OWASP.md @@ -0,0 +1,36 @@ +# OWASP +## 1. SQL Injection + +SQL Injection is a code injection technique that attackers can use to exploit vulnerabilities in a web application’s database layer. This technique involves inserting malicious SQL statements into input fields for execution. To prevent this, we use parameterized queries or prepared statements, which can ensure that user-provided data cannot interfere with the query structure. Also, we always validate and sanitize user inputs + +The images below show an example of a payload attack where a common SQL Injection pattern: +```bash +' or 1=1-- +``` +and +```bash +' OR '1'='1 +``` +is used. However, due to the use of parameterized queries and input sanitization, the SOAP service treats this input as a regular string rather than a part of SQL command. As a result, the SQL Injection command gets inserted as a regular subscriber name, demonstrating that the SOAP service is correctly mitigating SQL Injection attacks. + +<img src="readme/sql_injection_1.png" width=350> +<img src="readme/sql_injection_2.png" width=350> + +## 2. HTML and CSS Injection + +HTML and CSS Injection is a type of attack where an attacker injects malicious HTML or CSS code into a web page, which is then rendered by the user’s browser. In our SOAP service, all string inputs are sanitized using `Jsoup.clean()` with `Safelist.none()`, which removes any HTML from the input. This effectively mitigates the risk of HTML and CSS Injection as it ensures that only safe and valid data is processed by our SOAP service. + +<img src="readme/html_css_injection_1.png" width=350> + +## 3. File Upload Vulnerabilities + +File Upload Vulnerabilities occur when an application allows file uploads without proper validation and sanitization, potentially allowing an attacker to upload malicious files. Our SOAP service does not have any file upload methods, and all inputs are either integer or string types. Therefore, it is not susceptible to File Upload Vulnerabilities. + +## 4. HTTP Parameter Pollution + +HTTP Parameter Pollution (HPP) is a type of attack where an attacker manipulates or injects HTTP parameters to cause changes in the application’s flow. Our SOAP service only has one endpoint and it only relies on parameters passed via SOAP envelopes. It does not use HTTP query parameters, so it is not susceptible to HPP attacks. + +## 5. JWT / OAuth Attack + +JWT/OAuth Attacks occur when an attacker exploits vulnerabilities in a JWT or OAuth-based authentication system. In JWT attacks, an attacker may attempt to modify the payload of the JWT to change the “claims†about the user, such as their role or permissions. In OAuth attacks, an attacker may exploit implementation mistakes such as not properly validating the redirect URI in the authorization request. Our SOAP service only accepts incoming requests from a valid private API key. If the API key is not valid, the request is unauthorized. This effectively mitigates the risk of JWT/OAuth Attacks as it ensures that only authorized requests are processed by our SOAP service. + diff --git a/README.md b/README.md index 9f411742945ca384d32475f7b5c75f980703d31a..6473f4bb80fc1fccf31a292316a22bf6e0a61ded 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,70 @@ -# podcastify-soap-service - - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - +# Podcastify SOAP Service + +Podcastify SOAP Service is a comprehensive service designed to handle incoming subscription requests from the Podcastify App (Monolith). It serves the Podcastify REST Service by providing subscription data and facilitating the approval or rejection of subscriptions. Additionally, it sends notification emails to the admin for every incoming subscription from the Podcastify App (Monolith). It also includes a logging service that saves logs to the Podcastify SOAP service’s database. + +## Functionality +1. <b>Incoming Subscriptions</b> </br> This service handles incoming subscription requests from the Podcastify App (Monolith). +2. <b>Subscription Status Update</b> </br> It provides an updateStatus function to update the subscription status (reject or accept) for the Podcastify REST service. When the status is updated, it sends a request to the Podcastify App (Monolith) to trigger push notifications, informing the user that the status has been updated to either rejected or accepted. +3. <b>Subscription Data</b> </br> The Podcastify SOAP service provides subscription data to the Podcastify REST service. This includes the types of incoming subscriptions. The Podcastify REST service can retrieve all subscriptions, retrieve subscriptions by creator ID or subscriber ID, and specify the status of the subscriptions it wants to retrieve. +4. <b>Notification Emails</b> </br> For every incoming subscription from the Podcastify App (Monolith), the Podcastify SOAP service sends a notification email to the administrator. +5. <b>Logging and Authentication</b> </br> The Podcastify SOAP service includes a logging service that saves logs to its own database, providing a record of transactions and events within the Podcastify SOAP service itself. This service also performs an authentication process by checking the API key of every incoming request. + +## DB Schema +<img src="readme/soap_erd.png" width=350> + +## API Endpoint +Please refer here [link postman] to get the full versions of the endpoints. + +### Subscription +|Method| URL | Explanation | Consumer | +|:--:|:--|:--|:--:| +| POST | /subscription | Base Subscription Endpoint | REST & Monolith | + +## SEI +|Method Name| Param | Explanation | Consumer | +|:--|:--|:--|:--:| +| subscribe | subscriber_id, creator_id, subscriber_name, creator_name | Sends a subscription request to a specific creator | Monolith | +| updateStatus | subscriber_id, creator_id, creator_name, status | Updates the status of a subscription request for a specific subscriber_id and creator_id | REST | +| getStatus | subscriber_id, creator_id | Retrieves the status for a specific subscriber_id and creator_id | REST | +| getSubscriptionBySubscriberID | subscriber_id, status | Retrieves subscription data by subscriber ID with a specified status | REST | +| getSubscriptionByCreatorID | creator_id, status | Retrieves subscription data by creator ID with a specified status | REST | +| getAllSubscriptions | - | Retrieves all subscription data | REST | + +## Tech Stacks +1. Docker +2. OpenJDK-8 +3. JAX-WS +4. Java Mail + +## How to Get Started +1. Clone this repository +2. Copy the `.env.example` file and rename it to `.env`: +```bash + cp .env.example .env ``` -cd existing_repo -git remote add origin https://gitlab.informatika.org/if3110-2023-01-44/podcastify-soap-service.git -git branch -M main -git push -uf origin main +3. Open the `.env` file and replace the placeholder values with your actual data. +4. On the root of this project, run the following commands: +```bash + docker-compose up -d --build ``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://gitlab.informatika.org/if3110-2023-01-44/podcastify-soap-service/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +5. To shut down the app, run +```bash + docker-compose down +``` +6. Ensure that the Docker Daemon is running + +## Tasking +| 13521055 | 13521072 | 13521102 | +| :---------------------------------- | :------------------------------------------ | :------------------------- | +| Setup Docker, DB, and Structure | Subscription Data Retrieval (by creator ID) | Setup DB and Structure | +| Email Notification | | Subscription Request | +| | | Subscription Update | +| | | Subscription Data Retrieval| +| | | Logging | +| | | Authentication | + +## OWASP +[OWASP Details](OWASP.md) + +## Copyright +2023 © Podcastify. All Rights Reserved. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b7652817cc108ddd5a38d42082bfde458ed962a9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3.8" +services: + podcastify-soap-service-db: + container_name: podcastify-soap-service-db + image: mysql:latest + hostname: podcastify-soap-service-db + ports: + - "3308:3306" + restart: always + healthcheck: + test: mysqladmin ping -h podcastify-soap-service-db -u$$MYSQL_USER -p$$MYSQL_PASSWORD + interval: 5s + timeout: 5s + retries: 20 + networks: + - podcastify-soap + env_file: + - .env + volumes: + - ./mysql:/var/lib/mysql + - ./migrations/init.sql:/docker-entrypoint-initdb.d/init.sql + + podcastify-soap-service-app: + container_name: podcastify-soap-service-app + image: podcastify-soap-service-app + hostname: podcastify-soap-service-app + env_file: + - .env + ports: + - "5555:5555" + depends_on: + podcastify-soap-service-db: + condition: service_healthy + volumes: + - ./src:/app/src + networks: + - podcastify-soap + +volumes: + mysql: + driver: local + +networks: + podcastify-soap: diff --git a/migrations/init.sql b/migrations/init.sql new file mode 100644 index 0000000000000000000000000000000000000000..307a5cb51149dcb8db88406bd7307f30c581f6c1 --- /dev/null +++ b/migrations/init.sql @@ -0,0 +1,34 @@ +DROP TABLE IF EXISTS logs; +DROP TABLE IF EXISTS statuses; +DROP TABLE IF EXISTS subscriptions; + +CREATE TABLE logs( + id int AUTO_INCREMENT PRIMARY KEY, + description varchar(255) NOT NULL, + IP varchar(16) NOT NULL, + endpoint varchar(255) NOT NULL, + requested_at timestamp NOT NULL DEFAULT NOW(), + from_service varchar(255) NOT NULL +); + +CREATE TABLE statuses( + status_id int AUTO_INCREMENT PRIMARY KEY, + name varchar(255) UNIQUE NOT NULL +); + +INSERT INTO statuses(name) VALUES + ('PENDING'), + ('ACCEPTED'), + ('REJECTED'); + +CREATE TABLE subscriptions( + creator_id int NOT NULL, + creator_name varchar(255) NOT NULL, + subscriber_id int NOT NULL, + subscriber_name varchar(255) NOT NULL, + status_id int NOT NULL DEFAULT 1, + created_at timestamp NOT NULL DEFAULT NOW(), + updated_at timestamp NOT NULL DEFAULT NOW(), + PRIMARY KEY (creator_id, subscriber_id), + FOREIGN KEY (status_id) REFERENCES statuses(status_id) +); \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..154d3212c9b5c11bef3e46988acef81f6467224b --- /dev/null +++ b/pom.xml @@ -0,0 +1,226 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.tubes-wbd-2</groupId> + <artifactId>podcastify-soap-service</artifactId> + <version>1.0-RELEASE</version> + <packaging>jar</packaging> + + <properties> + <maven.compiler.source>8</maven.compiler.source> + <maven.compiler.target>8</maven.compiler.target> + <tomcat.version>7.0.91</tomcat.version> + </properties> + + <dependencies> + <dependency> + <groupId>com.mysql</groupId> + <artifactId>mysql-connector-j</artifactId> + <version>8.0.32</version> + </dependency> + <dependency> + <groupId>io.github.cdimascio</groupId> + <artifactId>java-dotenv</artifactId> + <version>5.2.2</version> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <version>1.18.26</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.jsoup</groupId> + <artifactId>jsoup</artifactId> + <version>1.15.3</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat</artifactId> + <version>7.0.91</version> + <type>pom</type> + </dependency> + <dependency> + <groupId>com.sun.xml.ws</groupId> + <artifactId>jaxws-rt</artifactId> + <version>2.3.3</version> + <exclusions> + <exclusion> + <groupId>com.fasterxml.woodstox</groupId> + <artifactId>woodstox-core</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>javax.xml.ws</groupId> + <artifactId>jaxws-api</artifactId> + <version>2.3.1</version> + </dependency> + <dependency> + <groupId>com.github.javafaker</groupId> + <artifactId>javafaker</artifactId> + <version>1.0.2</version> + </dependency> + <dependency> + <groupId>org.eclipse.angus</groupId> + <artifactId>angus-mail</artifactId> + <version>2.0.1</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + <version>2.6.3</version> + </dependency> + <dependency> + <groupId>ognl</groupId> + <artifactId>ognl</artifactId> + <version>3.2.12</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <version>3.4.2</version> + <configuration> + <archive> + <manifest> + <mainClass>com.podcastify.main.Main</mainClass> + </manifest> + </archive> + <descriptorRefs> + <descriptorRef>jar-with-dependencies</descriptorRef> + </descriptorRefs> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.1</version> + </plugin> + <plugin> + <groupId>org.apache.tomcat.maven</groupId> + <artifactId>tomcat7-maven-plugin</artifactId> + <version>2.2</version> + <configuration> + <port>5555</port> + <path>/</path> + </configuration> + <dependencies> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-core</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-util</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-coyote</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-api</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-jdbc</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-dbcp</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-servlet-api</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-jsp-api</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-jasper</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-jasper-el</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-el-api</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-catalina</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-tribes</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-catalina-ha</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-annotations-api</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-juli</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-logging-juli</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-logging-log4j</artifactId> + <version>${tomcat.version}</version> + </dependency> + </dependencies> + </plugin> + </plugins> + <resources> + <resource> + <directory>${project.basedir}</directory> + <includes> + <include>.env</include> + </includes> + <targetPath>${project.build.outputDirectory}</targetPath> + </resource> + <resource> + <directory>src/main/resources</directory> + <includes> + <include>**/*.html</include> + </includes> + </resource> + </resources> + <finalName>podcastify-soap-service</finalName> + </build> + +</project> \ No newline at end of file diff --git a/readme/html_css_injection_1.png b/readme/html_css_injection_1.png new file mode 100644 index 0000000000000000000000000000000000000000..bcf452d1c6d34304813453fb9601220841f7709f Binary files /dev/null and b/readme/html_css_injection_1.png differ diff --git a/readme/soap_erd.png b/readme/soap_erd.png new file mode 100644 index 0000000000000000000000000000000000000000..5aff33f7ab51ea65052cc6d0a9bbf23480d997ee Binary files /dev/null and b/readme/soap_erd.png differ diff --git a/readme/sql_injection_1.png b/readme/sql_injection_1.png new file mode 100644 index 0000000000000000000000000000000000000000..89454db3e5c1896ba40b2d320e7cec3ca65c91a0 Binary files /dev/null and b/readme/sql_injection_1.png differ diff --git a/readme/sql_injection_2.png b/readme/sql_injection_2.png new file mode 100644 index 0000000000000000000000000000000000000000..7769e59c7480a9862820eb248a4bc34a0e87940a Binary files /dev/null and b/readme/sql_injection_2.png differ diff --git a/src/main/java/com/podcastify/constant/Response.java b/src/main/java/com/podcastify/constant/Response.java new file mode 100644 index 0000000000000000000000000000000000000000..111585808b4b170c7eac5a164ed5777fbaf621e3 --- /dev/null +++ b/src/main/java/com/podcastify/constant/Response.java @@ -0,0 +1,87 @@ +package com.podcastify.constant; + +import com.podcastify.model.ResponseModel; +import com.podcastify.model.BaseResponseModel; + +import java.util.List; +import java.util.ArrayList; +import java.sql.SQLException; + +public class Response { + public static final int HTTP_STATUS_OK = 200; + public static final int HTTP_STATUS_CREATED = 201; + public static final int HTTP_STATUS_ACCEPTED = 202; + public static final int HTTP_STATUS_MOVED_PERMANENTLY = 301; + public static final int HTTP_STATUS_FOUND = 302; + public static final int HTTP_STATUS_BAD_REQUEST = 400; + public static final int HTTP_STATUS_UNAUTHORIZED = 401; + public static final int HTTP_STATUS_FORBIDDEN = 403; + public static final int HTTP_STATUS_NOT_FOUND = 404; + public static final int HTTP_STATUS_METHOD_NOT_ALLOWED = 405; + public static final int HTTP_STATUS_NOT_ACCEPTABLE = 406; + public static final int HTTP_STATUS_PAYLOAD_TOO_LARGE = 413; + public static final int HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415; + public static final int HTTP_STATUS_INTERNAL_SERVER_ERROR = 500; + + public static ResponseModel createResponse(int statusCode, String message) { + ResponseModel response = new ResponseModel(); + response.setStatusCode(statusCode); + response.setMessage(message); + return response; + } + + public static ResponseModel createResponse(Exception e) { + if (e instanceof SecurityException) + return Response.createResponse(Response.HTTP_STATUS_UNAUTHORIZED, e.getMessage()); + if (e instanceof SQLException) + return Response.createResponse(Response.HTTP_STATUS_BAD_REQUEST, e.getMessage()); + if (e instanceof IllegalArgumentException) + return Response.createResponse(Response.HTTP_STATUS_BAD_REQUEST, e.getMessage()); + else { + String errMessage = e.getMessage() == null ? "internal server error" : e.getMessage(); + return Response.createResponse(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, errMessage); + } + } + + public static List<BaseResponseModel> createResponse(Exception e, String data) { + if (e instanceof SecurityException) + return Response.createResponse(Response.HTTP_STATUS_UNAUTHORIZED, e.getMessage(), data); + if (e instanceof SQLException) + return Response.createResponse(Response.HTTP_STATUS_BAD_REQUEST, e.getMessage(), data); + if (e instanceof IllegalArgumentException) + return Response.createResponse(Response.HTTP_STATUS_BAD_REQUEST, e.getMessage(), data); + else { + String errMessage = e.getMessage() == null ? "internal server error" : e.getMessage(); + return Response.createResponse(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, errMessage, data); + } + } + + public static List<BaseResponseModel> createResponse(int statusCode, String message, String data) { + BaseResponseModel baseResponse = new BaseResponseModel(); + baseResponse.setStatusCode(statusCode); + baseResponse.setMessage(message); + baseResponse.setData(data); + List<BaseResponseModel> response = new ArrayList<>(); + response.add(baseResponse); + return response; + } + + public static <T> List<T> createResponse(Exception e, List<T> data) { + if (e instanceof SecurityException) + return Response.createResponse(Response.HTTP_STATUS_UNAUTHORIZED, e.getMessage(), data); + if (e instanceof SQLException) + return Response.createResponse(Response.HTTP_STATUS_BAD_REQUEST, e.getMessage(), data); + if (e instanceof IllegalArgumentException) + return Response.createResponse(Response.HTTP_STATUS_BAD_REQUEST, e.getMessage(), data); + else { + String errMessage = e.getMessage() == null ? "internal server error" : e.getMessage(); + return Response.createResponse(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, errMessage, data); + } + } + + public static <T> List<T> createResponse(int statusCode, String message, List<T> data) { + List<T> response = new ArrayList<>(); + response.addAll(data); + return response; + } +} diff --git a/src/main/java/com/podcastify/constant/ServiceConstants.java b/src/main/java/com/podcastify/constant/ServiceConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..a4199491e918fabc6be50bd2f50be0382b631fa5 --- /dev/null +++ b/src/main/java/com/podcastify/constant/ServiceConstants.java @@ -0,0 +1,6 @@ +package com.podcastify.constant; + +public class ServiceConstants { + public static final String REST_SERVICE = "REST Service"; + public static final String APP_SERVICE = "APP Service"; +} diff --git a/src/main/java/com/podcastify/constant/Status.java b/src/main/java/com/podcastify/constant/Status.java new file mode 100644 index 0000000000000000000000000000000000000000..548b132d1046d85993c21a3d473993c302089a7a --- /dev/null +++ b/src/main/java/com/podcastify/constant/Status.java @@ -0,0 +1,7 @@ +package com.podcastify.constant; + +public class Status { + public static final int PENDING = 1; + public static final int ACCEPTED = 2; + public static final int REJECTED = 3; +} diff --git a/src/main/java/com/podcastify/db/Database.java b/src/main/java/com/podcastify/db/Database.java new file mode 100644 index 0000000000000000000000000000000000000000..f932eea69cef1ab7c494034ff7b5364384c1eb37 --- /dev/null +++ b/src/main/java/com/podcastify/db/Database.java @@ -0,0 +1,47 @@ +package com.podcastify.db; + +import io.github.cdimascio.dotenv.Dotenv; +import java.sql.Connection; +import java.sql.DriverManager; + +import com.mysql.cj.jdbc.exceptions.CommunicationsException; + +public class Database { + private Connection conn; + private String MYSQL_USER; + private String MYSQL_PASSWORD; + private String MYSQL_HOST; + private String MYSQL_DATABASE; + private int MYSQL_PORT; + + public Database() { + try { + Dotenv dotenv = Dotenv.load(); + this.MYSQL_USER = dotenv.get("MYSQL_USER"); + this.MYSQL_PASSWORD = dotenv.get("MYSQL_PASSWORD"); + this.MYSQL_HOST = dotenv.get("MYSQL_HOST", "localhost"); + this.MYSQL_PORT = Integer.parseInt(dotenv.get("MYSQL_PORT", "3306")); + this.MYSQL_DATABASE = dotenv.get("MYSQL_DATABASE"); + } catch (Exception e) { + e.printStackTrace(); + } + + // Setup connection + try { + String url = String.format( + "jdbc:mysql://%s:%d/%s?user=%s&password=%s&useSSL=false&allowPublicKeyRetrieval=true", + this.MYSQL_HOST, this.MYSQL_PORT, this.MYSQL_DATABASE, this.MYSQL_USER, this.MYSQL_PASSWORD); + this.conn = DriverManager.getConnection(url); + this.conn.setAutoCommit(false); + } catch (Exception e) { + if (!(e instanceof CommunicationsException)) { + System.out.println("Please check your env files! Exiting..."); + System.exit(1); + } + } + } + + public Connection getConnection() { + return this.conn; + } +} diff --git a/src/main/java/com/podcastify/implementor/EmailServiceImpl.java b/src/main/java/com/podcastify/implementor/EmailServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..e2d0666c6e83c50e08d5bb756369bcb9de488a0a --- /dev/null +++ b/src/main/java/com/podcastify/implementor/EmailServiceImpl.java @@ -0,0 +1,77 @@ +package com.podcastify.implementor; + +import java.util.Properties; + +import com.podcastify.service.EmailService; + +import io.github.cdimascio.dotenv.Dotenv; +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; + +public class EmailServiceImpl implements EmailService { + public void sendEmail(String to, String subject, String body) { + // Set SMTP server details + Dotenv dotenv = Dotenv.load(); + String host = dotenv.get("SMTP_HOST"); + String port = dotenv.get("SMTP_PORT"); + String username = dotenv.get("SENDER_EMAIL"); + String password = dotenv.get("SENDER_PASSWORD"); + + // Set up JavaMail properties + Properties properties = new Properties(); + properties.put("mail.smtp.host", host); + properties.put("mail.smtp.port", port); + properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.starttls.enable", "true"); + properties.put("mail.smtp.ssl.trust", host); + + // Create a mail session with authentication + Session session = Session.getInstance(properties, new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + + try { + // Create a MimeMessage object + MimeMessage message = new MimeMessage(session); + + // Set the sender's email address + message.setFrom(new InternetAddress(username)); + + // Set the recipient's email address + message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); + + // Set the email subject and body + message.setSubject(subject); + // message.setText(body); + + // Create the multipart/alternative part + Multipart multipart = new MimeMultipart("alternative"); + + // Add the HTML part + MimeBodyPart htmlPart = new MimeBodyPart(); + htmlPart.setContent(body, "text/html; charset=utf-8"); + + // Add parts to the multipart + multipart.addBodyPart(htmlPart); + + // Set the content of the message + message.setContent(multipart); + + // Send the email + Transport.send(message); + } catch (MessagingException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/podcastify/implementor/LogServiceImpl.java b/src/main/java/com/podcastify/implementor/LogServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a550b14cec73a7e633f164fec38625fcd1ac73cf --- /dev/null +++ b/src/main/java/com/podcastify/implementor/LogServiceImpl.java @@ -0,0 +1,22 @@ +package com.podcastify.implementor; + +import com.podcastify.service.LogService; +import com.podcastify.repository.LogRepository; +import com.podcastify.model.LogModel; +import com.podcastify.constant.Response; + +import java.sql.SQLException; + +public class LogServiceImpl implements LogService { + private LogRepository lr = new LogRepository(); + + public int addLog(LogModel lm) { + try { + lr.addLog(lm); + return Response.HTTP_STATUS_ACCEPTED; + } catch (SQLException e) { + e.printStackTrace(); + } + return Response.HTTP_STATUS_INTERNAL_SERVER_ERROR; + } +} diff --git a/src/main/java/com/podcastify/implementor/SubscribeServiceImpl.java b/src/main/java/com/podcastify/implementor/SubscribeServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..805d976a37fafba100eb7375bbf7f2e65e8b9b5c --- /dev/null +++ b/src/main/java/com/podcastify/implementor/SubscribeServiceImpl.java @@ -0,0 +1,305 @@ +package com.podcastify.implementor; + +import com.podcastify.constant.Response; +import com.podcastify.constant.ServiceConstants; +import com.podcastify.service.SubscribeService; +import com.podcastify.middleware.LogMiddleware; +import com.podcastify.repository.SubscriberRepository; +import com.podcastify.model.SubscriberModel; +import com.podcastify.model.BaseResponseModel; +import com.podcastify.model.ResponseModel; +import com.podcastify.utils.Request; +import com.podcastify.utils.EmailGenerator; +import com.podcastify.utils.MethodList; + +import io.github.cdimascio.dotenv.Dotenv; +import javax.jws.WebService; +import javax.xml.ws.WebServiceContext; +import javax.xml.ws.handler.MessageContext; +import javax.annotation.Resource; +import org.jsoup.Jsoup; +import org.jsoup.safety.Safelist; +import java.util.List; +import java.util.ArrayList; + +@WebService(targetNamespace = "http://com.podcastify.service/", endpointInterface = "com.podcastify.service.SubscribeService") +public class SubscribeServiceImpl implements SubscribeService { + @Resource + private WebServiceContext wsContext; + private SubscriberRepository sr; + + @Override + public ResponseModel subscribe(int subscriberID, int creatorID, String subscriberName, String creatorName) { + if (subscriberID <= 0 || creatorID <= 0) { + throw new IllegalArgumentException("Subscriber ID and Creator ID must be positive integers"); + } + String sanitizedSubscriberName = Jsoup.clean(subscriberName, Safelist.none()); + String sanitizedCreatorName = Jsoup.clean(creatorName, Safelist.none()); + if (sanitizedSubscriberName.length() <= 0 || sanitizedCreatorName.length() <= 0) { + throw new IllegalArgumentException("Name must not be empty"); + } + + String description = "Subscriber ID : " + subscriberID + "\n" + + "Creator ID : " + creatorID + "\n" + + "Subscriber Name : " + sanitizedSubscriberName + "\n" + + "Creator Name : " + sanitizedCreatorName + "\n" + + "Method : subscribe"; + MessageContext mc = wsContext.getMessageContext(); + this.sr = new SubscriberRepository(); + + try { + LogMiddleware loggingMiddleware = new LogMiddleware(mc, description, "/subscription"); + + if (loggingMiddleware.getServiceName().equals(ServiceConstants.APP_SERVICE)) { + SubscriberModel sm = new SubscriberModel(); + sm.setCreatorID(creatorID); + sm.setSubscriberID(subscriberID); + sm.setSubscriberName(sanitizedSubscriberName); + sm.setCreatorName(sanitizedCreatorName); + sr.addSubscriber(sm); + + // Send email to REST Admin + Dotenv dotenv = Dotenv.load(); + String receiver = dotenv.get("RECEIVER_EMAIL"); + + EmailServiceImpl emailServiceImpl = new EmailServiceImpl(); + + String emailContent = EmailGenerator.generateEmail(sanitizedSubscriberName, sanitizedCreatorName); + + emailServiceImpl.sendEmail(receiver, "Podcastify - [New Subscription]", emailContent); + + MethodList.printProcessStatus(Response.HTTP_STATUS_ACCEPTED, "/subscription", "subscribe"); + return Response.createResponse(Response.HTTP_STATUS_ACCEPTED, "success"); + } + + MethodList.printProcessStatus(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "/subscription", "subscribe"); + return Response.createResponse(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "method not allowed"); + + } catch (Exception e) { + System.out.println("Exception: " + e.getMessage()); + + MethodList.printProcessStatus(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, "/subscription", "subscribe"); + return Response.createResponse(e); + } + } + + @Override + public ResponseModel updateStatus(int subscriberID, int creatorID, String creatorName, String status) { + if (subscriberID <= 0 || creatorID <= 0) { + throw new IllegalArgumentException("Subscriber ID and Creator ID must be positive integers"); + } + + String sanitizedStatus = Jsoup.clean(status, Safelist.none()); + String sanitizedCreatorName = Jsoup.clean(creatorName, Safelist.none()); + if (sanitizedStatus.length() <= 0) { + throw new IllegalArgumentException("Status must not be empty"); + } + if (sanitizedCreatorName.length() <= 0) { + throw new IllegalArgumentException("Creator name must not be empty"); + } + + String description = "Subscriber ID : " + subscriberID + "\n" + + "Creator ID : " + creatorID + "\n" + + "Creator Name : " + sanitizedCreatorName + "\n" + + "Status : " + status + "\n" + + "Method : updateStatus"; + MessageContext mc = wsContext.getMessageContext(); + this.sr = new SubscriberRepository(); + + try { + LogMiddleware loggingMiddleware = new LogMiddleware(mc, description, "/subscription"); + + if (loggingMiddleware.getServiceName().equals(ServiceConstants.REST_SERVICE)) { + SubscriberModel sm = new SubscriberModel(); + sm.setCreatorID(creatorID); + sm.setSubscriberID(subscriberID); + sm.setStatus(sanitizedStatus); + sr.updateSubscriptionStatus(sm); + + Dotenv dotenv = Dotenv.load(); + String url = dotenv.get("APP_URL") + "/subscription"; + Request request = new Request(url); + + // Set the HTTP method to POST + request.setMethod("POST"); + + // Add parameters + request.addParam("subscriber_id", subscriberID); + request.addParam("creator_id", creatorID); + request.addParam("creator_name", sanitizedCreatorName); + request.addParam("status", status); + + // Add headers + request.addHeader("Content-Type", "application/x-www-form-urlencoded"); + + // Send request + String response = request.send(); + + MethodList.printProcessStatus(Response.HTTP_STATUS_OK, "/subscription", "updateStatus"); + return Response.createResponse(Response.HTTP_STATUS_OK, "success"); + } + + MethodList.printProcessStatus(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "/subscription", "updateStatus"); + return Response.createResponse(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "method not allowed"); + + } catch (Exception e) { + System.out.println("Exception: " + e.getMessage()); + + MethodList.printProcessStatus(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, "/subscription", "updateStatus"); + return Response.createResponse(e); + } + } + + @Override + public List<BaseResponseModel> getStatus(int subscriberID, int creatorID) { + if (subscriberID <= 0 || creatorID <= 0) { + throw new IllegalArgumentException("Subscriber ID and Creator ID must be positive integers"); + } + + String description = "Subscriber ID : " + subscriberID + "\n" + + "Creator ID : " + creatorID + "\n" + + "Method : getStatus"; + MessageContext mc = wsContext.getMessageContext(); + this.sr = new SubscriberRepository(); + + try { + LogMiddleware loggingMiddleware = new LogMiddleware(mc, description, "/subscription"); + + if (loggingMiddleware.getServiceName().equals(ServiceConstants.REST_SERVICE)) { + SubscriberModel sm = new SubscriberModel(); + sm.setCreatorID(creatorID); + sm.setSubscriberID(subscriberID); + + String status = sr.getSubscriptionStatus(sm); + if (status == null) { + return Response.createResponse(Response.HTTP_STATUS_OK, "success", "NOT_SUBSCRIBED"); + } + + MethodList.printProcessStatus(Response.HTTP_STATUS_OK, "/subscription", "getStatus"); + return Response.createResponse(Response.HTTP_STATUS_OK, "success", status); + } + + MethodList.printProcessStatus(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "/subscription", "getStatus"); + return Response.createResponse(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "method not allowed", + "method not allowed"); + + } catch (Exception e) { + System.out.println("Exception: " + e.getMessage()); + + MethodList.printProcessStatus(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, "/subscription", "getStatus"); + return Response.createResponse(e, "an exception occured"); + } + } + + @Override + public List<SubscriberModel> getSubscriptionBySubscriberID(int subscriberID, String status) { + if (subscriberID <= 0) { + throw new IllegalArgumentException("Subscriber ID must be positive integer"); + } + + String sanitizedStatus = Jsoup.clean(status, Safelist.none()); + if (sanitizedStatus.length() <= 0) { + throw new IllegalArgumentException("Status must not be empty"); + } + + String description = "Subscriber ID : " + subscriberID + "\n" + + "Status : " + sanitizedStatus + "\n" + + "Method : getSubscriptionBySubscriberID"; + MessageContext mc = wsContext.getMessageContext(); + this.sr = new SubscriberRepository(); + + try { + LogMiddleware loggingMiddleware = new LogMiddleware(mc, description, "/subscription"); + + if (loggingMiddleware.getServiceName().equals(ServiceConstants.REST_SERVICE)) { + List<SubscriberModel> subscribers = sr.getSubscriptionBySubscriberID(subscriberID, sanitizedStatus); + + MethodList.printProcessStatus(Response.HTTP_STATUS_OK, "/subscription", "getSubscriptionBySubscriberID"); + return Response.createResponse(Response.HTTP_STATUS_OK, "success", subscribers); + } + + MethodList.printProcessStatus(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "/subscription", + "getSubscriptionBySubscriberID"); + return Response.createResponse(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "method not allowed", + new ArrayList<>()); + + } catch (Exception e) { + System.out.println("Exception: " + e.getMessage()); + + MethodList.printProcessStatus(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, "/subscription", + "getSubscriptionBySubscriberID"); + return Response.createResponse(e, new ArrayList<>()); + } + } + + @Override + public List<SubscriberModel> getSubscriptionByCreatorID(int creatorID, String status) { + if (creatorID <= 0) { + throw new IllegalArgumentException("Creator ID must be positive integer"); + } + + String sanitizedStatus = Jsoup.clean(status, Safelist.none()); + if (sanitizedStatus.length() <= 0) { + throw new IllegalArgumentException("Status must not be empty"); + } + + String description = "Creator ID : " + creatorID + "\n" + + "Status : " + sanitizedStatus + "\n" + + "Method : getSubscriptionByCreatorID"; + MessageContext mc = wsContext.getMessageContext(); + this.sr = new SubscriberRepository(); + + try { + LogMiddleware loggingMiddleware = new LogMiddleware(mc, description, "/subscription"); + + if (loggingMiddleware.getServiceName().equals(ServiceConstants.REST_SERVICE)) { + List<SubscriberModel> subscribers = sr.getSubscriptionByCreatorID(creatorID, sanitizedStatus); + + MethodList.printProcessStatus(Response.HTTP_STATUS_OK, "/subscription", "getSubscriptionByCreatorID"); + return Response.createResponse(Response.HTTP_STATUS_OK, "success", subscribers); + } + + MethodList.printProcessStatus(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "/subscription", + "getSubscriptionByCreatorID"); + return Response.createResponse(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "method not allowed", + new ArrayList<>()); + + } catch (Exception e) { + System.out.println("Exception: " + e.getMessage()); + + MethodList.printProcessStatus(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, "/subscription", + "getSubscriptionByCreatorID"); + return Response.createResponse(e, new ArrayList<>()); + } + } + + @Override + public List<SubscriberModel> getAllSubscriptions() { + + String description = "Method : getAllSubscriptions"; + MessageContext mc = wsContext.getMessageContext(); + this.sr = new SubscriberRepository(); + + try { + LogMiddleware loggingMiddleware = new LogMiddleware(mc, description, "/subscription"); + + if (loggingMiddleware.getServiceName().equals(ServiceConstants.REST_SERVICE)) { + List<SubscriberModel> subscribers = sr.getAllSubscriptions(); + + MethodList.printProcessStatus(Response.HTTP_STATUS_OK, "/subscription", "getAllSubscriptions"); + return Response.createResponse(Response.HTTP_STATUS_OK, "success", subscribers); + } + + MethodList.printProcessStatus(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "/subscription", "getAllSubscriptions"); + return Response.createResponse(Response.HTTP_STATUS_METHOD_NOT_ALLOWED, "method not allowed", + new ArrayList<>()); + + } catch (Exception e) { + System.out.println("Exception: " + e.getMessage()); + + MethodList.printProcessStatus(Response.HTTP_STATUS_INTERNAL_SERVER_ERROR, "/subscription", + "getAllSubscriptions"); + return Response.createResponse(e, new ArrayList<>()); + } + } +} diff --git a/src/main/java/com/podcastify/main/Main.java b/src/main/java/com/podcastify/main/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..732dffdea8b6e01d25b9b11d42b2bad33f0e8dac --- /dev/null +++ b/src/main/java/com/podcastify/main/Main.java @@ -0,0 +1,31 @@ +package com.podcastify.main; + +import com.podcastify.implementor.SubscribeServiceImpl; +import com.podcastify.service.SubscribeService; +import com.podcastify.utils.Seed; +import com.podcastify.utils.MethodList; + +import javax.xml.ws.Endpoint; +import io.github.cdimascio.dotenv.Dotenv; + +public class Main { + public static void main(String[] args) { + try { + // Seeding + Seed s = new Seed(); + s.seedSubscriptions(); + + // Publish endpoint after done seeding + Dotenv dotenv = Dotenv.load(); + String port = dotenv.get("PORT", "5555"); + + // Subscription route + Endpoint.publish("http://0.0.0.0:" + port + "/api/v1/subscription", new SubscribeServiceImpl()); + MethodList.printAvailableMethods(SubscribeService.class); + + System.out.println("Listen and serve at: http://localhost:" + port + "/api/v1"); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/podcastify/middleware/LogMiddleware.java b/src/main/java/com/podcastify/middleware/LogMiddleware.java new file mode 100644 index 0000000000000000000000000000000000000000..a0b7446b33fb25e09f9a3f0e3c6fd661bda1dde5 --- /dev/null +++ b/src/main/java/com/podcastify/middleware/LogMiddleware.java @@ -0,0 +1,59 @@ +package com.podcastify.middleware; + +import com.podcastify.model.LogModel; +import com.podcastify.implementor.LogServiceImpl; +import com.podcastify.constant.ServiceConstants; + +import io.github.cdimascio.dotenv.Dotenv; +import com.sun.net.httpserver.HttpExchange; +import com.sun.xml.ws.developer.JAXWSProperties; +import javax.xml.ws.handler.MessageContext; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.HashMap; + +public class LogMiddleware { + private LogServiceImpl lsi = new LogServiceImpl(); + private Map<String, String> apiKeysToServices; + private String incomingService; + + public LogMiddleware(MessageContext mc, String description, String endpoint) throws SecurityException { + HttpExchange httpExchange = (HttpExchange) mc.get(JAXWSProperties.HTTP_EXCHANGE); + + // Initialize api keys & provided services + Dotenv dotenv = Dotenv.load(); + this.apiKeysToServices = new HashMap<>(); + this.apiKeysToServices.put(dotenv.get("REST_API_KEY"), ServiceConstants.REST_SERVICE); + this.apiKeysToServices.put(dotenv.get("APP_API_KEY"), ServiceConstants.APP_SERVICE); + + // Extract the API key from the request & validate + String apiKey = httpExchange.getRequestHeaders().getFirst("x-api-key"); + String fromService = this.getServiceFromApiKey(apiKey); + + InetSocketAddress remoteAddress = httpExchange.getRemoteAddress(); + String IP = remoteAddress.getAddress().toString().substring(1); + + LogModel lm = new LogModel(); + lm.setIP(IP); + lm.setDescription(description); + lm.setEndpoint(endpoint); + lm.setFromService(fromService); + + lsi.addLog(lm); + } + + private String getServiceFromApiKey(String apiKey) throws SecurityException { + String serviceName = apiKeysToServices.get(apiKey); + this.incomingService = serviceName; + + if (serviceName == null) { + throw new SecurityException("Invalid API Key!"); + } + + return serviceName; + } + + public String getServiceName() { + return this.incomingService; + } +} diff --git a/src/main/java/com/podcastify/model/BaseResponseModel.java b/src/main/java/com/podcastify/model/BaseResponseModel.java new file mode 100644 index 0000000000000000000000000000000000000000..6c7df7fd0e282150b33ab13c4b4887c2720f06ac --- /dev/null +++ b/src/main/java/com/podcastify/model/BaseResponseModel.java @@ -0,0 +1,32 @@ +package com.podcastify.model; + +import javax.xml.bind.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Setter; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Base Response Model + * Use this if the data is of primitive types. + */ + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "statusCode", + "message", + "data" +}) + +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +@XmlRootElement(name = "BaseResponseModel") +public class BaseResponseModel { + @XmlElement(required = true) + private int statusCode; + @XmlElement(required = true) + private String message; + private String data; +} diff --git a/src/main/java/com/podcastify/model/LogModel.java b/src/main/java/com/podcastify/model/LogModel.java new file mode 100644 index 0000000000000000000000000000000000000000..3fb616b557244efe90438579df726e8813cf55c0 --- /dev/null +++ b/src/main/java/com/podcastify/model/LogModel.java @@ -0,0 +1,17 @@ +package com.podcastify.model; + +import java.util.Date; + +import lombok.*; + +@Data +@Setter +@NoArgsConstructor +public class LogModel { + private int id; + @NonNull private String description; + @NonNull private String IP; + @NonNull private String endpoint; + @NonNull private Date timestamp; + @NonNull private String fromService; +} diff --git a/src/main/java/com/podcastify/model/ResponseModel.java b/src/main/java/com/podcastify/model/ResponseModel.java new file mode 100644 index 0000000000000000000000000000000000000000..96f50e3719cb26f89d4db706010b6def15f34eb6 --- /dev/null +++ b/src/main/java/com/podcastify/model/ResponseModel.java @@ -0,0 +1,22 @@ +package com.podcastify.model; + +import javax.xml.bind.annotation.*; + +import lombok.*; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "statusCode", + "message", +}) + +@NoArgsConstructor +@AllArgsConstructor +@Setter +@XmlRootElement(name = "ResponseModel") +public class ResponseModel { + @XmlElement(required = true) + private int statusCode; + @XmlElement(required = true) + private String message; +} diff --git a/src/main/java/com/podcastify/model/SubscriberModel.java b/src/main/java/com/podcastify/model/SubscriberModel.java new file mode 100644 index 0000000000000000000000000000000000000000..34fdf1d8921815c724a3a81cdf4d8630bed1f9c1 --- /dev/null +++ b/src/main/java/com/podcastify/model/SubscriberModel.java @@ -0,0 +1,17 @@ +package com.podcastify.model; + +import lombok.*; + +import java.util.Date; + +@Data +@NoArgsConstructor +public class SubscriberModel { + private int subscriberID; + @NonNull private String subscriberName; + private int creatorID; + @NonNull private String creatorName; + @NonNull private String status; + @NonNull private Date createdAt; + @NonNull private Date updatedAt; +} diff --git a/src/main/java/com/podcastify/repository/LogRepository.java b/src/main/java/com/podcastify/repository/LogRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..d5dae89d6020c64e90bcb50437b7da48c9e0725e --- /dev/null +++ b/src/main/java/com/podcastify/repository/LogRepository.java @@ -0,0 +1,21 @@ +package com.podcastify.repository; + +import com.podcastify.model.LogModel; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class LogRepository extends Repository { + + public void addLog(LogModel log) throws SQLException { + String query = "INSERT INTO logs (description, IP, endpoint, from_service) VALUES(?, ?, ?, ?)"; + try (PreparedStatement stmt = this.conn.prepareStatement(query)) { + stmt.setString(1, log.getDescription()); + stmt.setString(2, log.getIP()); + stmt.setString(3, log.getEndpoint()); + stmt.setString(4, log.getFromService()); + stmt.execute(); + this.conn.commit(); + } + } +} diff --git a/src/main/java/com/podcastify/repository/Repository.java b/src/main/java/com/podcastify/repository/Repository.java new file mode 100644 index 0000000000000000000000000000000000000000..273ce30a88dee6713cf112431dfcb67e70e3f845 --- /dev/null +++ b/src/main/java/com/podcastify/repository/Repository.java @@ -0,0 +1,15 @@ +package com.podcastify.repository; + +import com.podcastify.db.Database; + +import java.sql.Connection; + +public abstract class Repository { + protected Database db; + protected Connection conn; + + public Repository() { + this.db = new Database(); + this.conn = this.db.getConnection(); + } +} diff --git a/src/main/java/com/podcastify/repository/SubscriberRepository.java b/src/main/java/com/podcastify/repository/SubscriberRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..b6bee44e622a04721cfbbf5ba5018be4b8eb965f --- /dev/null +++ b/src/main/java/com/podcastify/repository/SubscriberRepository.java @@ -0,0 +1,203 @@ +package com.podcastify.repository; + +import com.podcastify.model.SubscriberModel; +import com.podcastify.constant.Status; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.sql.ResultSet; +import java.util.List; +import java.util.ArrayList; + +public class SubscriberRepository extends Repository { + + public void addSubscriber(SubscriberModel sm) throws SQLException { + String query = "INSERT INTO subscriptions (creator_id, subscriber_id, subscriber_name, creator_name) VALUES(?, ?, ?, ?)"; + + try (PreparedStatement stmt = this.conn.prepareStatement(query)) { + stmt.setInt(1, sm.getCreatorID()); + stmt.setInt(2, sm.getSubscriberID()); + stmt.setString(3, sm.getSubscriberName()); + stmt.setString(4, sm.getCreatorName()); + + int rowsUpdated = stmt.executeUpdate(); + if (rowsUpdated == 0) { + String message = "User with subscriber id: " + sm.getSubscriberID() + + " , has sent a subscription request to creator id: " + sm.getCreatorID(); + throw new SQLException(message); + } + this.conn.commit(); + } + } + + public void updateSubscriptionStatus(SubscriberModel sm) throws SQLException { + int statusId = 1; + String query = "SELECT status_id FROM statuses WHERE name = ?"; + + try (PreparedStatement stmt = this.conn.prepareStatement(query)) { + stmt.setString(1, sm.getStatus()); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + statusId = rs.getInt("status_id"); + } + } + } + + // Delete 'REJECTED' subscription to enable subscriber to subscribe again + if (statusId == Status.REJECTED) { + query = "DELETE FROM subscriptions WHERE creator_id = ? and subscriber_id = ?"; + } else { + query = "UPDATE subscriptions SET status_id = ?, updated_at = ? WHERE creator_id = ? and subscriber_id = ?"; + } + + try (PreparedStatement stmt = this.conn.prepareStatement(query)) { + if (statusId == Status.REJECTED) { + stmt.setInt(1, sm.getCreatorID()); + stmt.setInt(2, sm.getSubscriberID()); + } else { + Instant instant = Instant.now(); + Timestamp currentTimestamp = Timestamp.from(instant); + + stmt.setInt(1, statusId); + stmt.setTimestamp(2, currentTimestamp); + stmt.setInt(3, sm.getCreatorID()); + stmt.setInt(4, sm.getSubscriberID()); + } + + int rowsUpdated = stmt.executeUpdate(); + if (rowsUpdated == 0) { + String message = "No subscription found with the provided creator_id and subscriber_id"; + throw new SQLException(message); + } + this.conn.commit(); + } + } + + public String getSubscriptionStatus(SubscriberModel sm) throws SQLException { + String query = "SELECT st.name status FROM subscriptions su INNER JOIN statuses st ON su.status_id = st.status_id WHERE su.creator_id = ? AND su.subscriber_id = ?"; + String status = null; + + try (PreparedStatement stmt = this.conn.prepareStatement(query)) { + stmt.setInt(1, sm.getCreatorID()); + stmt.setInt(2, sm.getSubscriberID()); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + status = rs.getString("status"); + } + } + + this.conn.commit(); + } + + return status; + } + + public List<SubscriberModel> getSubscriptionBySubscriberID(int subscriberID, String status) throws SQLException { + StringBuilder query = new StringBuilder(); + query.append( + "SELECT su.creator_id creator_id, su.creator_name creator_name, su.subscriber_name subscriber_name, st.name status, su.created_at, su.updated_at ") + .append("FROM subscriptions su ") + .append("INNER JOIN statuses st ON su.status_id = st.status_id ") + .append("WHERE su.subscriber_id = ?"); + + if (!status.equals("ALL")) { + query.append(" AND st.name = ?"); + } + + List<SubscriberModel> subscribers = new ArrayList<>(); + try (PreparedStatement stmt = this.conn.prepareStatement(query.toString())) { + stmt.setInt(1, subscriberID); + if (!status.equals("ALL")) { + stmt.setString(2, status); + } + + ResultSet rs = stmt.executeQuery(); + + while (rs.next()) { + SubscriberModel subscriber = new SubscriberModel(); + subscriber.setSubscriberID(subscriberID); + subscriber.setSubscriberName(rs.getString("subscriber_name")); + subscriber.setCreatorID(rs.getInt("creator_id")); + subscriber.setCreatorName(rs.getString("creator_name")); + subscriber.setStatus(rs.getString("status")); + subscriber.setCreatedAt(rs.getTimestamp("created_at")); + subscriber.setUpdatedAt(rs.getTimestamp("updated_at")); + subscribers.add(subscriber); + } + this.conn.commit(); + } + + return subscribers; + } + + public List<SubscriberModel> getSubscriptionByCreatorID(int creatorID, String status) throws SQLException { + StringBuilder query = new StringBuilder(); + query.append( + "SELECT su.creator_id creator_id, su.creator_name creator_name, su.subscriber_name subscriber_name, su.subscriber_id subscriber_id, st.name status, su.created_at, su.updated_at ") + .append("FROM subscriptions su ") + .append("INNER JOIN statuses st ON su.status_id = st.status_id ") + .append("WHERE su.creator_id = ?"); + + if (!status.equals("ALL")) { + query.append(" AND st.name = ?"); + } + + List<SubscriberModel> subscribers = new ArrayList<>(); + try (PreparedStatement stmt = this.conn.prepareStatement(query.toString())) { + stmt.setInt(1, creatorID); + if (!status.equals("ALL")) { + stmt.setString(2, status); + } + + ResultSet rs = stmt.executeQuery(); + + while (rs.next()) { + SubscriberModel subscriber = new SubscriberModel(); + subscriber.setCreatorID(creatorID); + subscriber.setSubscriberName(rs.getString("subscriber_name")); + subscriber.setCreatorName(rs.getString("creator_name")); + subscriber.setStatus(rs.getString("status")); + subscriber.setCreatedAt(rs.getTimestamp("created_at")); + subscriber.setUpdatedAt(rs.getTimestamp("updated_at")); + subscriber.setSubscriberID(rs.getInt("subscriber_id")); + subscribers.add(subscriber); + } + this.conn.commit(); + } + + return subscribers; + } + + public List<SubscriberModel> getAllSubscriptions() throws SQLException { + StringBuilder query = new StringBuilder(); + query.append( + "SELECT su.creator_id creator_id, su.creator_name creator_name, su.subscriber_id subscriber_id, su.subscriber_name subscriber_name, st.name status, su.created_at, su.updated_at ") + .append("FROM subscriptions su ") + .append("INNER JOIN statuses st ON su.status_id = st.status_id "); + + List<SubscriberModel> subscribers = new ArrayList<>(); + try (PreparedStatement stmt = this.conn.prepareStatement(query.toString())) { + ResultSet rs = stmt.executeQuery(); + + while (rs.next()) { + SubscriberModel subscriber = new SubscriberModel(); + subscriber.setSubscriberID(rs.getInt("subscriber_id")); + subscriber.setSubscriberName(rs.getString("subscriber_name")); + subscriber.setCreatorID(rs.getInt("creator_id")); + subscriber.setCreatorName(rs.getString("creator_name")); + subscriber.setStatus(rs.getString("status")); + subscriber.setCreatedAt(rs.getTimestamp("created_at")); + subscriber.setUpdatedAt(rs.getTimestamp("updated_at")); + subscribers.add(subscriber); + } + this.conn.commit(); + } + + return subscribers; + } + +} diff --git a/src/main/java/com/podcastify/service/EmailService.java b/src/main/java/com/podcastify/service/EmailService.java new file mode 100644 index 0000000000000000000000000000000000000000..f5d33930d38ef0392440c29cc3d3d75a32b03479 --- /dev/null +++ b/src/main/java/com/podcastify/service/EmailService.java @@ -0,0 +1,10 @@ +package com.podcastify.service; + +import javax.jws.WebMethod; +import javax.jws.WebService; + +@WebService +public interface EmailService { + @WebMethod + public void sendEmail(String to, String subject, String body); +} diff --git a/src/main/java/com/podcastify/service/LogService.java b/src/main/java/com/podcastify/service/LogService.java new file mode 100644 index 0000000000000000000000000000000000000000..d0435d384799fc6bef59ef986ad94a6458ca3e8f --- /dev/null +++ b/src/main/java/com/podcastify/service/LogService.java @@ -0,0 +1,7 @@ +package com.podcastify.service; + +import com.podcastify.model.LogModel; + +public interface LogService { + int addLog(LogModel logModel); +} diff --git a/src/main/java/com/podcastify/service/SubscribeService.java b/src/main/java/com/podcastify/service/SubscribeService.java new file mode 100644 index 0000000000000000000000000000000000000000..b27cf44818bcc19ed3d5c354c7b6cd1d1a525708 --- /dev/null +++ b/src/main/java/com/podcastify/service/SubscribeService.java @@ -0,0 +1,33 @@ +package com.podcastify.service; + +import com.podcastify.model.ResponseModel; +import com.podcastify.model.BaseResponseModel; +import com.podcastify.model.SubscriberModel; + +import javax.jws.WebMethod; +import javax.jws.WebParam; +import javax.jws.WebService; +import javax.jws.soap.SOAPBinding; +import java.util.List; + +@WebService +@SOAPBinding(style = SOAPBinding.Style.DOCUMENT) +public interface SubscribeService { + @WebMethod + ResponseModel subscribe(@WebParam(name = "subscriber_id") int subscriberID, @WebParam(name = "creator_id") int creatorID, @WebParam(name = "subscriber_name") String subscriberName, @WebParam(name = "creator_name") String creatorName); + + @WebMethod + ResponseModel updateStatus(@WebParam(name = "subscriber_id") int subscriberID, @WebParam(name = "creator_id") int creatorID, @WebParam(name = "creator_name") String creatorName, @WebParam(name = "status") String status); + + @WebMethod + List<BaseResponseModel> getStatus(@WebParam(name = "subscriber_id") int subscriberID, @WebParam(name = "creator_id") int creatorID); + + @WebMethod + List<SubscriberModel> getSubscriptionBySubscriberID(@WebParam(name = "subscriber_id") int subscriberID, @WebParam(name = "status") String status); + + @WebMethod + List<SubscriberModel> getSubscriptionByCreatorID(@WebParam(name = "creator_id") int creatorID, @WebParam(name = "status") String status); + + @WebMethod + List<SubscriberModel> getAllSubscriptions(); +} diff --git a/src/main/java/com/podcastify/utils/EmailGenerator.java b/src/main/java/com/podcastify/utils/EmailGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..62aa75cefa193d22a78d871f6f22d3984f96fe2f --- /dev/null +++ b/src/main/java/com/podcastify/utils/EmailGenerator.java @@ -0,0 +1,26 @@ +package com.podcastify.utils; + +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +public class EmailGenerator { + public static String generateEmail(String from, String to) { + TemplateEngine templateEngine = new TemplateEngine(); + + ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setPrefix("templates/"); + templateResolver.setSuffix(".html"); + templateResolver.setTemplateMode("HTML"); + templateResolver.setCharacterEncoding("UTF-8"); + + templateEngine.addTemplateResolver(templateResolver); + + Context context = new Context(); + context.setVariable("From", from); + context.setVariable("To", to); + + // Process the Thymeleaf template with dynamic data + return templateEngine.process("email", context); + } +} diff --git a/src/main/java/com/podcastify/utils/MethodList.java b/src/main/java/com/podcastify/utils/MethodList.java new file mode 100644 index 0000000000000000000000000000000000000000..4d092275615143dec5d098b37a4df1a0b08373ce --- /dev/null +++ b/src/main/java/com/podcastify/utils/MethodList.java @@ -0,0 +1,19 @@ +package com.podcastify.utils; + +import javax.jws.WebMethod; +import java.lang.reflect.Method; + +public class MethodList { + public static void printAvailableMethods(Class<?> serviceInterface) { + System.out.println("Available methods:"); + for (Method method : serviceInterface.getDeclaredMethods()) { + if (method.isAnnotationPresent(WebMethod.class)) { + System.out.println("- " + method.getName()); + } + } + } + + public static void printProcessStatus(int code, String endpoint, String methodName) { + System.out.println("[" + code + "] Endpoint: " + endpoint + " --- Method: " + methodName); + } +} \ No newline at end of file diff --git a/src/main/java/com/podcastify/utils/Request.java b/src/main/java/com/podcastify/utils/Request.java new file mode 100644 index 0000000000000000000000000000000000000000..e5f41ad44d0d1512437b8991ce4f3c26c4ad76b2 --- /dev/null +++ b/src/main/java/com/podcastify/utils/Request.java @@ -0,0 +1,80 @@ +package com.podcastify.utils; + +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class Request { + private String url; + private Map<String, Object> params = new HashMap<>(); + private String method = "GET"; + private Map<String, String> headers = new HashMap<>(); + private String response; + private int statusCode; + + public Request(String url){ + this.url = url; + } + + public String getResponse(){ + return this.response; + } + + public int getStatusCode(HttpURLConnection conn) throws IOException { + return this.statusCode; + } + + public void setMethod(String method){ + this.method = method; + } + + public void addParam(String key, Object value){ + this.params.put(key, value); + } + + public void addHeader(String key, String value){ + this.headers.put(key, value); + } + + private byte[] buildPostData() throws UnsupportedEncodingException { + StringBuilder postData = new StringBuilder(); + for(Map.Entry<String,Object> param : this.params.entrySet()) { + if (postData.length() != 0) postData.append('&'); + postData.append(URLEncoder.encode(param.getKey(), "UTF-8")); + postData.append('='); + postData.append(URLEncoder.encode(String.valueOf(param.getValue()), "UTF-8")); + } + return postData.toString().getBytes(StandardCharsets.UTF_8); + } + + private String getStringResponse(HttpURLConnection conn) throws IOException { + this.statusCode = conn.getResponseCode(); + + Reader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + StringBuilder sb = new StringBuilder(); + for (int c; (c = in.read()) >= 0;) + sb.append((char)c); + String response = sb.toString(); + this.response = response; + return response; + } + + public String send() throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(this.url).openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod(this.method); + + for(Map.Entry<String, String> header : this.headers.entrySet()) { + conn.setRequestProperty(header.getKey(), header.getValue()); + } + + if(this.method.equals("POST")) { + byte[] postDataBytes = this.buildPostData(); + conn.getOutputStream().write(postDataBytes); + } + + return this.getStringResponse(conn); + } +} diff --git a/src/main/java/com/podcastify/utils/Seed.java b/src/main/java/com/podcastify/utils/Seed.java new file mode 100644 index 0000000000000000000000000000000000000000..b79a16f92b6a6027a57c5450cf6e9b0113fd0d79 --- /dev/null +++ b/src/main/java/com/podcastify/utils/Seed.java @@ -0,0 +1,61 @@ +package com.podcastify.utils; + +import com.podcastify.db.Database; + +import com.github.javafaker.Faker; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; + +public class Seed { + private Database db; + private Connection conn; + + public Seed() { + this.db = new Database(); + this.conn = this.db.getConnection(); + } + + public void seedSubscriptions() { + Faker faker = new Faker(); + Set<String> existingSubscriptions = new HashSet<>(); + + String checkQuery = "SELECT COUNT(*) FROM subscriptions"; + String insertQuery = "INSERT INTO subscriptions (creator_id, creator_name, subscriber_id, subscriber_name, status_id) VALUES (?, ?, ?, ?, ?)"; + + System.out.println("Seeding subscriptions table..."); + try { + PreparedStatement statement = conn.prepareStatement(checkQuery); + ResultSet resultSet = statement.executeQuery(checkQuery); + + // Only seed if the db is not empty + if (resultSet.next() && resultSet.getInt(1) == 0) { + PreparedStatement preparedStatement = conn.prepareStatement(insertQuery); + + for (int j = 1; j <= 10; j++) { + String subscriptionKey = j + "-" + 1; + if (!existingSubscriptions.contains(subscriptionKey)) { + preparedStatement.setInt(1, j); + preparedStatement.setString(2, faker.name().fullName()); + preparedStatement.setInt(3, 2); + preparedStatement.setString(4, "user"); + preparedStatement.setInt(5, faker.number().numberBetween(1, 3)); + preparedStatement.executeUpdate(); + + existingSubscriptions.add(subscriptionKey); + } + } + + this.conn.commit(); + System.out.println("Seeding completed successfully."); + } else { + System.out.println("Database is not empty, skipping seeding..."); + } + } catch (SQLException e) { + System.out.println("Error seeding data: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/resources/templates/email.html b/src/main/resources/templates/email.html new file mode 100644 index 0000000000000000000000000000000000000000..6bfb18c2ae71b529feceba80b06d6bbb0c308b1f --- /dev/null +++ b/src/main/resources/templates/email.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html lang="" xmlns:th="http://www.thymeleaf.org"> + +<head> + <title>Podcastify</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> + +<body style="font-family: 'Rubik', system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol !important; background-color: #ecf0f1; text-align: center;"> + <div style="width: 99%; max-width: 800px; margin: 0 auto; text-align: center; display: inline-block; color: #313131;"> + <div style="padding-bottom: 30px; padding-top: 30px; text-align: center; font-weight: bold; font-size: 20px; line-height: 25px;">Podcastify</div> + <div style="padding-bottom: 30px"> + <div style="background-color: white; display: inline-block; text-align: left; padding: 32px 24px; border-radius: 10px;"> + <span style="padding-bottom: 20px; font-weight: bold; font-size: 15px; line-height: 25px; display: block;">Dear Admin,</span> + + <div style="padding-bottom: 20px; font-size: 15px; line-height: 25px; display: block;">We are excited to inform you that a new subscription request has been received.</div> + + <div style="padding-bottom: 10px; font-size: 15px; line-height: 25px; display: block;"><b>From: </b><span th:text="${From}"></span></div> + <div style="padding-bottom: 40px; font-size: 15px; line-height: 25px; display: block;"><b>To: </b><span th:text="${To}"></span></div> + + <div style="text-align: center; padding-bottom: 40px;"> + <a href="http://localhost:5173/" style="display: inline-block; font-size: 15px; line-height: 15px; background-color: #2980b9; color: white; padding: 12px 20px; border-radius: 5px; text-decoration: none;">Check the details</a> + </div> + + <div style="padding-bottom: 20px; font-size: 15px; line-height: 25px; display: block;">Regards,<br>Podcastify</div> + + <div style="font-style: italic; font-size: 12px; line-height: 22px; display: block;">This message is an automated message and cannot receive a reply.</div> + </div> + </div> + <div style="padding-bottom: 30px;"> + <span style="text-align: center; font-size: 12px; line-height: 25px;">©Podcastify. All rights reserved.</span> + </div> + </div> +</body> + +</html> \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000000000000000000000000000000000..2dd43ee27d8af8b59f42a5cc8e2747b234a7112f --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" + version="4.0"> + <filter> + <filter-name>CorsFilter</filter-name> + <filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>CorsFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + <listener> + <listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class> + </listener> + + <servlet> + <servlet-name>podcastify</servlet-name> + <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <servlet-mapping> + <servlet-name>podcastify</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> +</web-app>