Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f7dabbf0 | ||
|
|
5041ebeeff | ||
|
|
fa8c84bb38 | ||
|
|
c3115bc002 | ||
|
|
91e48f2cbb | ||
|
|
4217ea11c8 | ||
|
|
f41e34486a | ||
|
|
29812e8175 | ||
|
|
0444ad5a5d | ||
|
|
89eb15d714 | ||
|
|
08ed7c485e |
@@ -1,44 +0,0 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
@@ -75,5 +75,3 @@ jobs:
|
||||
git tag -a ${{ steps.version.outputs.tag }} -m "Release ${{ steps.version.outputs.tag }}"
|
||||
git push origin ${{ steps.version.outputs.tag }}
|
||||
|
||||
- name: Trigger Dokploy deployment
|
||||
run: curl -X POST "${{ vars.DOKPLOY_WEBHOOK_APP }}"
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
cache: 'maven'
|
||||
|
||||
- name: Increment version in pom.xml
|
||||
@@ -71,7 +71,8 @@ jobs:
|
||||
working-directory: ./server
|
||||
run: |
|
||||
./mvnw -Pnative -DskipTests spring-boot:build-image \
|
||||
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest
|
||||
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
|
||||
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
|
||||
|
||||
- name: Tag and push Docker image
|
||||
run: |
|
||||
@@ -85,6 +86,3 @@ jobs:
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a api-v${{ steps.version.outputs.version }} -m "Release API v${{ steps.version.outputs.version }}"
|
||||
git push origin api-v${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Trigger Dokploy deployment
|
||||
run: curl -X POST "${{ vars.DOKPLOY_WEBHOOK_API }}"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
cache: 'maven'
|
||||
|
||||
- name: Run Check Style
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
### Frontend (React/TypeScript)
|
||||
```bash
|
||||
cd client
|
||||
npm start # Start development server on port 5000
|
||||
npm run build # Build production bundle (TypeScript compilation + Vite build)
|
||||
npm run preview # Preview production build
|
||||
npm test # Run Vitest tests
|
||||
npm run test:coverage # Run tests with coverage report
|
||||
npm run lint # Run ESLint
|
||||
npm run lint:fix # Run ESLint with auto-fix
|
||||
```
|
||||
|
||||
### Backend (Java/Spring Boot)
|
||||
```bash
|
||||
cd server
|
||||
./mvnw spring-boot:run # Start development server
|
||||
./mvnw clean compile # Compile Java sources
|
||||
./mvnw test # Run unit tests only
|
||||
./mvnw clean verify -Ptests # Run all tests (unit + integration) with coverage
|
||||
./mvnw spring-boot:build-image # Build Docker image
|
||||
./mvnw clean verify -Pnative # Build GraalVM native image
|
||||
```
|
||||
|
||||
### Docker Development Environment
|
||||
```bash
|
||||
bash tools/run-docker-db.sh # Start PostgreSQL database
|
||||
bash tools/run-docker-server.sh # Start backend in Docker
|
||||
bash tools/run-docker-client.sh # Start frontend in Docker
|
||||
bash tools/check-frontend.sh # Run frontend quality checks
|
||||
bash tools/check-backend.sh # Run backend quality checks
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
- `client/` - React TypeScript frontend (Vite + Vitest)
|
||||
- `server/` - Java Spring Boot REST API
|
||||
- `tools/` - Development and deployment scripts
|
||||
- `docker-compose.dev.yml` / `docker-compose.prod.yml` - Multi-service development and production environments
|
||||
- `Taskfile.yml` - Task automation for docker builds and workflows
|
||||
|
||||
### Frontend Architecture (client/)
|
||||
- **Framework**: React 19 with React Router v7
|
||||
- **State Management**: React Context API for authentication and sidebar state
|
||||
- **Authentication**: JWT tokens stored in localStorage with automatic refresh (2-minute intervals)
|
||||
- **Routing**: Dynamic router configuration based on auth status (signed vs not-signed routes)
|
||||
- **Internationalization**: i18next with support for English, Portuguese, Russian, Spanish
|
||||
- **Styling**: Bootstrap 5 + SCSS with dark/light theme support
|
||||
- **Testing**: Vitest with React Testing Library and coverage reporting
|
||||
- **API Layer**: Centralized API service in `src/api-service/api.ts` with automatic auth headers
|
||||
|
||||
### Backend Architecture (server/)
|
||||
- **Framework**: Spring Boot 3.5.9 with Java 21
|
||||
- **Security**: Spring Security with JWT authentication
|
||||
- **Database**: PostgreSQL with JPA/Hibernate and Flyway migrations
|
||||
- **Testing**: Separate unit tests and integration tests with 75% coverage requirement
|
||||
- **Build Options**: Traditional JAR or GraalVM native image compilation
|
||||
|
||||
### Key Components
|
||||
|
||||
**Authentication Flow:**
|
||||
1. User credentials → `/auth/sign-in` endpoint
|
||||
2. Server responds with JWT token and user data
|
||||
3. Token stored in localStorage, added to all API requests via Authorization header
|
||||
4. Automatic token refresh every 2 minutes via `/rest/user-sessions/refresh`
|
||||
5. Protected routes wrap authenticated pages, redirect on auth failure
|
||||
|
||||
**Database Schema:**
|
||||
- Core entities: Users, Tasks, Notes, Task URLs, Notes URLs
|
||||
- User management with password reset functionality
|
||||
- Task completion tracking and user statistics
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Manual Setup
|
||||
1. Start database: `bash tools/run-docker-db.sh`
|
||||
2. Start backend: `bash tools/run-docker-server.sh` or `cd server && ./mvnw spring-boot:run`
|
||||
3. Start frontend: `bash tools/run-docker-client.sh` or `cd client && npm start`
|
||||
4. Access app at http://localhost:5000
|
||||
|
||||
### Using Taskfile
|
||||
Alternatively, use the Taskfile.yml for automated workflows:
|
||||
```bash
|
||||
task dev-run # Start development environment (docker-compose.dev.yml)
|
||||
task prod-up # Start production environment (docker-compose.prod.yml)
|
||||
task docker-build-api # Build server Docker image
|
||||
task docker-build-web # Build app Docker image
|
||||
task -l # List all available tasks
|
||||
```
|
||||
|
||||
### Quality Checks
|
||||
Always run quality checks before submitting changes:
|
||||
- Frontend: `bash tools/check-frontend.sh`
|
||||
- Backend: `bash tools/check-backend.sh`
|
||||
|
||||
## Testing
|
||||
|
||||
### Frontend Testing
|
||||
- **Framework**: Vitest + React Testing Library
|
||||
- **Coverage**: Generated in `client/coverage/` directory
|
||||
- **Run**: `cd client && npm test` or `npm run test:coverage`
|
||||
|
||||
### Backend Testing
|
||||
- **Unit Tests**: Standard JUnit tests, run with `./mvnw test`
|
||||
- **Integration Tests**: Files ending in `*IntTest.java`, require database
|
||||
- **Coverage**: JaCoCo reports, 75% minimum requirement
|
||||
- **Full Test Suite**: `./mvnw clean verify -Ptests` (includes checkstyle, coverage)
|
||||
@@ -1,21 +1,10 @@
|
||||
# TaskNote
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||

|
||||
|
||||
### TaskNote API
|
||||
[](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_server)
|
||||
[](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_server)
|
||||
[](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver)
|
||||
[](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver)
|
||||
[](https://sonarcloud.io/summary/new_code?id=br.com.tasknoteapp%3Aserver)
|
||||
|
||||
### TaskNote WebApp
|
||||
[](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
|
||||
[](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
|
||||
[](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
|
||||
[](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
|
||||
[](https://sonarcloud.io/summary/new_code?id=ricardo-campos-org_react-typescript-todolist_client)
|
||||
[](https://github.com/RMCampos/tasknote/actions/workflows/client-ci.yml)
|
||||
[](https://github.com/RMCampos/tasknote/actions/workflows/server-ci.yml)
|
||||
[](https://github.com/RMCampos/tasknote/actions/workflows/main-client.yml)
|
||||
[](https://github.com/RMCampos/tasknote/actions/workflows/main-server.yml)
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
@@ -49,17 +38,17 @@ The project was born from a month-long technical challenge and has since grown t
|
||||
- **Responsive Design**: Mobile-first approach with Bootstrap 5 and dark/light theme support
|
||||
- **Data Visualization**: Task completion charts and productivity analytics
|
||||
- **File Attachments**: URL attachments for tasks and notes
|
||||
- **Tagging System**: `#tag` support for better organization
|
||||
- **Mobile App**: Native mobile applications for iOS and Android with PWA plugin
|
||||
- **Collaboration**: Share tasks and notes with other users
|
||||
|
||||
### Upcoming Features
|
||||
- **Tagging System**: `#tag` support for better organization
|
||||
- **Advanced Filters**: Enhanced search with date ranges, priority levels, and status filters
|
||||
- **Collaboration**: Share tasks and notes with other users
|
||||
- **Mobile App**: Native mobile applications for iOS and Android
|
||||
- **Notifications**: Email and push notifications for due dates and reminders
|
||||
|
||||
## 🚀 Tech Stack
|
||||
|
||||
### Frontend (React TypeScript)
|
||||
### Frontend (React & TypeScript)
|
||||
- **Framework**: React 19 with TypeScript for type safety
|
||||
- **Build Tool**: Vite for fast development and optimized production builds
|
||||
- **Testing**: Vitest with React Testing Library and comprehensive coverage reporting
|
||||
@@ -69,8 +58,8 @@ The project was born from a month-long technical challenge and has since grown t
|
||||
- **Internationalization**: i18next with automatic language detection
|
||||
- **API Client**: Centralized API service with automatic authentication headers
|
||||
|
||||
### Backend (Java Spring Boot)
|
||||
- **Framework**: Spring Boot 3.5+ with Java 17
|
||||
### Backend (Java & Spring Boot)
|
||||
- **Framework**: Spring Boot 4.x.x with Java 25
|
||||
- **Security**: Spring Security with JWT authentication and refresh tokens
|
||||
- **Database**: PostgreSQL with JPA/Hibernate ORM
|
||||
- **Migration**: Flyway for database schema versioning
|
||||
@@ -84,19 +73,16 @@ The project was born from a month-long technical challenge and has since grown t
|
||||
- **Containerization**: Docker and Docker Compose for development environment
|
||||
- **Web Server**: Caddy for reverse proxy and SSL termination
|
||||
- **CI/CD**: GitHub Actions with automated testing and quality gates
|
||||
- **Code Analysis**: SonarCloud integration for security and maintainability
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
```
|
||||
react-typescript-todolist/
|
||||
tasknote/
|
||||
├── client/ # React TypeScript frontend
|
||||
├── server/ # Java Spring Boot REST API
|
||||
├── angular/ # Alternative Angular frontend
|
||||
├── tools/ # Development and deployment scripts
|
||||
├── docker-compose.yml
|
||||
└── CLAUDE.md # AI assistant instructions
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
### Frontend Architecture
|
||||
@@ -117,15 +103,15 @@ react-typescript-todolist/
|
||||
|
||||
### Prerequisites
|
||||
- **Docker & Docker Compose** (recommended for easy setup)
|
||||
- **Node.js 18+** and **npm** (for frontend development)
|
||||
- **Java 17+** and **Maven 3.6+** (for backend development)
|
||||
- **PostgreSQL 13+** (if running without Docker)
|
||||
- **Node.js 20+** and **npm** (for frontend development)
|
||||
- **Java 25+** and **Maven 3.6+** (for backend development)
|
||||
- **PostgreSQL 15+** (if running without Docker)
|
||||
|
||||
### Quick Start with Docker
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/ricardo-campos-org/react-typescript-todolist.git
|
||||
cd react-typescript-todolist
|
||||
git clone https://github.com/rmcampos/tasknote.git
|
||||
cd tasknote
|
||||
```
|
||||
|
||||
2. **Start the database**
|
||||
@@ -145,7 +131,6 @@ react-typescript-todolist/
|
||||
|
||||
5. **Access the application**
|
||||
- Frontend: http://localhost:5000
|
||||
- API Documentation: http://localhost:8080/swagger-ui.html
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
@@ -234,7 +219,7 @@ We welcome contributions from the community! This project follows the **Fork & M
|
||||
1. **Fork the Project** on GitHub
|
||||
2. **Clone your fork** locally
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/react-typescript-todolist.git
|
||||
git clone https://github.com/YOUR_USERNAME/tasknote.git
|
||||
```
|
||||
3. **Create a feature branch**
|
||||
```bash
|
||||
@@ -269,9 +254,9 @@ For detailed setup instructions and development workflows, see [CONTRIBUTING.md]
|
||||
|
||||
**Ricardo Campos** - Full-Stack Developer & Project Maintainer
|
||||
|
||||
- **GitHub**: [@ricardo-campos-org](https://github.com/ricardo-campos-org)
|
||||
- **GitHub**: [@RMCampos](https://github.com/RMCampos)
|
||||
- **Twitter/X**: [@RMCamposs](https://x.com/RMCamposs)
|
||||
- **LinkedIn**: [Ricardo Campos](https://www.linkedin.com/in/ricardo-campos-org/)
|
||||
- **LinkedIn**: [Ricardo Campos](https://www.linkedin.com/in/ricardompcampos/)
|
||||
|
||||
### About the Developer
|
||||
Ricardo is a passionate full-stack developer with expertise in modern web technologies, cloud architecture, and agile development practices. This project showcases his skills in:
|
||||
@@ -290,8 +275,8 @@ For questions, suggestions, or collaboration opportunities:
|
||||
|
||||
- **Email**: Contact via GitHub issues or discussions
|
||||
- **Twitter/X**: [@RMCamposs](https://x.com/RMCamposs) for quick questions
|
||||
- **GitHub Issues**: [Create an issue](https://github.com/ricardo-campos-org/react-typescript-todolist/issues) for bugs or feature requests
|
||||
- **GitHub Discussions**: [Join discussions](https://github.com/ricardo-campos-org/react-typescript-todolist/discussions) for general questions
|
||||
- **GitHub Issues**: [Create an issue](https://github.com/rmcampos/tasknote/issues) for bugs or feature requests
|
||||
- **GitHub Discussions**: [Join discussions](https://github.com/rmcampos/tasknote/discussions) for general questions
|
||||
|
||||
## 📄 License
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ tasks:
|
||||
|
||||
docker-build-api:
|
||||
desc: Build the tasknote-api prod-ready docker image, tagging it as candidate
|
||||
cmd: cd server && mvn -Pnative -DskipTests spring-boot:build-image -Dspring-boot.build-image.imageName=ghcr.io/rmcampos/tasknote/api:latest
|
||||
cmd: cd server && mvn -Pnative -DskipTests spring-boot:build-image -Dspring-boot.build-image.imageName=ghcr.io/rmcampos/tasknote/api:latest -Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
|
||||
|
||||
prod-up-web:
|
||||
desc: Speed up the tasknote-web prod-like image, building it if required
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Client - Front-end
|
||||
|
||||
npm ci
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when installing dependencies. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$CHECK" ]; then
|
||||
npm start
|
||||
else
|
||||
echo "Running checks..."
|
||||
echo "1/3 - Lint started..."
|
||||
npm run lint:fix
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running lint. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "2/3 - Build started..."
|
||||
npm run build
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running build. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "3/3 - Tests started..."
|
||||
npm run test:no-watch
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running test. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "You're good to go! Good job!"
|
||||
exit 0
|
||||
fi
|
||||
@@ -16,6 +16,7 @@ import Register from './views/Register';
|
||||
import EmailConfirmation from './views/EmailConfirmation';
|
||||
import ResetPassword from './views/ResetPassword';
|
||||
import CompleteResetPassword from './views/CompleteResetPassword';
|
||||
import SharedNote from './views/SharedNote';
|
||||
import './styles/custom.scss';
|
||||
|
||||
/**
|
||||
@@ -65,6 +66,10 @@ function App(): React.ReactNode {
|
||||
path: '/finish-reset-password',
|
||||
element: <CompleteResetPassword />
|
||||
},
|
||||
{
|
||||
path: '/public/notes/:token',
|
||||
element: <SharedNote />
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/" replace />
|
||||
@@ -86,6 +91,10 @@ function App(): React.ReactNode {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/public/notes/:token',
|
||||
element: <SharedNote />
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFound />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { describe, vi, it, expect } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, vi, it, expect, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
|
||||
import ModalMarkdown from '../../components/ModalMarkdown';
|
||||
|
||||
describe('ModalMarkdown Component', () => {
|
||||
@@ -11,6 +11,14 @@ describe('ModalMarkdown Component', () => {
|
||||
onHide: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the modal with the correct title and markdown text', () => {
|
||||
render(<ModalMarkdown {...props} />);
|
||||
|
||||
@@ -36,4 +44,64 @@ describe('ModalMarkdown Component', () => {
|
||||
|
||||
expect(screen.getByTestId('modal-header-title').innerHTML).toBe('No title');
|
||||
});
|
||||
|
||||
it('should show Source and Copy buttons', () => {
|
||||
render(<ModalMarkdown {...props} />);
|
||||
|
||||
expect(screen.getByTestId('modal-source-button')).toBeDefined();
|
||||
expect(screen.getByTestId('modal-copy-button')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should toggle to source view when Source button is clicked', () => {
|
||||
render(<ModalMarkdown {...props} />);
|
||||
|
||||
expect(screen.queryByTestId('markdown-source-view')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-source-button'));
|
||||
|
||||
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
|
||||
expect(screen.getByTestId('markdown-source-view').textContent).toBe('# Test Markdown');
|
||||
});
|
||||
|
||||
it('should toggle back to rendered view when Source button is clicked again', () => {
|
||||
render(<ModalMarkdown {...props} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-source-button'));
|
||||
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-source-button'));
|
||||
expect(screen.queryByTestId('markdown-source-view')).toBeNull();
|
||||
});
|
||||
|
||||
it('should call clipboard writeText with markdownText when Copy button is clicked', async () => {
|
||||
render(<ModalMarkdown {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('modal-copy-button'));
|
||||
});
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('# Test Markdown');
|
||||
});
|
||||
|
||||
it('should show "Copied!" text after Copy button is clicked', async () => {
|
||||
render(<ModalMarkdown {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('modal-copy-button'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('modal-copy-button').textContent).toBe('Copied!');
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset source view state when modal is closed', () => {
|
||||
render(<ModalMarkdown {...props} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-source-button'));
|
||||
expect(screen.getByTestId('markdown-source-view')).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(props.onHide).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -143,7 +143,9 @@ describe('NoteAdd Component', () => {
|
||||
description: 'Note content',
|
||||
url: '',
|
||||
tag: '',
|
||||
lastUpdate: ''
|
||||
lastUpdate: '',
|
||||
shared: false,
|
||||
shareToken: null
|
||||
}
|
||||
expect(api.postJSON).toHaveBeenCalledWith(ApiConfig.notesUrl, newNote);
|
||||
});
|
||||
|
||||
@@ -81,6 +81,11 @@ const api = {
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
getJSONNoAuth: async (url: string) => {
|
||||
const response = await fetch(url, getRequestInit('GET', {}, false));
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
postJSON: async (url: string, payload: object) => {
|
||||
const response = await fetch(url, getRequestInit('POST', payload, isAddAuth(url)));
|
||||
return handleResponse(response);
|
||||
|
||||
@@ -26,6 +26,8 @@ const ApiConfig = {
|
||||
|
||||
notesUrl: `${server}/rest/notes`,
|
||||
|
||||
publicNotesUrl: `${server}/public/notes`,
|
||||
|
||||
userUrl: `${server}/rest/users`
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import Markdown from 'react-markdown';
|
||||
@@ -23,11 +23,31 @@ type Props = {
|
||||
* @returns {React.ReactNode} the Markdown component rendered.
|
||||
*/
|
||||
const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
const [showSource, setShowSource] = useState<boolean>(false);
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const handleToggleSource = () => setShowSource(prev => !prev);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(props.markdownText).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}).catch(() => {
|
||||
// clipboard write failed silently; no state change
|
||||
});
|
||||
};
|
||||
|
||||
const handleHide = () => {
|
||||
setShowSource(false);
|
||||
setCopied(false);
|
||||
props.onHide();
|
||||
};
|
||||
|
||||
return props.show
|
||||
? (
|
||||
<Modal
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
onHide={handleHide}
|
||||
backdrop="static"
|
||||
keyboard={false}
|
||||
size="xl"
|
||||
@@ -42,12 +62,34 @@ const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="markdown-modal">
|
||||
<Markdown remarkPlugins={[remarkGfm]}>{props.markdownText}</Markdown>
|
||||
{showSource
|
||||
? (
|
||||
<pre className="markdown-source" data-testid="markdown-source-view">
|
||||
{props.markdownText}
|
||||
</pre>
|
||||
)
|
||||
: (
|
||||
<Markdown remarkPlugins={[remarkGfm]}>{props.markdownText}</Markdown>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="outline-secondary" onClick={props.onHide}>
|
||||
<Modal.Footer className="d-flex flex-wrap gap-2 justify-content-end">
|
||||
<Button variant="outline-secondary" onClick={handleHide}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant={showSource ? 'info' : 'outline-info'}
|
||||
onClick={handleToggleSource}
|
||||
data-testid="modal-source-button"
|
||||
>
|
||||
Source
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCopy}
|
||||
data-testid="modal-copy-button"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -154,3 +154,24 @@
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-source {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
padding: 1em;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.markdown-source {
|
||||
font-size: 12px;
|
||||
padding: 0.75em;
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@ const enTranslations = {
|
||||
note_form_submit: 'Save note',
|
||||
note_table_btn_edit: 'Edit',
|
||||
note_table_btn_delete: 'Delete',
|
||||
note_action_share: 'Share',
|
||||
note_action_unshare: 'Unshare',
|
||||
note_action_copy_link: 'Copy link',
|
||||
|
||||
about_page_title_one: 'About the',
|
||||
about_page_title_two: 'TaskNote App',
|
||||
|
||||
@@ -114,6 +114,9 @@ const ptBrTranslations = {
|
||||
note_form_submit: 'Salvar nota',
|
||||
note_table_btn_edit: 'Alterar',
|
||||
note_table_btn_delete: 'Excluir',
|
||||
note_action_share: 'Compartilhar',
|
||||
note_action_unshare: 'Parar de compartilhar',
|
||||
note_action_copy_link: 'Copiar link',
|
||||
|
||||
about_page_title_one: 'Sobre o',
|
||||
about_page_title_two: 'App TaskNote',
|
||||
|
||||
@@ -114,6 +114,9 @@ const ruTranslations = {
|
||||
note_form_submit: 'Сохранить заметку',
|
||||
note_table_btn_edit: 'Редактировать',
|
||||
note_table_btn_delete: 'Удалить',
|
||||
note_action_share: 'Поделиться',
|
||||
note_action_unshare: 'Закрыть доступ',
|
||||
note_action_copy_link: 'Копировать ссылку',
|
||||
|
||||
about_page_title_one: 'около',
|
||||
about_page_title_two: 'TaskNote App',
|
||||
|
||||
@@ -114,6 +114,9 @@ const esTranslations = {
|
||||
note_form_submit: 'Guardar nota',
|
||||
note_table_btn_edit: 'Editar',
|
||||
note_table_btn_delete: 'Eliminar',
|
||||
note_action_share: 'Compartir',
|
||||
note_action_unshare: 'Dejar de compartir',
|
||||
note_action_copy_link: 'Copiar enlace',
|
||||
|
||||
about_page_title_one: 'Acerca de',
|
||||
about_page_title_two: 'TaskNote App',
|
||||
|
||||
@@ -5,6 +5,8 @@ type NoteResponse = {
|
||||
url: string | null;
|
||||
tag: string;
|
||||
lastUpdate: string;
|
||||
shared: boolean;
|
||||
shareToken: string | null;
|
||||
};
|
||||
|
||||
export type { NoteResponse };
|
||||
|
||||
@@ -104,6 +104,34 @@ function Home(): React.ReactNode {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Share or unshare a note.
|
||||
*
|
||||
* @param {NoteResponse} note The note to share or unshare.
|
||||
*/
|
||||
const toggleShareNote = async (note: NoteResponse): Promise<void> => {
|
||||
try {
|
||||
const action = note.shared ? 'unshare' : 'share';
|
||||
await api.putJSON(`${ApiConfig.notesUrl}/${note.id}/${action}`, {});
|
||||
loadAllNotes();
|
||||
}
|
||||
catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy share link to clipboard.
|
||||
*
|
||||
* @param {NoteResponse} note The shared note.
|
||||
*/
|
||||
const copyShareLink = (note: NoteResponse): void => {
|
||||
const link = `${window.location.origin}/public/notes/${note.shareToken}`;
|
||||
navigator.clipboard.writeText(link).catch(() => {
|
||||
setErrorMessage('Failed to copy link to clipboard.');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply filters to a given set of tasks and notes, updating displayed state.
|
||||
*
|
||||
@@ -528,6 +556,22 @@ function Home(): React.ReactNode {
|
||||
{t('task_table_action_clone')}
|
||||
</Dropdown.Item>
|
||||
</NavLink>
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={() => toggleShareNote(note)}
|
||||
data-testid={`note-dropdown-share-item-${note.id}`}
|
||||
>
|
||||
{note.shared ? t('note_action_unshare') : t('note_action_share')}
|
||||
</Dropdown.Item>
|
||||
{note.shared && note.shareToken && (
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={() => copyShareLink(note)}
|
||||
data-testid={`note-dropdown-copy-link-${note.id}`}
|
||||
>
|
||||
{t('note_action_copy_link')}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={() => deleteNote(note.id)}
|
||||
|
||||
@@ -140,7 +140,9 @@ function NoteAdd(): React.ReactNode {
|
||||
description: noteContent,
|
||||
url: noteUrl,
|
||||
tag: noteTag,
|
||||
lastUpdate: ''
|
||||
lastUpdate: '',
|
||||
shared: false,
|
||||
shareToken: null
|
||||
};
|
||||
|
||||
const added: boolean = await addNote(payload);
|
||||
@@ -157,7 +159,9 @@ function NoteAdd(): React.ReactNode {
|
||||
description: noteContent,
|
||||
url: noteUrl,
|
||||
tag: noteTag,
|
||||
lastUpdate: ''
|
||||
lastUpdate: '',
|
||||
shared: false,
|
||||
shareToken: null
|
||||
};
|
||||
|
||||
const edited: boolean = await submitEditNote(payload);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Col, Container, Row } from 'react-bootstrap';
|
||||
import { useParams } from 'react-router';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { NoteResponse } from '../../types/NoteResponse';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
|
||||
/**
|
||||
* SharedNote component for displaying a publicly shared note.
|
||||
* Accessible without authentication.
|
||||
*
|
||||
* @returns {React.ReactNode} The rendered SharedNote component.
|
||||
*/
|
||||
function SharedNote(): React.ReactNode {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const [note, setNote] = useState<NoteResponse | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setErrorMessage('Invalid share link.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
api
|
||||
.getJSONNoAuth(`${ApiConfig.publicNotesUrl}/${token}`)
|
||||
.then((data: NoteResponse) => {
|
||||
setNote(data);
|
||||
})
|
||||
.catch(() => {
|
||||
setErrorMessage('Note not found or no longer shared.');
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [token]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Container fluid className="mt-5 text-center">
|
||||
<p>Loading...</p>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage || !note) {
|
||||
return (
|
||||
<Container fluid className="mt-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col xs={12} md={8}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Note not found</Card.Title>
|
||||
<p className="text-muted">{errorMessage || 'This note is not available.'}</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container fluid className="mt-3">
|
||||
<Row className="justify-content-center">
|
||||
<Col xs={12} md={10} lg={8}>
|
||||
<Card>
|
||||
<Card.Header className="d-flex justify-content-between align-items-center">
|
||||
<small className="text-muted">TaskNote · Shared Note (Read only)</small>
|
||||
{note.tag && (
|
||||
<small className="text-muted">
|
||||
#
|
||||
{note.tag}
|
||||
</small>
|
||||
)}
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Card.Title>{note.title}</Card.Title>
|
||||
{note.url && (
|
||||
<p>
|
||||
<a href={note.url} target="_blank" rel="noopener noreferrer">
|
||||
{note.url}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
<Markdown remarkPlugins={[remarkGfm]}>{note.description}</Markdown>
|
||||
</Card.Body>
|
||||
{note.lastUpdate && (
|
||||
<Card.Footer className="text-muted">
|
||||
<small>
|
||||
Last updated:
|
||||
{' '}
|
||||
{note.lastUpdate}
|
||||
</small>
|
||||
</Card.Footer>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default SharedNote;
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
ports:
|
||||
- "8585:8585"
|
||||
- "5005:5005"
|
||||
image: maven:3.9.9-eclipse-temurin-21
|
||||
image: maven:3.9.12-eclipse-temurin-25
|
||||
entrypoint: './mvnw -ntp spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" -Dmaven.plugin.validation=VERBOSE'
|
||||
working_dir: /app
|
||||
volumes:
|
||||
|
||||
+11
-8
@@ -6,13 +6,13 @@ services:
|
||||
depends_on:
|
||||
tasknote-api:
|
||||
condition: service_healthy
|
||||
image: docker.io/rmcampos/tasknote:app-latest
|
||||
image: ghcr.io/rmcampos/tasknote/app:latest
|
||||
build:
|
||||
context: client
|
||||
context: ./client
|
||||
dockerfile: Dockerfile
|
||||
ports: ["5000:5000"]
|
||||
environment:
|
||||
VITE_BACKEND_SERVER: http://tasknote-api:8585
|
||||
VITE_BACKEND_SERVER: http://localhost:8585
|
||||
networks:
|
||||
- tasknote-network
|
||||
|
||||
@@ -33,16 +33,19 @@ services:
|
||||
SECURITY_KEY: this-is-a-very-long-security-key-for-dev
|
||||
MAILGUN_APIKEY: invalid-api-key-only-placeholder
|
||||
ports: ["8585:8585"]
|
||||
image: docker.io/rmcampos/tasknote:api-latest
|
||||
build:
|
||||
context: server
|
||||
dockerfile: Dockerfile
|
||||
image: ghcr.io/rmcampos/tasknote/api:latest
|
||||
healthcheck:
|
||||
test: ["CMD", "/layers/local_healthcheck/healthcheck/bin/healthcheck"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
networks:
|
||||
- tasknote-network
|
||||
|
||||
tasknote-db:
|
||||
container_name: tasknote-db
|
||||
image: postgres:15.8-bookworm
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_DB: tasknote
|
||||
POSTGRES_USER: tasknoteuser
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build check.go
|
||||
chmod +x check
|
||||
mv check buildpacks/healthcheck/healthcheck
|
||||
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
LAYERS_DIR="$1"
|
||||
|
||||
echo "CNB_BUILDPACK_DIR: ${CNB_BUILDPACK_DIR}"
|
||||
echo "Contents of buildpack dir:"
|
||||
ls -la "${CNB_BUILDPACK_DIR}"
|
||||
|
||||
BINARY_SOURCE="${CNB_BUILDPACK_DIR}/healthcheck"
|
||||
|
||||
if [ ! -f "${BINARY_SOURCE}" ]; then
|
||||
echo "ERROR: binary not found at ${BINARY_SOURCE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LAYER_DIR="${LAYERS_DIR}/healthcheck"
|
||||
mkdir -p "${LAYER_DIR}/bin"
|
||||
|
||||
cp "${BINARY_SOURCE}" "${LAYER_DIR}/bin/healthcheck"
|
||||
chmod +x "${LAYER_DIR}/bin/healthcheck"
|
||||
|
||||
cat > "${LAYER_DIR}.toml" <<EOL
|
||||
[types]
|
||||
launch = true
|
||||
build = false
|
||||
cache = false
|
||||
EOL
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
@@ -0,0 +1,11 @@
|
||||
api = "0.7"
|
||||
|
||||
[buildpack]
|
||||
id = "local/healthcheck"
|
||||
version = "0.1.0"
|
||||
name = "Healthcheck Binary Buildpack"
|
||||
[[stacks]]
|
||||
id = "io.buildpacks.stacks.jammy"
|
||||
|
||||
[[stacks]]
|
||||
id = "*"
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
// Use 127.0.0.1 as the healthcheck runs inside the container
|
||||
resp, err := client.Get("http://127.0.0.1:8585/health")
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
+27
-10
@@ -5,13 +5,13 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.9</version>
|
||||
<version>4.0.3</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>br.com.tasknoteapp</groupId>
|
||||
<artifactId>server</artifactId>
|
||||
<version>7</version>
|
||||
<version>11</version>
|
||||
<name>tasknote-api</name>
|
||||
<description>Java backend REST API to serve TaskNote frontend client</description>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<!-- Properties -->
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<java.version>25</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<skip.integration.tests>true</skip.integration.tests>
|
||||
@@ -80,7 +80,11 @@
|
||||
<!-- Spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-restclient</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -105,6 +109,10 @@
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-flyway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
@@ -117,12 +125,12 @@
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<artifactId>spring-boot-starter-test-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -153,8 +161,8 @@
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.hibernate.orm.tooling</groupId>
|
||||
<artifactId>hibernate-enhance-maven-plugin</artifactId>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-maven-plugin</artifactId>
|
||||
<version>${hibernate.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -165,7 +173,7 @@
|
||||
<configuration>
|
||||
<enableLazyInitialization>true</enableLazyInitialization>
|
||||
<enableDirtyTracking>true</enableDirtyTracking>
|
||||
<enableAssociationManagement>true</enableAssociationManagement>
|
||||
<enableAssociationManagement>false</enableAssociationManagement>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
@@ -351,6 +359,15 @@
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<image>
|
||||
<name>ghcr.io/rmcampos/tasknote/api:latest</name>
|
||||
<buildpacks>
|
||||
<buildpack>file://${project.basedir}/buildpacks/healthcheck</buildpack>
|
||||
<buildpack>urn:cnb:builder:paketo-buildpacks/java-native-image</buildpack>
|
||||
</buildpacks>
|
||||
</image>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[build]
|
||||
builder = "paketobuildpacks/builder-jammy-tiny:latest"
|
||||
|
||||
[[build.buildpacks]]
|
||||
uri = "buildpacks/healthcheck"
|
||||
|
||||
[[build.buildpacks]]
|
||||
uri = "urn:cnb:builder:paketo-buildpacks/java-native-image"
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Server - Back-end
|
||||
|
||||
if [ -z "$CHECK" ]; then
|
||||
./mvnw -ntp \
|
||||
spring-boot:run \
|
||||
-Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" \
|
||||
-Dmaven.plugin.validation=VERBOSE
|
||||
else
|
||||
echo "Running checks..."
|
||||
echo "1/3 - Check Style started..."
|
||||
./mvnw --no-transfer-progress checkstyle:check -Dcheckstyle.skip=false
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running Check Style. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "2/3 - Build started..."
|
||||
./mvnw --no-transfer-progress clean compile -DskipTests
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running build. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "3/3 - Tests started..."
|
||||
./mvnw --no-transfer-progress clean verify -P tests --file pom.xml
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running test. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "You're good to go! Good job!"
|
||||
exit 0
|
||||
fi
|
||||
@@ -5,7 +5,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@@ -23,8 +22,7 @@ public class CorsConfig implements WebMvcConfigurer {
|
||||
*
|
||||
* @param registry CorsRegistry instance.
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
public void addCorsMappings(@NonNull CorsRegistry registry) {
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
if (allowedOrigins != null && allowedOrigins.length > 0) {
|
||||
logger.info("CORS policy allowed origins: {}", Arrays.asList(allowedOrigins));
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.springframework.security.config.annotation.authentication.configurati
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@@ -51,12 +52,16 @@ public class SecurityConfig {
|
||||
.permitAll()
|
||||
.requestMatchers("/auth/**")
|
||||
.permitAll()
|
||||
.requestMatchers("/public/**")
|
||||
.permitAll()
|
||||
.requestMatchers("/rest/**")
|
||||
.authenticated()
|
||||
.anyRequest()
|
||||
.permitAll())
|
||||
.httpBasic(AbstractHttpConfigurer::disable)
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(
|
||||
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.exceptionHandling(
|
||||
exceptionHandling ->
|
||||
exceptionHandling.authenticationEntryPoint(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package br.com.tasknoteapp.server.controller;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.NoteEntity;
|
||||
import br.com.tasknoteapp.server.exception.NoteNotFoundException;
|
||||
import br.com.tasknoteapp.server.request.NotePatchRequest;
|
||||
import br.com.tasknoteapp.server.request.NoteRequest;
|
||||
@@ -10,12 +9,12 @@ import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -49,7 +48,7 @@ public class NoteController {
|
||||
* @throws NoteNotFoundException when note not found.
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public NoteResponse getTaskById(@NonNull @PathVariable Long id) {
|
||||
public NoteResponse getTaskById(@PathVariable Long id) {
|
||||
return noteService.getNoteById(id);
|
||||
}
|
||||
|
||||
@@ -63,7 +62,7 @@ public class NoteController {
|
||||
*/
|
||||
@PatchMapping("/{id}")
|
||||
public ResponseEntity<NoteResponse> patchNote(
|
||||
@PathVariable @NonNull Long id, @RequestBody @Valid NotePatchRequest noteRequest) {
|
||||
@PathVariable Long id, @RequestBody @Valid NotePatchRequest noteRequest) {
|
||||
|
||||
return ResponseEntity.ok(noteService.patchNote(id, noteRequest));
|
||||
}
|
||||
@@ -77,8 +76,8 @@ public class NoteController {
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<NoteResponse> postNotes(@RequestBody @Valid NoteRequest noteRequest) {
|
||||
NoteEntity createdNote = noteService.createNote(noteRequest);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(NoteResponse.fromEntity(createdNote));
|
||||
NoteResponse createdNote = noteService.createNote(noteRequest);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(createdNote);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,8 +87,32 @@ public class NoteController {
|
||||
* @throws NoteNotFoundException when note not found.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteNote(@NonNull @PathVariable Long id) {
|
||||
public ResponseEntity<Void> deleteNote(@PathVariable Long id) {
|
||||
noteService.deleteNote(id);
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Share a note publicly.
|
||||
*
|
||||
* @param id Note identification.
|
||||
* @return NoteResponse containing the share token.
|
||||
* @throws NoteNotFoundException when note not found.
|
||||
*/
|
||||
@PutMapping("/{id}/share")
|
||||
public ResponseEntity<NoteResponse> shareNote(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(noteService.shareNote(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unshare a note, revoking public access.
|
||||
*
|
||||
* @param id Note identification.
|
||||
* @return NoteResponse with the updated note.
|
||||
* @throws NoteNotFoundException when note not found.
|
||||
*/
|
||||
@PutMapping("/{id}/unshare")
|
||||
public ResponseEntity<NoteResponse> unshareNote(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(noteService.unshareNote(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package br.com.tasknoteapp.server.controller;
|
||||
|
||||
import br.com.tasknoteapp.server.exception.NoteNotFoundException;
|
||||
import br.com.tasknoteapp.server.response.NoteResponse;
|
||||
import br.com.tasknoteapp.server.service.NoteService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** This class provides public (unauthenticated) resources for shared notes. */
|
||||
@RestController
|
||||
@RequestMapping("/public/notes")
|
||||
public class PublicNoteController {
|
||||
|
||||
private final NoteService noteService;
|
||||
|
||||
public PublicNoteController(NoteService noteService) {
|
||||
this.noteService = noteService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a publicly shared note by its share token.
|
||||
*
|
||||
* @param token The unique share token for the note.
|
||||
* @return NoteResponse containing the shared note data.
|
||||
* @throws NoteNotFoundException when note is not found or not shared.
|
||||
*/
|
||||
@GetMapping("/{token}")
|
||||
public ResponseEntity<NoteResponse> getSharedNote(@PathVariable String token) {
|
||||
return ResponseEntity.ok(noteService.getSharedNote(token));
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
@@ -48,7 +47,7 @@ public class TaskController {
|
||||
* @throws TaskNotFoundException when task not found.
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public TaskResponse getTaskById(@NonNull @PathVariable Long id) {
|
||||
public TaskResponse getTaskById(@PathVariable Long id) {
|
||||
return taskService.getTaskById(id);
|
||||
}
|
||||
|
||||
@@ -62,7 +61,7 @@ public class TaskController {
|
||||
*/
|
||||
@PatchMapping("/{id}")
|
||||
public ResponseEntity<TaskResponse> patchTask(
|
||||
@PathVariable @NonNull Long id, @RequestBody @Valid TaskPatchRequest taskRequest) {
|
||||
@PathVariable Long id, @RequestBody @Valid TaskPatchRequest taskRequest) {
|
||||
return ResponseEntity.ok(taskService.patchTask(id, taskRequest));
|
||||
}
|
||||
|
||||
@@ -86,7 +85,7 @@ public class TaskController {
|
||||
* @throws TaskNotFoundException when task not found.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteTask(@NonNull @PathVariable Long id) {
|
||||
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
|
||||
taskService.deleteTask(id);
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -30,15 +29,18 @@ public class NoteEntity {
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
private UserEntity user;
|
||||
|
||||
@OneToOne(mappedBy = "note", fetch = FetchType.LAZY)
|
||||
private NoteUrlEntity noteUrl;
|
||||
|
||||
@Column(name = "tag", nullable = true, length = 30)
|
||||
private String tag;
|
||||
|
||||
@Column(name = "last_update")
|
||||
private LocalDateTime lastUpdate;
|
||||
|
||||
@Column(name = "shared", nullable = false)
|
||||
private boolean shared = false;
|
||||
|
||||
@Column(name = "share_token", length = 36)
|
||||
private String shareToken;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -71,14 +73,6 @@ public class NoteEntity {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public NoteUrlEntity getNoteUrl() {
|
||||
return noteUrl;
|
||||
}
|
||||
|
||||
public void setNoteUrl(NoteUrlEntity noteUrl) {
|
||||
this.noteUrl = noteUrl;
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
return tag;
|
||||
}
|
||||
@@ -95,6 +89,22 @@ public class NoteEntity {
|
||||
this.lastUpdate = lastUpdate;
|
||||
}
|
||||
|
||||
public boolean isShared() {
|
||||
return shared;
|
||||
}
|
||||
|
||||
public void setShared(boolean shared) {
|
||||
this.shared = shared;
|
||||
}
|
||||
|
||||
public String getShareToken() {
|
||||
return shareToken;
|
||||
}
|
||||
|
||||
public void setShareToken(String shareToken) {
|
||||
this.shareToken = shareToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
@@ -41,9 +40,6 @@ public class UserEntity implements UserDetails {
|
||||
@Column(name = "name", length = 20)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "user")
|
||||
private List<TaskEntity> tasks;
|
||||
|
||||
@Column(name = "email_confirmed_at", nullable = true)
|
||||
private LocalDateTime emailConfirmedAt;
|
||||
|
||||
@@ -147,14 +143,6 @@ public class UserEntity implements UserDetails {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<TaskEntity> getTasks() {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
public void setTasks(List<TaskEntity> tasks) {
|
||||
this.tasks = tasks;
|
||||
}
|
||||
|
||||
public LocalDateTime getEmailConfirmedAt() {
|
||||
return emailConfirmedAt;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@@ -19,9 +18,9 @@ public class HeaderVersionFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
response.setHeader("X-BUILD-INFO", appVersionService.getVersion());
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
@@ -9,7 +9,6 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
@@ -28,9 +27,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
final String authorizationHeader = request.getHeader("Authorization");
|
||||
|
||||
|
||||
+1
-3
@@ -4,14 +4,12 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.aot.hint.ProxyHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/** This class creates a RuntimeHint for the ServletRequest class. */
|
||||
public class HttpServletRequestRuntimeHint implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
try {
|
||||
ProxyHints proxies = hints.proxies();
|
||||
proxies.registerJdkProxy(HttpServletRequest.class);
|
||||
|
||||
@@ -7,8 +7,6 @@ import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* This class creates RuntimeHints for JJWT exceptions to ensure they are available at runtime in
|
||||
@@ -20,7 +18,7 @@ public class JjwtRuntimeHints {
|
||||
|
||||
static class JjwtHintsRegistrar implements RuntimeHintsRegistrar {
|
||||
@Override
|
||||
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
// Register JJWT exceptions for reflection
|
||||
hints
|
||||
.reflection()
|
||||
|
||||
@@ -2,6 +2,7 @@ package br.com.tasknoteapp.server.repository;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.NoteEntity;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
@@ -10,6 +11,10 @@ public interface NoteRepository extends JpaRepository<NoteEntity, Long> {
|
||||
|
||||
List<NoteEntity> findAllByUser_id(Long userId);
|
||||
|
||||
Optional<NoteEntity> findByShareToken(String shareToken);
|
||||
|
||||
Optional<NoteEntity> findByIdAndUser_id(Long id, Long userId);
|
||||
|
||||
@Query(
|
||||
"select n from NoteEntity n where (upper(n.title) like %?1% or upper(n.description) like"
|
||||
+ " %?1%) and n.user.id = ?2")
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package br.com.tasknoteapp.server.repository;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.NoteUrlEntity;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/** This interface represents a note url repository, for database access. */
|
||||
public interface NoteUrlRepository extends JpaRepository<NoteUrlEntity, Long> {
|
||||
|
||||
Optional<NoteUrlEntity> findByNote_id(Long noteId);
|
||||
|
||||
List<NoteUrlEntity> findAllByNote_idIn(List<Long> noteIds);
|
||||
|
||||
void deleteByNote_id(Long noteId);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public interface UserPwdLimitRepository extends JpaRepository<UserPwdLimitEntity
|
||||
|
||||
List<UserPwdLimitEntity> findAllByUser_id(Long userId, Sort sort);
|
||||
|
||||
List<UserPwdLimitEntity> findTop3ByUser_idOrderByWhenHappenedDesc(Long userId);
|
||||
|
||||
@Modifying
|
||||
@Query("delete UserPwdLimitEntity u where u.user.id = ?1")
|
||||
void deleteAllForUser(Long userId);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package br.com.tasknoteapp.server.response;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.NoteEntity;
|
||||
import br.com.tasknoteapp.server.entity.NoteUrlEntity;
|
||||
import br.com.tasknoteapp.server.util.TimeAgoUtil;
|
||||
import java.util.Objects;
|
||||
|
||||
/** This record represents a task and its urls object to be returned. */
|
||||
public record NoteResponse(
|
||||
Long id, String title, String description, String url, String lastUpdate, String tag) {
|
||||
Long id, String title, String description, String url, String lastUpdate, String tag,
|
||||
boolean shared, String shareToken) {
|
||||
|
||||
/**
|
||||
* Creates a NoteResponse given a NoteEntity and its Urls.
|
||||
@@ -15,9 +14,7 @@ public record NoteResponse(
|
||||
* @param entity The NoteEntity source data.
|
||||
* @return NoteResponse instance with all note data and urls, if any.
|
||||
*/
|
||||
public static NoteResponse fromEntity(NoteEntity entity) {
|
||||
NoteUrlEntity noteUrl = entity.getNoteUrl();
|
||||
String url = Objects.isNull(noteUrl) ? null : noteUrl.getUrl();
|
||||
public static NoteResponse fromEntity(NoteEntity entity, String url) {
|
||||
String timeAgoFmt = TimeAgoUtil.format(entity.getLastUpdate());
|
||||
|
||||
return new NoteResponse(
|
||||
@@ -26,6 +23,8 @@ public record NoteResponse(
|
||||
entity.getDescription(),
|
||||
url,
|
||||
timeAgoFmt,
|
||||
entity.getTag());
|
||||
entity.getTag(),
|
||||
entity.isShared(),
|
||||
entity.getShareToken());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Service to retrieve application version information. */
|
||||
@@ -22,7 +21,7 @@ public class AppVersionService {
|
||||
*
|
||||
* @param buildProperties the build properties injected by Spring Boot
|
||||
*/
|
||||
public AppVersionService(@Autowired(required = false) @Nullable BuildProperties buildProperties) {
|
||||
public AppVersionService(@Autowired(required = false) BuildProperties buildProperties) {
|
||||
this.buildProperties = buildProperties;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ import java.util.UUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
@@ -513,8 +511,10 @@ public class AuthService {
|
||||
}
|
||||
|
||||
private void checkLoginAttemptLimit(Long userId) {
|
||||
Sort sort = Sort.by(Direction.DESC, "whenHappened");
|
||||
List<UserPwdLimitEntity> userPwdList = userPwdLimitRepository.findAllByUser_id(userId, sort);
|
||||
// Fetch only the 3 most recent failed attempts to avoid loading unbounded rows for
|
||||
// targeted/brute-forced accounts.
|
||||
List<UserPwdLimitEntity> userPwdList =
|
||||
userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(userId);
|
||||
|
||||
logger.warn("login count attempt for user {}: {}", userId, userPwdList.size());
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwd;
|
||||
import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwdConfirm;
|
||||
import br.com.tasknoteapp.server.templates.MailgunTemplateSignUp;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.restclient.RestTemplateBuilder;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -53,7 +54,11 @@ public class MailgunEmailService {
|
||||
this.senderEmail = sender;
|
||||
this.targetEnv = targetEnv;
|
||||
this.restTemplate =
|
||||
templateBuilder.defaultHeader(HttpHeaders.AUTHORIZATION, basicAuth("api", apiKey)).build();
|
||||
templateBuilder
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.readTimeout(Duration.ofSeconds(10))
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, basicAuth("api", apiKey))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,6 @@ import br.com.tasknoteapp.server.entity.NoteEntity;
|
||||
import br.com.tasknoteapp.server.entity.NoteUrlEntity;
|
||||
import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
import br.com.tasknoteapp.server.exception.NoteNotFoundException;
|
||||
import br.com.tasknoteapp.server.exception.TaskNotFoundException;
|
||||
import br.com.tasknoteapp.server.repository.NoteRepository;
|
||||
import br.com.tasknoteapp.server.repository.NoteUrlRepository;
|
||||
import br.com.tasknoteapp.server.request.NotePatchRequest;
|
||||
@@ -14,11 +13,13 @@ import br.com.tasknoteapp.server.util.AuthUtil;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** This class implements the NoteService interface methods. */
|
||||
@@ -67,35 +68,39 @@ public class NoteService {
|
||||
List<NoteEntity> notes = noteRepository.findAllByUser_id(user.getId());
|
||||
logger.info(notes.size() + " notes found!");
|
||||
|
||||
return notes.stream().map(NoteResponse::fromEntity).toList();
|
||||
return getNotesUrl(notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a note by its id.
|
||||
*
|
||||
* @param noteId The task id in the database.
|
||||
* @return {@link NoteResponse} with the found task or throw a {@link TaskNotFoundException}.
|
||||
* @param noteId The note id in the database.
|
||||
* @return {@link NoteResponse} with the found note or throw a {@link NoteNotFoundException}.
|
||||
*/
|
||||
public NoteResponse getNoteById(@NonNull Long noteId) {
|
||||
public NoteResponse getNoteById(Long noteId) {
|
||||
UserEntity user = getCurrentUser();
|
||||
logger.info("Get note " + noteId + " to user " + user.getId());
|
||||
|
||||
Optional<NoteEntity> task = noteRepository.findById(noteId);
|
||||
if (task.isEmpty()) {
|
||||
Optional<NoteEntity> note = noteRepository.findById(noteId);
|
||||
if (note.isEmpty()) {
|
||||
throw new NoteNotFoundException();
|
||||
}
|
||||
|
||||
if (!note.get().getUser().getId().equals(user.getId())) {
|
||||
throw new NoteNotFoundException();
|
||||
}
|
||||
|
||||
logger.info("Note found! Id " + noteId);
|
||||
return NoteResponse.fromEntity(task.get());
|
||||
return NoteResponse.fromEntity(note.get(), getNoteUrl(noteId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a note for the user.
|
||||
*
|
||||
* @param noteRequest The note content.
|
||||
* @return {@link NoteEntity} created in the database
|
||||
* @return {@link NoteResponse} with created note data.
|
||||
*/
|
||||
public NoteEntity createNote(NoteRequest noteRequest) {
|
||||
public NoteResponse createNote(NoteRequest noteRequest) {
|
||||
UserEntity user = getCurrentUser();
|
||||
|
||||
logger.info("Creating note to user " + user.getId());
|
||||
@@ -110,13 +115,14 @@ public class NoteService {
|
||||
|
||||
logger.info("Note created! Id " + created.getId());
|
||||
|
||||
String savedUrl = null;
|
||||
if (!Objects.isNull(noteRequest.url()) && !noteRequest.url().isEmpty()) {
|
||||
NoteUrlEntity urlEntity = saveUrl(note, noteRequest.url());
|
||||
note.setNoteUrl(urlEntity);
|
||||
NoteUrlEntity urlEntity = saveUrl(created, noteRequest.url());
|
||||
savedUrl = urlEntity.getUrl();
|
||||
}
|
||||
|
||||
logger.info("Finished note creation!");
|
||||
return created;
|
||||
return NoteResponse.fromEntity(created, savedUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,12 +133,12 @@ public class NoteService {
|
||||
* @return {@link NoteResponse} containing the updated note.
|
||||
*/
|
||||
@Transactional
|
||||
public NoteResponse patchNote(@NonNull Long noteId, NotePatchRequest patch) {
|
||||
public NoteResponse patchNote(Long noteId, NotePatchRequest patch) {
|
||||
UserEntity user = getCurrentUser();
|
||||
|
||||
logger.info("Patching task " + noteId + " to user " + user.getId());
|
||||
|
||||
Optional<NoteEntity> note = noteRepository.findById(noteId);
|
||||
Optional<NoteEntity> note = noteRepository.findByIdAndUser_id(noteId, user.getId());
|
||||
if (note.isEmpty()) {
|
||||
throw new NoteNotFoundException();
|
||||
}
|
||||
@@ -154,8 +160,7 @@ public class NoteService {
|
||||
logger.info("URL deleted from task " + noteId);
|
||||
|
||||
if (!Objects.isNull(patch.url()) && !patch.url().isBlank()) {
|
||||
NoteUrlEntity urlEntity = saveUrl(noteEntity, patch.url());
|
||||
noteEntity.setNoteUrl(urlEntity);
|
||||
saveUrl(noteEntity, patch.url());
|
||||
} else {
|
||||
logger.info("No urls to patch for task " + noteId);
|
||||
}
|
||||
@@ -165,7 +170,7 @@ public class NoteService {
|
||||
|
||||
logger.info("Note patched! Id " + patchedNote.getId());
|
||||
|
||||
return NoteResponse.fromEntity(patchedNote);
|
||||
return NoteResponse.fromEntity(patchedNote, getNoteUrl(patchedNote.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,25 +179,20 @@ public class NoteService {
|
||||
* @param noteId The note id from the database.
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteNote(@NonNull Long noteId) {
|
||||
public void deleteNote(Long noteId) {
|
||||
UserEntity user = getCurrentUser();
|
||||
|
||||
logger.info("Deleting note " + noteId + " to user " + user.getId());
|
||||
|
||||
Optional<NoteEntity> note = noteRepository.findById(noteId);
|
||||
Optional<NoteEntity> note = noteRepository.findByIdAndUser_id(noteId, user.getId());
|
||||
if (note.isEmpty()) {
|
||||
throw new NoteNotFoundException();
|
||||
}
|
||||
|
||||
NoteEntity noteEntity = note.get();
|
||||
|
||||
NoteUrlEntity noteUrl = noteEntity.getNoteUrl();
|
||||
if (!Objects.isNull(noteUrl)) {
|
||||
noteUrlRepository.delete(noteUrl);
|
||||
logger.info("URL Deleted from task " + noteId);
|
||||
} else {
|
||||
logger.info("No urls to delete for task " + noteId);
|
||||
}
|
||||
noteUrlRepository.deleteByNote_id(noteId);
|
||||
logger.info("URL deleted from task " + noteId);
|
||||
|
||||
noteRepository.delete(noteEntity);
|
||||
|
||||
@@ -213,7 +213,76 @@ public class NoteService {
|
||||
List<NoteEntity> notes =
|
||||
noteRepository.findAllBySearchTerm(searchTerm.toUpperCase(), user.getId());
|
||||
logger.info(notes.size() + " tasks found!");
|
||||
return notes.stream().map(NoteResponse::fromEntity).toList();
|
||||
return getNotesUrl(notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Share a note publicly, generating a unique share token.
|
||||
*
|
||||
* @param noteId The note id from the database.
|
||||
* @return {@link NoteResponse} containing the updated note with share token.
|
||||
*/
|
||||
@Transactional
|
||||
public NoteResponse shareNote(Long noteId) {
|
||||
UserEntity user = getCurrentUser();
|
||||
logger.info("Sharing note " + noteId + " for user " + user.getId());
|
||||
|
||||
Optional<NoteEntity> noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId());
|
||||
if (noteOpt.isEmpty()) {
|
||||
throw new NoteNotFoundException();
|
||||
}
|
||||
|
||||
NoteEntity noteEntity = noteOpt.get();
|
||||
|
||||
if (!noteEntity.isShared()) {
|
||||
noteEntity.setShared(true);
|
||||
noteEntity.setShareToken(UUID.randomUUID().toString());
|
||||
noteRepository.save(noteEntity);
|
||||
logger.info("Note " + noteId + " shared with token " + noteEntity.getShareToken());
|
||||
}
|
||||
|
||||
return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unshare a note, revoking public access.
|
||||
*
|
||||
* @param noteId The note id from the database.
|
||||
* @return {@link NoteResponse} containing the updated note.
|
||||
*/
|
||||
public NoteResponse unshareNote(Long noteId) {
|
||||
UserEntity user = getCurrentUser();
|
||||
logger.info("Unsharing note " + noteId + " for user " + user.getId());
|
||||
|
||||
Optional<NoteEntity> noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId());
|
||||
if (noteOpt.isEmpty()) {
|
||||
throw new NoteNotFoundException();
|
||||
}
|
||||
|
||||
NoteEntity noteEntity = noteOpt.get();
|
||||
noteEntity.setShared(false);
|
||||
noteEntity.setShareToken(null);
|
||||
noteRepository.save(noteEntity);
|
||||
logger.info("Note " + noteId + " unshared.");
|
||||
|
||||
return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a publicly shared note by its share token (no authentication required).
|
||||
*
|
||||
* @param shareToken The unique share token for the note.
|
||||
* @return {@link NoteResponse} containing the shared note.
|
||||
*/
|
||||
public NoteResponse getSharedNote(String shareToken) {
|
||||
logger.info("Fetching shared note with token " + shareToken);
|
||||
|
||||
Optional<NoteEntity> noteOpt = noteRepository.findByShareToken(shareToken);
|
||||
if (noteOpt.isEmpty() || !noteOpt.get().isShared()) {
|
||||
throw new NoteNotFoundException();
|
||||
}
|
||||
|
||||
return NoteResponse.fromEntity(noteOpt.get(), getNoteUrl(noteOpt.get().getId()));
|
||||
}
|
||||
|
||||
private UserEntity getCurrentUser() {
|
||||
@@ -222,6 +291,22 @@ public class NoteService {
|
||||
return authService.findByEmail(email).orElseThrow();
|
||||
}
|
||||
|
||||
private String getNoteUrl(Long noteId) {
|
||||
return noteUrlRepository.findByNote_id(noteId).map(NoteUrlEntity::getUrl).orElse(null);
|
||||
}
|
||||
|
||||
private List<NoteResponse> getNotesUrl(List<NoteEntity> notes) {
|
||||
List<Long> noteIds = notes.stream().map(NoteEntity::getId).toList();
|
||||
if (noteIds.isEmpty()) {
|
||||
return notes.stream().map(n -> NoteResponse.fromEntity(n, null)).toList();
|
||||
}
|
||||
List<NoteUrlEntity> urls = noteUrlRepository.findAllByNote_idIn(noteIds);
|
||||
Map<Long, String> noteUrls =
|
||||
urls.stream().collect(Collectors.toMap(nu -> nu.getNote().getId(), NoteUrlEntity::getUrl));
|
||||
|
||||
return notes.stream().map(n -> NoteResponse.fromEntity(n, noteUrls.get(n.getId()))).toList();
|
||||
}
|
||||
|
||||
private NoteUrlEntity saveUrl(NoteEntity noteEntity, String url) {
|
||||
NoteUrlEntity noteUrl = new NoteUrlEntity();
|
||||
noteUrl.setUrl(url);
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** This class contains the implementation for the Task Service class. */
|
||||
@@ -80,7 +79,7 @@ public class TaskService {
|
||||
* @param taskId The task id in the database.
|
||||
* @return {@link TaskResponse} with the found task or throw a {@link TaskNotFoundException}.
|
||||
*/
|
||||
public TaskResponse getTaskById(@NonNull Long taskId) {
|
||||
public TaskResponse getTaskById(Long taskId) {
|
||||
UserEntity user = getCurrentUser();
|
||||
logger.info("Get task " + taskId + " to user " + user.getId());
|
||||
|
||||
@@ -131,7 +130,7 @@ public class TaskService {
|
||||
* @return {@link TaskResponse} with the updated content.
|
||||
*/
|
||||
@Transactional
|
||||
public TaskResponse patchTask(@NonNull Long taskId, TaskPatchRequest patch) {
|
||||
public TaskResponse patchTask(Long taskId, TaskPatchRequest patch) {
|
||||
UserEntity user = getCurrentUser();
|
||||
|
||||
logger.info("Patching task " + taskId + " to user " + user.getId());
|
||||
@@ -177,7 +176,7 @@ public class TaskService {
|
||||
* @param taskId The task id in the database
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteTask(@NonNull Long taskId) {
|
||||
public void deleteTask(Long taskId) {
|
||||
UserEntity user = getCurrentUser();
|
||||
|
||||
logger.info("Deleting task " + taskId + " to user " + user.getId());
|
||||
|
||||
@@ -4,7 +4,6 @@ import br.com.tasknoteapp.server.entity.UserEntity;
|
||||
import br.com.tasknoteapp.server.repository.UserRepository;
|
||||
import br.com.tasknoteapp.server.service.UserService;
|
||||
import java.util.Optional;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -12,24 +11,21 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
class UserServiceImpl implements UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final UserDetailsService cachedUserDetailsService;
|
||||
|
||||
public UserServiceImpl(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.cachedUserDetailsService =
|
||||
email -> {
|
||||
Optional<UserEntity> user = userRepository.findByEmail(email);
|
||||
if (user.isEmpty()) {
|
||||
throw new RuntimeException("User not found: " + email);
|
||||
}
|
||||
return user.get();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetailsService userDetailsService() {
|
||||
return new UserDetailsService() {
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String email) {
|
||||
Optional<UserEntity> user = userRepository.findByEmail(email);
|
||||
if (user.isEmpty()) {
|
||||
throw new RuntimeException("User not found: " + email);
|
||||
}
|
||||
|
||||
return user.get();
|
||||
}
|
||||
};
|
||||
return this.cachedUserDetailsService;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ spring:
|
||||
locations: classpath:db/migration
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
open-in-view: false
|
||||
properties:
|
||||
hibernate:
|
||||
default_schema: tasknote
|
||||
show-sql: true
|
||||
show-sql: false
|
||||
|
||||
@@ -36,7 +36,8 @@ spring:
|
||||
locations: classpath:db/migration
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
open-in-view: false
|
||||
properties:
|
||||
hibernate:
|
||||
default_schema: tasknote
|
||||
show-sql: true
|
||||
show-sql: false
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE tasknote.notes
|
||||
ADD COLUMN shared BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN share_token VARCHAR(36) NULL;
|
||||
+1
-2
@@ -15,15 +15,14 @@ import java.time.LocalDateTime;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@SuppressWarnings("null")
|
||||
class AuthenticationControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@@ -11,8 +11,8 @@ import java.util.List;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
@@ -27,7 +27,6 @@ class HomeControllerTest {
|
||||
@MockitoBean private HomeService homeService;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("null")
|
||||
@DisplayName("Get task tags following the happy path it should succeed")
|
||||
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
|
||||
void getTasksTags_happyPath_shouldSucceed() throws Exception {
|
||||
@@ -46,9 +45,8 @@ class HomeControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("null")
|
||||
@DisplayName("Get task tags not authorized it should fail")
|
||||
void getTasksTags_notAuthorized_shouldSucceed() throws Exception {
|
||||
void getTasksTags_notAuthorized_shouldFail() throws Exception {
|
||||
mockMvc
|
||||
.perform(
|
||||
get("/rest/home/tasks/tags")
|
||||
|
||||
@@ -8,10 +8,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import br.com.tasknoteapp.server.entity.NoteEntity;
|
||||
import br.com.tasknoteapp.server.exception.NoteNotFoundException;
|
||||
import br.com.tasknoteapp.server.request.NotePatchRequest;
|
||||
import br.com.tasknoteapp.server.request.NoteRequest;
|
||||
@@ -23,8 +23,8 @@ import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
@@ -32,7 +32,6 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@SuppressWarnings("null")
|
||||
class NoteControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
@@ -45,7 +44,7 @@ class NoteControllerTest {
|
||||
void getAllNotes_notesFound_shouldSucceed() throws Exception {
|
||||
NoteUrlResponse noteUrl = new NoteUrlResponse(111L, "https://test.com");
|
||||
NoteResponse note =
|
||||
new NoteResponse(111L, "title", "description", "https://test.com", null, "tag");
|
||||
new NoteResponse(111L, "title", "description", "https://test.com", null, "tag", false, null);
|
||||
|
||||
when(noteService.getAllNotes()).thenReturn(List.of(note));
|
||||
|
||||
@@ -103,7 +102,14 @@ class NoteControllerTest {
|
||||
|
||||
NoteResponse response =
|
||||
new NoteResponse(
|
||||
noteId, patchRequest.title(), patchRequest.description(), null, null, "tag");
|
||||
noteId,
|
||||
patchRequest.title(),
|
||||
patchRequest.description(),
|
||||
null,
|
||||
null,
|
||||
"tag",
|
||||
false,
|
||||
null);
|
||||
|
||||
when(noteService.patchNote(noteId, patchRequest)).thenReturn(response);
|
||||
|
||||
@@ -192,10 +198,8 @@ class NoteControllerTest {
|
||||
void postNotes_happyPath_shouldSucceed() throws Exception {
|
||||
NoteRequest request = new NoteRequest("Title", "Description", null, null);
|
||||
|
||||
NoteEntity entity = new NoteEntity();
|
||||
entity.setId(1L);
|
||||
entity.setTitle(request.title());
|
||||
entity.setDescription(request.description());
|
||||
NoteResponse entity = new NoteResponse(1L, request.title(), request.description(),
|
||||
null, null, null, false, null);
|
||||
|
||||
when(noteService.createNote(request)).thenReturn(entity);
|
||||
|
||||
@@ -216,9 +220,9 @@ class NoteControllerTest {
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.content(payloadJson))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value(entity.getId()))
|
||||
.andExpect(jsonPath("$.title").value(entity.getTitle()))
|
||||
.andExpect(jsonPath("$.description").value(entity.getDescription()))
|
||||
.andExpect(jsonPath("$.id").value(entity.id()))
|
||||
.andExpect(jsonPath("$.title").value(entity.title()))
|
||||
.andExpect(jsonPath("$.description").value(entity.description()))
|
||||
.andExpect(jsonPath("$.url", Matchers.nullValue()))
|
||||
.andReturn();
|
||||
}
|
||||
@@ -316,4 +320,75 @@ class NoteControllerTest {
|
||||
.andExpect(status().isNotFound())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Share note happy path should succeed")
|
||||
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
|
||||
void shareNote_happyPath_shouldSucceed() throws Exception {
|
||||
final Long noteId = 1L;
|
||||
final String token = "test-token-uuid";
|
||||
NoteResponse response =
|
||||
new NoteResponse(noteId, "title", "description", null, null, "tag", true, token);
|
||||
|
||||
when(noteService.shareNote(noteId)).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/rest/notes/{id}/share", noteId)
|
||||
.with(csrf().asHeader())
|
||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.shared").value(true))
|
||||
.andExpect(jsonPath("$.shareToken").value(token))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Share note with 401 unauthorized should fail")
|
||||
void shareNote_unauthorized_shouldFail() throws Exception {
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/rest/notes/{id}/share", 1L)
|
||||
.with(csrf().asHeader())
|
||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unshare note happy path should succeed")
|
||||
@WithMockUser(username = "user@domain.com", password = "abcde123456A@")
|
||||
void unshareNote_happyPath_shouldSucceed() throws Exception {
|
||||
final Long noteId = 1L;
|
||||
NoteResponse response =
|
||||
new NoteResponse(noteId, "title", "description", null, null, "tag", false, null);
|
||||
|
||||
when(noteService.unshareNote(noteId)).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/rest/notes/{id}/unshare", noteId)
|
||||
.with(csrf().asHeader())
|
||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.shared").value(false))
|
||||
.andExpect(jsonPath("$.shareToken", Matchers.nullValue()))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unshare note with 401 unauthorized should fail")
|
||||
void unshareNote_unauthorized_shouldFail() throws Exception {
|
||||
mockMvc
|
||||
.perform(
|
||||
put("/rest/notes/{id}/unshare", 1L)
|
||||
.with(csrf().asHeader())
|
||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package br.com.tasknoteapp.server.controller;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import br.com.tasknoteapp.server.exception.NoteNotFoundException;
|
||||
import br.com.tasknoteapp.server.response.NoteResponse;
|
||||
import br.com.tasknoteapp.server.service.NoteService;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
class PublicNoteControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean private NoteService noteService;
|
||||
|
||||
@Test
|
||||
@DisplayName("Get shared note by token happy path should succeed")
|
||||
void getSharedNote_happyPath_shouldSucceed() throws Exception {
|
||||
final String token = "test-share-token";
|
||||
NoteResponse response =
|
||||
new NoteResponse(1L, "title", "description", null, null, "tag", true, token);
|
||||
|
||||
when(noteService.getSharedNote(token)).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
get("/public/notes/{token}", token)
|
||||
.with(csrf().asHeader())
|
||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(1L))
|
||||
.andExpect(jsonPath("$.title").value("title"))
|
||||
.andExpect(jsonPath("$.shared").value(true))
|
||||
.andExpect(jsonPath("$.shareToken").value(token))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get shared note by token not found should fail with 404")
|
||||
void getSharedNote_notFound_shouldFail() throws Exception {
|
||||
final String token = "invalid-token";
|
||||
|
||||
when(noteService.getSharedNote(token)).thenThrow(new NoteNotFoundException());
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
get("/public/notes/{token}", token)
|
||||
.with(csrf().asHeader())
|
||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isNotFound())
|
||||
.andReturn();
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
@@ -30,7 +30,6 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@SuppressWarnings("null")
|
||||
class TaskControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@@ -14,8 +14,8 @@ import java.util.List;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
@@ -23,7 +23,6 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@SuppressWarnings("null")
|
||||
class UserControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
+1
-2
@@ -13,8 +13,8 @@ import br.com.tasknoteapp.server.service.UserSessionService;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
@@ -22,7 +22,6 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@SuppressWarnings("null")
|
||||
class UserSessionControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@ import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase.Replace;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
+3
-3
@@ -9,9 +9,9 @@ import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase.Replace;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
@DataJpaTest
|
||||
|
||||
+3
-3
@@ -6,9 +6,9 @@ import java.util.UUID;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase.Replace;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
@DataJpaTest
|
||||
|
||||
@@ -34,17 +34,14 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthServiceTest {
|
||||
|
||||
@Mock private UserRepository userRepository;
|
||||
@@ -95,8 +92,6 @@ class AuthServiceTest {
|
||||
entity.setEmailUuid(UUID.randomUUID());
|
||||
|
||||
when(userRepository.save(any())).thenReturn(entity);
|
||||
when(jwtService.generateToken(any())).thenReturn("a1b2c3");
|
||||
doNothing().when(mailgunEmailService).sendNewUser(any());
|
||||
|
||||
UserResponseWithToken token = authService.signUpNewUser(request);
|
||||
|
||||
@@ -204,8 +199,8 @@ class AuthServiceTest {
|
||||
existing.setEmail(request.email());
|
||||
when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing));
|
||||
|
||||
Sort sort = Sort.by(Direction.DESC, "whenHappened");
|
||||
when(userPwdLimitRepository.findAllByUser_id(existing.getId(), sort)).thenReturn(List.of());
|
||||
when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId()))
|
||||
.thenReturn(List.of());
|
||||
when(authenticationManager.authenticate(any())).thenReturn(null);
|
||||
when(jwtService.generateToken(existing)).thenReturn("a1b2c3");
|
||||
|
||||
@@ -244,8 +239,7 @@ class AuthServiceTest {
|
||||
limit1.setWhenHappened(LocalDateTime.now().minusMinutes(1));
|
||||
UserPwdLimitEntity limit2 = new UserPwdLimitEntity();
|
||||
UserPwdLimitEntity limit3 = new UserPwdLimitEntity();
|
||||
Sort sort = Sort.by(Direction.DESC, "whenHappened");
|
||||
when(userPwdLimitRepository.findAllByUser_id(existing.getId(), sort))
|
||||
when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId()))
|
||||
.thenReturn(List.of(limit1, limit2, limit3));
|
||||
|
||||
Assertions.assertThrows(
|
||||
@@ -264,8 +258,8 @@ class AuthServiceTest {
|
||||
existing.setId(919L);
|
||||
when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing));
|
||||
|
||||
Sort sort = Sort.by(Direction.DESC, "whenHappened");
|
||||
when(userPwdLimitRepository.findAllByUser_id(existing.getId(), sort)).thenReturn(List.of());
|
||||
when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId()))
|
||||
.thenReturn(List.of());
|
||||
when(authenticationManager.authenticate(any())).thenThrow(new BadCredentialsException("Wrong"));
|
||||
|
||||
UserResponseWithToken token = authService.signInUser(request);
|
||||
@@ -532,8 +526,6 @@ class AuthServiceTest {
|
||||
existing.setEmail(email);
|
||||
when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing));
|
||||
|
||||
doNothing().when(mailgunEmailService).sendNewUser(existing);
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> authService.resendEmailConfirmation(email));
|
||||
verify(mailgunEmailService, times(0)).sendNewUser(existing);
|
||||
}
|
||||
|
||||
@@ -13,14 +13,13 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.restclient.RestTemplateBuilder;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/** Test class generated by Copilot. I had to tweak it, to make it work. */
|
||||
@SuppressWarnings("null")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MailgunEmailServiceTest {
|
||||
|
||||
@@ -31,14 +30,15 @@ class MailgunEmailServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(restTemplateBuilder.connectTimeout(any())).thenReturn(restTemplateBuilder);
|
||||
when(restTemplateBuilder.readTimeout(any())).thenReturn(restTemplateBuilder);
|
||||
when(restTemplateBuilder.defaultHeader(any(), any())).thenReturn(restTemplateBuilder);
|
||||
when(restTemplateBuilder.build()).thenReturn(restTemplate);
|
||||
|
||||
String apiKey = "abx123";
|
||||
String domain = "domain.com";
|
||||
String sender = "no-reply@domain.com";
|
||||
String target = "development";
|
||||
|
||||
when(restTemplateBuilder.defaultHeader(any(), any())).thenReturn(restTemplateBuilder);
|
||||
when(restTemplateBuilder.build()).thenReturn(restTemplate);
|
||||
|
||||
mailgunEmailService =
|
||||
new MailgunEmailService(apiKey, domain, sender, target, restTemplateBuilder);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package br.com.tasknoteapp.server.service;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.anyString;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -28,7 +28,6 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class NoteServiceTest {
|
||||
|
||||
@@ -106,9 +105,9 @@ class NoteServiceTest {
|
||||
when(noteRepository.save(any(NoteEntity.class))).thenReturn(note);
|
||||
when(noteUrlRepository.save(any(NoteUrlEntity.class))).thenReturn(new NoteUrlEntity());
|
||||
|
||||
NoteEntity createdNote = noteService.createNote(noteRequest);
|
||||
NoteResponse createdNote = noteService.createNote(noteRequest);
|
||||
|
||||
assertEquals("Test Note", createdNote.getTitle());
|
||||
assertEquals("Test Note", createdNote.title());
|
||||
verify(noteRepository, times(1)).save(any(NoteEntity.class));
|
||||
verify(noteUrlRepository, times(1)).save(any(NoteUrlEntity.class));
|
||||
}
|
||||
@@ -117,13 +116,14 @@ class NoteServiceTest {
|
||||
void patchNote() {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
|
||||
when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
|
||||
when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note));
|
||||
when(noteRepository.findByIdAndUser_id(note.getId(), user.getId()))
|
||||
.thenReturn(Optional.of(note));
|
||||
when(noteRepository.save(any(NoteEntity.class))).thenReturn(note);
|
||||
|
||||
NoteResponse patchedNote = noteService.patchNote(note.getId(), notePatchRequest);
|
||||
|
||||
assertEquals("Updated Note", patchedNote.title());
|
||||
verify(noteRepository, times(1)).findById(note.getId());
|
||||
verify(noteRepository, times(1)).findByIdAndUser_id(note.getId(), user.getId());
|
||||
verify(noteRepository, times(1)).save(any(NoteEntity.class));
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ class NoteServiceTest {
|
||||
void patchNote_notFound() {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
|
||||
when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
|
||||
when(noteRepository.findById(note.getId())).thenReturn(Optional.empty());
|
||||
when(noteRepository.findByIdAndUser_id(note.getId(), user.getId()))
|
||||
.thenReturn(Optional.empty());
|
||||
Long noteId = note.getId();
|
||||
|
||||
assertThrows(
|
||||
@@ -142,11 +143,12 @@ class NoteServiceTest {
|
||||
void deleteNote() {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
|
||||
when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
|
||||
when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note));
|
||||
when(noteRepository.findByIdAndUser_id(note.getId(), user.getId()))
|
||||
.thenReturn(Optional.of(note));
|
||||
|
||||
noteService.deleteNote(note.getId());
|
||||
|
||||
verify(noteRepository, times(1)).findById(note.getId());
|
||||
verify(noteRepository, times(1)).findByIdAndUser_id(note.getId(), user.getId());
|
||||
verify(noteRepository, times(1)).delete(note);
|
||||
}
|
||||
|
||||
@@ -163,4 +165,86 @@ class NoteServiceTest {
|
||||
assertEquals("Test Note", notes.get(0).title());
|
||||
verify(noteRepository, times(1)).findAllBySearchTerm(anyString(), eq(user.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shareNote() {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
|
||||
when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
|
||||
when(noteRepository.findByIdAndUser_id(note.getId(), user.getId()))
|
||||
.thenReturn(Optional.of(note));
|
||||
when(noteRepository.save(any(NoteEntity.class))).thenReturn(note);
|
||||
|
||||
NoteResponse response = noteService.shareNote(note.getId());
|
||||
|
||||
assertEquals("Test Note", response.title());
|
||||
verify(noteRepository, times(1)).save(any(NoteEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shareNote_notFound() {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
|
||||
when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
|
||||
when(noteRepository.findByIdAndUser_id(note.getId(), user.getId()))
|
||||
.thenReturn(Optional.empty());
|
||||
Long noteId = note.getId();
|
||||
|
||||
assertThrows(NoteNotFoundException.class, () -> noteService.shareNote(noteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unshareNote() {
|
||||
note.setShared(true);
|
||||
note.setShareToken("some-token");
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
|
||||
when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
|
||||
when(noteRepository.findByIdAndUser_id(note.getId(), user.getId()))
|
||||
.thenReturn(Optional.of(note));
|
||||
when(noteRepository.save(any(NoteEntity.class))).thenReturn(note);
|
||||
|
||||
NoteResponse response = noteService.unshareNote(note.getId());
|
||||
|
||||
assertEquals("Test Note", response.title());
|
||||
verify(noteRepository, times(1)).save(any(NoteEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unshareNote_notFound() {
|
||||
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
|
||||
when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
|
||||
when(noteRepository.findByIdAndUser_id(note.getId(), user.getId()))
|
||||
.thenReturn(Optional.empty());
|
||||
Long noteId = note.getId();
|
||||
|
||||
assertThrows(NoteNotFoundException.class, () -> noteService.unshareNote(noteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSharedNote() {
|
||||
final String token = "share-token-123";
|
||||
note.setShared(true);
|
||||
note.setShareToken(token);
|
||||
when(noteRepository.findByShareToken(token)).thenReturn(Optional.of(note));
|
||||
|
||||
NoteResponse response = noteService.getSharedNote(token);
|
||||
|
||||
assertEquals("Test Note", response.title());
|
||||
verify(noteRepository, times(1)).findByShareToken(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSharedNote_notFound() {
|
||||
when(noteRepository.findByShareToken("bad-token")).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(NoteNotFoundException.class, () -> noteService.getSharedNote("bad-token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSharedNote_notShared() {
|
||||
final String token = "share-token-456";
|
||||
note.setShared(false);
|
||||
note.setShareToken(token);
|
||||
when(noteRepository.findByShareToken(token)).thenReturn(Optional.of(note));
|
||||
|
||||
assertThrows(NoteNotFoundException.class, () -> noteService.getSharedNote(token));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TaskServiceTest {
|
||||
|
||||
@Mock TaskRepository taskRepository;
|
||||
@@ -535,8 +534,6 @@ class TaskServiceTest {
|
||||
|
||||
String searchTerm = null;
|
||||
|
||||
when(taskRepository.findAllBySearchTerm(null, USER_ID)).thenReturn(List.of());
|
||||
|
||||
List<TaskResponse> responses = taskService.searchTasks(searchTerm);
|
||||
|
||||
assertNotNull(responses);
|
||||
|
||||
@@ -16,10 +16,9 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserSessionServiceTest {
|
||||
|
||||
// Test class generated by GitHub Copilot
|
||||
@@ -44,7 +43,8 @@ class UserSessionServiceTest {
|
||||
|
||||
TaskResponse task =
|
||||
new TaskResponse(1L, "Task 1", false, true, null, null, null, null, List.of());
|
||||
NoteResponse note = new NoteResponse(1L, "Note 1", "Description", null, null, null);
|
||||
NoteResponse note =
|
||||
new NoteResponse(1L, "Note 1", "Description", null, null, null, false, null);
|
||||
|
||||
when(authService.getCurrentUser()).thenReturn(Optional.of(user));
|
||||
when(taskService.getAllTasks()).thenReturn(List.of(task));
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtServiceImplTest {
|
||||
|
||||
|
||||
+27
-22
@@ -1,28 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Server - Back-end
|
||||
|
||||
DOCKER_IMG="tasknote-api:nightly"
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
docker image inspect $DOCKER_IMG --format="ignore me"
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Image not found locally! Building it..."
|
||||
|
||||
if [ ! -f "server/.env" ]; then
|
||||
echo "No env file found for back-end. Creating one for you.."
|
||||
bash tools/run-create-env.sh "back"
|
||||
else
|
||||
echo "Env file in place. Moving on.."
|
||||
fi
|
||||
|
||||
echo "Getting env vars and making them visible"
|
||||
cd server/
|
||||
export $(cat .env | xargs)
|
||||
echo "Done!"
|
||||
|
||||
cd ..
|
||||
docker build --file server/Dockerfile.dev --tag tasknote-api:nightly .
|
||||
else
|
||||
echo "Image found! Let's keep going."
|
||||
if [ ! -f "$CURRENT_DIR/pom.xml" ]; then
|
||||
cd "$CURRENT_DIR/server"
|
||||
fi
|
||||
|
||||
echo "Running checks..."
|
||||
echo "1/3 - Check Style started..."
|
||||
./mvnw --no-transfer-progress checkstyle:check -Dcheckstyle.skip=false
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running Check Style. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run -it --rm --name tasknote-api -e CHECK="TRUE" -v ./server:/app tasknote-api:nightly
|
||||
echo "2/3 - Build started..."
|
||||
./mvnw --no-transfer-progress clean compile -DskipTests
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running build. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "3/3 - Tests started..."
|
||||
./mvnw --no-transfer-progress clean verify -P tests --file pom.xml
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running test. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "You're good to go! Good job!"
|
||||
exit 0
|
||||
|
||||
Regular → Executable
+37
-1
@@ -1,3 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Client - Front-end
|
||||
|
||||
docker run -it --rm --name tasknote-web -e CHECK="TRUE" -v ./client:/app tasknote-web:nightly
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
if [ ! -f "$CURRENT_DIR/package.json" ]; then
|
||||
cd "$CURRENT_DIR/client"
|
||||
fi
|
||||
|
||||
npm ci
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when installing dependencies. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running checks..."
|
||||
echo "1/3 - Lint started..."
|
||||
npm run lint:fix
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running lint. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "2/3 - Build started..."
|
||||
npm run build
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running build. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "3/3 - Tests started..."
|
||||
npm run test:no-watch
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when running test. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "You're good to go! Good job!"
|
||||
exit 0
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "${DEPLOY_DOMAIN}" ]; then
|
||||
echo "DEPLOY_DOMAIN not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${API_KEY}" ]; then
|
||||
echo "API_KEY not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${CLIENT_APPID}" ]; then
|
||||
echo "CLIENT_APPID not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${PROD_URL}" ]; then
|
||||
echo "PROD_URL not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "DEPLOY_DOMAIN=${DEPLOY_DOMAIN}"
|
||||
echo "API_KEY=${API_KEY}"
|
||||
echo "CLIENT_APPID=${CLIENT_APPID}"
|
||||
echo "PROD_URL=${PROD_URL}"
|
||||
echo "cURL version $(curl --version)"
|
||||
|
||||
echo -e "\nPre-deployment check..."
|
||||
if ! curl -s -f "${PROD_URL}" > /dev/null ; then
|
||||
echo "Prod environment is not healthy"
|
||||
else
|
||||
echo "Prod environment is healthy"
|
||||
fi
|
||||
|
||||
echo -e "\nUpdating Docker image tag..."
|
||||
response=$(curl -X POST \
|
||||
"${DEPLOY_DOMAIN}/api/application.saveDockerProvider" \
|
||||
--max-time 30 \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d '{
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:prod-v359",
|
||||
"applicationId": "'"${CLIENT_APPID}"'",
|
||||
"username": "'"${GHCR_USERNAME}"'",
|
||||
"password": "'"${GHCR_PASSWORD}"'",
|
||||
"registryUrl": "ghcr.io"
|
||||
}' \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
echo -e "\nResponse: $response"
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
echo -e "\nStatus code: $status_code"
|
||||
|
||||
if [ "$status_code" != "200" ]; then
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Update failed with status code $status_code"
|
||||
echo "Response body: $body"
|
||||
exit 1
|
||||
else
|
||||
echo "Update succeeded!"
|
||||
fi
|
||||
|
||||
echo -e "\nDeploying to prod (target: ${PROD_URL})..."
|
||||
response=$(curl -X POST \
|
||||
"${DEPLOY_DOMAIN}/api/application.deploy" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d "{\"applicationId\":\"${CLIENT_APPID}\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
echo -e "\nStatus code: $status_code"
|
||||
|
||||
if [ "$status_code" != "200" ]; then
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Deployment failed with status code $status_code"
|
||||
echo "Response body: $body"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment succeeded!"
|
||||
fi
|
||||
|
||||
echo -e "\nVerifying deployment..."
|
||||
|
||||
sleep 15
|
||||
|
||||
echo -e "\nPre-deployment check..."
|
||||
if ! curl -s -f "${PROD_URL}" > /dev/null ; then
|
||||
echo "Prod environment is not healthy"
|
||||
else
|
||||
echo "Prod environment is healthy"
|
||||
fi
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "${DEPLOY_DOMAIN}" ]; then
|
||||
echo "DEPLOY_DOMAIN not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${API_KEY}" ]; then
|
||||
echo "API_KEY not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${CLIENT_APPID}" ]; then
|
||||
echo "CLIENT_APPID not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${STAGE_URL}" ]; then
|
||||
echo "STAGE_URL not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "DEPLOY_DOMAIN=${DEPLOY_DOMAIN}"
|
||||
echo "API_KEY=${API_KEY}"
|
||||
echo "CLIENT_APPID=${CLIENT_APPID}"
|
||||
echo "STAGE_URL=${STAGE_URL}"
|
||||
echo "cURL version $(curl --version)"
|
||||
|
||||
echo -e "\nPre-deployment check..."
|
||||
if ! curl -s -f "${STAGE_URL}" > /dev/null ; then
|
||||
echo "Stage environment is not healthy"
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
|
||||
echo -e "\nDeploying to stage (target: ${STAGE_URL})..."
|
||||
response=$(curl -X POST \
|
||||
"${DEPLOY_DOMAIN}/api/application.deploy" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d "{\"applicationId\":\"${CLIENT_APPID}\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
echo -e "\nStatus code: $status_code"
|
||||
|
||||
if [ "$status_code" != "200" ]; then
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Deployment failed with status code $status_code"
|
||||
echo "Response body: $body"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment succeeded!"
|
||||
fi
|
||||
|
||||
echo -e "\nVerifying deployment..."
|
||||
|
||||
sleep 15
|
||||
|
||||
echo -e "\nPre-deployment check..."
|
||||
if ! curl -s -f "${STAGE_URL}" > /dev/null ; then
|
||||
echo "Stage environment is not healthy"
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Checks if a given image and tag are present locally to prevent running when it's not
|
||||
|
||||
TARGET="$1"
|
||||
|
||||
IS=$(docker images $TARGET:candidate | wc -l)
|
||||
|
||||
if [ $IS -eq 0 ]; then
|
||||
echo "Image $TARGET:candidate not found locally, please build it"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Image $TARGET:candidate found"
|
||||
exit 0
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Check if a given service is not running
|
||||
# Return 0 (success) if it's not
|
||||
|
||||
TARGET="$1"
|
||||
echo "Check for running image for $TARGET"
|
||||
|
||||
IS=$(docker ps --filter name=$TARGET --filter status=running | grep $TARGET | wc -l)
|
||||
|
||||
if [ $IS -eq 0 ]; then
|
||||
echo "Service $TARGET is not running"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Service $TARGET is running"
|
||||
exit 1
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
TARGET="$1"
|
||||
echo "Check for running image for $TARGET"
|
||||
|
||||
IS=$(docker ps --filter name=$TARGET --filter status=running | grep $TARGET | wc -l)
|
||||
|
||||
if [ $IS -eq 1 ]; then
|
||||
echo "Service $TARGET is running"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Service $TARGET not running"
|
||||
exit 1
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "server/.env" ]; then
|
||||
echo "No env file found for front-end. Creating one for you.."
|
||||
bash tools/run-create-env.sh "front"
|
||||
else
|
||||
echo "Env file in place. Moving on.."
|
||||
fi
|
||||
|
||||
echo "Getting env vars and making them visible"
|
||||
cd client/
|
||||
export $(cat .env | xargs)
|
||||
echo "Done!"
|
||||
|
||||
cd ..
|
||||
|
||||
docker build --file client/Dockerfile.dev --tag client:nightly .
|
||||
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "Issues when building Docker image. Please review.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVER_HOST="http://"$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tasknote-api):8585"
|
||||
|
||||
if [ "$SERVER_HOST" == "http://:8585" ]; then
|
||||
echo "Back-end tasknote-api not running. Make sure to run it before starting the web app."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "SERVER_HOST=$SERVER_HOST"
|
||||
|
||||
docker run -d --rm \
|
||||
--name client \
|
||||
-p 5000:5000 \
|
||||
-e VITE_BACKEND_SERVER="$SERVER_HOST" \
|
||||
-e VITE_BUILD="$VITE_BUILD" \
|
||||
-v ./client:/app \
|
||||
client:nightly
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "server/.env" ]; then
|
||||
echo "No env file found for back-end. Creating one for you.."
|
||||
bash tools/run-create-env.sh "back"
|
||||
else
|
||||
echo "Env file in place. Moving on.."
|
||||
fi
|
||||
|
||||
echo "Getting env vars and making them visible"
|
||||
cd server/
|
||||
export $(cat .env | xargs)
|
||||
echo "Done!"
|
||||
|
||||
cd ..
|
||||
|
||||
echo "Starting Postgres DB..."
|
||||
docker run -d --rm \
|
||||
--name db \
|
||||
-p 5432:5432 \
|
||||
-e POSTGRES_DB=$POSTGRES_DB \
|
||||
-e POSTGRES_USER=$POSTGRES_USER \
|
||||
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
|
||||
-e PGDATA=/tmp \
|
||||
-v ./data:/tmp \
|
||||
postgres:15.8-bookworm
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
VERSION="$1"
|
||||
|
||||
echo "Received version $VERSION"
|
||||
|
||||
if [ ! -f "server/.env" ]; then
|
||||
echo "No env file found for back-end. Creating one for you.."
|
||||
bash tools/run-create-env.sh "back"
|
||||
else
|
||||
echo "Env file in place. Moving on.."
|
||||
fi
|
||||
|
||||
echo "Getting env vars and making them visible"
|
||||
cd server/
|
||||
export $(cat .env | xargs)
|
||||
echo "Done!"
|
||||
|
||||
cd ..
|
||||
|
||||
DB_HOST=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' db)
|
||||
|
||||
docker run -d --rm \
|
||||
--name server \
|
||||
-p 8585:8585 \
|
||||
-p 5005:5005 \
|
||||
-e POSTGRES_DB=$POSTGRES_DB \
|
||||
-e POSTGRES_USER=$POSTGRES_USER \
|
||||
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
|
||||
-e POSTGRES_PORT=$POSTGRES_PORT \
|
||||
-e POSTGRES_HOST=$DB_HOST \
|
||||
-e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \
|
||||
-e SERVER_SERVLET_CONTEXT_PATH=$SERVER_SERVLET_CONTEXT_PATH \
|
||||
$VERSION
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "server/.env" ]; then
|
||||
echo "No env file found for back-end. Creating one for you.."
|
||||
bash tools/run-create-env.sh "back"
|
||||
else
|
||||
echo "Env file in place. Moving on.."
|
||||
fi
|
||||
|
||||
echo "Getting env vars and making them visible"
|
||||
cd server/
|
||||
export $(cat .env | xargs)
|
||||
echo "Done!"
|
||||
|
||||
cd ..
|
||||
|
||||
docker build --file server/Dockerfile.dev --tag tasknote-api:nightly .
|
||||
|
||||
DB_HOST=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' db)
|
||||
|
||||
docker run -d --rm \
|
||||
--name tasknote-api \
|
||||
-p 8585:8585 \
|
||||
-p 5005:5005 \
|
||||
-e POSTGRES_DB=$POSTGRES_DB \
|
||||
-e POSTGRES_USER=$POSTGRES_USER \
|
||||
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
|
||||
-e POSTGRES_PORT=$POSTGRES_PORT \
|
||||
-e POSTGRES_HOST=$DB_HOST \
|
||||
-e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \
|
||||
-e SERVER_SERVLET_CONTEXT_PATH=$SERVER_SERVLET_CONTEXT_PATH \
|
||||
-v ./server:/app \
|
||||
tasknote-api:nightly
|
||||
Reference in New Issue
Block a user