V1 - Personal Account and Budgeting Done
Personal Account and Budgeting
This commit is contained in:
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "java",
|
||||||
|
"name": "KifiApiApplication",
|
||||||
|
"request": "launch",
|
||||||
|
"mainClass": "com.kifi.api.KifiApiApplication",
|
||||||
|
"projectName": "kifi-api"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic"
|
||||||
|
}
|
||||||
2
kifi-api/.gitattributes
vendored
Normal file
2
kifi-api/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/mvnw text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
33
kifi-api/.gitignore
vendored
Normal file
33
kifi-api/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
3
kifi-api/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
kifi-api/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
wrapperVersion=3.3.4
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
|
||||||
13
kifi-api/Dockerfile
Normal file
13
kifi-api/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM eclipse-temurin:21-jdk-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY .mvn/ .mvn
|
||||||
|
COPY mvnw pom.xml ./
|
||||||
|
RUN ./mvnw dependency:go-offline
|
||||||
|
COPY src ./src
|
||||||
|
RUN ./mvnw clean package -DskipTests
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/target/*.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
360
kifi-api/Kifi-API.postman_collection.json
Normal file
360
kifi-api/Kifi-API.postman_collection.json
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"name": "Kifi API",
|
||||||
|
"description": "Postman Collection for Kifi API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "base_url",
|
||||||
|
"value": "https://app.technobeesolutions.in/api/kifi",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "YOUR_JWT_TOKEN",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "{{token}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Auth",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Signup",
|
||||||
|
"request": {
|
||||||
|
"auth": { "type": "noauth" },
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"phone\": \"+1234567890\",\n \"password\": \"securepassword\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/auth/signup",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "auth", "signup" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Verify OTP",
|
||||||
|
"request": {
|
||||||
|
"auth": { "type": "noauth" },
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"phone\": \"+1234567890\",\n \"otp\": \"123456\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/auth/verify-otp",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "auth", "verify-otp" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Login",
|
||||||
|
"request": {
|
||||||
|
"auth": { "type": "noauth" },
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"phone\": \"+1234567890\",\n \"password\": \"securepassword\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/auth/login",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "auth", "login" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wallets",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Wallets",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/wallets",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "wallets" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Wallet",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"My Family Wallet\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/wallets",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "wallets" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Invite User",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"userPhone\": \"+1987654321\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/wallets/1/invite",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "wallets", "1", "invite" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Transactions",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Transactions",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/transactions",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "transactions" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Add Transaction",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"amount\": 50.0,\n \"type\": \"EXPENSE\",\n \"categoryId\": 1,\n \"merchantId\": 1,\n \"date\": \"2023-10-01T10:00:00\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/transactions",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "transactions" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Transaction",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"amount\": 75.0,\n \"type\": \"EXPENSE\",\n \"categoryId\": 1,\n \"merchantId\": 1,\n \"date\": \"2023-10-01T10:00:00\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/transactions/1",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "transactions", "1" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Transaction",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/transactions/1",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "transactions", "1" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Budgets",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Budgets",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/budgets",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "budgets" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Add/Update Budget",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"categoryId\": 1,\n \"limit\": 500.0,\n \"month\": 10,\n \"year\": 2023\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/budgets",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "budgets" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Budget",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/budgets/1",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "budgets", "1" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Categories",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Categories",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/categories",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "categories" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Add Category",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"Groceries\",\n \"icon\": \"shopping_cart\",\n \"color\": \"#FF0000\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/categories",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "categories" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Merchants",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Merchants",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/merchants",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "merchants" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Add Merchant",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"Walmart\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/merchants",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "merchants" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Recurring Transactions",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Recurring Transactions",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/recurring-transactions",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "recurring-transactions" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Add Recurring Transaction",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [ { "key": "Content-Type", "value": "application/json" } ],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"amount\": 15.99,\n \"type\": \"EXPENSE\",\n \"categoryId\": 1,\n \"frequency\": \"MONTHLY\",\n \"nextDate\": \"2023-11-01T10:00:00\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/recurring-transactions",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "recurring-transactions" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Recurring Transaction",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/recurring-transactions/1",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "recurring-transactions", "1" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reports",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Export CSV",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/reports/export",
|
||||||
|
"host": [ "{{base_url}}" ],
|
||||||
|
"path": [ "reports", "export" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
20
kifi-api/build_n_push.sh
Executable file
20
kifi-api/build_n_push.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
REGISTRY="hub.technobeesolutions.in"
|
||||||
|
USERNAME="technobee_admin"
|
||||||
|
PASSWORD='M@tr!x#149@dm!N'
|
||||||
|
IMAGE_NAME="kifi-api"
|
||||||
|
TAG="latest"
|
||||||
|
FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG"
|
||||||
|
|
||||||
|
# Stop on any error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Logging into Docker registry: $REGISTRY..."
|
||||||
|
echo "$PASSWORD" | docker login "$REGISTRY" -u "$USERNAME" --password-stdin
|
||||||
|
|
||||||
|
echo "Building and pushing Docker image for linux/amd64: $FULL_IMAGE_NAME..."
|
||||||
|
docker buildx build --platform linux/amd64 -t "$FULL_IMAGE_NAME" --push .
|
||||||
|
|
||||||
|
echo "Done! Image built and pushed successfully."
|
||||||
30
kifi-api/docker-compose.yml
Normal file
30
kifi-api/docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
image: hub.technobeesolutions.in/kifi-api:latest
|
||||||
|
container_name: kifi-api
|
||||||
|
ports:
|
||||||
|
- "8056:8080"
|
||||||
|
environment:
|
||||||
|
- SPRING_R2DBC_URL=r2dbc:postgresql://host.docker.internal:5432/kifi
|
||||||
|
- SPRING_R2DBC_USERNAME=postgres
|
||||||
|
- SPRING_R2DBC_PASSWORD=M@triXPostgr3s@6202
|
||||||
|
- SPRING_DATA_REDIS_HOST=redis
|
||||||
|
- SPRING_DATA_REDIS_PORT=6379
|
||||||
|
- SPRING_DATA_REDIS_PASSWORD=M@triXR3d1s@6202
|
||||||
|
- SPRING_MAIL_HOST=smtp.gmail.com
|
||||||
|
- SPRING_MAIL_PORT=587
|
||||||
|
- SPRING_MAIL_USERNAME=technobeesolutions@gmail.com
|
||||||
|
- SPRING_MAIL_PASSWORD=lrideibfakickldg
|
||||||
|
- MINIO_SERVICE_URL=http://minio-service:1500
|
||||||
|
- MINIO_SERVICE_BUCKET=kifi
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
networks:
|
||||||
|
commons-network:
|
||||||
|
ipv4_address: 172.19.0.224
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
commons-network:
|
||||||
|
external: true
|
||||||
|
name: commons_commons-network
|
||||||
295
kifi-api/mvnw
vendored
Executable file
295
kifi-api/mvnw
vendored
Executable file
@@ -0,0 +1,295 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||||
|
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euf
|
||||||
|
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||||
|
|
||||||
|
# OS specific support.
|
||||||
|
native_path() { printf %s\\n "$1"; }
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN* | MINGW*)
|
||||||
|
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||||
|
native_path() { cygpath --path --windows "$1"; }
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# set JAVACMD and JAVACCMD
|
||||||
|
set_java_home() {
|
||||||
|
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||||
|
if [ -n "${JAVA_HOME-}" ]; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||||
|
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||||
|
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v java
|
||||||
|
)" || :
|
||||||
|
JAVACCMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v javac
|
||||||
|
)" || :
|
||||||
|
|
||||||
|
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||||
|
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# hash string like Java String::hashCode
|
||||||
|
hash_string() {
|
||||||
|
str="${1:-}" h=0
|
||||||
|
while [ -n "$str" ]; do
|
||||||
|
char="${str%"${str#?}"}"
|
||||||
|
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||||
|
str="${str#?}"
|
||||||
|
done
|
||||||
|
printf %x\\n $h
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose() { :; }
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf %s\\n "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
# MWRAPPER-139:
|
||||||
|
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||||
|
# Needed for removing poorly interpreted newline sequences when running in more
|
||||||
|
# exotic environments such as mingw bash on Windows.
|
||||||
|
printf "%s" "${1}" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptDir="$(dirname "$0")"
|
||||||
|
scriptName="$(basename "$0")"
|
||||||
|
|
||||||
|
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "${key-}" in
|
||||||
|
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||||
|
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||||
|
esac
|
||||||
|
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
|
||||||
|
case "${distributionUrl##*/}" in
|
||||||
|
maven-mvnd-*bin.*)
|
||||||
|
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||||
|
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||||
|
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||||
|
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||||
|
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||||
|
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||||
|
*)
|
||||||
|
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||||
|
distributionPlatform=linux-amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||||
|
;;
|
||||||
|
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||||
|
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||||
|
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||||
|
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||||
|
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||||
|
|
||||||
|
exec_maven() {
|
||||||
|
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||||
|
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -d "$MAVEN_HOME" ]; then
|
||||||
|
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
exec_maven "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${distributionUrl-}" in
|
||||||
|
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||||
|
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||||
|
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||||
|
trap clean HUP INT TERM EXIT
|
||||||
|
else
|
||||||
|
die "cannot create temp dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
verbose "Downloading from: $distributionUrl"
|
||||||
|
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
# select .zip or .tar.gz
|
||||||
|
if ! command -v unzip >/dev/null; then
|
||||||
|
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# verbose opt
|
||||||
|
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||||
|
|
||||||
|
# normalize http auth
|
||||||
|
case "${MVNW_PASSWORD:+has-password}" in
|
||||||
|
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||||
|
verbose "Found wget ... using wget"
|
||||||
|
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||||
|
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||||
|
verbose "Found curl ... using curl"
|
||||||
|
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||||
|
elif set_java_home; then
|
||||||
|
verbose "Falling back to use Java to download"
|
||||||
|
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||||
|
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
cat >"$javaSource" <<-END
|
||||||
|
public class Downloader extends java.net.Authenticator
|
||||||
|
{
|
||||||
|
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||||
|
{
|
||||||
|
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||||
|
}
|
||||||
|
public static void main( String[] args ) throws Exception
|
||||||
|
{
|
||||||
|
setDefault( new Downloader() );
|
||||||
|
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END
|
||||||
|
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||||
|
verbose " - Compiling Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||||
|
verbose " - Running Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
if [ -n "${distributionSha256Sum-}" ]; then
|
||||||
|
distributionSha256Result=false
|
||||||
|
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||||
|
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||||
|
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
elif command -v sha256sum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||||
|
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $distributionSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
if command -v unzip >/dev/null; then
|
||||||
|
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||||
|
else
|
||||||
|
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
actualDistributionDir=""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||||
|
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$distributionUrlNameMain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
# enable globbing to iterate over items
|
||||||
|
set +f
|
||||||
|
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$(basename "$dir")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||||
|
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||||
|
die "Could not find Maven distribution directory in extracted archive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||||
|
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||||
|
|
||||||
|
clean || :
|
||||||
|
exec_maven "$@"
|
||||||
189
kifi-api/mvnw.cmd
vendored
Normal file
189
kifi-api/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<# : batch portion
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
@REM
|
||||||
|
@REM Optional ENV vars
|
||||||
|
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||||
|
@SET __MVNW_CMD__=
|
||||||
|
@SET __MVNW_ERROR__=
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||||
|
@SET PSModulePath=
|
||||||
|
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||||
|
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||||
|
)
|
||||||
|
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=
|
||||||
|
@SET __MVNW_ARG0_NAME__=
|
||||||
|
@SET MVNW_USERNAME=
|
||||||
|
@SET MVNW_PASSWORD=
|
||||||
|
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||||
|
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||||
|
@GOTO :EOF
|
||||||
|
: end batch / begin powershell #>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ($env:MVNW_VERBOSE -eq "true") {
|
||||||
|
$VerbosePreference = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||||
|
if (!$distributionUrl) {
|
||||||
|
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||||
|
"maven-mvnd-*" {
|
||||||
|
$USE_MVND = $true
|
||||||
|
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||||
|
$MVN_CMD = "mvnd.cmd"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$USE_MVND = $false
|
||||||
|
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
if ($env:MVNW_REPOURL) {
|
||||||
|
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||||
|
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||||
|
}
|
||||||
|
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||||
|
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||||
|
|
||||||
|
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||||
|
if ($env:MAVEN_USER_HOME) {
|
||||||
|
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||||
|
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_WRAPPER_DISTS = $null
|
||||||
|
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||||
|
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||||
|
} else {
|
||||||
|
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||||
|
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||||
|
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||||
|
|
||||||
|
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||||
|
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||||
|
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||||
|
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||||
|
trap {
|
||||||
|
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
Write-Verbose "Downloading from: $distributionUrl"
|
||||||
|
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
$webclient = New-Object System.Net.WebClient
|
||||||
|
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||||
|
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||||
|
if ($distributionSha256Sum) {
|
||||||
|
if ($USE_MVND) {
|
||||||
|
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||||
|
}
|
||||||
|
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||||
|
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||||
|
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
$actualDistributionDir = ""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||||
|
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||||
|
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||||
|
$actualDistributionDir = $distributionUrlNameMain
|
||||||
|
}
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||||
|
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||||
|
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||||
|
$actualDistributionDir = $_.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||||
|
try {
|
||||||
|
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||||
|
} catch {
|
||||||
|
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||||
|
Write-Error "fail to move MAVEN_HOME"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
171
kifi-api/pom.xml
Normal file
171
kifi-api/pom.xml
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?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>4.1.0</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.kifi</groupId>
|
||||||
|
<artifactId>kifi-api</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>kifi-api</name>
|
||||||
|
<description>Kifi Backend API</description>
|
||||||
|
<url/>
|
||||||
|
<licenses>
|
||||||
|
<license/>
|
||||||
|
</licenses>
|
||||||
|
<developers>
|
||||||
|
<developer/>
|
||||||
|
</developers>
|
||||||
|
<scm>
|
||||||
|
<connection/>
|
||||||
|
<developerConnection/>
|
||||||
|
<tag/>
|
||||||
|
<url/>
|
||||||
|
</scm>
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-mail</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>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>r2dbc-postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-r2dbc-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis-reactive-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-mail-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.5</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-csv</artifactId>
|
||||||
|
<version>1.10.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>default-compile</id>
|
||||||
|
<phase>compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>default-testCompile</id>
|
||||||
|
<phase>test-compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>testCompile</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
16
kifi-api/src/main/java/com/kifi/api/KifiApiApplication.java
Normal file
16
kifi-api/src/main/java/com/kifi/api/KifiApiApplication.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.kifi.api;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
|
public class KifiApiApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(KifiApiApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.kifi.api.config;
|
||||||
|
|
||||||
|
import com.kifi.api.security.AuthenticationManager;
|
||||||
|
import com.kifi.api.security.SecurityContextRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final AuthenticationManager authenticationManager;
|
||||||
|
private final SecurityContextRepository securityContextRepository;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||||
|
return http
|
||||||
|
.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
|
||||||
|
.authenticationEntryPoint((swe, e) ->
|
||||||
|
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
|
||||||
|
)
|
||||||
|
.accessDeniedHandler((swe, e) ->
|
||||||
|
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.csrf(ServerHttpSecurity.CsrfSpec::disable)
|
||||||
|
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
||||||
|
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
||||||
|
.authenticationManager(authenticationManager)
|
||||||
|
.securityContextRepository(securityContextRepository)
|
||||||
|
.authorizeExchange(exchange -> exchange
|
||||||
|
.pathMatchers(HttpMethod.OPTIONS).permitAll()
|
||||||
|
.pathMatchers("/api/kifi/auth/**").permitAll()
|
||||||
|
.pathMatchers("/api/kifi/health/**").permitAll()
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.kifi.api.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebClientConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebClient.Builder webClientBuilder() {
|
||||||
|
return WebClient.builder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import com.kifi.api.dto.AuthRequest;
|
||||||
|
import com.kifi.api.dto.OtpVerificationRequest;
|
||||||
|
import com.kifi.api.dto.ResetPasswordRequest;
|
||||||
|
import com.kifi.api.service.AuthService;
|
||||||
|
import com.kifi.api.service.CryptoService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
private final CryptoService cryptoService;
|
||||||
|
|
||||||
|
@GetMapping("/public-key")
|
||||||
|
public Mono<ResponseEntity<Object>> getPublicKey() {
|
||||||
|
return Mono.just(ResponseEntity.ok().body((Object) Map.of("publicKey", cryptoService.getPublicKeyBase64())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/signup")
|
||||||
|
public Mono<ResponseEntity<Object>> signup(@RequestBody AuthRequest request) {
|
||||||
|
return authService.signup(request)
|
||||||
|
.map(msg -> ResponseEntity.ok().body((Object) Map.of("message", msg)))
|
||||||
|
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().body(Map.of("error", e.getMessage()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/verify-otp")
|
||||||
|
public Mono<ResponseEntity<Object>> verifyOtp(@RequestBody OtpVerificationRequest request) {
|
||||||
|
return authService.verifyOtp(request.getEmail(), request.getOtp())
|
||||||
|
.map(response -> ResponseEntity.ok().body((Object) response))
|
||||||
|
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().body(Map.of("error", e.getMessage()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public Mono<ResponseEntity<Object>> login(@RequestBody AuthRequest request) {
|
||||||
|
return authService.login(request)
|
||||||
|
.map(response -> ResponseEntity.ok().body((Object) response))
|
||||||
|
.onErrorResume(e -> Mono.just(ResponseEntity.status(401).body(Map.of("error", e.getMessage()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/forgot-password")
|
||||||
|
public Mono<ResponseEntity<Object>> forgotPassword(@RequestBody Map<String, String> request) {
|
||||||
|
String email = request.get("email");
|
||||||
|
return authService.forgotPassword(email)
|
||||||
|
.map(msg -> ResponseEntity.ok().body((Object) Map.of("message", msg)))
|
||||||
|
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().body(Map.of("error", e.getMessage()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/reset-password")
|
||||||
|
public Mono<ResponseEntity<Object>> resetPassword(@RequestBody ResetPasswordRequest request) {
|
||||||
|
return authService.resetPassword(request.getEmail(), request.getOtp(), request.getNewPassword())
|
||||||
|
.map(msg -> ResponseEntity.ok().body((Object) Map.of("message", msg)))
|
||||||
|
.onErrorResume(e -> Mono.just(ResponseEntity.badRequest().body(Map.of("error", e.getMessage()))));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Budget;
|
||||||
|
import com.kifi.api.dto.BudgetSummary;
|
||||||
|
import com.kifi.api.service.BudgetService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/budgets")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BudgetController {
|
||||||
|
private final BudgetService budgetService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Mono<List<Budget>> getBudgets(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return budgetService.getBudgets(userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/summary")
|
||||||
|
public Mono<List<BudgetSummary>> getBudgetSummaries(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return budgetService.getBudgetSummaries(userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Mono<Budget> addOrUpdateBudget(Authentication authentication, @RequestBody Budget budget) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return budgetService.addOrUpdateBudget(userId, budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Mono<Void> deleteBudget(@PathVariable Long id) {
|
||||||
|
return budgetService.deleteBudget(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Category;
|
||||||
|
import com.kifi.api.service.CategoryService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/categories")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CategoryController {
|
||||||
|
private final CategoryService categoryService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Mono<List<Category>> getCategories(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return categoryService.getCategories(userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Mono<Category> addCategory(Authentication authentication, @RequestBody Category category) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return categoryService.addCategory(userId, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/health")
|
||||||
|
public class HealthController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Mono<Map<String, String>> healthCheck() {
|
||||||
|
return Mono.just(Map.of("status", "UP", "message", "Kifi API is running."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.RecurringTransaction;
|
||||||
|
import com.kifi.api.service.RecurringTransactionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.util.List;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/recurring-transactions")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RecurringTransactionController {
|
||||||
|
|
||||||
|
private final RecurringTransactionService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Mono<List<RecurringTransaction>> getRecurringTransactions(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return service.getRecurringTransactions(userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Mono<RecurringTransaction> addRecurringTransaction(Authentication authentication, @RequestBody RecurringTransaction transaction) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return service.addRecurringTransaction(userId, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Mono<RecurringTransaction> updateRecurringTransaction(@PathVariable Long id, Authentication authentication, @RequestBody RecurringTransaction transaction) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return service.updateRecurringTransaction(id, userId, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Mono<Void> deleteRecurringTransaction(@PathVariable Long id) {
|
||||||
|
return service.deleteRecurringTransaction(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import com.kifi.api.service.ReportService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/reports")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ReportController {
|
||||||
|
private final ReportService reportService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/export", produces = "text/csv")
|
||||||
|
public Mono<ResponseEntity<byte[]>> exportTransactions(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return reportService.exportTransactionsToCsv(userId)
|
||||||
|
.map(bytes -> ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"transactions.csv\"")
|
||||||
|
.contentType(MediaType.parseMediaType("text/csv"))
|
||||||
|
.body(bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Transaction;
|
||||||
|
import com.kifi.api.service.TransactionService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.util.List;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/transactions")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TransactionController {
|
||||||
|
private final TransactionService transactionService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Mono<List<Transaction>> getTransactions(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return transactionService.getTransactions(userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public Mono<Page<Transaction>> searchTransactions(
|
||||||
|
Authentication authentication,
|
||||||
|
@RequestParam(required = false) String type,
|
||||||
|
@RequestParam(required = false) Long walletId,
|
||||||
|
@RequestParam(required = false) Long categoryId,
|
||||||
|
@RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate startDate,
|
||||||
|
@RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate endDate,
|
||||||
|
@RequestParam(required = false) String search,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size
|
||||||
|
) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
Pageable pageable = PageRequest.of(page, size);
|
||||||
|
return transactionService.searchTransactions(userId, type, walletId, categoryId, startDate, endDate, search, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Mono<Transaction> addTransaction(Authentication authentication, @RequestBody Transaction transaction) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return transactionService.addTransaction(userId, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Mono<Transaction> updateTransaction(Authentication authentication, @PathVariable Long id, @RequestBody Transaction transaction) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return transactionService.updateTransaction(id, userId, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Mono<Void> deleteTransaction(@PathVariable Long id) {
|
||||||
|
return transactionService.deleteTransaction(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/{id}/attachments", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public Mono<com.kifi.api.entity.TransactionAttachment> addAttachment(@PathVariable Long id, @RequestPart("file") org.springframework.http.codec.multipart.FilePart filePart) {
|
||||||
|
return org.springframework.core.io.buffer.DataBufferUtils.join(filePart.content())
|
||||||
|
.flatMap(dataBuffer -> {
|
||||||
|
byte[] bytes = new byte[dataBuffer.readableByteCount()];
|
||||||
|
dataBuffer.read(bytes);
|
||||||
|
org.springframework.core.io.buffer.DataBufferUtils.release(dataBuffer);
|
||||||
|
String base64Content = java.util.Base64.getEncoder().encodeToString(bytes);
|
||||||
|
String fileName = filePart.filename();
|
||||||
|
String contentType = filePart.headers().getContentType() != null ? filePart.headers().getContentType().toString() : "application/octet-stream";
|
||||||
|
return transactionService.addAttachment(id, fileName, contentType, base64Content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/attachments/{attachmentId}/content")
|
||||||
|
public Mono<org.springframework.http.ResponseEntity<byte[]>> downloadAttachment(@PathVariable Long id, @PathVariable Long attachmentId) {
|
||||||
|
return transactionService.downloadAttachment(attachmentId)
|
||||||
|
.map(response -> {
|
||||||
|
byte[] decodedBytes = java.util.Base64.getDecoder().decode(response.getBase64Content());
|
||||||
|
return org.springframework.http.ResponseEntity.ok()
|
||||||
|
.header(org.springframework.http.HttpHeaders.CONTENT_TYPE, "image/jpeg") // Ideally dynamic based on db
|
||||||
|
.body(decodedBytes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/attachments/{attachmentId}")
|
||||||
|
public Mono<Void> deleteAttachment(@PathVariable Long attachmentId) {
|
||||||
|
return transactionService.deleteAttachment(attachmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/close-investment")
|
||||||
|
public Mono<Transaction> closeInvestment(Authentication authentication, @PathVariable Long id, @RequestBody java.util.Map<String, Object> payload) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
java.math.BigDecimal maturityAmount = new java.math.BigDecimal(payload.get("maturityAmount").toString());
|
||||||
|
java.time.LocalDate closingDate = payload.get("closingDate") != null ? java.time.LocalDate.parse(payload.get("closingDate").toString()) : java.time.LocalDate.now();
|
||||||
|
Long toWalletId = payload.get("toWalletId") != null ? Long.valueOf(payload.get("toWalletId").toString()) : null;
|
||||||
|
return transactionService.closeInvestment(id, userId, maturityAmount, closingDate, toWalletId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.kifi.api.controller;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.UserWallet;
|
||||||
|
import com.kifi.api.entity.Wallet;
|
||||||
|
import com.kifi.api.entity.WalletInvitation;
|
||||||
|
import com.kifi.api.service.WalletService;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/kifi/wallets")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WalletController {
|
||||||
|
|
||||||
|
private final WalletService walletService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Mono<List<Wallet>> getWallets(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.getWalletsForUser(userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Mono<Wallet> createWallet(@RequestBody CreateWalletRequest request, Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.createWallet(userId, request.getName(), request.getNature(), request.getIcon(), request.getColor(), request.getCurrency(), request.getInitialBalance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{walletId}")
|
||||||
|
public Mono<Wallet> editWallet(@PathVariable Long walletId, @RequestBody CreateWalletRequest request, Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.editWallet(walletId, userId, request.getName(), request.getNature(), request.getIcon(), request.getColor(), request.getCurrency());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{walletId}")
|
||||||
|
public Mono<Void> deleteWallet(@PathVariable Long walletId, Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.deleteWallet(walletId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{walletId}/invite")
|
||||||
|
public Mono<WalletInvitation> inviteUser(@PathVariable Long walletId, @RequestBody InviteRequest request, Authentication authentication) {
|
||||||
|
Long inviterId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.inviteUserByEmail(inviterId, walletId, request.getEmail());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/invitations")
|
||||||
|
public Mono<List<WalletInvitation>> getInvitations(Authentication authentication) {
|
||||||
|
String email = authentication.getName();
|
||||||
|
return walletService.getPendingInvitations(email).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/invitations/{id}/accept")
|
||||||
|
public Mono<Void> acceptInvitation(@PathVariable Long id, Authentication authentication) {
|
||||||
|
String email = authentication.getName();
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.respondToInvitation(id, email, userId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/invitations/{id}/reject")
|
||||||
|
public Mono<Void> rejectInvitation(@PathVariable Long id, Authentication authentication) {
|
||||||
|
String email = authentication.getName();
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.respondToInvitation(id, email, userId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{walletId}/members")
|
||||||
|
public Mono<List<com.kifi.api.dto.WalletMemberDTO>> getWalletMembers(@PathVariable Long walletId, Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.getWalletMembers(walletId, userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{walletId}/members/{memberIdToRemove}")
|
||||||
|
public Mono<Void> removeWalletMember(@PathVariable Long walletId, @PathVariable Long memberIdToRemove, Authentication authentication) {
|
||||||
|
Long ownerId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.removeWalletMember(walletId, ownerId, memberIdToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user-contacts")
|
||||||
|
public Mono<List<String>> getKnownContacts(Authentication authentication) {
|
||||||
|
Long userId = Long.valueOf(authentication.getDetails().toString());
|
||||||
|
return walletService.getKnownContacts(userId).collectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
static class CreateWalletRequest {
|
||||||
|
private String name;
|
||||||
|
private String nature;
|
||||||
|
private String icon;
|
||||||
|
private String color;
|
||||||
|
private String currency;
|
||||||
|
private java.math.BigDecimal initialBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
static class InviteRequest {
|
||||||
|
private String email;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
kifi-api/src/main/java/com/kifi/api/dto/AuthRequest.java
Normal file
9
kifi-api/src/main/java/com/kifi/api/dto/AuthRequest.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kifi.api.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AuthRequest {
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
16
kifi-api/src/main/java/com/kifi/api/dto/AuthResponse.java
Normal file
16
kifi-api/src/main/java/com/kifi/api/dto/AuthResponse.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.kifi.api.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AuthResponse {
|
||||||
|
private String token;
|
||||||
|
private Long userId;
|
||||||
|
private String email;
|
||||||
|
}
|
||||||
14
kifi-api/src/main/java/com/kifi/api/dto/BudgetSummary.java
Normal file
14
kifi-api/src/main/java/com/kifi/api/dto/BudgetSummary.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.kifi.api.dto;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Budget;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class BudgetSummary {
|
||||||
|
private Budget budget;
|
||||||
|
private BigDecimal spentAmount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kifi.api.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class OtpVerificationRequest {
|
||||||
|
private String email;
|
||||||
|
private String otp;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.kifi.api.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ResetPasswordRequest {
|
||||||
|
private String email;
|
||||||
|
private String otp;
|
||||||
|
private String newPassword;
|
||||||
|
}
|
||||||
19
kifi-api/src/main/java/com/kifi/api/dto/WalletMemberDTO.java
Normal file
19
kifi-api/src/main/java/com/kifi/api/dto/WalletMemberDTO.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.kifi.api.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class WalletMemberDTO {
|
||||||
|
private Long userId;
|
||||||
|
private String email;
|
||||||
|
private String role;
|
||||||
|
private LocalDateTime joinedAt;
|
||||||
|
}
|
||||||
21
kifi-api/src/main/java/com/kifi/api/entity/Budget.java
Normal file
21
kifi-api/src/main/java/com/kifi/api/entity/Budget.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Table("budgets")
|
||||||
|
public class Budget {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private Long categoryId;
|
||||||
|
private Long walletId;
|
||||||
|
private BigDecimal monthlyLimit;
|
||||||
|
private Boolean isShared;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
24
kifi-api/src/main/java/com/kifi/api/entity/Category.java
Normal file
24
kifi-api/src/main/java/com/kifi/api/entity/Category.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Table("categories")
|
||||||
|
public class Category {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private String name;
|
||||||
|
private String iconName;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Table("recurring_transactions")
|
||||||
|
public class RecurringTransaction {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private Long categoryId;
|
||||||
|
private Long fromWalletId;
|
||||||
|
private Long toWalletId;
|
||||||
|
private String type;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String frequency; // DAILY, WEEKLY, MONTHLY, YEARLY
|
||||||
|
private LocalDate nextExecutionDate;
|
||||||
|
private LocalDate endDate;
|
||||||
|
private String status; // ACTIVE, PAUSED
|
||||||
|
private String description;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
50
kifi-api/src/main/java/com/kifi/api/entity/Transaction.java
Normal file
50
kifi-api/src/main/java/com/kifi/api/entity/Transaction.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Table("transactions")
|
||||||
|
public class Transaction {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private Long categoryId;
|
||||||
|
private Long fromWalletId;
|
||||||
|
private Long toWalletId;
|
||||||
|
private String notes;
|
||||||
|
private String type; // INCOME or EXPENSE or INVESTMENT
|
||||||
|
private BigDecimal amount;
|
||||||
|
private LocalDate date;
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
// Payable fields
|
||||||
|
private LocalDate dueDate;
|
||||||
|
private String alertSchedule;
|
||||||
|
private java.time.LocalTime alertTime;
|
||||||
|
|
||||||
|
// Investment fields
|
||||||
|
private String investmentStatus; // OPEN or CLOSED
|
||||||
|
private BigDecimal maturityAmount;
|
||||||
|
private BigDecimal profitLoss;
|
||||||
|
private LocalDate closingDate;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.Transient
|
||||||
|
private java.util.List<TransactionItem> items;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.Transient
|
||||||
|
private java.util.List<TransactionAttachment> attachments;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Table("transaction_attachments")
|
||||||
|
public class TransactionAttachment {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private Long transactionId;
|
||||||
|
private String fileName;
|
||||||
|
private String filePath;
|
||||||
|
private String contentType;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Table("transaction_items")
|
||||||
|
public class TransactionItem {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private Long transactionId;
|
||||||
|
private String name;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
public enum TransactionType {
|
||||||
|
INCOME,
|
||||||
|
EXPENSE
|
||||||
|
}
|
||||||
26
kifi-api/src/main/java/com/kifi/api/entity/User.java
Normal file
26
kifi-api/src/main/java/com/kifi/api/entity/User.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Table("users")
|
||||||
|
public class User {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean enabled = false;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
18
kifi-api/src/main/java/com/kifi/api/entity/UserWallet.java
Normal file
18
kifi-api/src/main/java/com/kifi/api/entity/UserWallet.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Table("user_wallets")
|
||||||
|
public class UserWallet {
|
||||||
|
@Id
|
||||||
|
private Long id; // R2DBC sometimes needs a synthetic ID, but we have a composite PK in DB. We might need to just use a regular ID if this gives issues, but let's try mapping.
|
||||||
|
private Long userId;
|
||||||
|
private Long walletId;
|
||||||
|
private String role;
|
||||||
|
private LocalDateTime joinedAt;
|
||||||
|
}
|
||||||
22
kifi-api/src/main/java/com/kifi/api/entity/Wallet.java
Normal file
22
kifi-api/src/main/java/com/kifi/api/entity/Wallet.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Table("wallets")
|
||||||
|
public class Wallet {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private Long ownerId;
|
||||||
|
private String nature;
|
||||||
|
private java.math.BigDecimal balance;
|
||||||
|
private String currency;
|
||||||
|
private String icon;
|
||||||
|
private String color;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.kifi.api.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Table("wallet_invitations")
|
||||||
|
public class WalletInvitation {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
private Long walletId;
|
||||||
|
private Long inviterId;
|
||||||
|
private String inviteeEmail;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Budget;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
public interface BudgetRepository extends R2dbcRepository<Budget, Long> {
|
||||||
|
@org.springframework.data.r2dbc.repository.Query("SELECT b.* FROM budgets b WHERE b.user_id = :userId OR (b.is_shared = TRUE AND b.wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId))")
|
||||||
|
Flux<Budget> findByUserId(Long userId);
|
||||||
|
reactor.core.publisher.Mono<Void> deleteByWalletId(Long walletId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Category;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CategoryRepository extends R2dbcRepository<Category, Long> {
|
||||||
|
Flux<Category> findByUserId(Long userId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.RecurringTransaction;
|
||||||
|
import org.springframework.data.r2dbc.repository.Query;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RecurringTransactionRepository extends R2dbcRepository<RecurringTransaction, Long> {
|
||||||
|
Flux<RecurringTransaction> findByUserId(Long userId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM recurring_transactions WHERE status = 'ACTIVE' AND next_execution_date <= :date")
|
||||||
|
Flux<RecurringTransaction> findDueTransactions(LocalDate date);
|
||||||
|
|
||||||
|
reactor.core.publisher.Mono<Long> countByFromWalletIdOrToWalletId(Long fromWalletId, Long toWalletId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.TransactionAttachment;
|
||||||
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TransactionAttachmentRepository extends ReactiveCrudRepository<TransactionAttachment, Long> {
|
||||||
|
Flux<TransactionAttachment> findByTransactionId(Long transactionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.TransactionItem;
|
||||||
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TransactionItemRepository extends ReactiveCrudRepository<TransactionItem, Long> {
|
||||||
|
Flux<TransactionItem> findByTransactionId(Long transactionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Transaction;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TransactionRepository extends R2dbcRepository<Transaction, Long>, TransactionRepositoryCustom {
|
||||||
|
@org.springframework.data.r2dbc.repository.Query("SELECT t.* FROM transactions t WHERE t.user_id = :userId OR t.from_wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId) OR t.to_wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId) ORDER BY t.date DESC")
|
||||||
|
Flux<Transaction> findVisibleTransactionsForUser(Long userId);
|
||||||
|
Mono<Long> countByFromWalletIdOrToWalletId(Long fromWalletId, Long toWalletId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Transaction;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public interface TransactionRepositoryCustom {
|
||||||
|
Mono<Page<Transaction>> findTransactionsWithFilters(
|
||||||
|
Long userId,
|
||||||
|
String type,
|
||||||
|
Long walletId,
|
||||||
|
Long categoryId,
|
||||||
|
LocalDate startDate,
|
||||||
|
LocalDate endDate,
|
||||||
|
String search,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Transaction;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.r2dbc.core.DatabaseClient;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class TransactionRepositoryImpl implements TransactionRepositoryCustom {
|
||||||
|
|
||||||
|
private final DatabaseClient databaseClient;
|
||||||
|
|
||||||
|
public TransactionRepositoryImpl(DatabaseClient databaseClient) {
|
||||||
|
this.databaseClient = databaseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Page<Transaction>> findTransactionsWithFilters(
|
||||||
|
Long userId,
|
||||||
|
String type,
|
||||||
|
Long walletId,
|
||||||
|
Long categoryId,
|
||||||
|
LocalDate startDate,
|
||||||
|
LocalDate endDate,
|
||||||
|
String search,
|
||||||
|
Pageable pageable) {
|
||||||
|
StringBuilder baseQuery = new StringBuilder("FROM transactions t WHERE (t.user_id = :userId OR t.from_wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId) OR t.to_wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId))");
|
||||||
|
|
||||||
|
if (type != null && !type.isEmpty()) {
|
||||||
|
baseQuery.append(" AND t.type = :type");
|
||||||
|
}
|
||||||
|
if (walletId != null) {
|
||||||
|
baseQuery.append(" AND (t.from_wallet_id = :walletId OR t.to_wallet_id = :walletId)");
|
||||||
|
}
|
||||||
|
if (categoryId != null) {
|
||||||
|
baseQuery.append(" AND t.category_id = :categoryId");
|
||||||
|
}
|
||||||
|
if (startDate != null) {
|
||||||
|
baseQuery.append(" AND t.date >= :startDate");
|
||||||
|
}
|
||||||
|
if (endDate != null) {
|
||||||
|
baseQuery.append(" AND t.date <= :endDate");
|
||||||
|
}
|
||||||
|
if (search != null && !search.trim().isEmpty()) {
|
||||||
|
baseQuery.append(" AND t.description ILIKE :search");
|
||||||
|
}
|
||||||
|
|
||||||
|
String dataQueryStr = "SELECT t.* " + baseQuery.toString() + " ORDER BY t.date DESC LIMIT " + pageable.getPageSize() + " OFFSET " + pageable.getOffset();
|
||||||
|
String countQueryStr = "SELECT COUNT(t.id) " + baseQuery.toString();
|
||||||
|
|
||||||
|
DatabaseClient.GenericExecuteSpec dataSpec = databaseClient.sql(dataQueryStr).bind("userId", userId);
|
||||||
|
DatabaseClient.GenericExecuteSpec countSpec = databaseClient.sql(countQueryStr).bind("userId", userId);
|
||||||
|
|
||||||
|
if (type != null && !type.isEmpty()) {
|
||||||
|
dataSpec = dataSpec.bind("type", type);
|
||||||
|
countSpec = countSpec.bind("type", type);
|
||||||
|
}
|
||||||
|
if (walletId != null) {
|
||||||
|
dataSpec = dataSpec.bind("walletId", walletId);
|
||||||
|
countSpec = countSpec.bind("walletId", walletId);
|
||||||
|
}
|
||||||
|
if (categoryId != null) {
|
||||||
|
dataSpec = dataSpec.bind("categoryId", categoryId);
|
||||||
|
countSpec = countSpec.bind("categoryId", categoryId);
|
||||||
|
}
|
||||||
|
if (startDate != null) {
|
||||||
|
dataSpec = dataSpec.bind("startDate", startDate);
|
||||||
|
countSpec = countSpec.bind("startDate", startDate);
|
||||||
|
}
|
||||||
|
if (endDate != null) {
|
||||||
|
dataSpec = dataSpec.bind("endDate", endDate);
|
||||||
|
countSpec = countSpec.bind("endDate", endDate);
|
||||||
|
}
|
||||||
|
if (search != null && !search.trim().isEmpty()) {
|
||||||
|
dataSpec = dataSpec.bind("search", "%" + search.trim() + "%");
|
||||||
|
countSpec = countSpec.bind("search", "%" + search.trim() + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<List<Transaction>> transactionsMono = dataSpec.map((row, metadata) -> {
|
||||||
|
Transaction t = new Transaction();
|
||||||
|
t.setId(row.get("id", Long.class));
|
||||||
|
t.setUserId(row.get("user_id", Long.class));
|
||||||
|
t.setFromWalletId(row.get("from_wallet_id", Long.class));
|
||||||
|
t.setToWalletId(row.get("to_wallet_id", Long.class));
|
||||||
|
t.setCategoryId(row.get("category_id", Long.class));
|
||||||
|
t.setType(row.get("type", String.class));
|
||||||
|
t.setAmount(row.get("amount", BigDecimal.class));
|
||||||
|
t.setDate(row.get("date", LocalDate.class));
|
||||||
|
t.setDescription(row.get("description", String.class));
|
||||||
|
t.setInvestmentStatus(row.get("investment_status", String.class));
|
||||||
|
t.setProfitLoss(row.get("profit_loss", BigDecimal.class));
|
||||||
|
return t;
|
||||||
|
}).all().collectList();
|
||||||
|
|
||||||
|
Mono<Long> countMono = countSpec.map((row, metadata) -> row.get(0, Long.class)).first().defaultIfEmpty(0L);
|
||||||
|
|
||||||
|
return Mono.zip(transactionsMono, countMono)
|
||||||
|
.map(tuple -> new PageImpl<>(tuple.getT1(), pageable, tuple.getT2()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.User;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import org.springframework.data.r2dbc.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserRepository extends R2dbcRepository<User, Long> {
|
||||||
|
Mono<User> findByEmail(String email);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.UserWallet;
|
||||||
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface UserWalletRepository extends ReactiveCrudRepository<UserWallet, Long> {
|
||||||
|
Flux<UserWallet> findByUserId(Long userId);
|
||||||
|
Flux<UserWallet> findByWalletId(Long walletId);
|
||||||
|
Mono<Void> deleteByWalletId(Long walletId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.WalletInvitation;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface WalletInvitationRepository extends R2dbcRepository<WalletInvitation, Long> {
|
||||||
|
Flux<WalletInvitation> findByInviteeEmailAndStatus(String inviteeEmail, String status);
|
||||||
|
reactor.core.publisher.Mono<Void> deleteByWalletId(Long walletId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kifi.api.repository;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Wallet;
|
||||||
|
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
public interface WalletRepository extends ReactiveCrudRepository<Wallet, Long> {
|
||||||
|
Flux<Wallet> findByOwnerId(Long ownerId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.kifi.api.scheduler;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.RecurringTransaction;
|
||||||
|
import com.kifi.api.entity.Transaction;
|
||||||
|
import com.kifi.api.repository.RecurringTransactionRepository;
|
||||||
|
import com.kifi.api.service.TransactionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class RecurringTransactionScheduler {
|
||||||
|
|
||||||
|
private final RecurringTransactionRepository recurringRepository;
|
||||||
|
private final TransactionService transactionService;
|
||||||
|
|
||||||
|
// Runs every day at midnight
|
||||||
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
|
public void processRecurringTransactions() {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
log.info("Starting recurring transaction processing for {}", today);
|
||||||
|
|
||||||
|
recurringRepository.findDueTransactions(today)
|
||||||
|
.flatMap(this::processSingle)
|
||||||
|
.subscribe(
|
||||||
|
success -> log.info("Processed recurring transaction: {}", success.getId()),
|
||||||
|
error -> log.error("Error processing recurring transactions", error),
|
||||||
|
() -> log.info("Finished processing recurring transactions for {}", today)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<RecurringTransaction> processSingle(RecurringTransaction rt) {
|
||||||
|
Transaction t = Transaction.builder()
|
||||||
|
.userId(rt.getUserId())
|
||||||
|
.categoryId(rt.getCategoryId())
|
||||||
|
.fromWalletId(rt.getFromWalletId())
|
||||||
|
.toWalletId(rt.getToWalletId())
|
||||||
|
.type(rt.getType())
|
||||||
|
.amount(rt.getAmount())
|
||||||
|
.date(rt.getNextExecutionDate()) // Process it as of the execution date
|
||||||
|
.description(rt.getDescription())
|
||||||
|
.notes("Auto-generated from recurring transaction")
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Calculate next date
|
||||||
|
LocalDate nextDate = rt.getNextExecutionDate();
|
||||||
|
switch (rt.getFrequency()) {
|
||||||
|
case "DAILY" -> nextDate = nextDate.plusDays(1);
|
||||||
|
case "WEEKLY" -> nextDate = nextDate.plusWeeks(1);
|
||||||
|
case "MONTHLY" -> nextDate = nextDate.plusMonths(1);
|
||||||
|
case "YEARLY" -> nextDate = nextDate.plusYears(1);
|
||||||
|
}
|
||||||
|
rt.setNextExecutionDate(nextDate);
|
||||||
|
if (rt.getEndDate() != null && nextDate.isAfter(rt.getEndDate())) {
|
||||||
|
rt.setStatus("COMPLETED");
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionService.addTransaction(rt.getUserId(), t)
|
||||||
|
.then(recurringRepository.save(rt));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.kifi.api.security;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthenticationManager implements ReactiveAuthenticationManager {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Authentication> authenticate(Authentication authentication) {
|
||||||
|
String authToken = authentication.getCredentials().toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (jwtUtil.validateToken(authToken)) {
|
||||||
|
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
|
||||||
|
String email = claims.getSubject();
|
||||||
|
Long userId = claims.get("userId", Long.class);
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
|
||||||
|
email,
|
||||||
|
null,
|
||||||
|
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
|
||||||
|
);
|
||||||
|
|
||||||
|
auth.setDetails(userId);
|
||||||
|
|
||||||
|
return Mono.just(auth);
|
||||||
|
} else {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
kifi-api/src/main/java/com/kifi/api/security/JwtUtil.java
Normal file
68
kifi-api/src/main/java/com/kifi/api/security/JwtUtil.java
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package com.kifi.api.security;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtUtil {
|
||||||
|
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Value("${jwt.expiration}")
|
||||||
|
private String expirationTime;
|
||||||
|
|
||||||
|
private SecretKey getSignKey() {
|
||||||
|
byte[] keyBytes = secret.getBytes();
|
||||||
|
return Keys.hmacShaKeyFor(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Claims getAllClaimsFromToken(String token) {
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(getSignKey())
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmailFromToken(String token) {
|
||||||
|
return getAllClaimsFromToken(token).getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getExpirationDateFromToken(String token) {
|
||||||
|
return getAllClaimsFromToken(token).getExpiration();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isTokenExpired(String token) {
|
||||||
|
final Date expiration = getExpirationDateFromToken(token);
|
||||||
|
return expiration.before(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(String email, Long userId) {
|
||||||
|
HashMap<String, Object> claims = new HashMap<>();
|
||||||
|
claims.put("userId", userId);
|
||||||
|
|
||||||
|
long expirationTimeLong = Long.parseLong(expirationTime);
|
||||||
|
final Date createdDate = new Date();
|
||||||
|
final Date expirationDate = new Date(createdDate.getTime() + expirationTimeLong);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.claims(claims)
|
||||||
|
.subject(email)
|
||||||
|
.issuedAt(createdDate)
|
||||||
|
.expiration(expirationDate)
|
||||||
|
.signWith(getSignKey())
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean validateToken(String token) {
|
||||||
|
return !isTokenExpired(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.kifi.api.security;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityContextRepository implements ServerSecurityContextRepository {
|
||||||
|
|
||||||
|
private final AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
|
||||||
|
throw new UnsupportedOperationException("Not supported yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<SecurityContext> load(ServerWebExchange exchange) {
|
||||||
|
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
String authToken = authHeader.substring(7);
|
||||||
|
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
|
||||||
|
return this.authenticationManager.authenticate(auth)
|
||||||
|
.map(SecurityContextImpl::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
157
kifi-api/src/main/java/com/kifi/api/service/AuthService.java
Normal file
157
kifi-api/src/main/java/com/kifi/api/service/AuthService.java
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import com.kifi.api.dto.AuthRequest;
|
||||||
|
import com.kifi.api.dto.AuthResponse;
|
||||||
|
import com.kifi.api.entity.User;
|
||||||
|
import com.kifi.api.repository.UserRepository;
|
||||||
|
import com.kifi.api.security.JwtUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
private final EmailService emailService;
|
||||||
|
private final ReactiveStringRedisTemplate redisTemplate;
|
||||||
|
private final CryptoService cryptoService;
|
||||||
|
|
||||||
|
private String decryptField(String value) {
|
||||||
|
try {
|
||||||
|
return cryptoService.decrypt(value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If decryption fails, treat as plaintext (backward compatibility)
|
||||||
|
log.warn("RSA decryption failed, treating as plaintext: {}", e.getMessage());
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<String> signup(AuthRequest request) {
|
||||||
|
String email = decryptField(request.getEmail());
|
||||||
|
String password = decryptField(request.getPassword());
|
||||||
|
|
||||||
|
return userRepository.findByEmail(email)
|
||||||
|
.flatMap(existingUser -> {
|
||||||
|
if (existingUser.getEnabled()) {
|
||||||
|
return Mono.error(new RuntimeException("User already exists and is verified"));
|
||||||
|
}
|
||||||
|
return generateAndSendOtp(existingUser, "REGISTRATION");
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
User newUser = User.builder()
|
||||||
|
.email(email)
|
||||||
|
.password(passwordEncoder.encode(password))
|
||||||
|
.enabled(false)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
return userRepository.save(newUser).flatMap(u -> generateAndSendOtp(u, "REGISTRATION"));
|
||||||
|
}))
|
||||||
|
.map(u -> "OTP sent to your email.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<User> generateAndSendOtp(User user, String purpose) {
|
||||||
|
String otp = String.format("%06d", new Random().nextInt(999999));
|
||||||
|
String redisKey = "OTP:" + purpose + ":" + user.getEmail();
|
||||||
|
return redisTemplate.opsForValue()
|
||||||
|
.set(redisKey, otp, Duration.ofMinutes(5))
|
||||||
|
.then(purpose.equals("REGISTRATION")
|
||||||
|
? emailService.sendRegistrationOtp(user.getEmail(), otp)
|
||||||
|
: emailService.sendResetPasswordOtp(user.getEmail(), otp))
|
||||||
|
.thenReturn(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<AuthResponse> verifyOtp(String email, String otp) {
|
||||||
|
String redisKey = "OTP:REGISTRATION:" + email;
|
||||||
|
return redisTemplate.opsForValue().get(redisKey)
|
||||||
|
.switchIfEmpty(
|
||||||
|
// Fallback to old key format for backward compatibility
|
||||||
|
redisTemplate.opsForValue().get("OTP:" + email)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("OTP not found or expired")))
|
||||||
|
)
|
||||||
|
.flatMap(savedOtp -> {
|
||||||
|
if (!savedOtp.equals(otp)) {
|
||||||
|
return Mono.error(new RuntimeException("Invalid OTP"));
|
||||||
|
}
|
||||||
|
return userRepository.findByEmail(email);
|
||||||
|
})
|
||||||
|
.flatMap(user -> {
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return userRepository.save(user);
|
||||||
|
})
|
||||||
|
.flatMap(user -> redisTemplate.opsForValue().delete(redisKey).thenReturn(user))
|
||||||
|
.map(user -> AuthResponse.builder()
|
||||||
|
.token(jwtUtil.generateToken(user.getEmail(), user.getId()))
|
||||||
|
.userId(user.getId())
|
||||||
|
.email(user.getEmail())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<AuthResponse> login(AuthRequest request) {
|
||||||
|
String email = decryptField(request.getEmail());
|
||||||
|
String password = decryptField(request.getPassword());
|
||||||
|
|
||||||
|
return userRepository.findByEmail(email)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("Invalid email or password")))
|
||||||
|
.flatMap(user -> {
|
||||||
|
if (!user.getEnabled()) {
|
||||||
|
return Mono.error(new RuntimeException("Email not verified"));
|
||||||
|
}
|
||||||
|
if (passwordEncoder.matches(password, user.getPassword())) {
|
||||||
|
return Mono.just(AuthResponse.builder()
|
||||||
|
.token(jwtUtil.generateToken(user.getEmail(), user.getId()))
|
||||||
|
.userId(user.getId())
|
||||||
|
.email(user.getEmail())
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
return Mono.error(new RuntimeException("Invalid email or password"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<String> forgotPassword(String email) {
|
||||||
|
return userRepository.findByEmail(email)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("No account found with this email")))
|
||||||
|
.flatMap(user -> {
|
||||||
|
if (!user.getEnabled()) {
|
||||||
|
return Mono.error(new RuntimeException("Account not verified yet"));
|
||||||
|
}
|
||||||
|
return generateAndSendOtp(user, "RESET");
|
||||||
|
})
|
||||||
|
.map(u -> "Password reset OTP sent to your email.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<String> resetPassword(String email, String otp, String encryptedNewPassword) {
|
||||||
|
String redisKey = "OTP:RESET:" + email;
|
||||||
|
String newPassword = decryptField(encryptedNewPassword);
|
||||||
|
|
||||||
|
return redisTemplate.opsForValue().get(redisKey)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("OTP not found or expired")))
|
||||||
|
.flatMap(savedOtp -> {
|
||||||
|
if (!savedOtp.equals(otp)) {
|
||||||
|
return Mono.error(new RuntimeException("Invalid OTP"));
|
||||||
|
}
|
||||||
|
return userRepository.findByEmail(email);
|
||||||
|
})
|
||||||
|
.flatMap(user -> {
|
||||||
|
user.setPassword(passwordEncoder.encode(newPassword));
|
||||||
|
user.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return userRepository.save(user);
|
||||||
|
})
|
||||||
|
.flatMap(user -> redisTemplate.opsForValue().delete(redisKey).thenReturn(user))
|
||||||
|
.map(u -> "Password reset successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import com.kifi.api.dto.BudgetSummary;
|
||||||
|
import com.kifi.api.entity.Budget;
|
||||||
|
import com.kifi.api.repository.BudgetRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.r2dbc.core.DatabaseClient;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BudgetService {
|
||||||
|
private final BudgetRepository budgetRepository;
|
||||||
|
private final DatabaseClient databaseClient;
|
||||||
|
|
||||||
|
public Flux<Budget> getBudgets(Long userId) {
|
||||||
|
return budgetRepository.findByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<BudgetSummary> getBudgetSummaries(Long userId) {
|
||||||
|
return budgetRepository.findByUserId(userId)
|
||||||
|
.flatMap(budget -> {
|
||||||
|
String sql = "SELECT COALESCE(SUM(amount), 0) FROM transactions " +
|
||||||
|
"WHERE type = 'EXPENSE' AND " +
|
||||||
|
"date >= date_trunc('month', current_date) AND " +
|
||||||
|
"(category_id = :categoryId OR from_wallet_id = :walletId) AND " +
|
||||||
|
"(user_id = :userId OR from_wallet_id IN (SELECT uw.wallet_id FROM user_wallets uw WHERE uw.user_id = :userId))";
|
||||||
|
|
||||||
|
return databaseClient.sql(sql)
|
||||||
|
.bind("userId", userId)
|
||||||
|
.bind("categoryId", budget.getCategoryId() != null ? budget.getCategoryId() : -1)
|
||||||
|
.bind("walletId", budget.getWalletId() != null ? budget.getWalletId() : -1)
|
||||||
|
.map((row, rowMetadata) -> row.get(0, BigDecimal.class))
|
||||||
|
.first()
|
||||||
|
.defaultIfEmpty(BigDecimal.ZERO)
|
||||||
|
.map(spent -> BudgetSummary.builder().budget(budget).spentAmount(spent).build());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Budget> addOrUpdateBudget(Long userId, Budget budget) {
|
||||||
|
return budgetRepository.findByUserId(userId)
|
||||||
|
.filter(b -> (b.getCategoryId() != null && b.getCategoryId().equals(budget.getCategoryId())) ||
|
||||||
|
(b.getWalletId() != null && b.getWalletId().equals(budget.getWalletId())))
|
||||||
|
.next()
|
||||||
|
.flatMap(existing -> {
|
||||||
|
existing.setMonthlyLimit(budget.getMonthlyLimit());
|
||||||
|
if (budget.getIsShared() != null) {
|
||||||
|
existing.setIsShared(budget.getIsShared());
|
||||||
|
}
|
||||||
|
return budgetRepository.save(existing);
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
budget.setUserId(userId);
|
||||||
|
budget.setCreatedAt(LocalDateTime.now());
|
||||||
|
if (budget.getIsShared() == null) {
|
||||||
|
budget.setIsShared(false);
|
||||||
|
}
|
||||||
|
return budgetRepository.save(budget);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> deleteBudget(Long id) {
|
||||||
|
return budgetRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Category;
|
||||||
|
import com.kifi.api.repository.CategoryRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CategoryService {
|
||||||
|
private final CategoryRepository categoryRepository;
|
||||||
|
|
||||||
|
public Flux<Category> getCategories(Long userId) {
|
||||||
|
return categoryRepository.findByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Category> addCategory(Long userId, Category category) {
|
||||||
|
category.setUserId(userId);
|
||||||
|
category.setCreatedAt(LocalDateTime.now());
|
||||||
|
return categoryRepository.save(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class CryptoService {
|
||||||
|
|
||||||
|
private KeyPair keyPair;
|
||||||
|
|
||||||
|
public CryptoService() {
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||||
|
keyGen.initialize(2048);
|
||||||
|
this.keyPair = keyGen.generateKeyPair();
|
||||||
|
log.info("RSA Key Pair generated successfully for CryptoService.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate RSA Key Pair", e);
|
||||||
|
throw new RuntimeException("Failed to initialize CryptoService", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPublicKeyBase64() {
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String decrypt(String encryptedBase64) {
|
||||||
|
try {
|
||||||
|
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);
|
||||||
|
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
|
||||||
|
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
|
||||||
|
return new String(decryptedBytes, "UTF-8");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to decrypt RSA payload", e);
|
||||||
|
throw new RuntimeException("Failed to decrypt payload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
kifi-api/src/main/java/com/kifi/api/service/EmailService.java
Normal file
172
kifi-api/src/main/java/com/kifi/api/service/EmailService.java
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class EmailService {
|
||||||
|
|
||||||
|
private final JavaMailSender javaMailSender;
|
||||||
|
|
||||||
|
public String buildEmailTemplate(String title, String subtitle, String otp, String footerNote) {
|
||||||
|
return """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background-color:#f4f4f7;font-family:'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||||
|
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7;padding:40px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="460" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:16px;box-shadow:0 4px 24px rgba(0,0,0,0.08);overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td style="background:linear-gradient(135deg,#6C63FF 0%%,#48C6EF 100%%);padding:32px 40px;text-align:center;">
|
||||||
|
<h1 style="margin:0;color:#ffffff;font-size:28px;font-weight:700;letter-spacing:1px;">Kifi</h1>
|
||||||
|
<p style="margin:6px 0 0;color:rgba(255,255,255,0.85);font-size:13px;font-weight:400;">Smart Financial Management</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:36px 40px 28px;">
|
||||||
|
<h2 style="margin:0 0 8px;color:#1a1a2e;font-size:22px;font-weight:600;">%s</h2>
|
||||||
|
<p style="margin:0 0 28px;color:#6b7280;font-size:15px;line-height:1.6;">%s</p>
|
||||||
|
<!-- OTP Box -->
|
||||||
|
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:20px 0;">
|
||||||
|
<div style="display:inline-block;background:linear-gradient(135deg,#6C63FF08,#48C6EF12);border:2px solid #6C63FF;border-radius:16px;padding:20px 48px;">
|
||||||
|
<span style="font-size:36px;font-weight:700;letter-spacing:12px;color:#6C63FF;font-family:'Courier New',monospace;">%s</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin:24px 0 0;color:#9ca3af;font-size:13px;text-align:center;line-height:1.5;">
|
||||||
|
⏱ This code expires in <strong style="color:#6C63FF;">5 minutes</strong>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Divider -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 40px;">
|
||||||
|
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 40px 32px;text-align:center;">
|
||||||
|
<p style="margin:0 0 8px;color:#9ca3af;font-size:12px;line-height:1.5;">%s</p>
|
||||||
|
<p style="margin:0;color:#d1d5db;font-size:11px;">© 2026 Kifi by Sarascore. All rights reserved.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""".formatted(title, subtitle, otp, footerNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> sendRegistrationOtp(String to, String otp) {
|
||||||
|
String html = buildEmailTemplate(
|
||||||
|
"Welcome to Kifi! 🎉",
|
||||||
|
"Thank you for signing up. Please use the verification code below to complete your registration.",
|
||||||
|
otp,
|
||||||
|
"If you didn't create an account, you can safely ignore this email."
|
||||||
|
);
|
||||||
|
return sendEmail(to, "Kifi – Verify Your Email", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildInvitationEmailTemplate(String title, String subtitle, String footerNote) {
|
||||||
|
return """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background-color:#f4f4f7;font-family:'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||||
|
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7;padding:40px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="460" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:16px;box-shadow:0 4px 24px rgba(0,0,0,0.08);overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td style="background:linear-gradient(135deg,#6C63FF 0%%,#48C6EF 100%%);padding:32px 40px;text-align:center;">
|
||||||
|
<h1 style="margin:0;color:#ffffff;font-size:28px;font-weight:700;letter-spacing:1px;">Kifi</h1>
|
||||||
|
<p style="margin:6px 0 0;color:rgba(255,255,255,0.85);font-size:13px;font-weight:400;">Smart Financial Management</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:36px 40px 28px;">
|
||||||
|
<h2 style="margin:0 0 8px;color:#1a1a2e;font-size:22px;font-weight:600;">%s</h2>
|
||||||
|
<p style="margin:0 0 28px;color:#6b7280;font-size:15px;line-height:1.6;">%s</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Divider -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 40px;">
|
||||||
|
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 40px 32px;text-align:center;">
|
||||||
|
<p style="margin:0 0 8px;color:#9ca3af;font-size:12px;line-height:1.5;">%s</p>
|
||||||
|
<p style="margin:0;color:#d1d5db;font-size:11px;">© 2026 Kifi by Sarascore. All rights reserved.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""".formatted(title, subtitle, footerNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> sendResetPasswordOtp(String to, String otp) {
|
||||||
|
String html = buildEmailTemplate(
|
||||||
|
"Reset Your Password 🔐",
|
||||||
|
"We received a request to reset your password. Use the code below to set a new password.",
|
||||||
|
otp,
|
||||||
|
"If you didn't request a password reset, please ignore this email. Your account is safe."
|
||||||
|
);
|
||||||
|
return sendEmail(to, "Kifi – Password Reset Code", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep old method for backward compatibility
|
||||||
|
public Mono<Void> sendOtpEmail(String to, String otp) {
|
||||||
|
return sendRegistrationOtp(to, otp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> sendEmail(String to, String subject, String htmlContent) {
|
||||||
|
return Mono.fromRunnable(() -> {
|
||||||
|
try {
|
||||||
|
MimeMessage message = javaMailSender.createMimeMessage();
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||||
|
helper.setTo(to);
|
||||||
|
helper.setSubject(subject);
|
||||||
|
helper.setText(htmlContent, true);
|
||||||
|
javaMailSender.send(message);
|
||||||
|
log.info("Email sent to: {} with subject: {}", to, subject);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send email", e);
|
||||||
|
throw new RuntimeException("Failed to send email");
|
||||||
|
}
|
||||||
|
}).subscribeOn(Schedulers.boundedElastic()).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MinioServiceClient {
|
||||||
|
|
||||||
|
private final WebClient webClient;
|
||||||
|
|
||||||
|
@Value("${minio.service.url:http://minio-service:1500}")
|
||||||
|
private String minioServiceUrl;
|
||||||
|
|
||||||
|
@Value("${minio.service.bucket:kifi}")
|
||||||
|
private String bucketName;
|
||||||
|
|
||||||
|
public MinioServiceClient(WebClient.Builder webClientBuilder) {
|
||||||
|
this.webClient = webClientBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MinioUploadRequest {
|
||||||
|
private String bucketName;
|
||||||
|
private String directoryPath;
|
||||||
|
private String contentType;
|
||||||
|
private String fileName;
|
||||||
|
private String fileContentBase64;
|
||||||
|
private boolean overwrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MinioDeleteRequest {
|
||||||
|
private String bucketName;
|
||||||
|
private String type;
|
||||||
|
private String filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MinioResponse {
|
||||||
|
private boolean success;
|
||||||
|
private String message;
|
||||||
|
private String filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<MinioResponse> uploadFile(String directoryPath, String contentType, String fileName, String base64Content) {
|
||||||
|
MinioUploadRequest request = new MinioUploadRequest();
|
||||||
|
request.setBucketName(bucketName);
|
||||||
|
request.setDirectoryPath(directoryPath);
|
||||||
|
request.setContentType(contentType);
|
||||||
|
request.setFileName(fileName);
|
||||||
|
request.setFileContentBase64(base64Content);
|
||||||
|
request.setOverwrite(true);
|
||||||
|
|
||||||
|
return webClient.post()
|
||||||
|
.uri(minioServiceUrl + "/api/v1/minio/upload")
|
||||||
|
.bodyValue(request)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(MinioResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<MinioResponse> deleteFile(String contentType, String filePath) {
|
||||||
|
MinioDeleteRequest request = new MinioDeleteRequest();
|
||||||
|
request.setBucketName(bucketName);
|
||||||
|
request.setType(contentType);
|
||||||
|
request.setFilePath(filePath);
|
||||||
|
|
||||||
|
return webClient.method(org.springframework.http.HttpMethod.DELETE)
|
||||||
|
.uri(minioServiceUrl + "/api/v1/minio/file")
|
||||||
|
.bodyValue(request)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(MinioResponse.class);
|
||||||
|
}
|
||||||
|
@Data
|
||||||
|
public static class MinioDownloadRequest {
|
||||||
|
private String bucketName;
|
||||||
|
private String type;
|
||||||
|
private String filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MinioDownloadResponse {
|
||||||
|
private boolean success;
|
||||||
|
private String message;
|
||||||
|
private String base64Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<MinioDownloadResponse> downloadFile(String contentType, String filePath) {
|
||||||
|
MinioDownloadRequest request = new MinioDownloadRequest();
|
||||||
|
request.setBucketName(bucketName);
|
||||||
|
request.setType(contentType);
|
||||||
|
request.setFilePath(filePath);
|
||||||
|
|
||||||
|
return webClient.post()
|
||||||
|
.uri(minioServiceUrl + "/api/v1/minio/download")
|
||||||
|
.bodyValue(request)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(MinioDownloadResponse.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.RecurringTransaction;
|
||||||
|
import com.kifi.api.repository.RecurringTransactionRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RecurringTransactionService {
|
||||||
|
private final RecurringTransactionRepository repository;
|
||||||
|
|
||||||
|
public Flux<RecurringTransaction> getRecurringTransactions(Long userId) {
|
||||||
|
return repository.findByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<RecurringTransaction> addRecurringTransaction(Long userId, RecurringTransaction transaction) {
|
||||||
|
transaction.setUserId(userId);
|
||||||
|
if (transaction.getStatus() == null) transaction.setStatus("ACTIVE");
|
||||||
|
transaction.setCreatedAt(LocalDateTime.now());
|
||||||
|
return repository.save(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<RecurringTransaction> updateRecurringTransaction(Long id, Long userId, RecurringTransaction updated) {
|
||||||
|
return repository.findById(id)
|
||||||
|
.filter(rt -> rt.getUserId().equals(userId))
|
||||||
|
.flatMap(existing -> {
|
||||||
|
existing.setAmount(updated.getAmount());
|
||||||
|
existing.setFrequency(updated.getFrequency());
|
||||||
|
existing.setNextExecutionDate(updated.getNextExecutionDate());
|
||||||
|
existing.setEndDate(updated.getEndDate());
|
||||||
|
existing.setStatus(updated.getStatus());
|
||||||
|
existing.setDescription(updated.getDescription());
|
||||||
|
existing.setFromWalletId(updated.getFromWalletId());
|
||||||
|
existing.setToWalletId(updated.getToWalletId());
|
||||||
|
existing.setCategoryId(updated.getCategoryId());
|
||||||
|
return repository.save(existing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> deleteRecurringTransaction(Long id) {
|
||||||
|
return repository.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Transaction;
|
||||||
|
import com.kifi.api.repository.TransactionRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.csv.CSVFormat;
|
||||||
|
import org.apache.commons.csv.CSVPrinter;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ReportService {
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
|
||||||
|
public Mono<byte[]> exportTransactionsToCsv(Long userId) {
|
||||||
|
return transactionRepository.findVisibleTransactionsForUser(userId)
|
||||||
|
.collectList()
|
||||||
|
.map(this::generateCsvBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateCsvBytes(List<Transaction> transactions) {
|
||||||
|
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
PrintWriter pw = new PrintWriter(out, true, StandardCharsets.UTF_8);
|
||||||
|
CSVPrinter csvPrinter = new CSVPrinter(pw, CSVFormat.DEFAULT.builder().setHeader("ID", "Date", "Type", "Amount", "Description").build())) {
|
||||||
|
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
for (Transaction tx : transactions) {
|
||||||
|
csvPrinter.printRecord(
|
||||||
|
tx.getId(),
|
||||||
|
tx.getDate() != null ? tx.getDate().format(formatter) : "",
|
||||||
|
tx.getType(),
|
||||||
|
tx.getAmount(),
|
||||||
|
tx.getDescription() != null ? tx.getDescription() : ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
csvPrinter.flush();
|
||||||
|
return out.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error generating CSV", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.Transaction;
|
||||||
|
import com.kifi.api.entity.TransactionAttachment;
|
||||||
|
import com.kifi.api.entity.TransactionItem;
|
||||||
|
import com.kifi.api.repository.TransactionAttachmentRepository;
|
||||||
|
import com.kifi.api.repository.TransactionItemRepository;
|
||||||
|
import com.kifi.api.repository.TransactionRepository;
|
||||||
|
import com.kifi.api.repository.WalletRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TransactionService {
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
private final TransactionItemRepository transactionItemRepository;
|
||||||
|
private final TransactionAttachmentRepository transactionAttachmentRepository;
|
||||||
|
private final WalletRepository walletRepository;
|
||||||
|
private final MinioServiceClient minioServiceClient;
|
||||||
|
|
||||||
|
public Flux<Transaction> getTransactions(Long userId) {
|
||||||
|
return transactionRepository.findVisibleTransactionsForUser(userId)
|
||||||
|
.flatMap(this::populateItemsAndAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Page<Transaction>> searchTransactions(Long userId, String type, Long walletId, Long categoryId, LocalDate startDate, LocalDate endDate, String search, Pageable pageable) {
|
||||||
|
return transactionRepository.findTransactionsWithFilters(userId, type, walletId, categoryId, startDate, endDate, search, pageable)
|
||||||
|
.flatMap(page -> Flux.fromIterable(page.getContent())
|
||||||
|
.flatMap(this::populateItemsAndAttachments)
|
||||||
|
.collectList()
|
||||||
|
.map(populatedList -> new org.springframework.data.domain.PageImpl<>(populatedList, pageable, page.getTotalElements())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Transaction> populateItemsAndAttachments(Transaction transaction) {
|
||||||
|
Mono<List<TransactionItem>> itemsMono = transactionItemRepository.findByTransactionId(transaction.getId()).collectList();
|
||||||
|
Mono<List<TransactionAttachment>> attachmentsMono = transactionAttachmentRepository.findByTransactionId(transaction.getId()).collectList();
|
||||||
|
|
||||||
|
return Mono.zip(itemsMono, attachmentsMono).map(tuple -> {
|
||||||
|
transaction.setItems(tuple.getT1());
|
||||||
|
transaction.setAttachments(tuple.getT2());
|
||||||
|
return transaction;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Mono<Transaction> addTransaction(Long userId, Transaction transaction) {
|
||||||
|
transaction.setUserId(userId);
|
||||||
|
transaction.setCreatedAt(LocalDateTime.now());
|
||||||
|
if ("INVESTMENT".equals(transaction.getType())) {
|
||||||
|
transaction.setInvestmentStatus("OPEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<Void> updateBalances = updateWalletBalances(transaction.getFromWalletId(), transaction.getToWalletId(), transaction.getAmount());
|
||||||
|
|
||||||
|
return updateBalances.then(transactionRepository.save(transaction)).flatMap(savedTx -> {
|
||||||
|
Mono<Void> itemsMono = Mono.empty();
|
||||||
|
if (transaction.getItems() != null && !transaction.getItems().isEmpty()) {
|
||||||
|
for (TransactionItem item : transaction.getItems()) {
|
||||||
|
item.setTransactionId(savedTx.getId());
|
||||||
|
item.setCreatedAt(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
itemsMono = transactionItemRepository.saveAll(transaction.getItems()).then();
|
||||||
|
}
|
||||||
|
return itemsMono.thenReturn(savedTx);
|
||||||
|
}).flatMap(this::populateItemsAndAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> updateWalletBalances(Long fromWalletId, Long toWalletId, BigDecimal amount) {
|
||||||
|
Mono<Void> deductFrom = fromWalletId != null ? walletRepository.findById(fromWalletId)
|
||||||
|
.flatMap(w -> {
|
||||||
|
w.setBalance(w.getBalance().subtract(amount));
|
||||||
|
return walletRepository.save(w);
|
||||||
|
}).then() : Mono.empty();
|
||||||
|
|
||||||
|
Mono<Void> addTo = toWalletId != null ? walletRepository.findById(toWalletId)
|
||||||
|
.flatMap(w -> {
|
||||||
|
w.setBalance(w.getBalance().add(amount));
|
||||||
|
return walletRepository.save(w);
|
||||||
|
}).then() : Mono.empty();
|
||||||
|
|
||||||
|
return deductFrom.then(addTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<TransactionAttachment> addAttachment(Long transactionId, String fileName, String contentType, String base64Content) {
|
||||||
|
String uniqueFileName = UUID.randomUUID().toString() + "_" + fileName;
|
||||||
|
String directoryPath = "transactions/" + transactionId;
|
||||||
|
|
||||||
|
return minioServiceClient.uploadFile(directoryPath, contentType, uniqueFileName, base64Content)
|
||||||
|
.flatMap(minioResponse -> {
|
||||||
|
if (minioResponse.isSuccess()) {
|
||||||
|
TransactionAttachment attachment = TransactionAttachment.builder()
|
||||||
|
.transactionId(transactionId)
|
||||||
|
.fileName(fileName)
|
||||||
|
.filePath(minioResponse.getFilePath())
|
||||||
|
.contentType(contentType)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
return transactionAttachmentRepository.save(attachment);
|
||||||
|
} else {
|
||||||
|
return Mono.error(new RuntimeException("Failed to upload to MinIO"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> deleteAttachment(Long attachmentId) {
|
||||||
|
return transactionAttachmentRepository.findById(attachmentId)
|
||||||
|
.flatMap(attachment ->
|
||||||
|
minioServiceClient.deleteFile(attachment.getContentType(), attachment.getFilePath())
|
||||||
|
.then(transactionAttachmentRepository.delete(attachment))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<MinioServiceClient.MinioDownloadResponse> downloadAttachment(Long attachmentId) {
|
||||||
|
return transactionAttachmentRepository.findById(attachmentId)
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("Attachment not found")))
|
||||||
|
.flatMap(attachment ->
|
||||||
|
minioServiceClient.downloadFile(attachment.getContentType(), attachment.getFilePath())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Mono<Transaction> updateTransaction(Long id, Long userId, Transaction updatedTransaction) {
|
||||||
|
return transactionRepository.findById(id)
|
||||||
|
.filter(t -> t.getUserId().equals(userId))
|
||||||
|
.flatMap(t -> {
|
||||||
|
// Revert old balances
|
||||||
|
Mono<Void> revertBalances = updateWalletBalances(t.getToWalletId(), t.getFromWalletId(), t.getAmount());
|
||||||
|
|
||||||
|
// Apply new balances
|
||||||
|
Mono<Void> applyNewBalances = updateWalletBalances(updatedTransaction.getFromWalletId(), updatedTransaction.getToWalletId(), updatedTransaction.getAmount());
|
||||||
|
|
||||||
|
t.setCategoryId(updatedTransaction.getCategoryId());
|
||||||
|
t.setFromWalletId(updatedTransaction.getFromWalletId());
|
||||||
|
t.setToWalletId(updatedTransaction.getToWalletId());
|
||||||
|
t.setNotes(updatedTransaction.getNotes());
|
||||||
|
t.setType(updatedTransaction.getType());
|
||||||
|
t.setAmount(updatedTransaction.getAmount());
|
||||||
|
t.setDate(updatedTransaction.getDate());
|
||||||
|
t.setDescription(updatedTransaction.getDescription());
|
||||||
|
|
||||||
|
return revertBalances.then(applyNewBalances).then(transactionRepository.save(t));
|
||||||
|
}).flatMap(this::populateItemsAndAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Mono<Transaction> closeInvestment(Long id, Long userId, BigDecimal maturityAmount, LocalDate closingDate, Long destinationWalletId) {
|
||||||
|
return transactionRepository.findById(id)
|
||||||
|
.filter(t -> t.getUserId().equals(userId) && "OPEN".equals(t.getInvestmentStatus()))
|
||||||
|
.flatMap(t -> {
|
||||||
|
t.setInvestmentStatus("CLOSED");
|
||||||
|
t.setClosingDate(closingDate);
|
||||||
|
t.setMaturityAmount(maturityAmount);
|
||||||
|
BigDecimal profitLoss = maturityAmount.subtract(t.getAmount());
|
||||||
|
t.setProfitLoss(profitLoss);
|
||||||
|
|
||||||
|
Mono<Void> profitLossProcess = Mono.empty();
|
||||||
|
if (profitLoss.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
Transaction profit = new Transaction();
|
||||||
|
profit.setUserId(userId);
|
||||||
|
profit.setType("INCOME");
|
||||||
|
profit.setAmount(profitLoss);
|
||||||
|
profit.setDate(closingDate);
|
||||||
|
profit.setToWalletId(t.getToWalletId());
|
||||||
|
profit.setDescription("Profit from " + (t.getDescription() != null ? t.getDescription() : "Investment"));
|
||||||
|
profitLossProcess = transactionRepository.save(profit)
|
||||||
|
.flatMap(savedProfit -> updateWalletBalances(null, savedProfit.getToWalletId(), profitLoss));
|
||||||
|
} else if (profitLoss.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
Transaction loss = new Transaction();
|
||||||
|
loss.setUserId(userId);
|
||||||
|
loss.setType("EXPENSE");
|
||||||
|
loss.setAmount(profitLoss.abs());
|
||||||
|
loss.setDate(closingDate);
|
||||||
|
loss.setFromWalletId(t.getToWalletId());
|
||||||
|
loss.setDescription("Loss from " + (t.getDescription() != null ? t.getDescription() : "Investment"));
|
||||||
|
profitLossProcess = transactionRepository.save(loss)
|
||||||
|
.flatMap(savedLoss -> updateWalletBalances(savedLoss.getFromWalletId(), null, profitLoss.abs()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transfer transaction from the investment wallet to the savings wallet
|
||||||
|
if (destinationWalletId != null) {
|
||||||
|
Transaction transfer = new Transaction();
|
||||||
|
transfer.setUserId(userId);
|
||||||
|
transfer.setType("TRANSFER");
|
||||||
|
transfer.setAmount(maturityAmount);
|
||||||
|
transfer.setDate(closingDate);
|
||||||
|
transfer.setFromWalletId(t.getToWalletId()); // The money was in the investment wallet
|
||||||
|
transfer.setToWalletId(destinationWalletId);
|
||||||
|
transfer.setDescription("Closure of " + (t.getDescription() != null ? t.getDescription() : "Investment"));
|
||||||
|
|
||||||
|
return transactionRepository.save(t)
|
||||||
|
.then(profitLossProcess)
|
||||||
|
.then(transactionRepository.save(transfer))
|
||||||
|
.flatMap(savedTransfer -> updateWalletBalances(savedTransfer.getFromWalletId(), savedTransfer.getToWalletId(), maturityAmount))
|
||||||
|
.thenReturn(t);
|
||||||
|
} else {
|
||||||
|
return transactionRepository.save(t).then(profitLossProcess).thenReturn(t);
|
||||||
|
}
|
||||||
|
}).flatMap(this::populateItemsAndAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Mono<Void> deleteTransaction(Long id) {
|
||||||
|
return transactionRepository.findById(id).flatMap(t -> {
|
||||||
|
Mono<Void> revertBalances = updateWalletBalances(t.getToWalletId(), t.getFromWalletId(), t.getAmount());
|
||||||
|
return revertBalances.then(
|
||||||
|
transactionAttachmentRepository.findByTransactionId(id)
|
||||||
|
.flatMap(attachment -> minioServiceClient.deleteFile(attachment.getContentType(), attachment.getFilePath()))
|
||||||
|
.then(transactionRepository.deleteById(id))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
192
kifi-api/src/main/java/com/kifi/api/service/WalletService.java
Normal file
192
kifi-api/src/main/java/com/kifi/api/service/WalletService.java
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package com.kifi.api.service;
|
||||||
|
|
||||||
|
import com.kifi.api.entity.UserWallet;
|
||||||
|
import com.kifi.api.entity.Wallet;
|
||||||
|
import com.kifi.api.entity.WalletInvitation;
|
||||||
|
import com.kifi.api.repository.BudgetRepository;
|
||||||
|
import com.kifi.api.repository.RecurringTransactionRepository;
|
||||||
|
import com.kifi.api.repository.TransactionRepository;
|
||||||
|
import com.kifi.api.repository.UserWalletRepository;
|
||||||
|
import com.kifi.api.repository.WalletInvitationRepository;
|
||||||
|
import com.kifi.api.repository.WalletRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WalletService {
|
||||||
|
private final WalletRepository walletRepository;
|
||||||
|
private final UserWalletRepository userWalletRepository;
|
||||||
|
private final WalletInvitationRepository walletInvitationRepository;
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
private final BudgetRepository budgetRepository;
|
||||||
|
private final RecurringTransactionRepository recurringTransactionRepository;
|
||||||
|
private final EmailService emailService;
|
||||||
|
private final com.kifi.api.repository.UserRepository userRepository;
|
||||||
|
|
||||||
|
public Mono<Wallet> createWallet(Long ownerId, String name, String nature, String icon, String color, String currency, java.math.BigDecimal initialBalance) {
|
||||||
|
Wallet wallet = new Wallet();
|
||||||
|
wallet.setOwnerId(ownerId);
|
||||||
|
wallet.setName(name);
|
||||||
|
wallet.setNature(nature != null ? nature : "CASH");
|
||||||
|
wallet.setBalance(initialBalance != null ? initialBalance : java.math.BigDecimal.ZERO);
|
||||||
|
wallet.setCurrency(currency != null ? currency : "INR");
|
||||||
|
wallet.setIcon(icon);
|
||||||
|
wallet.setColor(color);
|
||||||
|
wallet.setCreatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
return walletRepository.save(wallet)
|
||||||
|
.flatMap(savedWallet -> {
|
||||||
|
UserWallet uw = new UserWallet();
|
||||||
|
uw.setUserId(ownerId);
|
||||||
|
uw.setWalletId(savedWallet.getId());
|
||||||
|
uw.setRole("OWNER");
|
||||||
|
uw.setJoinedAt(LocalDateTime.now());
|
||||||
|
return userWalletRepository.save(uw).thenReturn(savedWallet);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<Wallet> getWalletsForUser(Long userId) {
|
||||||
|
return userWalletRepository.findByUserId(userId)
|
||||||
|
.flatMap(uw -> walletRepository.findById(uw.getWalletId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<UserWallet> inviteUserToWallet(Long walletId, Long userIdToInvite) {
|
||||||
|
UserWallet uw = new UserWallet();
|
||||||
|
uw.setUserId(userIdToInvite);
|
||||||
|
uw.setWalletId(walletId);
|
||||||
|
uw.setRole("MEMBER");
|
||||||
|
uw.setJoinedAt(LocalDateTime.now());
|
||||||
|
return userWalletRepository.save(uw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<WalletInvitation> inviteUserByEmail(Long inviterId, Long walletId, String inviteeEmail) {
|
||||||
|
return walletRepository.findById(walletId)
|
||||||
|
.flatMap(wallet -> {
|
||||||
|
WalletInvitation invitation = new WalletInvitation();
|
||||||
|
invitation.setWalletId(walletId);
|
||||||
|
invitation.setInviterId(inviterId);
|
||||||
|
invitation.setInviteeEmail(inviteeEmail);
|
||||||
|
invitation.setStatus("PENDING");
|
||||||
|
invitation.setCreatedAt(LocalDateTime.now());
|
||||||
|
return walletInvitationRepository.save(invitation)
|
||||||
|
.flatMap(savedInv -> {
|
||||||
|
String emailHtml = emailService.buildInvitationEmailTemplate(
|
||||||
|
"Wallet Invitation 🤝",
|
||||||
|
"You have been invited to join the wallet: " + wallet.getName(),
|
||||||
|
"Please log in to the Kifi app with this email to accept or decline the invitation."
|
||||||
|
);
|
||||||
|
return emailService.sendEmail(inviteeEmail, "Kifi – Wallet Invitation", emailHtml)
|
||||||
|
.thenReturn(savedInv)
|
||||||
|
.onErrorResume(e -> Mono.just(savedInv)); // Return saved invitation even if email fails
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<WalletInvitation> getPendingInvitations(String email) {
|
||||||
|
return walletInvitationRepository.findByInviteeEmailAndStatus(email, "PENDING");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> respondToInvitation(Long invitationId, String email, Long userId, boolean accept) {
|
||||||
|
return walletInvitationRepository.findById(invitationId)
|
||||||
|
.filter(inv -> inv.getInviteeEmail().equalsIgnoreCase(email) && "PENDING".equals(inv.getStatus()))
|
||||||
|
.flatMap(inv -> {
|
||||||
|
inv.setStatus(accept ? "ACCEPTED" : "REJECTED");
|
||||||
|
return walletInvitationRepository.save(inv)
|
||||||
|
.flatMap(savedInv -> {
|
||||||
|
if (accept) {
|
||||||
|
UserWallet uw = new UserWallet();
|
||||||
|
uw.setUserId(userId);
|
||||||
|
uw.setWalletId(savedInv.getWalletId());
|
||||||
|
uw.setRole("MEMBER");
|
||||||
|
uw.setJoinedAt(LocalDateTime.now());
|
||||||
|
return userWalletRepository.save(uw).then();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Wallet> editWallet(Long walletId, Long ownerId, String name, String nature, String icon, String color, String currency) {
|
||||||
|
return walletRepository.findById(walletId)
|
||||||
|
.filter(w -> w.getOwnerId().equals(ownerId))
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("Wallet not found or you are not the owner")))
|
||||||
|
.flatMap(w -> {
|
||||||
|
if (name != null) w.setName(name);
|
||||||
|
if (nature != null) w.setNature(nature);
|
||||||
|
if (icon != null) w.setIcon(icon);
|
||||||
|
if (color != null) w.setColor(color);
|
||||||
|
if (currency != null) w.setCurrency(currency);
|
||||||
|
return walletRepository.save(w);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> deleteWallet(Long walletId, Long ownerId) {
|
||||||
|
return walletRepository.findById(walletId)
|
||||||
|
.filter(w -> w.getOwnerId().equals(ownerId))
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("Wallet not found or you are not the owner")))
|
||||||
|
.flatMap(w -> transactionRepository.countByFromWalletIdOrToWalletId(walletId, walletId)
|
||||||
|
.flatMap(txCount -> recurringTransactionRepository.countByFromWalletIdOrToWalletId(walletId, walletId)
|
||||||
|
.flatMap(rtCount -> {
|
||||||
|
if (txCount > 0 || rtCount > 0) {
|
||||||
|
return Mono.error(new RuntimeException("Cannot delete wallet with transactions"));
|
||||||
|
}
|
||||||
|
return budgetRepository.deleteByWalletId(walletId)
|
||||||
|
.then(walletInvitationRepository.deleteByWalletId(walletId))
|
||||||
|
.then(userWalletRepository.deleteByWalletId(walletId))
|
||||||
|
.then(walletRepository.deleteById(walletId));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<com.kifi.api.dto.WalletMemberDTO> getWalletMembers(Long walletId, Long requestingUserId) {
|
||||||
|
// Ensure requesting user has access to this wallet
|
||||||
|
return userWalletRepository.findByUserId(requestingUserId)
|
||||||
|
.filter(uw -> uw.getWalletId().equals(walletId))
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("You do not have access to this wallet")))
|
||||||
|
.next()
|
||||||
|
.flatMapMany(uw -> userWalletRepository.findByWalletId(walletId)
|
||||||
|
.flatMap(memberUw -> userRepository.findById(memberUw.getUserId())
|
||||||
|
.map(user -> com.kifi.api.dto.WalletMemberDTO.builder()
|
||||||
|
.userId(user.getId())
|
||||||
|
.email(user.getEmail())
|
||||||
|
.role(memberUw.getRole())
|
||||||
|
.joinedAt(memberUw.getJoinedAt())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> removeWalletMember(Long walletId, Long ownerId, Long memberIdToRemove) {
|
||||||
|
return walletRepository.findById(walletId)
|
||||||
|
.filter(w -> w.getOwnerId().equals(ownerId))
|
||||||
|
.switchIfEmpty(Mono.error(new RuntimeException("Wallet not found or you are not the owner")))
|
||||||
|
.flatMap(w -> {
|
||||||
|
if (ownerId.equals(memberIdToRemove)) {
|
||||||
|
return Mono.error(new RuntimeException("Cannot remove the owner of the wallet"));
|
||||||
|
}
|
||||||
|
return userWalletRepository.findByWalletId(walletId)
|
||||||
|
.filter(uw -> uw.getUserId().equals(memberIdToRemove))
|
||||||
|
.next()
|
||||||
|
.flatMap(uw -> userWalletRepository.delete(uw));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private org.springframework.r2dbc.core.DatabaseClient databaseClient;
|
||||||
|
|
||||||
|
public Flux<String> getKnownContacts(Long currentUserId) {
|
||||||
|
String sql = "SELECT DISTINCT u.email FROM users u JOIN user_wallets uw ON u.id = uw.user_id WHERE uw.wallet_id IN (SELECT wallet_id FROM user_wallets WHERE user_id = :userId) AND u.id != :userId";
|
||||||
|
return databaseClient.sql(sql)
|
||||||
|
.bind("userId", currentUserId)
|
||||||
|
.map((row, rowMetadata) -> row.get("email", String.class))
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
kifi-api/src/main/resources/application.yml
Normal file
48
kifi-api/src/main/resources/application.yml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: kifi-api
|
||||||
|
|
||||||
|
r2dbc:
|
||||||
|
url: r2dbc:postgresql://103.125.129.116:5333/kifi
|
||||||
|
username: postgres
|
||||||
|
password: M@triXPostgr3s@6202
|
||||||
|
pool:
|
||||||
|
initial-size: 5
|
||||||
|
max-size: 20
|
||||||
|
|
||||||
|
sql:
|
||||||
|
init:
|
||||||
|
mode: never
|
||||||
|
schema-locations: classpath:schema.sql
|
||||||
|
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: 103.125.129.116
|
||||||
|
port: 7901
|
||||||
|
password: M@triXR3d1s@6202
|
||||||
|
|
||||||
|
mail:
|
||||||
|
host: smtp.gmail.com
|
||||||
|
port: 587
|
||||||
|
username: technobeesolutions@gmail.com
|
||||||
|
password: lrideibfakickldg
|
||||||
|
properties:
|
||||||
|
mail:
|
||||||
|
smtp:
|
||||||
|
auth: true
|
||||||
|
starttls:
|
||||||
|
enable: true
|
||||||
|
|
||||||
|
minio:
|
||||||
|
service:
|
||||||
|
url: http://103.125.129.116:1500
|
||||||
|
bucket: kifi
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
|
||||||
|
expiration: 604800000 # 7 days
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org.springframework.data.r2dbc: DEBUG
|
||||||
|
org.springframework.r2dbc: DEBUG
|
||||||
134
kifi-api/src/main/resources/schema.sql
Normal file
134
kifi-api/src/main/resources/schema.sql
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255),
|
||||||
|
enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS merchants (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
icon_name VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
icon_name VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
category_id INTEGER REFERENCES categories(id),
|
||||||
|
type VARCHAR(50) NOT NULL, -- 'INCOME' or 'EXPENSE' or 'INVESTMENT'
|
||||||
|
amount DECIMAL(10, 2) NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS budgets (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
category_id INTEGER REFERENCES categories(id),
|
||||||
|
monthly_limit DECIMAL(10, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, category_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS recurring_transactions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
category_id INTEGER REFERENCES categories(id),
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
amount DECIMAL(10, 2) NOT NULL,
|
||||||
|
frequency VARCHAR(50) NOT NULL,
|
||||||
|
next_execution_date DATE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
owner_id INTEGER REFERENCES users(id),
|
||||||
|
nature VARCHAR(50) DEFAULT 'CASH', -- 'CASH', 'DEPOSIT', 'EXPENSE', 'INCOME', 'SAVINGS', 'LOAN', etc.
|
||||||
|
balance DECIMAL(15, 2) DEFAULT 0.00,
|
||||||
|
currency VARCHAR(10) DEFAULT 'INR',
|
||||||
|
icon VARCHAR(255),
|
||||||
|
color VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_wallets (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
wallet_id INTEGER REFERENCES wallets(id),
|
||||||
|
role VARCHAR(50) DEFAULT 'MEMBER',
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (user_id, wallet_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS nature VARCHAR(50) DEFAULT 'CASH';
|
||||||
|
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS balance DECIMAL(15, 2) DEFAULT 0.00;
|
||||||
|
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS currency VARCHAR(10) DEFAULT 'INR';
|
||||||
|
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS icon VARCHAR(255);
|
||||||
|
ALTER TABLE wallets ADD COLUMN IF NOT EXISTS color VARCHAR(50);
|
||||||
|
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS wallet_id INTEGER REFERENCES wallets(id);
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS from_wallet_id INTEGER REFERENCES wallets(id);
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS to_wallet_id INTEGER REFERENCES wallets(id);
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS due_date DATE;
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS alert_schedule VARCHAR(50);
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS alert_time TIME;
|
||||||
|
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS investment_status VARCHAR(20) DEFAULT 'OPEN';
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS maturity_amount DECIMAL(10, 2);
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS profit_loss DECIMAL(10, 2);
|
||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS closing_date DATE;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS transaction_items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
amount DECIMAL(10, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS transaction_attachments (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
||||||
|
file_name VARCHAR(255) NOT NULL,
|
||||||
|
file_path VARCHAR(1024) NOT NULL,
|
||||||
|
content_type VARCHAR(100),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE budgets ADD COLUMN IF NOT EXISTS wallet_id INTEGER REFERENCES wallets(id);
|
||||||
|
ALTER TABLE budgets ADD COLUMN IF NOT EXISTS is_shared BOOLEAN DEFAULT FALSE;
|
||||||
|
-- Ensure budget can be either category-specific or wallet-specific
|
||||||
|
ALTER TABLE budgets ALTER COLUMN category_id DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS from_wallet_id INTEGER REFERENCES wallets(id);
|
||||||
|
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS to_wallet_id INTEGER REFERENCES wallets(id);
|
||||||
|
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'ACTIVE';
|
||||||
|
ALTER TABLE recurring_transactions ADD COLUMN IF NOT EXISTS end_date DATE;
|
||||||
|
ALTER TABLE recurring_transactions ALTER COLUMN category_id DROP NOT NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS wallet_invitations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
wallet_id INTEGER REFERENCES wallets(id),
|
||||||
|
inviter_id INTEGER REFERENCES users(id),
|
||||||
|
invitee_email VARCHAR(255) NOT NULL,
|
||||||
|
status VARCHAR(20) DEFAULT 'PENDING',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.kifi.api;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class KifiApiApplicationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
45
kifi-app/.gitignore
vendored
Normal file
45
kifi-app/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
45
kifi-app/.metadata
Normal file
45
kifi-app/.metadata
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "8b872868494e429d94fa06dca855c306438b22c0"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
- platform: android
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
- platform: web
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
16
kifi-app/README.md
Normal file
16
kifi-app/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# kifi_app
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
28
kifi-app/analysis_options.yaml
Normal file
28
kifi-app/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
kifi-app/android/.gitignore
vendored
Normal file
14
kifi-app/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
50
kifi-app/android/app/build.gradle.kts
Normal file
50
kifi-app/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.sarascore.kifi_app"
|
||||||
|
compileSdk = 37
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.sarascore.kifi_app"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
4
kifi-app/android/app/proguard-rules.pro
vendored
Normal file
4
kifi-app/android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-dontwarn com.google.mlkit.vision.text.chinese.**
|
||||||
|
-dontwarn com.google.mlkit.vision.text.devanagari.**
|
||||||
|
-dontwarn com.google.mlkit.vision.text.japanese.**
|
||||||
|
-dontwarn com.google.mlkit.vision.text.korean.**
|
||||||
7
kifi-app/android/app/src/debug/AndroidManifest.xml
Normal file
7
kifi-app/android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
45
kifi-app/android/app/src/main/AndroidManifest.xml
Normal file
45
kifi-app/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="kifi_app"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.sarascore.kifi_app
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
kifi-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
kifi-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
kifi-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
kifi-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
kifi-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
kifi-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
BIN
kifi-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
kifi-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
kifi-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
kifi-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
18
kifi-app/android/app/src/main/res/values-night/styles.xml
Normal file
18
kifi-app/android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
kifi-app/android/app/src/main/res/values/styles.xml
Normal file
18
kifi-app/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
kifi-app/android/app/src/profile/AndroidManifest.xml
Normal file
7
kifi-app/android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
24
kifi-app/android/build.gradle.kts
Normal file
24
kifi-app/android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
2
kifi-app/android/gradle.properties
Normal file
2
kifi-app/android/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
5
kifi-app/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
kifi-app/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||||
26
kifi-app/android/settings.gradle.kts
Normal file
26
kifi-app/android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
BIN
kifi-app/assets/icon.png
Normal file
BIN
kifi-app/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 666 KiB |
4
kifi-app/flutter_launcher_icons.yaml
Normal file
4
kifi-app/flutter_launcher_icons.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
flutter_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/icon.png"
|
||||||
34
kifi-app/ios/.gitignore
vendored
Normal file
34
kifi-app/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
26
kifi-app/ios/Flutter/AppFrameworkInfo.plist
Normal file
26
kifi-app/ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>13.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
2
kifi-app/ios/Flutter/Debug.xcconfig
Normal file
2
kifi-app/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
2
kifi-app/ios/Flutter/Release.xcconfig
Normal file
2
kifi-app/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user