diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..ed803dd4
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,73 @@
+name: Build and Deploy
+
+on:
+ push:
+ branches: [main, azure-deploy]
+
+env:
+ REGISTRY: ghcr.io
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set image names (lowercase)
+ run: |
+ OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
+ echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV
+ echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push backend image
+ uses: docker/build-push-action@v5
+ with:
+ context: ./backend
+ push: true
+ tags: ${{ env.BACKEND_IMAGE }}:latest
+
+ - name: Build and push frontend image
+ uses: docker/build-push-action@v5
+ with:
+ context: ./web
+ push: true
+ tags: ${{ env.FRONTEND_IMAGE }}:latest
+ build-args: |
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
+
+ - name: Log in to Azure
+ uses: azure/login@v1
+ with:
+ creds: ${{ secrets.AZURE_CREDENTIALS }}
+
+ - name: Deploy backend
+ run: |
+ az containerapp update \
+ --name ${{ secrets.AZURE_BACKEND_APP_NAME }} \
+ --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
+ --image ${{ env.BACKEND_IMAGE }}:latest \
+ --registry-server ${{ env.REGISTRY }} \
+ --registry-username ${{ github.actor }} \
+ --registry-password ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Deploy frontend
+ run: |
+ az containerapp update \
+ --name ${{ secrets.AZURE_FRONTEND_APP_NAME }} \
+ --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
+ --image ${{ env.FRONTEND_IMAGE }}:latest \
+ --registry-server ${{ env.REGISTRY }} \
+ --registry-username ${{ github.actor }} \
+ --registry-password ${{ secrets.GITHUB_TOKEN }}
diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
new file mode 100644
index 00000000..5022297f
--- /dev/null
+++ b/.idea/caches/deviceStreaming.xml
@@ -0,0 +1,1570 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 00000000..c61ea334
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/Dockerfile b/backend/Dockerfile
index c3479c8e..ed3552be 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -9,6 +9,5 @@ RUN mvn -q -DskipTests package
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
-COPY uploads ./uploads
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
diff --git a/backend/log.txt b/backend/log.txt
new file mode 100644
index 00000000..348f4d17
--- /dev/null
+++ b/backend/log.txt
@@ -0,0 +1,265 @@
+2026-04-14T20:46:27.127-06:00 INFO 332751 --- [petshop-backend] [main] org.flywaydb.core.FlywayExecutor : Database: jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC (MySQL 8.0)
+2026-04-14T20:46:27.163-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbValidate : Successfully validated 4 migrations (execution time 00:00.014s)
+2026-04-14T20:46:27.171-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Current version of schema `Petstoredb`: 3
+2026-04-14T20:46:27.179-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Migrating schema `Petstoredb` to version "4 - drop purchase order status"
+2026-04-14T20:46:27.207-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `Petstoredb`, now at version v4 (execution time 00:00.016s)
+2026-04-14T20:46:27.212-06:00 INFO 332751 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : Starting BackendApplication using Java 25.0.2 with PID 332751 (/home/user/threaded/main/backend/target/classes started by user in /home/user/threaded/main/backend)
+2026-04-14T20:46:27.213-06:00 INFO 332751 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : No active profile set, falling back to 1 default profile: "default"
+2026-04-14T20:46:27.648-06:00 INFO 332751 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
+2026-04-14T20:46:27.688-06:00 INFO 332751 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 36 ms. Found 22 JPA repository interfaces.
+2026-04-14T20:46:27.920-06:00 INFO 332751 --- [petshop-backend] [main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
+2026-04-14T20:46:27.926-06:00 INFO 332751 --- [petshop-backend] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
+2026-04-14T20:46:27.926-06:00 INFO 332751 --- [petshop-backend] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.18]
+2026-04-14T20:46:27.944-06:00 INFO 332751 --- [petshop-backend] [main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 711 ms
+2026-04-14T20:46:28.006-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
+2026-04-14T20:46:28.025-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.2.4.Final
+2026-04-14T20:46:28.185-06:00 INFO 332751 --- [petshop-backend] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
+2026-04-14T20:46:28.198-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
+2026-04-14T20:46:28.205-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@71e2ddd
+2026-04-14T20:46:28.206-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
+2026-04-14T20:46:28.232-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
+ Database JDBC URL [jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC]
+ Database driver: MySQL Connector/J
+ Database dialect: MySQLDialect
+ Database version: 8.0.45
+ Default catalog/schema: Petstoredb/undefined
+ Autocommit mode: undefined/unknown
+ Isolation level: REPEATABLE_READ [default REPEATABLE_READ]
+ JDBC fetch size: none
+ Pool: DataSourceConnectionProvider
+ Minimum pool size: undefined/unknown
+ Maximum pool size: undefined/unknown
+2026-04-14T20:46:28.790-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
+2026-04-14T20:46:28.813-06:00 INFO 332751 --- [petshop-backend] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
+2026-04-14T20:46:28.844-06:00 INFO 332751 --- [petshop-backend] [main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
+2026-04-14T20:46:29.610-06:00 ERROR 332751 --- [petshop-backend] [main] t.s.DeferredServletContainerInitializers : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'jwtAuthenticationFilter' defined in file [/home/user/threaded/main/backend/target/classes/com/petshop/backend/security/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
+2026-04-14T20:46:29.619-06:00 INFO 332751 --- [petshop-backend] [main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
+2026-04-14T20:46:29.620-06:00 WARN 332751 --- [petshop-backend] [main] o.a.c.loader.WebappClassLoaderBase : The web application [ROOT] appears to have started a thread named [HikariPool-1:housekeeper] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
+ java.base/jdk.internal.misc.Unsafe.park(Native Method)
+ java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:271)
+ java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1803)
+ java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1166)
+ java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:883)
+ java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1016)
+ java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
+ java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
+ java.base/java.lang.Thread.run(Thread.java:1474)
+2026-04-14T20:46:29.620-06:00 WARN 332751 --- [petshop-backend] [main] o.a.c.loader.WebappClassLoaderBase : The web application [ROOT] appears to have started a thread named [HikariPool-1:connection-adder] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
+ java.base/jdk.internal.misc.Unsafe.park(Native Method)
+ java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:271)
+ java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1803)
+ java.base/java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:460)
+ java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1015)
+ java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
+ java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
+ java.base/java.lang.Thread.run(Thread.java:1474)
+2026-04-14T20:46:29.621-06:00 WARN 332751 --- [petshop-backend] [main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server
+2026-04-14T20:46:29.621-06:00 INFO 332751 --- [petshop-backend] [main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
+2026-04-14T20:46:29.623-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
+2026-04-14T20:46:29.624-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
+2026-04-14T20:46:29.627-06:00 INFO 332751 --- [petshop-backend] [main] .s.b.a.l.ConditionEvaluationReportLogger :
+
+Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
+2026-04-14T20:46:29.633-06:00 ERROR 332751 --- [petshop-backend] [main] o.s.boot.SpringApplication : Application run failed
+
+org.springframework.context.ApplicationContextException: Unable to start web server
+ at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:167) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
+ at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:615) ~[spring-context-7.0.5.jar:7.0.5]
+ at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
+ at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.SpringApplication.run(SpringApplication.java:321) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:154) ~[spring-boot-4.0.3.jar:4.0.3]
+ at com.petshop.backend.BackendApplication.main(BackendApplication.java:17) ~[classes/:na]
+Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
+ at org.springframework.boot.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:150) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
+ at org.springframework.boot.tomcat.TomcatWebServer.(TomcatWebServer.java:110) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
+ at org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:408) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
+ at org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:166) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
+ at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:190) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
+ at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:164) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
+ ... 7 common frames omitted
+Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jwtAuthenticationFilter' defined in file [/home/user/threaded/main/backend/target/classes/com/petshop/backend/security/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
+ at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1382) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1221) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:565) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:231) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:185) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:180) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ServletContextInitializerBeans.java:165) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.web.servlet.ServletContextInitializerBeans.(ServletContextInitializerBeans.java:97) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.web.context.servlet.WebApplicationContextInitializer.initialize(WebApplicationContextInitializer.java:53) ~[spring-boot-4.0.3.jar:4.0.3]
+ at org.springframework.boot.tomcat.servlet.DeferredServletContainerInitializers.onStartup(DeferredServletContainerInitializers.java:55) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
+ at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4416) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1162) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1158) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) ~[na:na]
+ at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:149) ~[na:na]
+ at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:714) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:780) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1162) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1158) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) ~[na:na]
+ at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:149) ~[na:na]
+ at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:714) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:201) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.StandardService.startInternal(StandardService.java:410) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:864) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.apache.catalina.startup.Tomcat.start(Tomcat.java:435) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
+ at org.springframework.boot.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:131) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
+ ... 12 common frames omitted
+Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
+ at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:499) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1446) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:1225) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1704) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1651) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:912) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ~[spring-beans-7.0.5.jar:7.0.5]
+ ... 52 common frames omitted
+Caused by: org.springframework.util.PlaceholderResolutionException: Could not resolve placeholder 'JWT_SECRET' in value "${JWT_SECRET}" <-- "${jwt.secret}"
+ at org.springframework.util.PlaceholderResolutionException.withValue(PlaceholderResolutionException.java:81) ~[spring-core-7.0.5.jar:7.0.5]
+ at org.springframework.util.PlaceholderParser$ParsedValue.resolve(PlaceholderParser.java:296) ~[spring-core-7.0.5.jar:7.0.5]
+ at org.springframework.util.PlaceholderParser.replacePlaceholders(PlaceholderParser.java:129) ~[spring-core-7.0.5.jar:7.0.5]
+ at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:96) ~[spring-core-7.0.5.jar:7.0.5]
+ at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:286) ~[spring-core-7.0.5.jar:7.0.5]
+ at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:257) ~[spring-core-7.0.5.jar:7.0.5]
+ at org.springframework.context.support.PropertySourcesPlaceholderConfigurer.lambda$processProperties$0(PropertySourcesPlaceholderConfigurer.java:184) ~[spring-context-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:959) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1672) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1651) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:764) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:748) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146) ~[spring-beans-7.0.5.jar:7.0.5]
+ at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:493) ~[spring-beans-7.0.5.jar:7.0.5]
+ ... 64 common frames omitted
+
+2026-04-14T20:47:58.809-06:00 INFO 333355 --- [petshop-backend] [main] org.flywaydb.core.FlywayExecutor : Database: jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC (MySQL 8.0)
+2026-04-14T20:47:58.842-06:00 INFO 333355 --- [petshop-backend] [main] o.f.core.internal.command.DbValidate : Successfully validated 4 migrations (execution time 00:00.012s)
+2026-04-14T20:47:58.850-06:00 INFO 333355 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Current version of schema `Petstoredb`: 4
+2026-04-14T20:47:58.851-06:00 INFO 333355 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Schema `Petstoredb` is up to date. No migration necessary.
+2026-04-14T20:47:58.857-06:00 INFO 333355 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : Starting BackendApplication using Java 25.0.2 with PID 333355 (/home/user/threaded/main/backend/target/classes started by user in /home/user/threaded/main/backend)
+2026-04-14T20:47:58.857-06:00 INFO 333355 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : The following 1 profile is active: "local"
+2026-04-14T20:47:59.251-06:00 INFO 333355 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
+2026-04-14T20:47:59.288-06:00 INFO 333355 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 33 ms. Found 22 JPA repository interfaces.
+2026-04-14T20:47:59.550-06:00 INFO 333355 --- [petshop-backend] [main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
+2026-04-14T20:47:59.557-06:00 INFO 333355 --- [petshop-backend] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
+2026-04-14T20:47:59.557-06:00 INFO 333355 --- [petshop-backend] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.18]
+2026-04-14T20:47:59.575-06:00 INFO 333355 --- [petshop-backend] [main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 699 ms
+2026-04-14T20:47:59.636-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
+2026-04-14T20:47:59.655-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.2.4.Final
+2026-04-14T20:47:59.799-06:00 INFO 333355 --- [petshop-backend] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
+2026-04-14T20:47:59.811-06:00 INFO 333355 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
+2026-04-14T20:47:59.817-06:00 INFO 333355 --- [petshop-backend] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6109366a
+2026-04-14T20:47:59.818-06:00 INFO 333355 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
+2026-04-14T20:47:59.845-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
+ Database JDBC URL [jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC]
+ Database driver: MySQL Connector/J
+ Database dialect: MySQLDialect
+ Database version: 8.0.45
+ Default catalog/schema: Petstoredb/undefined
+ Autocommit mode: undefined/unknown
+ Isolation level: REPEATABLE_READ [default REPEATABLE_READ]
+ JDBC fetch size: none
+ Pool: DataSourceConnectionProvider
+ Minimum pool size: undefined/unknown
+ Maximum pool size: undefined/unknown
+2026-04-14T20:48:00.410-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
+2026-04-14T20:48:00.429-06:00 INFO 333355 --- [petshop-backend] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
+2026-04-14T20:48:00.460-06:00 INFO 333355 --- [petshop-backend] [main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
+2026-04-14T20:48:01.322-06:00 INFO 333355 --- [petshop-backend] [main] o.s.boot.web.servlet.RegistrationBean : Filter activityLoggingFilterRegistration was not registered (disabled)
+2026-04-14T20:48:02.531-06:00 INFO 333355 --- [petshop-backend] [main] o.s.m.s.b.SimpleBrokerMessageHandler : Starting...
+2026-04-14T20:48:02.531-06:00 INFO 333355 --- [petshop-backend] [main] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@6a975fe4]]
+2026-04-14T20:48:02.532-06:00 INFO 333355 --- [petshop-backend] [main] o.s.m.s.b.SimpleBrokerMessageHandler : Started.
+2026-04-14T20:48:02.537-06:00 INFO 333355 --- [petshop-backend] [main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
+2026-04-14T20:48:03.256-06:00 INFO 333355 --- [petshop-backend] [main] c.p.backend.service.ActivityLogService : [ACTIVITY] CUSTOMER | ellie.washington | West Side Store | Email sent: Reminder: appointment tomorrow — Veterinary Checkup → ellie.washington@gmail.com
+2026-04-14T20:48:03.729-06:00 INFO 333355 --- [petshop-backend] [main] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | daniel.moore | West Side Store | Email sent: Reminder: appointment tomorrow — Veterinary Checkup → daniel.moore@petshop.com
+2026-04-14T20:48:03.742-06:00 INFO 333355 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : Started BackendApplication in 5.357 seconds (process running for 5.532)
+2026-04-14T20:48:05.562-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2026-04-14T20:48:05.562-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
+2026-04-14T20:48:05.563-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
+2026-04-14T20:48:05.571-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
+2026-04-14T20:48:10.402-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
+2026-04-14T20:48:10.431-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
+2026-04-14T20:48:12.470-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
+2026-04-14T20:48:14.398-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
+2026-04-14T20:48:29.437-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:48:29.539-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:48:34.923-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:48:43.731-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:48:50.154-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:48:50.197-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/adoptions -> 200
+2026-04-14T20:48:53.571-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:48:53.594-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/adoptions -> 200
+2026-04-14T20:48:57.295-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:48:57.340-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/adoptions -> 200
+2026-04-14T20:49:02.530-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-2] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 4, active threads = 1, queued tasks = 2, completed tasks = 1]
+2026-04-14T20:49:04.360-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:04.503-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-9] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
+2026-04-14T20:49:04.532-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/adoptions -> 500
+2026-04-14T20:49:09.172-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:09.305-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
+2026-04-14T20:49:13.132-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:13.158-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
+2026-04-14T20:49:18.817-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:18.967-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] org.hibernate.orm.jdbc.error : HHH000247: ErrorCode: 1048, SQLState: 23000
+2026-04-14T20:49:18.967-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] org.hibernate.orm.jdbc.error : Column 'sourceStoreId' cannot be null
+2026-04-14T20:49:18.973-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/adoptions -> 400
+2026-04-14T20:49:19.013-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
+2026-04-14T20:49:23.058-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:29.750-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:29.890-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users -> 200
+2026-04-14T20:49:30.434-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] CUSTOMER | customer | Downtown Branch | Email sent: Your adoption — Milo → customer@petshop.com
+2026-04-14T20:49:30.924-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | Email sent: Adoption assigned — Milo → staff@petshop.com
+2026-04-14T20:49:30.936-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/adoptions -> 201
+2026-04-14T20:49:36.666-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:36.690-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
+2026-04-14T20:49:36.719-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-9] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | DELETE /api/v1/adoptions/20 -> 204
+2026-04-14T20:49:36.736-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
+2026-04-14T20:49:44.162-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:56.940-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:49:57.066-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
+2026-04-14T20:49:57.103-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/appointments -> 400
+2026-04-14T20:49:57.140-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
+2026-04-14T20:49:57.654-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] CUSTOMER | customer | Downtown Branch | Email sent: Appointment confirmed — Pet Grooming → customer@petshop.com
+2026-04-14T20:49:58.138-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | Email sent: Appointment assigned — Pet Grooming → staff@petshop.com
+2026-04-14T20:49:58.147-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/appointments -> 201
+2026-04-14T20:50:03.962-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:50:04.077-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
+2026-04-14T20:50:07.463-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:50:07.485-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/products -> 200
+2026-04-14T20:50:14.352-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:50:14.473-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
+2026-04-14T20:50:14.509-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/sales -> 403
+2026-04-14T20:50:14.538-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
+2026-04-14T20:50:18.667-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:50:26.690-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-9] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:50:26.816-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | GET /api/v1/auth/me -> 200
+2026-04-14T20:50:26.893-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
+2026-04-14T20:50:26.913-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
+2026-04-14T20:50:26.929-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | POST /api/v1/sales -> 500
+2026-04-14T20:50:26.965-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
+2026-04-14T21:19:02.531-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-6] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 11, active threads = 1, queued tasks = 2, completed tasks = 8]
+2026-04-14T21:49:02.532-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-12] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 16, active threads = 1, queued tasks = 2, completed tasks = 15]
+2026-04-14T22:19:02.532-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-15] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 16, active threads = 1, queued tasks = 2, completed tasks = 22]
+2026-04-14T22:49:02.533-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-1] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 16, active threads = 1, queued tasks = 2, completed tasks = 29]
diff --git a/backend/pom.xml b/backend/pom.xml
index 67b9a807..a87811c2 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -102,6 +102,12 @@
3.1.0
+
+ com.azure
+ azure-storage-blob
+ 12.29.0
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java
index 5c3b4e08..59442d4f 100644
--- a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java
+++ b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java
@@ -1,7 +1,9 @@
package com.petshop.backend.service;
import com.petshop.backend.entity.User;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
@@ -22,10 +24,14 @@ import java.util.UUID;
public class AvatarStorageService {
private static final String STORED_PREFIX = "/uploads/avatars/";
+ private static final String BLOB_CONTAINER = "avatars";
@Value("${app.upload.base-dir:uploads}")
private String uploadBaseDir;
+ @Autowired
+ private AzureBlobService blobService;
+
private Path avatarDirectory;
@PostConstruct
@@ -34,18 +40,22 @@ public class AvatarStorageService {
}
public String storeAvatar(MultipartFile file) throws IOException {
- Files.createDirectories(avatarDirectory);
-
- String originalFilename = file.getOriginalFilename();
- String extension = resolveExtension(originalFilename);
+ String extension = resolveExtension(file.getOriginalFilename());
String filename = UUID.randomUUID() + extension;
- Path filePath = avatarDirectory.resolve(filename).normalize();
-
- Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
+ if (blobService.isEnabled()) {
+ blobService.upload(BLOB_CONTAINER, filename, file.getBytes());
+ } else {
+ Files.createDirectories(avatarDirectory);
+ Files.copy(file.getInputStream(), avatarDirectory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
+ }
return STORED_PREFIX + filename;
}
public Resource loadAvatarResource(User user) {
+ String filename = extractFilename(user.getAvatarUrl());
+ if (blobService.isEnabled()) {
+ return new ByteArrayResource(blobService.download(BLOB_CONTAINER, filename));
+ }
Path filePath = resolveStoredAvatarPath(user.getAvatarUrl());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new IllegalArgumentException("Avatar file was not found");
@@ -54,15 +64,21 @@ public class AvatarStorageService {
}
public void deleteAvatar(User user) throws IOException {
- if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) {
- return;
- }
- try {
- Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
- } catch (IllegalArgumentException ignored) {
+ if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) return;
+ if (blobService.isEnabled()) {
+ blobService.delete(BLOB_CONTAINER, extractFilename(user.getAvatarUrl()));
+ } else {
+ try {
+ Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
+ } catch (IllegalArgumentException ignored) {}
}
}
+ private String extractFilename(String avatarUrl) {
+ if (avatarUrl == null || !avatarUrl.startsWith(STORED_PREFIX)) throw new IllegalArgumentException("Avatar file was not found");
+ return avatarUrl.substring(STORED_PREFIX.length());
+ }
+
public String toOwnerAvatarUrl(User user) {
return hasAvatar(user) ? "/api/v1/users/" + user.getId() + "/avatar/file" : null;
}
diff --git a/backend/src/main/java/com/petshop/backend/service/AzureBlobService.java b/backend/src/main/java/com/petshop/backend/service/AzureBlobService.java
new file mode 100644
index 00000000..cbdfc62d
--- /dev/null
+++ b/backend/src/main/java/com/petshop/backend/service/AzureBlobService.java
@@ -0,0 +1,44 @@
+package com.petshop.backend.service;
+
+import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.core.util.BinaryData;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AzureBlobService {
+
+ private final BlobServiceClient client;
+ private final String containerPrefix;
+
+ public AzureBlobService(
+ @Value("${azure.storage.connection-string:}") String connectionString,
+ @Value("${azure.storage.container-prefix:petshop}") String containerPrefix) {
+ this.containerPrefix = containerPrefix;
+ this.client = (connectionString != null && !connectionString.isBlank())
+ ? new BlobServiceClientBuilder().connectionString(connectionString).buildClient()
+ : null;
+ }
+
+ public boolean isEnabled() {
+ return client != null;
+ }
+
+ public void upload(String container, String blobName, byte[] bytes) {
+ getContainerClient(container).getBlobClient(blobName).upload(BinaryData.fromBytes(bytes), true);
+ }
+
+ public byte[] download(String container, String blobName) {
+ return getContainerClient(container).getBlobClient(blobName).downloadContent().toBytes();
+ }
+
+ public void delete(String container, String blobName) {
+ getContainerClient(container).getBlobClient(blobName).deleteIfExists();
+ }
+
+ private BlobContainerClient getContainerClient(String container) {
+ return client.createBlobContainerIfNotExists(containerPrefix + "-" + container);
+ }
+}
diff --git a/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java b/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java
index 1068e094..48565753 100644
--- a/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java
+++ b/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java
@@ -1,6 +1,8 @@
package com.petshop.backend.service;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
@@ -21,31 +23,58 @@ public class CatalogImageStorageService {
private static final String PET_PREFIX = "/uploads/pets/";
private static final String PRODUCT_PREFIX = "/uploads/products/";
+ private static final String BLOB_PETS = "pets";
+ private static final String BLOB_PRODUCTS = "products";
@Value("${app.upload.base-dir:uploads}")
private String uploadBaseDir;
+ @Autowired
+ private AzureBlobService blobService;
+
public String storePetImage(MultipartFile file) throws IOException {
- return storeImage(file, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX);
+ String extension = resolveExtension(file.getOriginalFilename());
+ String filename = UUID.randomUUID() + extension;
+ if (blobService.isEnabled()) {
+ blobService.upload(BLOB_PETS, filename, file.getBytes());
+ } else {
+ Path directory = Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize();
+ Files.createDirectories(directory);
+ Files.copy(file.getInputStream(), directory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ return PET_PREFIX + filename;
}
public String storeProductImage(MultipartFile file) throws IOException {
- return storeImage(file, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
+ String extension = resolveExtension(file.getOriginalFilename());
+ String filename = UUID.randomUUID() + extension;
+ if (blobService.isEnabled()) {
+ blobService.upload(BLOB_PRODUCTS, filename, file.getBytes());
+ } else {
+ Path directory = Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize();
+ Files.createDirectories(directory);
+ Files.copy(file.getInputStream(), directory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ return PRODUCT_PREFIX + filename;
}
public Resource loadPetImage(String storedPath) {
- Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
- if (!resource.exists()) {
- throw new IllegalArgumentException("Image file was not found");
+ String filename = extractFilename(storedPath, PET_PREFIX);
+ if (blobService.isEnabled()) {
+ return new ByteArrayResource(blobService.download(BLOB_PETS, filename));
}
+ Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
+ if (!resource.exists()) throw new IllegalArgumentException("Image file was not found");
return resource;
}
public Resource loadProductImage(String storedPath) {
- Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
- if (!resource.exists()) {
- throw new IllegalArgumentException("Image file was not found");
+ String filename = extractFilename(storedPath, PRODUCT_PREFIX);
+ if (blobService.isEnabled()) {
+ return new ByteArrayResource(blobService.download(BLOB_PRODUCTS, filename));
}
+ Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
+ if (!resource.exists()) throw new IllegalArgumentException("Image file was not found");
return resource;
}
@@ -53,28 +82,35 @@ public class CatalogImageStorageService {
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
}
+ public MediaType resolveMediaTypeByName(String filename) {
+ return MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM);
+ }
+
public void deletePetImage(String storedPath) throws IOException {
- deleteImage(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX);
+ if (storedPath == null || storedPath.isBlank()) return;
+ String filename = extractFilename(storedPath, PET_PREFIX);
+ if (blobService.isEnabled()) {
+ blobService.delete(BLOB_PETS, filename);
+ } else {
+ Files.deleteIfExists(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
+ }
}
public void deleteProductImage(String storedPath) throws IOException {
- deleteImage(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
- }
-
- private String storeImage(MultipartFile file, Path directory, String prefix) throws IOException {
- Files.createDirectories(directory);
- String extension = resolveExtension(file.getOriginalFilename());
- String filename = UUID.randomUUID() + extension;
- Path filePath = directory.resolve(filename).normalize();
- Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
- return prefix + filename;
- }
-
- private void deleteImage(String storedPath, Path directory, String prefix) throws IOException {
- if (storedPath == null || storedPath.isBlank()) {
- return;
+ if (storedPath == null || storedPath.isBlank()) return;
+ String filename = extractFilename(storedPath, PRODUCT_PREFIX);
+ if (blobService.isEnabled()) {
+ blobService.delete(BLOB_PRODUCTS, filename);
+ } else {
+ Files.deleteIfExists(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
}
- Files.deleteIfExists(resolveStoredPath(storedPath, directory, prefix));
+ }
+
+ private String extractFilename(String storedPath, String prefix) {
+ if (storedPath == null || !storedPath.startsWith(prefix)) throw new IllegalArgumentException("Image file was not found");
+ String filename = storedPath.substring(prefix.length());
+ if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) throw new IllegalArgumentException("Image file was not found");
+ return filename;
}
private Path resolveStoredPath(String storedPath, Path directory, String prefix) {
diff --git a/backend/src/main/java/com/petshop/backend/service/ChatAttachmentStorageService.java b/backend/src/main/java/com/petshop/backend/service/ChatAttachmentStorageService.java
index 720c8079..55932b92 100644
--- a/backend/src/main/java/com/petshop/backend/service/ChatAttachmentStorageService.java
+++ b/backend/src/main/java/com/petshop/backend/service/ChatAttachmentStorageService.java
@@ -1,5 +1,8 @@
package com.petshop.backend.service;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
@@ -18,26 +21,37 @@ import java.util.UUID;
public class ChatAttachmentStorageService {
private static final String STORED_PREFIX = "/uploads/chat/";
- private final Path chatDirectory = Paths.get("uploads", "chat").toAbsolutePath().normalize();
+ private static final String BLOB_CONTAINER = "chat";
+
+ @Value("${app.upload.base-dir:uploads}")
+ private String uploadBaseDir;
+
+ @Autowired
+ private AzureBlobService blobService;
public String storeAttachment(MultipartFile file) throws IOException {
- Files.createDirectories(chatDirectory);
-
String originalFilename = file.getOriginalFilename();
- String extension = "";
- if (originalFilename != null && originalFilename.contains(".")) {
- extension = originalFilename.substring(originalFilename.lastIndexOf("."));
- }
-
+ String extension = (originalFilename != null && originalFilename.contains("."))
+ ? originalFilename.substring(originalFilename.lastIndexOf(".")) : "";
String filename = UUID.randomUUID() + extension;
- Path filePath = chatDirectory.resolve(filename).normalize();
- Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
+ if (blobService.isEnabled()) {
+ blobService.upload(BLOB_CONTAINER, filename, file.getBytes());
+ } else {
+ Path chatDirectory = Paths.get(uploadBaseDir, "chat").toAbsolutePath().normalize();
+ Files.createDirectories(chatDirectory);
+ Files.copy(file.getInputStream(), chatDirectory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
+ }
return STORED_PREFIX + filename;
}
public Resource loadAttachmentResource(String attachmentUrl) {
- Path filePath = resolveStoredPath(attachmentUrl);
+ String filename = extractFilename(attachmentUrl);
+ if (blobService.isEnabled()) {
+ return new ByteArrayResource(blobService.download(BLOB_CONTAINER, filename));
+ }
+ Path chatDirectory = Paths.get(uploadBaseDir, "chat").toAbsolutePath().normalize();
+ Path filePath = chatDirectory.resolve(filename).normalize();
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new IllegalArgumentException("Attachment file was not found");
}
@@ -45,27 +59,18 @@ public class ChatAttachmentStorageService {
}
public MediaType resolveMediaType(String attachmentUrl) {
- try {
- return MediaTypeFactory.getMediaType(loadAttachmentResource(attachmentUrl)).orElse(MediaType.APPLICATION_OCTET_STREAM);
- } catch (IllegalArgumentException ex) {
- return MediaType.APPLICATION_OCTET_STREAM;
- }
+ String filename = extractFilename(attachmentUrl);
+ return MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM);
}
- private Path resolveStoredPath(String attachmentUrl) {
+ private String extractFilename(String attachmentUrl) {
if (attachmentUrl == null || attachmentUrl.isBlank() || !attachmentUrl.startsWith(STORED_PREFIX)) {
throw new IllegalArgumentException("Invalid attachment URL");
}
-
String filename = attachmentUrl.substring(STORED_PREFIX.length());
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
throw new IllegalArgumentException("Invalid attachment filename");
}
-
- Path resolved = chatDirectory.resolve(filename).normalize();
- if (!resolved.startsWith(chatDirectory)) {
- throw new IllegalArgumentException("Invalid attachment path");
- }
- return resolved;
+ return filename;
}
}
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index e006d6c1..c054ea4e 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -1,6 +1,8 @@
spring:
application:
name: petshop-backend
+ main:
+ lazy-initialization: true
config:
import: optional:file:.env[.properties]
@@ -33,7 +35,7 @@ spring:
open-in-view: false
flyway:
- enabled: false
+ enabled: ${FLYWAY_ENABLED:false}
server:
port: ${SERVER_PORT:8080}
@@ -52,6 +54,11 @@ app:
base-dir: ${UPLOAD_BASE_DIR:uploads}
frontend-url: ${FRONTEND_URL:http://localhost:3000}
+azure:
+ storage:
+ connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
+ container-prefix: ${AZURE_STORAGE_CONTAINER_PREFIX:petshop}
+
resend:
api-key: ${RESEND_API_KEY:}
from: ${RESEND_FROM:PetShop }
diff --git a/backend/src/main/resources/static/stores/downtown.webp b/backend/src/main/resources/static/stores/downtown.webp
new file mode 100644
index 00000000..44d13f8a
Binary files /dev/null and b/backend/src/main/resources/static/stores/downtown.webp differ
diff --git a/backend/src/main/resources/static/stores/north.webp b/backend/src/main/resources/static/stores/north.webp
new file mode 100644
index 00000000..89843e86
Binary files /dev/null and b/backend/src/main/resources/static/stores/north.webp differ
diff --git a/backend/src/main/resources/static/stores/west.webp b/backend/src/main/resources/static/stores/west.webp
new file mode 100644
index 00000000..401dc932
Binary files /dev/null and b/backend/src/main/resources/static/stores/west.webp differ
diff --git a/backend/uploads/pets/052.webp b/backend/uploads/pets/052.webp
deleted file mode 100644
index 6c334394..00000000
Binary files a/backend/uploads/pets/052.webp and /dev/null differ
diff --git a/backend/uploads/pets/b9492e16-835f-40d3-a348-867d685fc54b.jpg b/backend/uploads/pets/b9492e16-835f-40d3-a348-867d685fc54b.jpg
new file mode 100644
index 00000000..8111a157
Binary files /dev/null and b/backend/uploads/pets/b9492e16-835f-40d3-a348-867d685fc54b.jpg differ
diff --git a/desktop/.gitignore b/desktop/.gitignore
index 2dc38ee8..b486ea24 100644
--- a/desktop/.gitignore
+++ b/desktop/.gitignore
@@ -39,9 +39,6 @@ build/
### Mac OS ###
.DS_Store
-## Database related
-connectionpetstore.properties
-
# Log files
*.log
log.txt
diff --git a/desktop/src/main/resources/connectionpetstore.properties b/desktop/src/main/resources/connectionpetstore.properties
new file mode 100644
index 00000000..95369ec9
--- /dev/null
+++ b/desktop/src/main/resources/connectionpetstore.properties
@@ -0,0 +1,3 @@
+# Backend URL — swap comments to switch between local and remote
+api.baseUrl=http://localhost:8080
+#api.baseUrl=https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io
diff --git a/web/.env.example b/web/.env.example
index d74ca824..3d56459f 100644
--- a/web/.env.example
+++ b/web/.env.example
@@ -1 +1,4 @@
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
+# Backend URL for the API proxy — swap comments to switch between local and remote
+BACKEND_URL=http://localhost:8080
+#BACKEND_URL=https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 00000000..e850494d
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,17 @@
+FROM node:22-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+COPY . .
+ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
+ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
+RUN npm run build
+
+FROM node:22-alpine
+WORKDIR /app
+ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
+COPY --from=build /app/.next/standalone ./
+COPY --from=build /app/.next/static ./.next/static
+COPY --from=build /app/public ./public
+EXPOSE 3000
+CMD ["node", "server.js"]
diff --git a/web/app/api/[...path]/route.js b/web/app/api/[...path]/route.js
new file mode 100644
index 00000000..ecfb4241
--- /dev/null
+++ b/web/app/api/[...path]/route.js
@@ -0,0 +1,25 @@
+const BACKEND = process.env.BACKEND_URL || 'http://localhost:8080'
+
+async function proxy(request, { params }) {
+ const path = (await params).path.join('/')
+ const { search } = new URL(request.url)
+ const url = `${BACKEND}/api/${path}${search}`
+
+ const headers = new Headers(request.headers)
+ headers.delete('host')
+
+ const init = { method: request.method, headers }
+ if (!['GET', 'HEAD'].includes(request.method)) {
+ init.body = request.body
+ init.duplex = 'half'
+ }
+
+ const res = await fetch(url, init)
+ return new Response(res.body, {
+ status: res.status,
+ statusText: res.statusText,
+ headers: res.headers,
+ })
+}
+
+export { proxy as GET, proxy as POST, proxy as PUT, proxy as DELETE, proxy as PATCH }
diff --git a/web/lib/fetchAllPages.js b/web/lib/fetchAllPages.js
index 624d172c..4e563d83 100644
--- a/web/lib/fetchAllPages.js
+++ b/web/lib/fetchAllPages.js
@@ -6,7 +6,9 @@ export async function fetchAllPages(urlBuilder) {
while (page < totalPages) {
const res = await fetch(urlBuilder(page));
if (!res.ok) {
- throw new Error(`HTTP ${res.status} – ${res.statusText}`);
+ const body = await res.text().catch(() => '')
+ const detail = body ? `: ${body.slice(0, 200)}` : ''
+ throw new Error(`HTTP ${res.status}${detail}`)
}
const data = await res.json();
diff --git a/web/next.config.mjs b/web/next.config.mjs
index b725bf69..2a1407d5 100644
--- a/web/next.config.mjs
+++ b/web/next.config.mjs
@@ -1,14 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
+ output: 'standalone',
reactCompiler: true,
- async rewrites() {
- return [
- {
- source: "/api/:path*",
- destination: "http://localhost:8080/api/:path*",
- },
- ];
- },
};
export default nextConfig;