commit
b3aad60800
@ -0,0 +1,177 @@ |
|||||||
|
# Byte-compiled / optimized / DLL files |
||||||
|
__pycache__/ |
||||||
|
*.py[cod] |
||||||
|
*$py.class |
||||||
|
|
||||||
|
# C extensions |
||||||
|
*.so |
||||||
|
|
||||||
|
# Distribution / packaging |
||||||
|
.Python |
||||||
|
build/ |
||||||
|
develop-eggs/ |
||||||
|
dist/ |
||||||
|
downloads/ |
||||||
|
eggs/ |
||||||
|
.eggs/ |
||||||
|
lib/ |
||||||
|
lib64/ |
||||||
|
parts/ |
||||||
|
sdist/ |
||||||
|
var/ |
||||||
|
wheels/ |
||||||
|
share/python-wheels/ |
||||||
|
*.egg-info/ |
||||||
|
.installed.cfg |
||||||
|
*.egg |
||||||
|
MANIFEST |
||||||
|
|
||||||
|
# PyInstaller |
||||||
|
# Usually these files are written by a python script from a template |
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
||||||
|
*.manifest |
||||||
|
*.spec |
||||||
|
|
||||||
|
# Installer logs |
||||||
|
pip-log.txt |
||||||
|
pip-delete-this-directory.txt |
||||||
|
|
||||||
|
# Unit test / coverage reports |
||||||
|
htmlcov/ |
||||||
|
.tox/ |
||||||
|
.nox/ |
||||||
|
.coverage |
||||||
|
.coverage.* |
||||||
|
.cache |
||||||
|
nosetests.xml |
||||||
|
coverage.xml |
||||||
|
*.cover |
||||||
|
*.py,cover |
||||||
|
.hypothesis/ |
||||||
|
.pytest_cache/ |
||||||
|
cover/ |
||||||
|
|
||||||
|
# Translations |
||||||
|
*.mo |
||||||
|
*.pot |
||||||
|
|
||||||
|
# Django stuff: |
||||||
|
*.log |
||||||
|
local_settings.py |
||||||
|
db.sqlite3 |
||||||
|
db.sqlite3-journal |
||||||
|
|
||||||
|
# Flask stuff: |
||||||
|
instance/ |
||||||
|
.webassets-cache |
||||||
|
|
||||||
|
# Scrapy stuff: |
||||||
|
.scrapy |
||||||
|
|
||||||
|
# Sphinx documentation |
||||||
|
docs/_build/ |
||||||
|
|
||||||
|
# PyBuilder |
||||||
|
.pybuilder/ |
||||||
|
target/ |
||||||
|
|
||||||
|
# Jupyter Notebook |
||||||
|
.ipynb_checkpoints |
||||||
|
|
||||||
|
# IPython |
||||||
|
profile_default/ |
||||||
|
ipython_config.py |
||||||
|
|
||||||
|
# pyenv |
||||||
|
# For a library or package, you might want to ignore these files since the code is |
||||||
|
# intended to run in multiple environments; otherwise, check them in: |
||||||
|
# .python-version |
||||||
|
|
||||||
|
# pipenv |
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. |
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies |
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not |
||||||
|
# install all needed dependencies. |
||||||
|
#Pipfile.lock |
||||||
|
|
||||||
|
# UV |
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. |
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more |
||||||
|
# commonly ignored for libraries. |
||||||
|
#uv.lock |
||||||
|
|
||||||
|
# poetry |
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. |
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more |
||||||
|
# commonly ignored for libraries. |
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control |
||||||
|
#poetry.lock |
||||||
|
|
||||||
|
# pdm |
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. |
||||||
|
#pdm.lock |
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it |
||||||
|
# in version control. |
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control |
||||||
|
.pdm.toml |
||||||
|
.pdm-python |
||||||
|
.pdm-build/ |
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm |
||||||
|
__pypackages__/ |
||||||
|
|
||||||
|
# Celery stuff |
||||||
|
celerybeat-schedule |
||||||
|
celerybeat.pid |
||||||
|
|
||||||
|
# SageMath parsed files |
||||||
|
*.sage.py |
||||||
|
|
||||||
|
# Environments |
||||||
|
.env |
||||||
|
.venv |
||||||
|
env/ |
||||||
|
venv/ |
||||||
|
ENV/ |
||||||
|
env.bak/ |
||||||
|
venv.bak/ |
||||||
|
|
||||||
|
# Spyder project settings |
||||||
|
.spyderproject |
||||||
|
.spyproject |
||||||
|
|
||||||
|
# Rope project settings |
||||||
|
.ropeproject |
||||||
|
|
||||||
|
# mkdocs documentation |
||||||
|
/site |
||||||
|
|
||||||
|
# mypy |
||||||
|
.mypy_cache/ |
||||||
|
.dmypy.json |
||||||
|
dmypy.json |
||||||
|
|
||||||
|
# Pyre type checker |
||||||
|
.pyre/ |
||||||
|
|
||||||
|
# pytype static type analyzer |
||||||
|
.pytype/ |
||||||
|
|
||||||
|
# Cython debug symbols |
||||||
|
cython_debug/ |
||||||
|
|
||||||
|
# PyCharm |
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can |
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore |
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear |
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder. |
||||||
|
#.idea/ |
||||||
|
|
||||||
|
# Ruff stuff: |
||||||
|
.ruff_cache/ |
||||||
|
|
||||||
|
# PyPI configuration file |
||||||
|
.pypirc |
||||||
|
|
||||||
|
frontend/node_modules |
||||||
|
node_modules |
@ -0,0 +1,15 @@ |
|||||||
|
# このファイルを.envにコピーして、実際の値を設定してください |
||||||
|
|
||||||
|
# ================================ |
||||||
|
# JWT 秘密鍵 |
||||||
|
# この設定はアプリケーションで使用します |
||||||
|
# ================================ |
||||||
|
JWT_SECRET="your-JWT-Key" |
||||||
|
|
||||||
|
# ================================ |
||||||
|
# ローカル開発用 PostgreSQL 接続設定 |
||||||
|
# この設定はローカル環境で使用します |
||||||
|
# ================================ |
||||||
|
LOCAL_DB_NAME="local_db" |
||||||
|
LOCAL_DB_USER="local_user" |
||||||
|
LOCAL_DB_PASSWORD="your_local_password" |
@ -0,0 +1,106 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
|
||||||
|
<parent> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-parent</artifactId> |
||||||
|
<version>3.2.3</version> |
||||||
|
<relativePath/> |
||||||
|
</parent> |
||||||
|
|
||||||
|
<groupId>com.example</groupId> |
||||||
|
<artifactId>todo-app</artifactId> |
||||||
|
<version>0.0.1-SNAPSHOT</version> |
||||||
|
<name>todo-app</name> |
||||||
|
<description>ToDo Application Backend</description> |
||||||
|
|
||||||
|
<properties> |
||||||
|
<java.version>17</java.version> |
||||||
|
<jjwt.version>0.11.5</jjwt.version> |
||||||
|
</properties> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<!-- Spring Boot Starters --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-web</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-security</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-validation</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- PostgreSQL Driver --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.postgresql</groupId> |
||||||
|
<artifactId>postgresql</artifactId> |
||||||
|
<scope>runtime</scope> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- JWT --> |
||||||
|
<dependency> |
||||||
|
<groupId>io.jsonwebtoken</groupId> |
||||||
|
<artifactId>jjwt-api</artifactId> |
||||||
|
<version>${jjwt.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.jsonwebtoken</groupId> |
||||||
|
<artifactId>jjwt-impl</artifactId> |
||||||
|
<version>${jjwt.version}</version> |
||||||
|
<scope>runtime</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.jsonwebtoken</groupId> |
||||||
|
<artifactId>jjwt-jackson</artifactId> |
||||||
|
<version>${jjwt.version}</version> |
||||||
|
<scope>runtime</scope> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Lombok --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.projectlombok</groupId> |
||||||
|
<artifactId>lombok</artifactId> |
||||||
|
<optional>true</optional> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Test --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-test</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.security</groupId> |
||||||
|
<artifactId>spring-security-test</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
|
||||||
|
<build> |
||||||
|
<plugins> |
||||||
|
<plugin> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId> |
||||||
|
<configuration> |
||||||
|
<excludes> |
||||||
|
<exclude> |
||||||
|
<groupId>org.projectlombok</groupId> |
||||||
|
<artifactId>lombok</artifactId> |
||||||
|
</exclude> |
||||||
|
</excludes> |
||||||
|
</configuration> |
||||||
|
</plugin> |
||||||
|
</plugins> |
||||||
|
</build> |
||||||
|
</project> |
@ -0,0 +1,19 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# 変数を呼び出し側に影響させないために関数化 |
||||||
|
function get_env_file_path() { |
||||||
|
# スクリプト自身のディレクトリを取得 |
||||||
|
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
||||||
|
# 1つ上の階層にある .env ファイルを読み込む |
||||||
|
local ENV_FILE="$SCRIPT_DIR/../.env" |
||||||
|
echo $ENV_FILE |
||||||
|
} |
||||||
|
|
||||||
|
if [ -f $(get_env_file_path) ]; then |
||||||
|
echo "Loading .env from $(get_env_file_path)" |
||||||
|
export $(grep -v '^#' "$(get_env_file_path)" | xargs) |
||||||
|
echo "環境変数を設定しました" |
||||||
|
else |
||||||
|
echo ".env ファイルが見つかりません: $(get_env_file_path)" |
||||||
|
exit 1 |
||||||
|
fi |
@ -0,0 +1,88 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# PostgreSQLセットアップスクリプト |
||||||
|
# このスクリプトは、データベースとユーザーを作成し、必要な権限を付与します |
||||||
|
# 再実行可能なスクリプトです - 既存のユーザーやデータベースは削除されます |
||||||
|
|
||||||
|
# エラーが発生した場合にスクリプトを停止 |
||||||
|
set -e |
||||||
|
|
||||||
|
# このスクリプト自身の絶対パスを取得 |
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
||||||
|
|
||||||
|
# すべての変数を export 扱いに |
||||||
|
set -a |
||||||
|
# .env を読み込む |
||||||
|
. $SCRIPT_DIR/../.env |
||||||
|
# 通常モードに戻す |
||||||
|
set +a |
||||||
|
|
||||||
|
# 変数の定義 |
||||||
|
DB_NAME=$LOCAL_DB_NAME |
||||||
|
DB_USER=$LOCAL_DB_USER |
||||||
|
DB_PASSWORD=$LOCAL_DB_PASSWORD |
||||||
|
DB_SCHEMA="public" |
||||||
|
|
||||||
|
echo "PostgreSQLセットアップを開始します..." |
||||||
|
echo "データベース: $DB_NAME" |
||||||
|
echo "ユーザー: $DB_USER" |
||||||
|
echo "スキーマ: $DB_SCHEMA" |
||||||
|
|
||||||
|
# PostgreSQLのsuperuserとして実行する必要があります |
||||||
|
# 通常はpostgresユーザーを使用します |
||||||
|
|
||||||
|
# 既存の接続を切断 |
||||||
|
echo "既存の接続を切断します..." |
||||||
|
sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB_NAME';" || true |
||||||
|
|
||||||
|
# データベースの削除(存在する場合) |
||||||
|
echo "既存のデータベースを削除します(存在する場合)..." |
||||||
|
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;" |
||||||
|
|
||||||
|
# ユーザーの削除(存在する場合)と作成 |
||||||
|
echo "ユーザー '$DB_USER' を再作成します..." |
||||||
|
sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;" |
||||||
|
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';" |
||||||
|
|
||||||
|
# データベースの作成 |
||||||
|
echo "データベース '$DB_NAME' を作成します..." |
||||||
|
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME;" |
||||||
|
|
||||||
|
# データベースの所有者を設定 |
||||||
|
echo "データベース所有者を設定します..." |
||||||
|
sudo -u postgres psql -c "ALTER DATABASE $DB_NAME OWNER TO $DB_USER;" |
||||||
|
|
||||||
|
# 権限の付与 |
||||||
|
echo "データベース権限を付与します..." |
||||||
|
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" |
||||||
|
|
||||||
|
# データベースに接続してスキーマ権限を付与 |
||||||
|
echo "スキーマ権限を付与します..." |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "GRANT ALL ON SCHEMA $DB_SCHEMA TO $DB_USER;" |
||||||
|
|
||||||
|
# 既存のオブジェクトに対する権限を付与 |
||||||
|
echo "既存のオブジェクトに対する権限を付与します..." |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA $DB_SCHEMA TO $DB_USER;" || true |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA $DB_SCHEMA TO $DB_USER;" || true |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA $DB_SCHEMA TO $DB_USER;" || true |
||||||
|
|
||||||
|
# 今後作成されるオブジェクトに対する権限を設定(postgres ユーザーが作成する場合) |
||||||
|
echo "postgres ユーザーが今後作成するオブジェクトに対する権限を設定します..." |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER postgres IN SCHEMA $DB_SCHEMA GRANT ALL ON TABLES TO $DB_USER;" |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER postgres IN SCHEMA $DB_SCHEMA GRANT ALL ON SEQUENCES TO $DB_USER;" |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER postgres IN SCHEMA $DB_SCHEMA GRANT ALL ON FUNCTIONS TO $DB_USER;" |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER postgres IN SCHEMA $DB_SCHEMA GRANT ALL ON TYPES TO $DB_USER;" |
||||||
|
|
||||||
|
# ユーザー自身が作成するオブジェクトに対する権限を設定 |
||||||
|
echo "$DB_USER が今後作成するオブジェクトに対する権限を設定します..." |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER $DB_USER IN SCHEMA $DB_SCHEMA GRANT ALL ON TABLES TO $DB_USER;" |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER $DB_USER IN SCHEMA $DB_SCHEMA GRANT ALL ON SEQUENCES TO $DB_USER;" |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER $DB_USER IN SCHEMA $DB_SCHEMA GRANT ALL ON FUNCTIONS TO $DB_USER;" |
||||||
|
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES FOR USER $DB_USER IN SCHEMA $DB_SCHEMA GRANT ALL ON TYPES TO $DB_USER;" |
||||||
|
|
||||||
|
echo "PostgreSQLセットアップが完了しました" |
||||||
|
echo "データベース: $DB_NAME" |
||||||
|
echo "ユーザー: $DB_USER" |
||||||
|
echo "パスワード: $DB_PASSWORD" |
||||||
|
echo "" |
||||||
|
echo "このスクリプトは再実行可能です。再実行すると、既存のデータベースとユーザーは削除され、新しく作成されます。" |
@ -0,0 +1,33 @@ |
|||||||
|
-- ============================================ |
||||||
|
-- データ初期化およテストデータ投入スクリプト |
||||||
|
-- 対象テーブル:users, tasks |
||||||
|
-- 実行順に注意(外部キー制約あり) |
||||||
|
-- ============================================ |
||||||
|
|
||||||
|
-- 1. 既存データの削除(テーブル自体は削除されません) |
||||||
|
-- tasksテーブルはusersに外部キー参照しているため、削除順は tasks → users |
||||||
|
DELETE FROM tasks; |
||||||
|
DELETE FROM users; |
||||||
|
|
||||||
|
-- 2. usersテーブルへ初期ユーザーデータを挿入 |
||||||
|
-- パスワードは事前にBCrypt(ラウンド回数10)でハッシュ化しておくこと |
||||||
|
-- ハッシュ生成例:https://toolbase.cc/text/bcrypt などを使用 |
||||||
|
|
||||||
|
-- パスワード対応表(すべてラウンド10で生成済み): |
||||||
|
-- "password1" → $2a$10$QvmWALLG44WrDJ/y9Jh7p.fehSkDZfC84bXiM4ZQteM3T6x/1aEDK |
||||||
|
-- "password2" → $2a$10$O8QS5GcMVSz7pFD3pY7c0e6SsbmDtKVLNAL7zKhnfn3hEm6.P6vvO |
||||||
|
-- "password3" → $2a$10$5k/c3/R29iyZ7Oe9vso6ZeQvcEYkyQMS7rJ1CVNGiY9dwCL05J9VK |
||||||
|
|
||||||
|
INSERT INTO users (id, username, password, name, email) VALUES |
||||||
|
(1, 'testuser1', '$2a$10$QvmWALLG44WrDJ/y9Jh7p.fehSkDZfC84bXiM4ZQteM3T6x/1aEDK', 'testuser1', 'test1@test.test'), |
||||||
|
(2, 'testuser2', '$2a$10$O8QS5GcMVSz7pFD3pY7c0e6SsbmDtKVLNAL7zKhnfn3hEm6.P6vvO', 'testuser2', 'test2@test.test'), |
||||||
|
(3, 'testuser3', '$2a$10$5k/c3/R29iyZ7Oe9vso6ZeQvcEYkyQMS7rJ1CVNGiY9dwCL05J9VK', 'testuser3', 'test3@test.test'); |
||||||
|
|
||||||
|
-- 3. tasksテーブルへ初期タスクデータを挿入 |
||||||
|
-- created_at / updated_at は明示的に設定 |
||||||
|
-- user_id は上記で追加した users.id を参照 |
||||||
|
|
||||||
|
INSERT INTO tasks (id, title, description, completed, user_id, created_at, updated_at) VALUES |
||||||
|
(1, 'テストタスク1', '説明1', true, 1, '2025-06-01 00:00:00', '2025-06-01 00:00:00'), |
||||||
|
(2, 'テストタスク2', '説明2', true, 2, '2025-06-01 01:00:00', '2025-06-01 01:00:00'), |
||||||
|
(3, 'テストタスク3', '説明3', false, 3, '2025-06-01 02:00:00', '2025-06-01 02:00:00'); |
@ -0,0 +1,23 @@ |
|||||||
|
package com.example.todoapp; |
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication; |
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||||
|
|
||||||
|
/** |
||||||
|
* Todoアプリケーションのメインクラス |
||||||
|
* <p> |
||||||
|
* このクラスはSpring Bootアプリケーションのエントリーポイントです。 |
||||||
|
* アプリケーションの起動と初期化を担当します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@SpringBootApplication |
||||||
|
public class TodoApplication { |
||||||
|
/** |
||||||
|
* アプリケーションのメインメソッド |
||||||
|
* |
||||||
|
* @param args コマンドライン引数 |
||||||
|
*/ |
||||||
|
public static void main(String[] args) { |
||||||
|
SpringApplication.run(TodoApplication.class, args); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
package com.example.todoapp.config; |
||||||
|
|
||||||
|
import com.example.todoapp.model.Task; |
||||||
|
import com.example.todoapp.model.User; |
||||||
|
import com.example.todoapp.repository.TaskRepository; |
||||||
|
import com.example.todoapp.repository.UserRepository; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.CommandLineRunner; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder; |
||||||
|
|
||||||
|
/** |
||||||
|
* アプリケーション起動時のデータ初期化を行うクラス |
||||||
|
* <p> |
||||||
|
* このクラスはアプリケーション起動時に実行され、サンプルデータを |
||||||
|
* データベースに投入します。開発環境やデモ環境での利用を想定しています。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
public class DataInitializer { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private UserRepository userRepository; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private TaskRepository taskRepository; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private PasswordEncoder passwordEncoder; |
||||||
|
|
||||||
|
/** |
||||||
|
* アプリケーション起動時に実行されるデータ初期化メソッド |
||||||
|
* |
||||||
|
* @return CommandLineRunner インスタンス |
||||||
|
*/ |
||||||
|
@Bean |
||||||
|
public CommandLineRunner initData() { |
||||||
|
return args -> { |
||||||
|
// サンプルユーザーの作成
|
||||||
|
if (userRepository.count() == 0) { |
||||||
|
User testUser = new User(); |
||||||
|
testUser.setUsername("testuser"); |
||||||
|
testUser.setPassword(passwordEncoder.encode("password")); |
||||||
|
userRepository.save(testUser); |
||||||
|
|
||||||
|
// サンプルタスクの作成
|
||||||
|
Task task1 = new Task(); |
||||||
|
task1.setTitle("サンプルタスク1"); |
||||||
|
task1.setDescription("これはサンプルタスクです"); |
||||||
|
task1.setCompleted(false); |
||||||
|
task1.setUser(testUser); |
||||||
|
taskRepository.save(task1); |
||||||
|
|
||||||
|
Task task2 = new Task(); |
||||||
|
task2.setTitle("サンプルタスク2"); |
||||||
|
task2.setDescription("これは完了済みのサンプルタスクです"); |
||||||
|
task2.setCompleted(true); |
||||||
|
task2.setUser(testUser); |
||||||
|
taskRepository.save(task2); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,124 @@ |
|||||||
|
package com.example.todoapp.config; |
||||||
|
|
||||||
|
import com.example.todoapp.security.JwtAuthenticationFilter; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.beans.factory.annotation.Value; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.security.authentication.AuthenticationManager; |
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||||
|
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; |
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
||||||
|
import org.springframework.web.cors.CorsConfiguration; |
||||||
|
import org.springframework.web.cors.CorsConfigurationSource; |
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
|
||||||
|
/** |
||||||
|
* セキュリティ設定クラス |
||||||
|
* <p> |
||||||
|
* このクラスはアプリケーションのセキュリティ設定を定義します。 |
||||||
|
* JWT認証、CORS設定、エンドポイントのアクセス制御などを設定します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
@EnableWebSecurity |
||||||
|
public class SecurityConfig { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private JwtAuthenticationFilter jwtAuthenticationFilter; |
||||||
|
|
||||||
|
/** |
||||||
|
* セキュリティフィルターチェーンの設定 |
||||||
|
* |
||||||
|
* @param http HttpSecurityオブジェクト |
||||||
|
* @return 設定済みのSecurityFilterChain |
||||||
|
* @throws Exception セキュリティ設定中に例外が発生した場合 |
||||||
|
*/ |
||||||
|
@Bean |
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
||||||
|
http |
||||||
|
.cors().and() |
||||||
|
.csrf().disable() |
||||||
|
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) |
||||||
|
.and() |
||||||
|
.authorizeHttpRequests() |
||||||
|
.requestMatchers("/auth/**").permitAll() // 認証エンドポイントは認証不要
|
||||||
|
.anyRequest().authenticated(); // その他のエンドポイントは認証必要
|
||||||
|
|
||||||
|
// JWTフィルターを認証フィルターの前に追加
|
||||||
|
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); |
||||||
|
|
||||||
|
return http.build(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証マネージャーの設定 |
||||||
|
* |
||||||
|
* @param authConfig 認証設定 |
||||||
|
* @return AuthenticationManagerインスタンス |
||||||
|
* @throws Exception 設定中に例外が発生した場合 |
||||||
|
*/ |
||||||
|
@Bean |
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { |
||||||
|
return authConfig.getAuthenticationManager(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* パスワードエンコーダーの設定 |
||||||
|
* |
||||||
|
* @return BCryptPasswordEncoderインスタンス |
||||||
|
*/ |
||||||
|
@Bean |
||||||
|
public PasswordEncoder passwordEncoder() { |
||||||
|
return new BCryptPasswordEncoder(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* CORS設定 |
||||||
|
* <p> |
||||||
|
* クロスオリジンリソース共有の設定を行います。 |
||||||
|
* フロントエンドアプリケーションからのリクエストを許可します。 |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @return CORSの設定 |
||||||
|
*/ |
||||||
|
@Value("${cors.allowed-origins}") |
||||||
|
private String allowedOrigins; |
||||||
|
|
||||||
|
@Value("${cors.allowed-methods}") |
||||||
|
private String allowedMethods; |
||||||
|
|
||||||
|
@Value("${cors.allowed-headers}") |
||||||
|
private String allowedHeaders; |
||||||
|
|
||||||
|
@Value("${cors.exposed-headers}") |
||||||
|
private String exposedHeaders; |
||||||
|
|
||||||
|
@Value("${cors.allow-credentials}") |
||||||
|
private boolean allowCredentials; |
||||||
|
|
||||||
|
@Value("${cors.max-age}") |
||||||
|
private long maxAge; |
||||||
|
|
||||||
|
@Bean |
||||||
|
public CorsConfigurationSource corsConfigurationSource() { |
||||||
|
CorsConfiguration configuration = new CorsConfiguration(); |
||||||
|
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(","))); |
||||||
|
configuration.setAllowedMethods(Arrays.asList(allowedMethods.split(","))); |
||||||
|
configuration.setAllowedHeaders(Arrays.asList(allowedHeaders.split(","))); |
||||||
|
configuration.setExposedHeaders(Arrays.asList(exposedHeaders.split(","))); |
||||||
|
configuration.setAllowCredentials(allowCredentials); |
||||||
|
configuration.setMaxAge(maxAge); |
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); |
||||||
|
source.registerCorsConfiguration("/**", configuration); |
||||||
|
return source; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
package com.example.todoapp.controller; |
||||||
|
|
||||||
|
import com.example.todoapp.security.dto.AuthRequest; |
||||||
|
import com.example.todoapp.security.dto.AuthResponse; |
||||||
|
import com.example.todoapp.security.dto.RegisterRequest; |
||||||
|
import com.example.todoapp.service.AuthService; |
||||||
|
import jakarta.validation.Valid; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.http.ResponseEntity; |
||||||
|
import org.springframework.web.bind.annotation.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証機能のRESTコントローラー |
||||||
|
* <p> |
||||||
|
* このコントローラーはユーザー登録とログイン機能のエンドポイントを提供します。 |
||||||
|
* 認証に成功するとJWTトークンを発行します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@RestController |
||||||
|
@RequestMapping("/auth") |
||||||
|
public class AuthController { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private AuthService authService; |
||||||
|
|
||||||
|
/** |
||||||
|
* 新規ユーザー登録を行う |
||||||
|
* |
||||||
|
* @param request ユーザー登録情報(ユーザー名、パスワード) |
||||||
|
* @return JWT認証トークンと登録されたユーザー情報 |
||||||
|
*/ |
||||||
|
@PostMapping("/register") |
||||||
|
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) { |
||||||
|
return ResponseEntity.ok(authService.register(request)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザーログイン処理を行う |
||||||
|
* |
||||||
|
* @param request ログイン情報(ユーザー名、パスワード) |
||||||
|
* @return JWT認証トークンとユーザー情報 |
||||||
|
*/ |
||||||
|
@PostMapping("/login") |
||||||
|
public ResponseEntity<AuthResponse> login(@Valid @RequestBody AuthRequest request) { |
||||||
|
return ResponseEntity.ok(authService.login(request)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,106 @@ |
|||||||
|
package com.example.todoapp.controller; |
||||||
|
|
||||||
|
import com.example.todoapp.dto.TaskDTO; |
||||||
|
import com.example.todoapp.model.Task; |
||||||
|
import com.example.todoapp.service.TaskService; |
||||||
|
import jakarta.validation.Valid; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.http.ResponseEntity; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.web.bind.annotation.*; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスク管理のRESTコントローラー |
||||||
|
* <p> |
||||||
|
* このコントローラーはタスクの取得、作成、更新、削除などの |
||||||
|
* エンドポイントを提供します。すべてのエンドポイントは認証が必要です。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@RestController |
||||||
|
@RequestMapping("/tasks") |
||||||
|
public class TaskController { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private TaskService taskService; |
||||||
|
|
||||||
|
/** |
||||||
|
* ログインユーザーのすべてのタスクを取得する |
||||||
|
* |
||||||
|
* @param authentication 認証情報 |
||||||
|
* @return ユーザーのタスクリスト |
||||||
|
*/ |
||||||
|
@GetMapping |
||||||
|
public ResponseEntity<List<TaskDTO>> getAllTasks(Authentication authentication) { |
||||||
|
List<Task> tasks = taskService.getAllTasksByUser(authentication.getName()); |
||||||
|
// エンティティからDTOへの変換
|
||||||
|
List<TaskDTO> taskDTOs = tasks.stream() |
||||||
|
.map(TaskDTO::fromEntity) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
return ResponseEntity.ok(taskDTOs); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたIDのタスクを取得する |
||||||
|
* |
||||||
|
* @param authentication 認証情報 |
||||||
|
* @param taskId タスクID |
||||||
|
* @return タスク情報 |
||||||
|
*/ |
||||||
|
@GetMapping("/{id}") |
||||||
|
public ResponseEntity<TaskDTO> getTaskById( |
||||||
|
Authentication authentication, |
||||||
|
@PathVariable("id") Long taskId) { |
||||||
|
Task task = taskService.getTaskById(authentication.getName(), taskId); |
||||||
|
return ResponseEntity.ok(TaskDTO.fromEntity(task)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 新しいタスクを作成する |
||||||
|
* |
||||||
|
* @param authentication 認証情報 |
||||||
|
* @param task 作成するタスクの情報 |
||||||
|
* @return 作成されたタスク |
||||||
|
*/ |
||||||
|
@PostMapping |
||||||
|
public ResponseEntity<TaskDTO> createTask( |
||||||
|
Authentication authentication, |
||||||
|
@Valid @RequestBody Task task) { |
||||||
|
Task createdTask = taskService.createTask(authentication.getName(), task); |
||||||
|
return ResponseEntity.ok(TaskDTO.fromEntity(createdTask)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたIDのタスクを更新する |
||||||
|
* |
||||||
|
* @param authentication 認証情報 |
||||||
|
* @param taskId 更新するタスクのID |
||||||
|
* @param taskDetails 更新内容 |
||||||
|
* @return 更新されたタスク |
||||||
|
*/ |
||||||
|
@PutMapping("/{id}") |
||||||
|
public ResponseEntity<TaskDTO> updateTask( |
||||||
|
Authentication authentication, |
||||||
|
@PathVariable("id") Long taskId, |
||||||
|
@Valid @RequestBody Task taskDetails) { |
||||||
|
Task updatedTask = taskService.updateTask(authentication.getName(), taskId, taskDetails); |
||||||
|
return ResponseEntity.ok(TaskDTO.fromEntity(updatedTask)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたIDのタスクを削除する |
||||||
|
* |
||||||
|
* @param authentication 認証情報 |
||||||
|
* @param taskId 削除するタスクのID |
||||||
|
* @return 空のレスポンス |
||||||
|
*/ |
||||||
|
@DeleteMapping("/{id}") |
||||||
|
public ResponseEntity<?> deleteTask( |
||||||
|
Authentication authentication, |
||||||
|
@PathVariable("id") Long taskId) { |
||||||
|
taskService.deleteTask(authentication.getName(), taskId); |
||||||
|
return ResponseEntity.ok().build(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package com.example.todoapp.dto; |
||||||
|
|
||||||
|
import com.example.todoapp.model.Task; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
import java.time.LocalDateTime; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクのデータ転送オブジェクト(DTO) |
||||||
|
* <p> |
||||||
|
* このクラスはクライアントとサーバー間でタスク情報をやり取りするために使用されます。 |
||||||
|
* エンティティとは異なり、必要な情報のみを含み、関連エンティティへの参照ではなくIDのみを保持します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class TaskDTO { |
||||||
|
private Long id; |
||||||
|
private String title; |
||||||
|
private String description; |
||||||
|
private boolean completed; |
||||||
|
private Long userId; |
||||||
|
private LocalDateTime createdAt; |
||||||
|
private LocalDateTime updatedAt; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクエンティティからDTOを作成する |
||||||
|
* |
||||||
|
* @param task 変換元のタスクエンティティ |
||||||
|
* @return 変換されたTaskDTOオブジェクト |
||||||
|
*/ |
||||||
|
public static TaskDTO fromEntity(Task task) { |
||||||
|
TaskDTO dto = new TaskDTO(); |
||||||
|
dto.setId(task.getId()); |
||||||
|
dto.setTitle(task.getTitle()); |
||||||
|
dto.setDescription(task.getDescription()); |
||||||
|
dto.setCompleted(task.isCompleted()); |
||||||
|
dto.setUserId(task.getUser() != null ? task.getUser().getId() : null); |
||||||
|
dto.setCreatedAt(task.getCreatedAt()); |
||||||
|
dto.setUpdatedAt(task.getUpdatedAt()); |
||||||
|
return dto; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
package com.example.todoapp.exception; |
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity; |
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice; |
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler; |
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* アプリケーション全体の例外ハンドリングを行うクラス |
||||||
|
* <p> |
||||||
|
* このクラスはアプリケーション内で発生した例外を捕捉し、 |
||||||
|
* クライアントに適切なエラーレスポンスを返します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@ControllerAdvice |
||||||
|
public class GlobalExceptionHandler { |
||||||
|
|
||||||
|
/** |
||||||
|
* RuntimeExceptionとそのサブクラスの例外をハンドリングする |
||||||
|
* |
||||||
|
* @param ex 発生した例外 |
||||||
|
* @return エラーメッセージを含むレスポンス |
||||||
|
*/ |
||||||
|
@ExceptionHandler(RuntimeException.class) |
||||||
|
public ResponseEntity<?> handleRuntimeException(RuntimeException ex) { |
||||||
|
Map<String, String> response = new HashMap<>(); |
||||||
|
response.put("message", ex.getMessage()); |
||||||
|
return ResponseEntity.badRequest().body(response); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* バリデーション例外をハンドリングする |
||||||
|
* <p> |
||||||
|
* リクエストボディのバリデーションエラーを処理し、 |
||||||
|
* フィールド名とエラーメッセージのマップを返します。 |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @param ex バリデーション例外 |
||||||
|
* @return フィールド別のエラーメッセージを含むレスポンス |
||||||
|
*/ |
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class) |
||||||
|
public ResponseEntity<?> handleValidationException(MethodArgumentNotValidException ex) { |
||||||
|
Map<String, String> response = new HashMap<>(); |
||||||
|
ex.getBindingResult().getFieldErrors().forEach(error -> { |
||||||
|
response.put(error.getField(), error.getDefaultMessage()); |
||||||
|
}); |
||||||
|
return ResponseEntity.badRequest().body(response); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
package com.example.todoapp.model; |
||||||
|
|
||||||
|
import jakarta.persistence.*; |
||||||
|
import jakarta.validation.constraints.NotBlank; |
||||||
|
import lombok.Data; |
||||||
|
import lombok.NoArgsConstructor; |
||||||
|
|
||||||
|
import java.time.LocalDateTime; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクエンティティクラス |
||||||
|
* <p> |
||||||
|
* このクラスはユーザーのタスク情報を表します。 |
||||||
|
* タスクはタイトル、説明、完了状態などの情報を持ちます。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
@NoArgsConstructor |
||||||
|
@Entity |
||||||
|
@Table(name = "tasks") |
||||||
|
public class Task { |
||||||
|
/** |
||||||
|
* タスクID(主キー) |
||||||
|
*/ |
||||||
|
@Id |
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY) |
||||||
|
private Long id; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクのタイトル(必須) |
||||||
|
*/ |
||||||
|
@NotBlank |
||||||
|
@Column(length = 255, nullable = false) |
||||||
|
private String title; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクの詳細説明(任意) |
||||||
|
*/ |
||||||
|
@Column(length = 255) |
||||||
|
private String description; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクの完了状態 |
||||||
|
* デフォルトは未完了(false) |
||||||
|
*/ |
||||||
|
@Column(nullable = false) |
||||||
|
private boolean completed = false; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクの所有者(ユーザー) |
||||||
|
* 多対一の関係で、遅延ロードを使用 |
||||||
|
*/ |
||||||
|
@ManyToOne(fetch = FetchType.LAZY) |
||||||
|
@JoinColumn(name = "user_id", nullable = false) |
||||||
|
private User user; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクの作成日時 |
||||||
|
*/ |
||||||
|
@Column(name = "created_at", nullable = false) |
||||||
|
private LocalDateTime createdAt; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクの最終更新日時 |
||||||
|
*/ |
||||||
|
@Column(name = "updated_at", nullable = false) |
||||||
|
private LocalDateTime updatedAt; |
||||||
|
|
||||||
|
/** |
||||||
|
* エンティティ作成時に自動的に実行される処理 |
||||||
|
* 作成日時と更新日時を現在時刻に設定します |
||||||
|
*/ |
||||||
|
@PrePersist |
||||||
|
protected void onCreate() { |
||||||
|
createdAt = LocalDateTime.now(); |
||||||
|
updatedAt = LocalDateTime.now(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* エンティティ更新時に自動的に実行される処理 |
||||||
|
* 更新日時を現在時刻に設定します |
||||||
|
*/ |
||||||
|
@PreUpdate |
||||||
|
protected void onUpdate() { |
||||||
|
updatedAt = LocalDateTime.now(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
package com.example.todoapp.model; |
||||||
|
|
||||||
|
import jakarta.persistence.*; |
||||||
|
import jakarta.validation.constraints.NotBlank; |
||||||
|
import lombok.Data; |
||||||
|
import lombok.NoArgsConstructor; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザーエンティティクラス |
||||||
|
* <p> |
||||||
|
* このクラスはアプリケーションのユーザー情報を表します。 |
||||||
|
* ユーザー認証情報と個人情報、および関連するタスクを管理します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
@NoArgsConstructor |
||||||
|
@Entity |
||||||
|
@Table(name = "users") |
||||||
|
public class User { |
||||||
|
/** |
||||||
|
* ユーザーID(主キー) |
||||||
|
*/ |
||||||
|
@Id |
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY) |
||||||
|
private Long id; |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザー名(ログインID) |
||||||
|
* システム内で一意である必要があります |
||||||
|
*/ |
||||||
|
@NotBlank |
||||||
|
@Column(unique = true, length = 255, nullable = false) |
||||||
|
private String username; |
||||||
|
|
||||||
|
/** |
||||||
|
* パスワード(ハッシュ化して保存) |
||||||
|
*/ |
||||||
|
@NotBlank |
||||||
|
@Column(length = 255, nullable = false) |
||||||
|
private String password; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザーが所有するタスクのリスト |
||||||
|
* ユーザーが削除された場合、関連するタスクも削除されます |
||||||
|
*/ |
||||||
|
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) |
||||||
|
private List<Task> tasks = new ArrayList<>(); |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package com.example.todoapp.repository; |
||||||
|
|
||||||
|
import com.example.todoapp.model.Task; |
||||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||||
|
import org.springframework.stereotype.Repository; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクエンティティのリポジトリインターフェース |
||||||
|
* <p> |
||||||
|
* このインターフェースはタスクデータへのアクセスと操作を提供します。 |
||||||
|
* Spring Data JPAによって自動的に実装されます。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Repository |
||||||
|
public interface TaskRepository extends JpaRepository<Task, Long> { |
||||||
|
/** |
||||||
|
* 指定されたユーザーIDに関連するすべてのタスクを作成日時の降順で取得する |
||||||
|
* |
||||||
|
* @param userId ユーザーID |
||||||
|
* @return 指定されたユーザーのタスクリスト(作成日時の降順) |
||||||
|
*/ |
||||||
|
List<Task> findByUserIdOrderByCreatedAtDesc(Long userId); |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたユーザーIDと完了状態に一致するタスクを作成日時の降順で取得する |
||||||
|
* |
||||||
|
* @param userId ユーザーID |
||||||
|
* @param completed タスクの完了状態 |
||||||
|
* @return 条件に一致するタスクのリスト(作成日時の降順) |
||||||
|
*/ |
||||||
|
List<Task> findByUserIdAndCompletedOrderByCreatedAtDesc(Long userId, boolean completed); |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたユーザーIDとタスクIDに一致するタスクを削除する |
||||||
|
* |
||||||
|
* @param userId ユーザーID |
||||||
|
* @param taskId タスクID |
||||||
|
*/ |
||||||
|
void deleteByUserIdAndId(Long userId, Long taskId); |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package com.example.todoapp.repository; |
||||||
|
|
||||||
|
import com.example.todoapp.model.User; |
||||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||||
|
import org.springframework.stereotype.Repository; |
||||||
|
|
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザーエンティティのリポジトリインターフェース |
||||||
|
* <p> |
||||||
|
* このインターフェースはユーザーデータへのアクセスと操作を提供します。 |
||||||
|
* Spring Data JPAによって自動的に実装されます。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Repository |
||||||
|
public interface UserRepository extends JpaRepository<User, Long> { |
||||||
|
/** |
||||||
|
* ユーザー名からユーザーを検索する |
||||||
|
* |
||||||
|
* @param username 検索するユーザー名 |
||||||
|
* @return 見つかったユーザー(存在しない場合は空のOptional) |
||||||
|
*/ |
||||||
|
Optional<User> findByUsername(String username); |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたユーザー名が既に存在するかを確認する |
||||||
|
* |
||||||
|
* @param username 確認するユーザー名 |
||||||
|
* @return ユーザー名が存在する場合はtrue、存在しない場合はfalse |
||||||
|
*/ |
||||||
|
boolean existsByUsername(String username); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,103 @@ |
|||||||
|
package com.example.todoapp.security; |
||||||
|
|
||||||
|
import com.example.todoapp.util.MessageUtils; |
||||||
|
import jakarta.servlet.FilterChain; |
||||||
|
import jakarta.servlet.ServletException; |
||||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.beans.factory.annotation.Value; |
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||||
|
import org.springframework.security.core.context.SecurityContextHolder; |
||||||
|
import org.springframework.security.core.userdetails.UserDetails; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.web.filter.OncePerRequestFilter; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
/** |
||||||
|
* JWT認証フィルタークラス |
||||||
|
* <p> |
||||||
|
* このフィルターはHTTPリクエストからJWTトークンを抽出し、 |
||||||
|
* トークンの検証とユーザー認証を行います。 |
||||||
|
* 認証に成功した場合、SecurityContextに認証情報を設定します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private JwtTokenProvider tokenProvider; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private UserDetailsService userDetailsService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MessageUtils messageUtils; |
||||||
|
|
||||||
|
@Value("${jwt.token-prefix}") |
||||||
|
private String tokenPrefix; |
||||||
|
|
||||||
|
@Value("${jwt.header-name}") |
||||||
|
private String headerName; |
||||||
|
|
||||||
|
/** |
||||||
|
* リクエスト処理の内部メソッド |
||||||
|
* <p> |
||||||
|
* リクエストからJWTトークンを抽出し、検証して認証を行います。 |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @param request HTTPリクエスト |
||||||
|
* @param response HTTPレスポンス |
||||||
|
* @param filterChain フィルターチェーン |
||||||
|
* @throws ServletException サーブレット例外 |
||||||
|
* @throws IOException I/O例外 |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
||||||
|
throws ServletException, IOException { |
||||||
|
try { |
||||||
|
// リクエストからJWTトークンを取得
|
||||||
|
String jwt = getJwtFromRequest(request); |
||||||
|
|
||||||
|
// トークンが存在し、有効な場合は認証を行う
|
||||||
|
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { |
||||||
|
String username = tokenProvider.getUsernameFromToken(jwt); |
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username); |
||||||
|
|
||||||
|
// 認証トークンを作成
|
||||||
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( |
||||||
|
userDetails, null, userDetails.getAuthorities()); |
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
||||||
|
|
||||||
|
// SecurityContextに認証情報を設定
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication); |
||||||
|
} |
||||||
|
} catch (Exception ex) { |
||||||
|
logger.error(messageUtils.getMessage("error.auth.failed"), ex); |
||||||
|
} |
||||||
|
|
||||||
|
// 次のフィルターを実行
|
||||||
|
filterChain.doFilter(request, response); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* HTTPリクエストからJWTトークンを抽出する |
||||||
|
* |
||||||
|
* @param request HTTPリクエスト |
||||||
|
* @return 抽出されたJWTトークン、存在しない場合はnull |
||||||
|
*/ |
||||||
|
private String getJwtFromRequest(HttpServletRequest request) { |
||||||
|
String bearerToken = request.getHeader(headerName); |
||||||
|
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(tokenPrefix + " ")) { |
||||||
|
// 例: "Bearer eyJhbGciOiJIUzI1NiJ9..." から "Bearer " を除去して
|
||||||
|
// 実際のトークン部分 "eyJhbGciOiJIUzI1NiJ9..." のみを返す
|
||||||
|
// tokenPrefix.length() + 1 は "Bearer" の長さ + スペース1文字分のインデックス
|
||||||
|
return bearerToken.substring(tokenPrefix.length() + 1); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
package com.example.todoapp.security; |
||||||
|
|
||||||
|
import io.jsonwebtoken.*; |
||||||
|
import io.jsonwebtoken.security.Keys; |
||||||
|
import org.springframework.beans.factory.annotation.Value; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.core.userdetails.UserDetails; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.security.Key; |
||||||
|
import java.util.Date; |
||||||
|
|
||||||
|
/** |
||||||
|
* JWTトークン生成・検証プロバイダークラス |
||||||
|
* <p> |
||||||
|
* このクラスはJWTトークンの生成、検証、およびトークンからの情報抽出を担当します。 |
||||||
|
* アプリケーションの認証システムで使用されます。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
public class JwtTokenProvider { |
||||||
|
|
||||||
|
@Value("${jwt.secret}") |
||||||
|
private String jwtSecret; |
||||||
|
|
||||||
|
@Value("${jwt.expiration}") |
||||||
|
private long jwtExpirationInMs; |
||||||
|
|
||||||
|
/** |
||||||
|
* JWT署名用の鍵を取得する |
||||||
|
* |
||||||
|
* @return 署名用のキー |
||||||
|
*/ |
||||||
|
private Key getSigningKey() { |
||||||
|
byte[] keyBytes = jwtSecret.getBytes(); |
||||||
|
return Keys.hmacShaKeyFor(keyBytes); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証情報からJWTトークンを生成する |
||||||
|
* |
||||||
|
* @param authentication 認証情報 |
||||||
|
* @return 生成されたJWTトークン |
||||||
|
*/ |
||||||
|
public String generateToken(Authentication authentication) { |
||||||
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); |
||||||
|
Date now = new Date(); |
||||||
|
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); |
||||||
|
|
||||||
|
return Jwts.builder() |
||||||
|
.setSubject(userDetails.getUsername()) |
||||||
|
.setIssuedAt(now) |
||||||
|
.setExpiration(expiryDate) |
||||||
|
.signWith(getSigningKey()) |
||||||
|
.compact(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* トークンからユーザー名を抽出する |
||||||
|
* |
||||||
|
* @param token JWTトークン |
||||||
|
* @return トークンに含まれるユーザー名 |
||||||
|
*/ |
||||||
|
public String getUsernameFromToken(String token) { |
||||||
|
Claims claims = Jwts.parserBuilder() |
||||||
|
.setSigningKey(getSigningKey()) |
||||||
|
.build() |
||||||
|
.parseClaimsJws(token) |
||||||
|
.getBody(); |
||||||
|
|
||||||
|
return claims.getSubject(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* トークンの有効性を検証する |
||||||
|
* |
||||||
|
* @param token 検証するJWTトークン |
||||||
|
* @return トークンが有効な場合はtrue、無効な場合はfalse |
||||||
|
*/ |
||||||
|
public boolean validateToken(String token) { |
||||||
|
try { |
||||||
|
Jwts.parserBuilder() |
||||||
|
.setSigningKey(getSigningKey()) |
||||||
|
.build() |
||||||
|
.parseClaimsJws(token); |
||||||
|
return true; |
||||||
|
} catch (JwtException | IllegalArgumentException e) { |
||||||
|
// トークンの検証に失敗した場合
|
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
package com.example.todoapp.security; |
||||||
|
|
||||||
|
import com.example.todoapp.model.User; |
||||||
|
import com.example.todoapp.repository.UserRepository; |
||||||
|
import com.example.todoapp.util.MessageUtils; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.core.userdetails.UserDetails; |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService; |
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
import org.springframework.transaction.annotation.Transactional; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
|
||||||
|
/** |
||||||
|
* Spring Securityのユーザー詳細サービス実装クラス |
||||||
|
* <p> |
||||||
|
* このクラスはユーザー名からユーザー情報を取得し、 |
||||||
|
* Spring Securityの認証システムで使用するUserDetailsオブジェクトを提供します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Service |
||||||
|
public class UserDetailsServiceImpl implements UserDetailsService { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private UserRepository userRepository; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MessageUtils messageUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザー名からユーザー詳細情報を読み込む |
||||||
|
* <p> |
||||||
|
* データベースからユーザー情報を検索し、Spring Securityで使用する |
||||||
|
* UserDetailsオブジェクトに変換します。 |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @param username 検索するユーザー名 |
||||||
|
* @return UserDetailsオブジェクト |
||||||
|
* @throws UsernameNotFoundException ユーザーが見つからない場合 |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
@Transactional |
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { |
||||||
|
User user = userRepository.findByUsername(username) |
||||||
|
.orElseThrow(() -> new UsernameNotFoundException( |
||||||
|
messageUtils.getMessage("error.auth.user.not.found.with.username", new Object[]{username}))); |
||||||
|
|
||||||
|
// 権限リストは現在空のリストを使用(将来的に拡張可能)
|
||||||
|
return new org.springframework.security.core.userdetails.User( |
||||||
|
user.getUsername(), |
||||||
|
user.getPassword(), |
||||||
|
new ArrayList<>() |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package com.example.todoapp.security.dto; |
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証リクエストのデータ転送オブジェクト(DTO) |
||||||
|
* <p> |
||||||
|
* このクラスはログイン処理で使用される認証情報を表します。 |
||||||
|
* クライアントからサーバーへのログインリクエストに使用されます。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class AuthRequest { |
||||||
|
/** |
||||||
|
* ユーザー名(必須) |
||||||
|
*/ |
||||||
|
@NotBlank |
||||||
|
private String username; |
||||||
|
|
||||||
|
/** |
||||||
|
* パスワード(必須) |
||||||
|
*/ |
||||||
|
@NotBlank |
||||||
|
private String password; |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package com.example.todoapp.security.dto; |
||||||
|
|
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証レスポンスのデータ転送オブジェクト(DTO) |
||||||
|
* <p> |
||||||
|
* このクラスは認証成功時にクライアントに返される情報を表します。 |
||||||
|
* JWTトークンとユーザー情報を含みます。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
@AllArgsConstructor |
||||||
|
public class AuthResponse { |
||||||
|
/** |
||||||
|
* JWT認証トークン |
||||||
|
*/ |
||||||
|
private String token; |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザー名(ログインID) |
||||||
|
*/ |
||||||
|
private String username; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
package com.example.todoapp.security.dto; |
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザー登録リクエストのデータ転送オブジェクト(DTO) |
||||||
|
* <p> |
||||||
|
* このクラスは新規ユーザー登録時に必要な情報を表します。 |
||||||
|
* クライアントからサーバーへのユーザー登録リクエストに使用されます。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class RegisterRequest { |
||||||
|
/** |
||||||
|
* ユーザー名(ログインID)(必須) |
||||||
|
*/ |
||||||
|
@NotBlank |
||||||
|
private String username; |
||||||
|
|
||||||
|
/** |
||||||
|
* パスワード(必須) |
||||||
|
*/ |
||||||
|
@NotBlank |
||||||
|
private String password; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
package com.example.todoapp.service; |
||||||
|
|
||||||
|
import com.example.todoapp.model.User; |
||||||
|
import com.example.todoapp.util.MessageUtils; |
||||||
|
import com.example.todoapp.repository.UserRepository; |
||||||
|
import com.example.todoapp.security.JwtTokenProvider; |
||||||
|
import com.example.todoapp.security.dto.AuthRequest; |
||||||
|
import com.example.todoapp.security.dto.AuthResponse; |
||||||
|
import com.example.todoapp.security.dto.RegisterRequest; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.authentication.AuthenticationManager; |
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
import org.springframework.transaction.annotation.Transactional; |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証機能のサービスクラス |
||||||
|
* <p> |
||||||
|
* このクラスはユーザー登録とログイン処理のビジネスロジックを提供します。 |
||||||
|
* 認証に成功した場合、JWTトークンを発行します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Service |
||||||
|
public class AuthService { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private AuthenticationManager authenticationManager; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private UserRepository userRepository; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private PasswordEncoder passwordEncoder; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private JwtTokenProvider tokenProvider; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MessageUtils messageUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* 新規ユーザー登録を行う |
||||||
|
* <p> |
||||||
|
* ユーザー名の重複チェックを行い、 |
||||||
|
* 問題がなければ新規ユーザーを作成します。 |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @param request ユーザー登録情報 |
||||||
|
* @return JWT認証トークンとユーザー情報 |
||||||
|
* @throws RuntimeException ユーザー名が既に使用されている場合 |
||||||
|
*/ |
||||||
|
@Transactional |
||||||
|
public AuthResponse register(RegisterRequest request) { |
||||||
|
// ユーザー名の重複チェック
|
||||||
|
if (userRepository.existsByUsername(request.getUsername())) { |
||||||
|
throw new RuntimeException(messageUtils.getMessage("error.auth.username.exists")); |
||||||
|
} |
||||||
|
|
||||||
|
// 新規ユーザーの作成
|
||||||
|
User user = new User(); |
||||||
|
user.setUsername(request.getUsername()); |
||||||
|
user.setPassword(passwordEncoder.encode(request.getPassword())); // パスワードをハッシュ化
|
||||||
|
|
||||||
|
userRepository.save(user); |
||||||
|
|
||||||
|
// 認証処理
|
||||||
|
Authentication authentication = authenticationManager.authenticate( |
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) |
||||||
|
); |
||||||
|
|
||||||
|
// JWTトークンの生成
|
||||||
|
String token = tokenProvider.generateToken(authentication); |
||||||
|
|
||||||
|
return new AuthResponse(token, user.getUsername()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザーログイン処理を行う |
||||||
|
* |
||||||
|
* @param request ログイン情報(ユーザー名、パスワード) |
||||||
|
* @return JWT認証トークンとユーザー情報 |
||||||
|
* @throws RuntimeException 認証に失敗した場合 |
||||||
|
*/ |
||||||
|
public AuthResponse login(AuthRequest request) { |
||||||
|
// 認証処理
|
||||||
|
Authentication authentication = authenticationManager.authenticate( |
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) |
||||||
|
); |
||||||
|
|
||||||
|
// JWTトークンの生成
|
||||||
|
String token = tokenProvider.generateToken(authentication); |
||||||
|
User user = userRepository.findByUsername(request.getUsername()) |
||||||
|
.orElseThrow(() -> new RuntimeException(messageUtils.getMessage("error.auth.user.not.found"))); |
||||||
|
|
||||||
|
return new AuthResponse(token, user.getUsername()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,114 @@ |
|||||||
|
package com.example.todoapp.service; |
||||||
|
|
||||||
|
import com.example.todoapp.model.Task; |
||||||
|
import com.example.todoapp.util.MessageUtils; |
||||||
|
import com.example.todoapp.model.User; |
||||||
|
import com.example.todoapp.repository.TaskRepository; |
||||||
|
import com.example.todoapp.repository.UserRepository; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
import org.springframework.transaction.annotation.Transactional; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスク管理のサービスクラス |
||||||
|
* <p> |
||||||
|
* このクラスはタスクの取得、作成、更新、削除などのビジネスロジックを提供します。 |
||||||
|
* ユーザーごとのタスク管理を行い、他のユーザーのタスクへのアクセスを制限します。 |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
@Service |
||||||
|
public class TaskService { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private TaskRepository taskRepository; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private UserRepository userRepository; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MessageUtils messageUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたユーザーのすべてのタスクを取得する |
||||||
|
* |
||||||
|
* @param username ユーザー名 |
||||||
|
* @return ユーザーのタスクリスト(作成日時の降順) |
||||||
|
*/ |
||||||
|
public List<Task> getAllTasksByUser(String username) { |
||||||
|
User user = getUserByUsername(username); |
||||||
|
return taskRepository.findByUserIdOrderByCreatedAtDesc(user.getId()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたユーザーの特定のタスクを取得する |
||||||
|
* |
||||||
|
* @param username ユーザー名 |
||||||
|
* @param taskId タスクID |
||||||
|
* @return タスクエンティティ |
||||||
|
* @throws RuntimeException タスクが見つからない場合、または他のユーザーのタスクにアクセスしようとした場合 |
||||||
|
*/ |
||||||
|
public Task getTaskById(String username, Long taskId) { |
||||||
|
User user = getUserByUsername(username); |
||||||
|
return taskRepository.findById(taskId) |
||||||
|
.filter(task -> task.getUser().getId().equals(user.getId())) // ユーザーのタスクかどうかを確認
|
||||||
|
.orElseThrow(() -> new RuntimeException(messageUtils.getMessage("error.task.not.found"))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 新しいタスクを作成する |
||||||
|
* |
||||||
|
* @param username ユーザー名 |
||||||
|
* @param task 作成するタスクの情報 |
||||||
|
* @return 保存されたタスクエンティティ |
||||||
|
*/ |
||||||
|
@Transactional |
||||||
|
public Task createTask(String username, Task task) { |
||||||
|
User user = getUserByUsername(username); |
||||||
|
task.setUser(user); |
||||||
|
return taskRepository.save(task); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたタスクを更新する |
||||||
|
* |
||||||
|
* @param username ユーザー名 |
||||||
|
* @param taskId 更新するタスクのID |
||||||
|
* @param taskDetails 更新内容 |
||||||
|
* @return 更新されたタスクエンティティ |
||||||
|
*/ |
||||||
|
@Transactional |
||||||
|
public Task updateTask(String username, Long taskId, Task taskDetails) { |
||||||
|
Task task = getTaskById(username, taskId); |
||||||
|
task.setTitle(taskDetails.getTitle()); |
||||||
|
task.setDescription(taskDetails.getDescription()); |
||||||
|
task.setCompleted(taskDetails.isCompleted()); |
||||||
|
return taskRepository.save(task); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定されたタスクを削除する |
||||||
|
* |
||||||
|
* @param username ユーザー名 |
||||||
|
* @param taskId 削除するタスクのID |
||||||
|
*/ |
||||||
|
@Transactional |
||||||
|
public void deleteTask(String username, Long taskId) { |
||||||
|
Task task = getTaskById(username, taskId); |
||||||
|
taskRepository.delete(task); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザー名からユーザーエンティティを取得する |
||||||
|
* |
||||||
|
* @param username ユーザー名 |
||||||
|
* @return ユーザーエンティティ |
||||||
|
* @throws UsernameNotFoundException ユーザーが見つからない場合 |
||||||
|
*/ |
||||||
|
private User getUserByUsername(String username) { |
||||||
|
return userRepository.findByUsername(username) |
||||||
|
.orElseThrow(() -> new UsernameNotFoundException(messageUtils.getMessage("error.auth.user.not.found.with.name", new Object[]{username}))); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package com.example.todoapp.util; |
||||||
|
|
||||||
|
import org.springframework.context.MessageSource; |
||||||
|
import org.springframework.context.i18n.LocaleContextHolder; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.util.Locale; |
||||||
|
|
||||||
|
/** |
||||||
|
* プロパティファイルからメッセージを読み込むユーティリティクラス |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
public class MessageUtils { |
||||||
|
|
||||||
|
private final MessageSource messageSource; |
||||||
|
|
||||||
|
public MessageUtils(MessageSource messageSource) { |
||||||
|
this.messageSource = messageSource; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* メッセージコードに対応するメッセージを取得します |
||||||
|
* |
||||||
|
* @param code メッセージコード |
||||||
|
* @return メッセージ |
||||||
|
*/ |
||||||
|
public String getMessage(String code) { |
||||||
|
return getMessage(code, null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* メッセージコードに対応するメッセージを取得し、引数でフォーマットします |
||||||
|
* |
||||||
|
* @param code メッセージコード |
||||||
|
* @param args フォーマット引数 |
||||||
|
* @return フォーマットされたメッセージ |
||||||
|
*/ |
||||||
|
public String getMessage(String code, Object[] args) { |
||||||
|
Locale locale = LocaleContextHolder.getLocale(); |
||||||
|
return messageSource.getMessage(code, args, locale); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
spring: |
||||||
|
datasource: |
||||||
|
url: jdbc:postgresql://${AZURE_DB_HOST}:5432/${AZURE_DB_NAME}?sslmode=require |
||||||
|
driver-class-name: org.postgresql.Driver |
||||||
|
username: ${AZURE_DB_USER} |
||||||
|
password: ${AZURE_DB_PASSWORD} |
||||||
|
jpa: |
||||||
|
database-platform: org.hibernate.dialect.PostgreSQLDialect |
||||||
|
properties: |
||||||
|
hibernate: |
||||||
|
jdbc: |
||||||
|
lob: |
||||||
|
non_contextual_creation: true |
||||||
|
cors: |
||||||
|
allowed-origins: ${AZURE_STATIC_WEBAPP_URL} |
@ -0,0 +1,15 @@ |
|||||||
|
spring: |
||||||
|
datasource: |
||||||
|
url: jdbc:postgresql://localhost:5432/${LOCAL_DB_NAME} |
||||||
|
driver-class-name: org.postgresql.Driver |
||||||
|
username: ${LOCAL_DB_USER} |
||||||
|
password: ${LOCAL_DB_PASSWORD} |
||||||
|
jpa: |
||||||
|
database-platform: org.hibernate.dialect.PostgreSQLDialect |
||||||
|
properties: |
||||||
|
hibernate: |
||||||
|
jdbc: |
||||||
|
lob: |
||||||
|
non_contextual_creation: true |
||||||
|
cors: |
||||||
|
allowed-origins: http://localhost:3000 |
@ -0,0 +1,9 @@ |
|||||||
|
# 認証関連のエラーメッセージ |
||||||
|
error.auth.username.exists=このユーザー名は既に使用されています |
||||||
|
error.auth.user.not.found=ユーザーが見つかりません |
||||||
|
error.auth.user.not.found.with.name=ユーザーが見つかりません: %s |
||||||
|
error.auth.user.not.found.with.username=ユーザーが見つかりません: %s |
||||||
|
error.auth.failed=認証情報の設定に失敗しました |
||||||
|
|
||||||
|
# タスク関連のエラーメッセージ |
||||||
|
error.task.not.found=タスクが見つかりません |
@ -0,0 +1 @@ |
|||||||
|
REACT_APP_API_BASE_URL=${AZURE_APP_SERVICE_URL} |
@ -0,0 +1,23 @@ |
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
||||||
|
|
||||||
|
# dependencies |
||||||
|
/node_modules |
||||||
|
/.pnp |
||||||
|
.pnp.js |
||||||
|
|
||||||
|
# testing |
||||||
|
/coverage |
||||||
|
|
||||||
|
# production |
||||||
|
/build |
||||||
|
|
||||||
|
# misc |
||||||
|
.DS_Store |
||||||
|
.env.local |
||||||
|
.env.development.local |
||||||
|
.env.test.local |
||||||
|
.env.production.local |
||||||
|
|
||||||
|
npm-debug.log* |
||||||
|
yarn-debug.log* |
||||||
|
yarn-error.log* |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@ |
|||||||
|
{ |
||||||
|
"name": "frontend", |
||||||
|
"version": "0.1.0", |
||||||
|
"private": true, |
||||||
|
"homepage": ".", |
||||||
|
"proxy": "http://localhost:8080", |
||||||
|
"dependencies": { |
||||||
|
"@emotion/react": "^11.11.3", |
||||||
|
"@emotion/styled": "^11.11.0", |
||||||
|
"@mui/icons-material": "^5.15.10", |
||||||
|
"@mui/material": "^5.15.10", |
||||||
|
"@testing-library/jest-dom": "^5.17.0", |
||||||
|
"@testing-library/react": "^13.4.0", |
||||||
|
"@testing-library/user-event": "^13.5.0", |
||||||
|
"@types/jest": "^27.5.2", |
||||||
|
"@types/node": "^16.18.68", |
||||||
|
"@types/react": "^18.2.45", |
||||||
|
"@types/react-dom": "^18.2.18", |
||||||
|
"react": "^18.2.0", |
||||||
|
"react-dom": "^18.2.0", |
||||||
|
"react-router-dom": "^6.21.1", |
||||||
|
"react-scripts": "5.0.1", |
||||||
|
"typescript": "^4.9.5", |
||||||
|
"web-vitals": "^2.1.4" |
||||||
|
}, |
||||||
|
"scripts": { |
||||||
|
"start": "react-scripts start", |
||||||
|
"build": "react-scripts build", |
||||||
|
"test": "react-scripts test", |
||||||
|
"eject": "react-scripts eject" |
||||||
|
}, |
||||||
|
"eslintConfig": { |
||||||
|
"extends": [ |
||||||
|
"react-app", |
||||||
|
"react-app/jest" |
||||||
|
] |
||||||
|
}, |
||||||
|
"browserslist": { |
||||||
|
"production": [ |
||||||
|
">0.2%", |
||||||
|
"not dead", |
||||||
|
"not op_mini all" |
||||||
|
], |
||||||
|
"development": [ |
||||||
|
"last 1 chrome version", |
||||||
|
"last 1 firefox version", |
||||||
|
"last 1 safari version" |
||||||
|
] |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,42 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||||
|
<meta name="theme-color" content="#000000" /> |
||||||
|
<meta |
||||||
|
name="description" |
||||||
|
content="Web site created using create-react-app" |
||||||
|
/> |
||||||
|
<!-- |
||||||
|
manifest.json provides metadata used when your web app is installed on a |
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
||||||
|
--> |
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||||
|
<!-- |
||||||
|
Notice the use of %PUBLIC_URL% in the tags above. |
||||||
|
It will be replaced with the URL of the `public` folder during the build. |
||||||
|
Only files inside the `public` folder can be referenced from the HTML. |
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
||||||
|
work correctly both with client-side routing and a non-root public URL. |
||||||
|
Learn how to configure a non-root public URL by running `npm run build`. |
||||||
|
--> |
||||||
|
<title>React App</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||||
|
<div id="root"></div> |
||||||
|
<!-- |
||||||
|
This HTML file is a template. |
||||||
|
If you open it directly in the browser, you will see an empty page. |
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file. |
||||||
|
The build step will place the bundled scripts into the <body> tag. |
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`. |
||||||
|
To create a production bundle, use `npm run build` or `yarn build`. |
||||||
|
--> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,15 @@ |
|||||||
|
{ |
||||||
|
"short_name": "React App", |
||||||
|
"name": "Create React App Sample", |
||||||
|
"icons": [ |
||||||
|
{ |
||||||
|
"src": "favicon.ico", |
||||||
|
"sizes": "64x64 32x32 24x24 16x16", |
||||||
|
"type": "image/x-icon" |
||||||
|
} |
||||||
|
], |
||||||
|
"start_url": ".", |
||||||
|
"display": "standalone", |
||||||
|
"theme_color": "#000000", |
||||||
|
"background_color": "#ffffff" |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
# https://www.robotstxt.org/robotstxt.html |
||||||
|
User-agent: * |
||||||
|
Disallow: |
@ -0,0 +1,33 @@ |
|||||||
|
/* ボディ要素の基本スタイルリセット */ |
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
/* アプリケーションのルートコンテナスタイル */ |
||||||
|
.app { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; /* 子要素を縦方向に配置 */ |
||||||
|
min-height: 100vh; /* ビューポートの高さいっぱいに広げる */ |
||||||
|
background-color: #f5f5f5; /* 薄いグレーの背景色 */ |
||||||
|
} |
||||||
|
|
||||||
|
/* メインコンテンツエリアのスタイル */ |
||||||
|
.main-content { |
||||||
|
flex: 1; /* 利用可能なスペースをすべて使用 */ |
||||||
|
padding: 24px; |
||||||
|
width: 100%; |
||||||
|
max-width: 1200px; /* コンテンツの最大幅を制限 */ |
||||||
|
margin: 0 auto; /* 中央揃え */ |
||||||
|
} |
||||||
|
|
||||||
|
/* Material UIのコンテナコンポーネントのカスタマイズ */ |
||||||
|
.MuiContainer-root { |
||||||
|
padding-top: 24px; |
||||||
|
padding-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
/* Material UIのペーパーコンポーネントのカスタマイズ */ |
||||||
|
.MuiPaper-root { |
||||||
|
background-color: white; /* 白背景を確保 */ |
||||||
|
} |
@ -0,0 +1,103 @@ |
|||||||
|
/** |
||||||
|
* アプリケーションのルートコンポーネント |
||||||
|
* ルーティング設定、テーマ設定、認証保護などの基本構成を提供する |
||||||
|
*/ |
||||||
|
import React from 'react'; |
||||||
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; |
||||||
|
import { ThemeProvider, createTheme, CssBaseline, Box } from '@mui/material'; |
||||||
|
import Layout from './components/Layout'; |
||||||
|
import LoginPage from './pages/LoginPage'; |
||||||
|
import RegisterPage from './pages/RegisterPage'; |
||||||
|
import TaskListPage from './pages/TaskListPage'; |
||||||
|
import './App.css'; |
||||||
|
|
||||||
|
/** |
||||||
|
* アプリケーション全体のMaterial UIテーマを定義 |
||||||
|
* カラーパレット、タイポグラフィ、コンポーネントのスタイルオーバーライドを設定 |
||||||
|
*/ |
||||||
|
const theme = createTheme({ |
||||||
|
palette: { |
||||||
|
mode: 'light', |
||||||
|
primary: { |
||||||
|
main: '#1976d2', |
||||||
|
}, |
||||||
|
secondary: { |
||||||
|
main: '#dc004e', |
||||||
|
}, |
||||||
|
background: { |
||||||
|
default: '#f5f5f5', |
||||||
|
paper: '#ffffff', |
||||||
|
}, |
||||||
|
}, |
||||||
|
typography: { |
||||||
|
fontFamily: [ |
||||||
|
'-apple-system', |
||||||
|
'BlinkMacSystemFont', |
||||||
|
'"Segoe UI"', |
||||||
|
'Roboto', |
||||||
|
'"Helvetica Neue"', |
||||||
|
'Arial', |
||||||
|
'sans-serif', |
||||||
|
].join(','), |
||||||
|
}, |
||||||
|
components: { |
||||||
|
MuiPaper: { |
||||||
|
styleOverrides: { |
||||||
|
root: { |
||||||
|
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証が必要なルートを保護するためのコンポーネント |
||||||
|
* ローカルストレージにトークンがない場合はログインページにリダイレクト |
||||||
|
* トークンがある場合は子コンポーネントをレンダリング |
||||||
|
*/ |
||||||
|
const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { |
||||||
|
const token = localStorage.getItem('token'); |
||||||
|
if (!token) { |
||||||
|
return <Navigate to="/login" replace />; |
||||||
|
} |
||||||
|
return <>{children}</>; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* アプリケーションのメインコンポーネント |
||||||
|
* ルーティング構造を定義し、適切なページコンポーネントを表示 |
||||||
|
* - 未認証ユーザー: ログイン/登録ページにアクセス可能 |
||||||
|
* - 認証済みユーザー: タスク一覧ページにアクセス可能 |
||||||
|
* - ルートパスへのアクセスは自動的にタスク一覧にリダイレクト |
||||||
|
*/ |
||||||
|
const App: React.FC = () => { |
||||||
|
return ( |
||||||
|
<ThemeProvider theme={theme}> |
||||||
|
<CssBaseline /> {/* MUIのリセットCSSを適用 */} |
||||||
|
<Box className="app"> |
||||||
|
<BrowserRouter> |
||||||
|
<Routes> |
||||||
|
<Route path="/login" element={<LoginPage />} /> |
||||||
|
<Route path="/register" element={<RegisterPage />} /> |
||||||
|
<Route path="/" element={<Layout />}> |
||||||
|
{/* ルートパスへのアクセスはタスク一覧にリダイレクト */} |
||||||
|
<Route index element={<Navigate to="/tasks" replace />} /> |
||||||
|
{/* タスク一覧は認証が必要なため、PrivateRouteでラップ */} |
||||||
|
<Route |
||||||
|
path="tasks" |
||||||
|
element={ |
||||||
|
<PrivateRoute> |
||||||
|
<TaskListPage /> |
||||||
|
</PrivateRoute> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Route> |
||||||
|
</Routes> |
||||||
|
</BrowserRouter> |
||||||
|
</Box> |
||||||
|
</ThemeProvider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default App; |
@ -0,0 +1,116 @@ |
|||||||
|
/** |
||||||
|
* アプリケーションの共通レイアウトを提供するコンポーネント |
||||||
|
* ヘッダー(AppBar)とメインコンテンツ領域を含む基本的なページ構造を定義 |
||||||
|
*/ |
||||||
|
import React, { useState } from 'react'; |
||||||
|
import {
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
Typography,
|
||||||
|
Container,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Drawer, |
||||||
|
List, |
||||||
|
ListItemText, |
||||||
|
ListItemIcon, |
||||||
|
ListItemButton, |
||||||
|
Divider, |
||||||
|
IconButton |
||||||
|
} from '@mui/material'; |
||||||
|
import {
|
||||||
|
Menu as MenuIcon, |
||||||
|
ListAlt as ListAltIcon, |
||||||
|
} from '@mui/icons-material'; |
||||||
|
import { useNavigate, Outlet, useLocation } from 'react-router-dom'; |
||||||
|
|
||||||
|
const Layout: React.FC = () => { |
||||||
|
const navigate = useNavigate(); |
||||||
|
const location = useLocation(); |
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false); |
||||||
|
|
||||||
|
/** |
||||||
|
* ログアウト処理を行うハンドラー関数 |
||||||
|
* ローカルストレージからトークンを削除し、ログインページにリダイレクト |
||||||
|
*/ |
||||||
|
const handleLogout = () => { |
||||||
|
localStorage.removeItem('token'); |
||||||
|
navigate('/login'); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 画面遷移処理を行うハンドラー関数 |
||||||
|
* 指定されたパスに遷移し、サイドメニューを閉じる |
||||||
|
*/ |
||||||
|
const handleNavigate = (path: string) => { |
||||||
|
navigate(path); |
||||||
|
setDrawerOpen(false); |
||||||
|
}; |
||||||
|
|
||||||
|
// 現在のパスに基づいてメニュー項目が選択状態かどうかを判定
|
||||||
|
const isSelected = (path: string): boolean => { |
||||||
|
return location.pathname === path; |
||||||
|
}; |
||||||
|
|
||||||
|
// メニューを開閉するハンドラー
|
||||||
|
const toggleDrawer = () => { |
||||||
|
setDrawerOpen(!drawerOpen); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}> |
||||||
|
{/* ヘッダー部分 - アプリ名とログアウトボタンを表示 */} |
||||||
|
<AppBar position="static" elevation={0}> |
||||||
|
<Toolbar> |
||||||
|
<IconButton |
||||||
|
edge="start" |
||||||
|
color="inherit" |
||||||
|
aria-label="menu" |
||||||
|
onClick={toggleDrawer} |
||||||
|
sx={{ mr: 2 }} |
||||||
|
> |
||||||
|
<MenuIcon /> |
||||||
|
</IconButton> |
||||||
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> |
||||||
|
ToDoアプリ |
||||||
|
</Typography> |
||||||
|
<Button color="inherit" onClick={handleLogout}> |
||||||
|
ログアウト |
||||||
|
</Button> |
||||||
|
</Toolbar> |
||||||
|
</AppBar> |
||||||
|
|
||||||
|
{/* サイドメニュー */} |
||||||
|
<Drawer |
||||||
|
anchor="left" |
||||||
|
open={drawerOpen} |
||||||
|
onClose={() => setDrawerOpen(false)} |
||||||
|
> |
||||||
|
<Box |
||||||
|
sx={{ width: 250 }} |
||||||
|
role="presentation" |
||||||
|
> |
||||||
|
<List> |
||||||
|
<ListItemButton
|
||||||
|
onClick={() => handleNavigate('/tasks')}
|
||||||
|
selected={isSelected('/tasks')} |
||||||
|
> |
||||||
|
<ListItemIcon><ListAltIcon /></ListItemIcon> |
||||||
|
<ListItemText primary="タスク一覧" /> |
||||||
|
</ListItemButton> |
||||||
|
<Divider /> |
||||||
|
</List> |
||||||
|
</Box> |
||||||
|
</Drawer> |
||||||
|
|
||||||
|
{/* メインコンテンツ領域 - 子ルートのコンポーネントがここに表示される */} |
||||||
|
<Box component="main" sx={{ flexGrow: 1, bgcolor: 'background.default', py: 3 }}> |
||||||
|
<Container> |
||||||
|
<Outlet /> {/* React Router の Outlet - 子ルートのコンポーネントがここにレンダリングされる */} |
||||||
|
</Container> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Layout; |
@ -0,0 +1,23 @@ |
|||||||
|
/** |
||||||
|
* エラーメッセージの定数定義 |
||||||
|
* アプリケーション全体で使用されるエラーメッセージを一元管理 |
||||||
|
*/ |
||||||
|
|
||||||
|
// 一般的なエラーメッセージ
|
||||||
|
export const GENERAL_ERRORS = { |
||||||
|
UNEXPECTED_ERROR: '予期せぬエラーが発生しました', |
||||||
|
}; |
||||||
|
|
||||||
|
// 認証関連のエラーメッセージ
|
||||||
|
export const AUTH_ERRORS = { |
||||||
|
LOGIN_FAILED: 'ログインに失敗しました', |
||||||
|
REGISTER_FAILED: 'ユーザー登録に失敗しました', |
||||||
|
}; |
||||||
|
|
||||||
|
// タスク関連のエラーメッセージ
|
||||||
|
export const TASK_ERRORS = { |
||||||
|
FETCH_FAILED: 'タスクの取得に失敗しました', |
||||||
|
CREATE_FAILED: 'タスクの作成に失敗しました', |
||||||
|
UPDATE_FAILED: 'タスクの更新に失敗しました', |
||||||
|
DELETE_FAILED: 'タスクの削除に失敗しました', |
||||||
|
}; |
@ -0,0 +1,35 @@ |
|||||||
|
/* グローバルリセット - すべての要素のマージン、パディング、ボックスサイズを統一 */ |
||||||
|
* { |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
/* HTML/Body要素の基本スタイル設定 */ |
||||||
|
html, body { |
||||||
|
height: 100%; /* 画面全体の高さを使用 */ |
||||||
|
margin: 0; |
||||||
|
/* システムフォントスタックを使用して最適なフォントを表示 */ |
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
||||||
|
sans-serif; |
||||||
|
/* フォントのスムージング設定 - より読みやすく美しい表示に */ |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
-moz-osx-font-smoothing: grayscale; |
||||||
|
/* スクロールバーのレイアウトシフトを防止 */ |
||||||
|
scrollbar-gutter: stable both-edges; |
||||||
|
overflow: auto |
||||||
|
} |
||||||
|
|
||||||
|
/* ルート要素のスタイル - Reactアプリケーションのマウントポイント */ |
||||||
|
#root { |
||||||
|
height: 100%; /* コンテナの高さを100%に */ |
||||||
|
display: flex; |
||||||
|
flex-direction: column; /* 子要素を縦方向に配置 */ |
||||||
|
} |
||||||
|
|
||||||
|
/* コードブロックのスタイル - 等幅フォントを使用 */ |
||||||
|
code { |
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', |
||||||
|
monospace; |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
/** |
||||||
|
* アプリケーションのエントリーポイント |
||||||
|
* Reactアプリケーションの初期化とルートコンポーネントのレンダリングを行う |
||||||
|
*/ |
||||||
|
import React from 'react'; |
||||||
|
import { createRoot } from 'react-dom/client'; |
||||||
|
import './index.css'; |
||||||
|
import App from './App'; |
||||||
|
|
||||||
|
// DOMからルート要素を取得
|
||||||
|
const container = document.getElementById('root'); |
||||||
|
if (!container) { |
||||||
|
// ルート要素が見つからない場合はエラーをスロー
|
||||||
|
throw new Error('Failed to find the root element'); |
||||||
|
} |
||||||
|
|
||||||
|
// React 18のcreateRootAPIを使用してルートを作成
|
||||||
|
const root = createRoot(container); |
||||||
|
// Appコンポーネントをレンダリング
|
||||||
|
// StrictModeで囲むことで開発時の潜在的な問題を検出
|
||||||
|
root.render( |
||||||
|
<React.StrictMode> |
||||||
|
<App /> |
||||||
|
</React.StrictMode> |
||||||
|
); |
@ -0,0 +1,127 @@ |
|||||||
|
/** |
||||||
|
* ユーザーログイン機能を提供するページコンポーネント |
||||||
|
* ユーザー名とパスワードによる認証フォームを表示し、認証APIと連携 |
||||||
|
*/ |
||||||
|
import React, { useState } from 'react'; |
||||||
|
import { useNavigate } from 'react-router-dom'; |
||||||
|
import { |
||||||
|
Container, |
||||||
|
Box, |
||||||
|
Typography, |
||||||
|
TextField, |
||||||
|
Button, |
||||||
|
Paper, |
||||||
|
Alert, |
||||||
|
Link, |
||||||
|
Grid, |
||||||
|
} from '@mui/material'; |
||||||
|
import { LoginCredentials } from '../types/types'; |
||||||
|
import { authApi } from '../services/api'; |
||||||
|
import { GENERAL_ERRORS } from '../constants/errorMessages'; |
||||||
|
|
||||||
|
const LoginPage: React.FC = () => { |
||||||
|
const navigate = useNavigate(); |
||||||
|
// ログイン情報の状態管理
|
||||||
|
const [credentials, setCredentials] = useState<LoginCredentials>({ |
||||||
|
username: '', |
||||||
|
password: '', |
||||||
|
}); |
||||||
|
// エラーメッセージの状態管理
|
||||||
|
const [error, setError] = useState<string>(''); |
||||||
|
|
||||||
|
/** |
||||||
|
* フォーム入力値の変更を処理するハンドラー |
||||||
|
* 入力フィールドの変更をcredentials状態に反映 |
||||||
|
*/ |
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
const { name, value } = e.target; |
||||||
|
setCredentials(prev => ({ |
||||||
|
...prev, |
||||||
|
[name]: value, // 動的にプロパティ名を使用して状態を更新
|
||||||
|
})); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* ログインフォームの送信を処理するハンドラー |
||||||
|
* 認証APIを呼び出し、成功時はトークンを保存してタスク一覧ページに遷移 |
||||||
|
* 失敗時はエラーメッセージを表示 |
||||||
|
*/ |
||||||
|
const handleSubmit = async (e: React.FormEvent) => { |
||||||
|
e.preventDefault(); // フォームのデフォルト送信動作を防止
|
||||||
|
try { |
||||||
|
const response = await authApi.login(credentials); |
||||||
|
localStorage.setItem('token', response.token); // 認証トークンをローカルストレージに保存
|
||||||
|
navigate('/tasks'); // タスク一覧ページにリダイレクト
|
||||||
|
} catch (err) { |
||||||
|
setError(err instanceof Error ? err.message : GENERAL_ERRORS.UNEXPECTED_ERROR); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container maxWidth="md" sx={{ mt: 8, height: '100vh'}}> |
||||||
|
<Grid container justifyContent="center" alignItems="center" sx={{ height: '100%' }}> |
||||||
|
<Grid item xs={12} md={5}> |
||||||
|
<Paper elevation={3} sx={{ p: 4 }}> |
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> |
||||||
|
<Typography component="h1" variant="h5" gutterBottom> |
||||||
|
ToDoアプリ ログイン |
||||||
|
</Typography> |
||||||
|
{/* エラーがある場合のみアラートを表示 */} |
||||||
|
{error && ( |
||||||
|
<Alert severity="error" sx={{ mb: 2, width: '100%' }}> |
||||||
|
{error} |
||||||
|
</Alert> |
||||||
|
)} |
||||||
|
{/* ログインフォーム */} |
||||||
|
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}> |
||||||
|
{/* ユーザー名入力フィールド */} |
||||||
|
<TextField |
||||||
|
margin="normal" |
||||||
|
required |
||||||
|
fullWidth |
||||||
|
id="username" |
||||||
|
label="ユーザー名" |
||||||
|
name="username" |
||||||
|
autoComplete="username" |
||||||
|
autoFocus |
||||||
|
value={credentials.username} |
||||||
|
onChange={handleChange} |
||||||
|
/> |
||||||
|
{/* パスワード入力フィールド */} |
||||||
|
<TextField |
||||||
|
margin="normal" |
||||||
|
required |
||||||
|
fullWidth |
||||||
|
name="password" |
||||||
|
label="パスワード" |
||||||
|
type="password" |
||||||
|
id="password" |
||||||
|
autoComplete="current-password" |
||||||
|
value={credentials.password} |
||||||
|
onChange={handleChange} |
||||||
|
/> |
||||||
|
{/* ログインボタン */} |
||||||
|
<Button |
||||||
|
type="submit" |
||||||
|
fullWidth |
||||||
|
variant="contained" |
||||||
|
sx={{ mt: 3, mb: 2 }} |
||||||
|
> |
||||||
|
ログイン |
||||||
|
</Button> |
||||||
|
{/* 新規登録ページへのリンク */} |
||||||
|
<Box sx={{ textAlign: 'center' }}> |
||||||
|
<Link href="/register" variant="body2"> |
||||||
|
アカウントをお持ちでない方はこちら |
||||||
|
</Link> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</Paper> |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
</Container> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default LoginPage; |
@ -0,0 +1,127 @@ |
|||||||
|
/** |
||||||
|
* ユーザー登録機能を提供するページコンポーネント |
||||||
|
* 新規ユーザーの登録フォームを表示し、登録APIと連携 |
||||||
|
*/ |
||||||
|
import React, { useState } from 'react'; |
||||||
|
import { useNavigate } from 'react-router-dom'; |
||||||
|
import { |
||||||
|
Container, |
||||||
|
Box, |
||||||
|
Typography, |
||||||
|
TextField, |
||||||
|
Button, |
||||||
|
Paper, |
||||||
|
Alert, |
||||||
|
Link, |
||||||
|
Grid, |
||||||
|
} from '@mui/material'; |
||||||
|
import { RegisterCredentials } from '../types/types'; |
||||||
|
import { authApi } from '../services/api'; |
||||||
|
import { GENERAL_ERRORS } from '../constants/errorMessages'; |
||||||
|
|
||||||
|
const RegisterPage: React.FC = () => { |
||||||
|
const navigate = useNavigate(); |
||||||
|
// 登録情報の状態管理 - ユーザー名、パスワードを含む
|
||||||
|
const [credentials, setCredentials] = useState<RegisterCredentials>({ |
||||||
|
username: '', |
||||||
|
password: '', |
||||||
|
}); |
||||||
|
// エラーメッセージの状態管理
|
||||||
|
const [error, setError] = useState<string>(''); |
||||||
|
|
||||||
|
/** |
||||||
|
* フォーム入力値の変更を処理するハンドラー |
||||||
|
* 入力フィールドの変更をcredentials状態に反映 |
||||||
|
*/ |
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
const { name, value } = e.target; |
||||||
|
setCredentials(prev => ({ |
||||||
|
...prev, |
||||||
|
[name]: value, // 動的にプロパティ名を使用して状態を更新
|
||||||
|
})); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 登録フォームの送信を処理するハンドラー |
||||||
|
* 登録APIを呼び出し、成功時はトークンを保存してタスク一覧ページに遷移 |
||||||
|
* 失敗時はエラーメッセージを表示 |
||||||
|
*/ |
||||||
|
const handleSubmit = async (e: React.FormEvent) => { |
||||||
|
e.preventDefault(); // フォームのデフォルト送信動作を防止
|
||||||
|
try { |
||||||
|
const response = await authApi.register(credentials); |
||||||
|
localStorage.setItem('token', response.token); // 認証トークンをローカルストレージに保存
|
||||||
|
navigate('/tasks'); // タスク一覧ページにリダイレクト
|
||||||
|
} catch (err) { |
||||||
|
setError(err instanceof Error ? err.message : GENERAL_ERRORS.UNEXPECTED_ERROR); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container maxWidth="md" sx={{ mt: 8, height: '100vh' }}> |
||||||
|
<Grid container justifyContent="center" alignItems="center" sx={{ height: '100%' }}> |
||||||
|
<Grid item xs={12} md={5}> |
||||||
|
<Paper elevation={3} sx={{ p: 4 }}> |
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> |
||||||
|
<Typography component="h1" variant="h5" gutterBottom> |
||||||
|
ユーザー登録 |
||||||
|
</Typography> |
||||||
|
{/* エラーがある場合のみアラートを表示 */} |
||||||
|
{error && ( |
||||||
|
<Alert severity="error" sx={{ mb: 2, width: '100%' }}> |
||||||
|
{error} |
||||||
|
</Alert> |
||||||
|
)} |
||||||
|
{/* 登録フォーム */} |
||||||
|
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}> |
||||||
|
{/* ユーザー名入力フィールド */} |
||||||
|
<TextField |
||||||
|
margin="normal" |
||||||
|
required |
||||||
|
fullWidth |
||||||
|
id="username" |
||||||
|
label="ユーザー名" |
||||||
|
name="username" |
||||||
|
autoComplete="username" |
||||||
|
autoFocus |
||||||
|
value={credentials.username} |
||||||
|
onChange={handleChange} |
||||||
|
/> |
||||||
|
{/* パスワード入力フィールド */} |
||||||
|
<TextField |
||||||
|
margin="normal" |
||||||
|
required |
||||||
|
fullWidth |
||||||
|
name="password" |
||||||
|
label="パスワード" |
||||||
|
type="password" |
||||||
|
id="password" |
||||||
|
autoComplete="new-password" |
||||||
|
value={credentials.password} |
||||||
|
onChange={handleChange} |
||||||
|
/> |
||||||
|
{/* 登録ボタン */} |
||||||
|
<Button |
||||||
|
type="submit" |
||||||
|
fullWidth |
||||||
|
variant="contained" |
||||||
|
sx={{ mt: 3, mb: 2 }} |
||||||
|
> |
||||||
|
登録 |
||||||
|
</Button> |
||||||
|
{/* ログインページへのリンク */} |
||||||
|
<Box sx={{ textAlign: 'center' }}> |
||||||
|
<Link href="/login" variant="body2"> |
||||||
|
既にアカウントをお持ちの方はこちら |
||||||
|
</Link> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</Paper> |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
</Container> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default RegisterPage; |
@ -0,0 +1,195 @@ |
|||||||
|
/** |
||||||
|
* タスク一覧を表示・管理するページコンポーネント |
||||||
|
* タスクの表示、作成、完了状態の切り替え、削除などの機能を提供 |
||||||
|
*/ |
||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import { taskApi } from '../services/api'; |
||||||
|
import { |
||||||
|
Container, |
||||||
|
Typography, |
||||||
|
List, |
||||||
|
ListItem, |
||||||
|
ListItemText, |
||||||
|
ListItemSecondaryAction, |
||||||
|
IconButton, |
||||||
|
Checkbox, |
||||||
|
Fab, |
||||||
|
Dialog, |
||||||
|
DialogTitle, |
||||||
|
DialogContent, |
||||||
|
DialogActions, |
||||||
|
TextField, |
||||||
|
Button, |
||||||
|
Box, |
||||||
|
} from '@mui/material'; |
||||||
|
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material'; |
||||||
|
import { Task } from '../types/types'; |
||||||
|
import { TASK_ERRORS } from '../constants/errorMessages'; |
||||||
|
|
||||||
|
// 新規タスクの初期状態
|
||||||
|
const EMPTY_TASK = { title: '', description: '', completed: false }; |
||||||
|
|
||||||
|
const TaskListPage: React.FC = () => { |
||||||
|
// タスク一覧の状態管理
|
||||||
|
const [tasks, setTasks] = useState<Task[]>([]); |
||||||
|
// 新規タスク作成ダイアログの表示状態
|
||||||
|
const [openDialog, setOpenDialog] = useState(false); |
||||||
|
// 新規タスクの入力内容
|
||||||
|
const [newTask, setNewTask] = useState(EMPTY_TASK); |
||||||
|
|
||||||
|
// コンポーネントマウント時にタスク一覧を取得
|
||||||
|
useEffect(() => { |
||||||
|
fetchTasks(); |
||||||
|
}, []); |
||||||
|
|
||||||
|
/** |
||||||
|
* APIからタスク一覧を取得する関数 |
||||||
|
* 取得したタスクをstate(tasks)に設定 |
||||||
|
*/ |
||||||
|
const fetchTasks = async () => { |
||||||
|
try { |
||||||
|
const tasks = await taskApi.getTasks(); |
||||||
|
setTasks(tasks); |
||||||
|
} catch (error) { |
||||||
|
console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクの完了状態を切り替えるハンドラー |
||||||
|
* 対象タスクの完了状態を反転させてAPIに更新を要求 |
||||||
|
*/ |
||||||
|
const handleToggleComplete = async (taskId: number) => { |
||||||
|
try { |
||||||
|
const task = tasks.find(t => t.id === taskId); |
||||||
|
if (!task) return; |
||||||
|
|
||||||
|
await taskApi.updateTask(taskId, { ...task, completed: !task.completed }); |
||||||
|
fetchTasks(); // 更新後のタスク一覧を再取得
|
||||||
|
} catch (error) { |
||||||
|
console.error(`${TASK_ERRORS.UPDATE_FAILED}:`, error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクを削除するハンドラー |
||||||
|
* 指定されたIDのタスクをAPIを通じて削除 |
||||||
|
*/ |
||||||
|
const handleDeleteTask = async (taskId: number) => { |
||||||
|
try { |
||||||
|
await taskApi.deleteTask(taskId); |
||||||
|
fetchTasks(); // 削除後のタスク一覧を再取得
|
||||||
|
} catch (error) { |
||||||
|
console.error(`${TASK_ERRORS.DELETE_FAILED}:`, error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 新規タスクを作成するハンドラー |
||||||
|
* 入力されたタスク情報をAPIに送信して新規作成 |
||||||
|
* 作成後はダイアログを閉じ、入力内容をリセット |
||||||
|
*/ |
||||||
|
const handleCreateTask = async () => { |
||||||
|
try { |
||||||
|
await taskApi.createTask(newTask); |
||||||
|
setOpenDialog(false); // ダイアログを閉じる
|
||||||
|
setNewTask(EMPTY_TASK); // 入力内容をリセット
|
||||||
|
fetchTasks(); // 作成後のタスク一覧を再取得
|
||||||
|
} catch (error) { |
||||||
|
console.error(`${TASK_ERRORS.CREATE_FAILED}:`, error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container> |
||||||
|
<Typography variant="h4" component="h1" gutterBottom> |
||||||
|
タスク一覧 |
||||||
|
</Typography> |
||||||
|
{/* タスク一覧表示エリア - 青い背景のコンテナ */} |
||||||
|
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px'}}> |
||||||
|
<List> |
||||||
|
{/* タスク一覧をマップして各タスクをリストアイテムとして表示 */} |
||||||
|
{tasks.map((task) => ( |
||||||
|
<ListItem |
||||||
|
key={task.id} |
||||||
|
sx={{ |
||||||
|
bgcolor: 'background.paper', |
||||||
|
mb: 1, |
||||||
|
borderRadius: 1, |
||||||
|
boxShadow: 1, |
||||||
|
}} |
||||||
|
> |
||||||
|
{/* タスク完了状態を切り替えるチェックボックス */} |
||||||
|
<Checkbox |
||||||
|
checked={task.completed} |
||||||
|
onChange={() => handleToggleComplete(task.id)} |
||||||
|
/> |
||||||
|
{/* タスクのタイトルと説明 - 完了状態に応じて取り消し線を表示 */} |
||||||
|
<ListItemText |
||||||
|
primary={task.title} |
||||||
|
secondary={task.description} |
||||||
|
sx={{ |
||||||
|
textDecoration: task.completed ? 'line-through' : 'none', |
||||||
|
}} |
||||||
|
/> |
||||||
|
{/* タスク削除ボタン */} |
||||||
|
<ListItemSecondaryAction> |
||||||
|
<IconButton |
||||||
|
edge="end" |
||||||
|
aria-label="delete" |
||||||
|
onClick={() => handleDeleteTask(task.id)} |
||||||
|
> |
||||||
|
<DeleteIcon /> |
||||||
|
</IconButton> |
||||||
|
</ListItemSecondaryAction> |
||||||
|
</ListItem> |
||||||
|
))} |
||||||
|
</List> |
||||||
|
</div> |
||||||
|
{/* 新規タスク作成ボタン - 画面下部に固定表示 */} |
||||||
|
<Fab |
||||||
|
color="primary" |
||||||
|
sx={{ position: 'fixed', bottom: 16, left: '50%', transform: 'translateX(-50%)'}} |
||||||
|
onClick={() => setOpenDialog(true)} |
||||||
|
> |
||||||
|
<AddIcon /> |
||||||
|
</Fab> |
||||||
|
|
||||||
|
{/* 新規タスク作成ダイアログ */} |
||||||
|
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true}> |
||||||
|
<DialogTitle>新規タスク</DialogTitle> |
||||||
|
<DialogContent> |
||||||
|
<Box sx={{ pt: 1 }}> |
||||||
|
{/* タスクタイトル入力フィールド */} |
||||||
|
<TextField |
||||||
|
autoFocus |
||||||
|
margin="dense" |
||||||
|
label="タイトル" |
||||||
|
fullWidth |
||||||
|
value={newTask.title} |
||||||
|
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })} |
||||||
|
/> |
||||||
|
{/* タスク説明入力フィールド - 複数行入力可能 */} |
||||||
|
<TextField |
||||||
|
margin="dense" |
||||||
|
label="説明" |
||||||
|
fullWidth |
||||||
|
multiline |
||||||
|
rows={4} |
||||||
|
value={newTask.description} |
||||||
|
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</DialogContent> |
||||||
|
<DialogActions> |
||||||
|
<Button onClick={() => setOpenDialog(false)}>キャンセル</Button> |
||||||
|
<Button onClick={handleCreateTask} variant="contained"> |
||||||
|
作成 |
||||||
|
</Button> |
||||||
|
</DialogActions> |
||||||
|
</Dialog> |
||||||
|
</Container> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default TaskListPage; |
@ -0,0 +1,12 @@ |
|||||||
|
/** |
||||||
|
* TypeScript型定義参照ファイル |
||||||
|
* |
||||||
|
* このファイルはCreate React Appによって自動生成され、 |
||||||
|
* react-scriptsパッケージの型定義を参照しています。 |
||||||
|
* |
||||||
|
* これにより、環境変数(process.env)や画像インポートなどの |
||||||
|
* Create React App特有の機能に対する型定義が提供されます。 |
||||||
|
* |
||||||
|
* このファイルを編集・削除すると型チェックに問題が生じる可能性があります。 |
||||||
|
*/ |
||||||
|
/// <reference types="react-scripts" />
|
@ -0,0 +1,176 @@ |
|||||||
|
/** |
||||||
|
* APIサービス |
||||||
|
* バックエンドAPIとの通信を担当するモジュール |
||||||
|
* 認証、タスク管理などの機能を提供 |
||||||
|
*/ |
||||||
|
import { LoginCredentials, RegisterCredentials, AuthResponse, Task } from '../types/types'; |
||||||
|
import { AUTH_ERRORS, TASK_ERRORS } from '../constants/errorMessages'; |
||||||
|
|
||||||
|
// APIのベースURL - 環境変数から取得するか、デフォルト値を使用
|
||||||
|
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080'; |
||||||
|
|
||||||
|
/** |
||||||
|
* APIリクエスト用のヘッダーを生成する関数 |
||||||
|
* @param includeAuth 認証トークンをヘッダーに含めるかどうか |
||||||
|
* @returns リクエストヘッダーオブジェクト |
||||||
|
*/ |
||||||
|
const getHeaders = (includeAuth: boolean = true) => { |
||||||
|
const headers: Record<string, string> = { |
||||||
|
'Content-Type': 'application/json', |
||||||
|
}; |
||||||
|
|
||||||
|
// 認証トークンが必要な場合はローカルストレージから取得してヘッダーに追加
|
||||||
|
if (includeAuth) { |
||||||
|
const token = localStorage.getItem('token'); |
||||||
|
if (token) { |
||||||
|
headers['Authorization'] = `Bearer ${token}`; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return headers; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証関連のAPI機能を提供するオブジェクト |
||||||
|
* ログインと新規ユーザー登録の機能を含む |
||||||
|
*/ |
||||||
|
export const authApi = { |
||||||
|
/** |
||||||
|
* ユーザーログイン処理 |
||||||
|
* @param credentials ログイン情報(ユーザー名とパスワード) |
||||||
|
* @returns 認証レスポンス(トークンとユーザー情報) |
||||||
|
*/ |
||||||
|
login: async (credentials: LoginCredentials): Promise<AuthResponse> => { |
||||||
|
const response = await fetch(`${API_BASE_URL}/api/auth/login`, { |
||||||
|
method: 'POST', |
||||||
|
headers: getHeaders(false), // 認証前なのでトークンは不要
|
||||||
|
body: JSON.stringify(credentials), |
||||||
|
}); |
||||||
|
|
||||||
|
// エラーレスポンスの処理
|
||||||
|
if (!response.ok) { |
||||||
|
const errorData = await response.json().catch(() => null); |
||||||
|
throw new Error( |
||||||
|
errorData?.message || AUTH_ERRORS.LOGIN_FAILED |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return response.json(); |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 新規ユーザー登録処理 |
||||||
|
* @param credentials 登録情報(ユーザー名、パスワード) |
||||||
|
* @returns 認証レスポンス(トークンとユーザー情報) |
||||||
|
*/ |
||||||
|
register: async (credentials: RegisterCredentials): Promise<AuthResponse> => { |
||||||
|
const response = await fetch(`${API_BASE_URL}/api/auth/register`, { |
||||||
|
method: 'POST', |
||||||
|
headers: getHeaders(false), // 認証前なのでトークンは不要
|
||||||
|
body: JSON.stringify(credentials), |
||||||
|
}); |
||||||
|
|
||||||
|
// エラーレスポンスの処理
|
||||||
|
if (!response.ok) { |
||||||
|
const errorData = await response.json().catch(() => null); |
||||||
|
throw new Error( |
||||||
|
errorData?.message || AUTH_ERRORS.REGISTER_FAILED |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return response.json(); |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* タスク管理関連のAPI機能を提供するオブジェクト |
||||||
|
* タスクの取得、作成、更新、削除などの機能を含む |
||||||
|
*/ |
||||||
|
export const taskApi = { |
||||||
|
/** |
||||||
|
* 全タスクを取得 |
||||||
|
* @returns タスク一覧 |
||||||
|
*/ |
||||||
|
getTasks: async (): Promise<Task[]> => { |
||||||
|
const response = await fetch(`${API_BASE_URL}/api/tasks`, { |
||||||
|
headers: getHeaders(), // 認証トークンを含むヘッダー
|
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(TASK_ERRORS.FETCH_FAILED); |
||||||
|
} |
||||||
|
|
||||||
|
return response.json(); |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定IDのタスクを取得 |
||||||
|
* @param id タスクID |
||||||
|
* @returns 単一のタスク情報 |
||||||
|
*/ |
||||||
|
getTask: async (id: number): Promise<Task> => { |
||||||
|
const response = await fetch(`${API_BASE_URL}/api/tasks/${id}`, { |
||||||
|
headers: getHeaders(), |
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(TASK_ERRORS.FETCH_FAILED); |
||||||
|
} |
||||||
|
|
||||||
|
return response.json(); |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 新規タスクを作成 |
||||||
|
* @param task 作成するタスク情報(ID、ユーザーID、作成日時、更新日時は除外) |
||||||
|
* @returns 作成されたタスク情報 |
||||||
|
*/ |
||||||
|
createTask: async (task: Omit<Task, 'id' | 'userId' | 'createdAt' | 'updatedAt'>): Promise<Task> => { |
||||||
|
const response = await fetch(`${API_BASE_URL}/api/tasks`, { |
||||||
|
method: 'POST', |
||||||
|
headers: getHeaders(), |
||||||
|
body: JSON.stringify(task), |
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(TASK_ERRORS.CREATE_FAILED); |
||||||
|
} |
||||||
|
|
||||||
|
return response.json(); |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクを更新 |
||||||
|
* @param id 更新対象のタスクID |
||||||
|
* @param task 更新するタスク情報(部分的な更新も可能) |
||||||
|
* @returns 更新後のタスク情報 |
||||||
|
*/ |
||||||
|
updateTask: async (id: number, task: Partial<Task>): Promise<Task> => { |
||||||
|
const response = await fetch(`${API_BASE_URL}/api/tasks/${id}`, { |
||||||
|
method: 'PUT', |
||||||
|
headers: getHeaders(), |
||||||
|
body: JSON.stringify(task), |
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(TASK_ERRORS.UPDATE_FAILED); |
||||||
|
} |
||||||
|
|
||||||
|
return response.json(); |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* タスクを削除 |
||||||
|
* @param id 削除対象のタスクID |
||||||
|
*/ |
||||||
|
deleteTask: async (id: number): Promise<void> => { |
||||||
|
const response = await fetch(`${API_BASE_URL}/api/tasks/${id}`, { |
||||||
|
method: 'DELETE', |
||||||
|
headers: getHeaders(), |
||||||
|
}); |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(TASK_ERRORS.DELETE_FAILED); |
||||||
|
} |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,50 @@ |
|||||||
|
/** |
||||||
|
* タスク情報を表す型定義 |
||||||
|
* タスクの基本情報と状態を管理 |
||||||
|
*/ |
||||||
|
export interface Task { |
||||||
|
id: number; // タスクの一意識別子
|
||||||
|
title: string; // タスクのタイトル
|
||||||
|
description?: string; // タスクの詳細説明(任意)
|
||||||
|
completed: boolean; // タスクの完了状態
|
||||||
|
userId: number; // タスクの所有者ID
|
||||||
|
createdAt: string; // タスク作成日時
|
||||||
|
updatedAt: string; // タスク更新日時
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* ユーザー情報を表す型定義 |
||||||
|
* ユーザーの基本情報を管理 |
||||||
|
*/ |
||||||
|
export interface User { |
||||||
|
id: number; // ユーザーの一意識別子
|
||||||
|
username: string; // ユーザー名(ログイン用)
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 認証レスポンスを表す型定義 |
||||||
|
* ログインや登録成功時のサーバーからのレスポンス |
||||||
|
*/ |
||||||
|
export interface AuthResponse { |
||||||
|
token: string; // JWT認証トークン
|
||||||
|
user: User; // ログインしたユーザーの情報
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* ログイン認証情報を表す型定義 |
||||||
|
* ログインフォームからの入力情報 |
||||||
|
*/ |
||||||
|
export interface LoginCredentials { |
||||||
|
username: string; // ユーザー名
|
||||||
|
password: string; // パスワード
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 登録認証情報を表す型定義 |
||||||
|
* 登録フォームからの入力情報 |
||||||
|
*/ |
||||||
|
export interface RegisterCredentials { |
||||||
|
username: string; // ユーザー名
|
||||||
|
password: string; // パスワード
|
||||||
|
} |
||||||
|
|
@ -0,0 +1,53 @@ |
|||||||
|
{ |
||||||
|
"navigationFallback": { |
||||||
|
"rewrite": "/index.html", |
||||||
|
"exclude": ["/images/*.{png,jpg,gif}", "/css/*", "/js/*", "/static/*"] |
||||||
|
}, |
||||||
|
"routes": [ |
||||||
|
{ |
||||||
|
"route": "/static/*", |
||||||
|
"headers": { |
||||||
|
"cache-control": "must-revalidate, max-age=15770000" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"route": "/login", |
||||||
|
"rewrite": "/index.html" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"route": "/register", |
||||||
|
"rewrite": "/index.html" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"route": "/tasks", |
||||||
|
"rewrite": "/index.html" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"route": "/*", |
||||||
|
"rewrite": "/index.html" |
||||||
|
} |
||||||
|
], |
||||||
|
"responseOverrides": { |
||||||
|
"404": { |
||||||
|
"rewrite": "/index.html", |
||||||
|
"statusCode": 200 |
||||||
|
} |
||||||
|
}, |
||||||
|
"globalHeaders": { |
||||||
|
"content-security-policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.azurewebsites.net https://*.japaneast-01.azurewebsites.net https://*.azurestaticapps.net; connect-src 'self' https://*.azurewebsites.net https://*.japaneast-01.azurewebsites.net; img-src 'self' data:; font-src 'self' data:;", |
||||||
|
"cache-control": "no-cache, no-store, must-revalidate" |
||||||
|
}, |
||||||
|
"mimeTypes": { |
||||||
|
".json": "application/json", |
||||||
|
".html": "text/html", |
||||||
|
".js": "text/javascript", |
||||||
|
".css": "text/css", |
||||||
|
".png": "image/png", |
||||||
|
".jpg": "image/jpeg", |
||||||
|
".svg": "image/svg+xml", |
||||||
|
".ico": "image/x-icon" |
||||||
|
}, |
||||||
|
"networking": { |
||||||
|
"allowedIpRanges": [] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es5", |
||||||
|
"lib": [ |
||||||
|
"dom", |
||||||
|
"dom.iterable", |
||||||
|
"esnext" |
||||||
|
], |
||||||
|
"allowJs": true, |
||||||
|
"skipLibCheck": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"strict": true, |
||||||
|
"forceConsistentCasingInFileNames": true, |
||||||
|
"noFallthroughCasesInSwitch": true, |
||||||
|
"module": "esnext", |
||||||
|
"moduleResolution": "node", |
||||||
|
"resolveJsonModule": true, |
||||||
|
"isolatedModules": true, |
||||||
|
"noEmit": true, |
||||||
|
"jsx": "react-jsx" |
||||||
|
}, |
||||||
|
"include": [ |
||||||
|
"src" |
||||||
|
] |
||||||
|
} |
Loading…
Reference in new issue