blah
This commit is contained in:
parent
cc3a2c0e52
commit
ff0e97e36b
10
.env.example
10
.env.example
@ -1 +1,9 @@
|
|||||||
DATABASE_USERNAME=
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASS=postgres
|
||||||
|
DB_NAME=khmereid_backend_db
|
||||||
|
DB_SSL_MODE=Disable
|
||||||
|
|
||||||
|
Kyc__EKYC_Solutions__BaseUrl=
|
||||||
|
Kyc__EKYC_Solutions__PrivateKey=
|
||||||
10
Dockerfile
10
Dockerfile
@ -3,17 +3,17 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy the project files
|
# Copy the project files
|
||||||
COPY khmer_eid_backend/*.csproj ./
|
COPY khmereid_backend/*.csproj ./
|
||||||
|
|
||||||
# Restore dependencies
|
# Restore dependencies
|
||||||
RUN dotnet restore
|
RUN dotnet restore
|
||||||
|
|
||||||
# Copy the rest of the application code
|
# Copy the rest of the application code
|
||||||
COPY khmer_eid_backend/ ./
|
COPY khmereid_backend/ ./
|
||||||
|
|
||||||
# Build the application
|
# Build the application
|
||||||
# Change to Release for prod
|
# Change to Release for prod
|
||||||
RUN dotnet publish khmer_eid_backend.csproj -c Debug -o out
|
RUN dotnet publish khmereid_backend.csproj -c Debug -o out
|
||||||
|
|
||||||
# Use the .NET runtime image for running the application
|
# Use the .NET runtime image for running the application
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||||
@ -23,11 +23,11 @@ WORKDIR /app
|
|||||||
COPY --from=build /app/out ./
|
COPY --from=build /app/out ./
|
||||||
|
|
||||||
# Copy Program.cs into the runtime image so you can inspect it
|
# Copy Program.cs into the runtime image so you can inspect it
|
||||||
COPY khmer_eid_backend/Program.cs ./
|
COPY khmereid_backend/Program.cs ./
|
||||||
|
|
||||||
# Install vim
|
# Install vim
|
||||||
RUN apt-get update && apt-get install -y vim && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y vim && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Expose the port and run the application
|
# Expose the port and run the application
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
ENTRYPOINT ["dotnet", "khmer_eid_backend.dll"]
|
ENTRYPOINT ["dotnet", "khmereid_backend.dll"]
|
||||||
|
|||||||
61
Makefile
61
Makefile
@ -1,32 +1,41 @@
|
|||||||
.PHONY: dev-up dev-down prod-up prod-down logs build
|
.PHONY: bootstrap, dev
|
||||||
|
|
||||||
dev-up:
|
bootstrap:
|
||||||
docker compose -f docker-compose.dev.yml up -d --remove-orphans
|
@echo "Creating kind cluster 'khmereid' if it doesn't exist..."
|
||||||
|
@if ! kind get clusters | grep -q '^khmereid$$'; then \
|
||||||
|
kind create cluster --name khmereid; \
|
||||||
|
else \
|
||||||
|
echo "Cluster 'khmereid' already exists, skipping creation"; \
|
||||||
|
fi
|
||||||
|
|
||||||
dev-down:
|
@echo "Applying Kubernetes bootstrap manifests..."
|
||||||
docker compose -f docker-compose.dev.yml down
|
@kubectl apply -k k8s/bootstrap
|
||||||
|
@kubectl apply -f k8s/bootstrap/bootstrap.yml
|
||||||
|
|
||||||
dev-restore:
|
@echo "Waiting for namespace 'envoy-gateway-system' to appear..."
|
||||||
docker compose -f docker-compose.dev.yml run khmer-eid-api dotnet restore
|
@while ! kubectl get namespace envoy-gateway-system >/dev/null 2>&1; do \
|
||||||
|
printf "."; sleep 2; \
|
||||||
|
done; \
|
||||||
|
echo "\nNamespace 'envoy-gateway-system' is ready!"
|
||||||
|
|
||||||
dev-build:
|
@echo "Waiting for all pods in all namespaces to be ready (timeout 10 minutes)..."
|
||||||
docker compose -f docker-compose.dev.yml build
|
@kubectl wait --for=condition=Ready pod --all --all-namespaces --timeout=600s
|
||||||
|
@echo "All pods are ready!"
|
||||||
|
|
||||||
dev-list-packages:
|
dev-migrate:
|
||||||
docker exec -it khmer-eid-api-dev dotnet list package
|
@skaffold run --cleanup=false
|
||||||
|
@sleep 5
|
||||||
dev-exec:
|
@cd khmereid_backend && dotnet ef database update
|
||||||
docker exec -it khmer-eid-api-dev /bin/bash
|
@echo "Waiting for all pods in all namespaces to be ready (timeout 10 minutes)..."
|
||||||
|
@kubectl wait --for=condition=Ready pod --all --all-namespaces --timeout=600s
|
||||||
prod-up:
|
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
|
|
||||||
prod-down:
|
|
||||||
docker compose -f docker-compose.prod.yml down
|
|
||||||
|
|
||||||
dev-logs:
|
|
||||||
docker compose -f docker-compose.dev.yml logs -f khmer-eid-api
|
|
||||||
|
|
||||||
prod-logs:
|
|
||||||
docker compose -f docker-compose.prod.yml logs -f khmer-eid-api
|
|
||||||
|
|
||||||
|
dev:
|
||||||
|
@echo "Running Skaffold to build and deploy once..."
|
||||||
|
@skaffold run --cleanup=false
|
||||||
|
@echo "Starting local development environment..."
|
||||||
|
# Port-forward Kratos
|
||||||
|
@kubectl port-forward svc/kratos-app-public 4433:80 &
|
||||||
|
# Wait a moment for port-forwards to initialize
|
||||||
|
@sleep 5
|
||||||
|
# Run dotnet in watch mode
|
||||||
|
@cd khmereid_backend && dotnet watch run --urls http://localhost:5200
|
||||||
|
|||||||
48
README.md
48
README.md
@ -1,22 +1,42 @@
|
|||||||
### In order to add a new package, run the following commands
|
## Quick Start (Local Development)
|
||||||
1. dotnet add package <PackageName>
|
|
||||||
2. dotnet resotre
|
|
||||||
3. docker compose build
|
|
||||||
4. docker compose up
|
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Make sure you have:
|
||||||
|
|
||||||
### config kratos
|
- [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download)
|
||||||
create schema kratos
|
- [Docker](https://www.docker.com/)
|
||||||
|
- kind ```brew install kind```
|
||||||
|
- skaffold ```brew install skaffold```
|
||||||
|
- PostgreSQL
|
||||||
|
|
||||||
docker exec -it kratos kratos migrate sql \
|
---
|
||||||
"postgres://dev:dev@postgres:5432/backend_db?sslmode=disable&search_path=kratos" \
|
|
||||||
-y
|
|
||||||
|
|
||||||
|
### Clone and set up
|
||||||
|
|
||||||
### config hanko
|
1. Clone the repository and navigate into it:
|
||||||
create schema hanko
|
```bash
|
||||||
|
git clone https://github.com/khmer-eid/khmereid-backend.git
|
||||||
|
cd khmereid-backend
|
||||||
|
```
|
||||||
|
|
||||||
docker compose run --rm hanko migrate up
|
2. Bootstrap the environment:
|
||||||
|
```bash
|
||||||
|
make bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
docker compose up -d hanko
|
3. Set up environment variables:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# update db config part your .env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run database migrations:
|
||||||
|
```bash
|
||||||
|
make dev-migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Start the development server:
|
||||||
|
```bash
|
||||||
|
make dev
|
||||||
|
```
|
||||||
@ -1,5 +1,5 @@
|
|||||||
version: v1.3.0
|
version: v1.3.0
|
||||||
dsn: "postgres://dev:dev@postgres:5432/backend_db?sslmode=disable&search_path=kratos"
|
dsn: ${DSN}
|
||||||
log:
|
log:
|
||||||
leak_sensitive_values: true
|
leak_sensitive_values: true
|
||||||
|
|
||||||
@ -31,14 +31,14 @@ identity:
|
|||||||
default_schema_id: default
|
default_schema_id: default
|
||||||
schemas:
|
schemas:
|
||||||
- id: default
|
- id: default
|
||||||
url: file:///etc/config/identity.schema.json
|
url: file:///etc/config/custom/identity.schema.json
|
||||||
courier:
|
courier:
|
||||||
channels:
|
channels:
|
||||||
- id: sms
|
- id: sms
|
||||||
type: http
|
type: http
|
||||||
request_config:
|
request_config:
|
||||||
method: POST
|
method: POST
|
||||||
url: https://webhook.site/b9e137a6-f184-47b5-ac43-50c95a95cd14
|
url: https://webhook.site/d67a9ea6-e152-4d7f-8432-7ab92b2ace1f
|
||||||
body: base64://ZnVuY3Rpb24oY3R4KSB7CiAgcmVjaXBpZW50OiBjdHgucmVjaXBpZW50LAogIHRlbXBsYXRlX3R5cGU6IGN0eC50ZW1wbGF0ZV90eXBlLAogIHRvOiBpZiAidGVtcGxhdGVfZGF0YSIgaW4gY3R4ICYmICJ0byIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS50byBlbHNlIG51bGwsCiAgcmVjb3ZlcnlfY29kZTogaWYgInRlbXBsYXRlX2RhdGEiIGluIGN0eCAmJiAicmVjb3ZlcnlfY29kZSIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV9jb2RlIGVsc2UgbnVsbCwKICByZWNvdmVyeV91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInJlY292ZXJ5X3VybCIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV91cmwgZWxzZSBudWxsLAogIHZlcmlmaWNhdGlvbl91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl91cmwiIGluIGN0eC50ZW1wbGF0ZV9kYXRhIHRoZW4gY3R4LnRlbXBsYXRlX2RhdGEudmVyaWZpY2F0aW9uX3VybCBlbHNlIG51bGwsCiAgdmVyaWZpY2F0aW9uX2NvZGU6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl9jb2RlIiBpbiBjdHgudGVtcGxhdGVfZGF0YSB0aGVuIGN0eC50ZW1wbGF0ZV9kYXRhLnZlcmlmaWNhdGlvbl9jb2RlIGVsc2UgbnVsbCwKICBzdWJqZWN0OiBjdHguc3ViamVjdCwKICBib2R5OiBjdHguYm9keQp9Cg==
|
body: base64://ZnVuY3Rpb24oY3R4KSB7CiAgcmVjaXBpZW50OiBjdHgucmVjaXBpZW50LAogIHRlbXBsYXRlX3R5cGU6IGN0eC50ZW1wbGF0ZV90eXBlLAogIHRvOiBpZiAidGVtcGxhdGVfZGF0YSIgaW4gY3R4ICYmICJ0byIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS50byBlbHNlIG51bGwsCiAgcmVjb3ZlcnlfY29kZTogaWYgInRlbXBsYXRlX2RhdGEiIGluIGN0eCAmJiAicmVjb3ZlcnlfY29kZSIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV9jb2RlIGVsc2UgbnVsbCwKICByZWNvdmVyeV91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInJlY292ZXJ5X3VybCIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV91cmwgZWxzZSBudWxsLAogIHZlcmlmaWNhdGlvbl91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl91cmwiIGluIGN0eC50ZW1wbGF0ZV9kYXRhIHRoZW4gY3R4LnRlbXBsYXRlX2RhdGEudmVyaWZpY2F0aW9uX3VybCBlbHNlIG51bGwsCiAgdmVyaWZpY2F0aW9uX2NvZGU6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl9jb2RlIiBpbiBjdHgudGVtcGxhdGVfZGF0YSB0aGVuIGN0eC50ZW1wbGF0ZV9kYXRhLnZlcmlmaWNhdGlvbl9jb2RlIGVsc2UgbnVsbCwKICBzdWJqZWN0OiBjdHguc3ViamVjdCwKICBib2R5OiBjdHguYm9keQp9Cg==
|
||||||
headers:
|
headers:
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"log": {
|
|
||||||
"level": "info"
|
|
||||||
},
|
|
||||||
"playground": {
|
|
||||||
"enabled": true,
|
|
||||||
"port": 3000
|
|
||||||
},
|
|
||||||
"datastore": {
|
|
||||||
"engine": "postgres",
|
|
||||||
"uri": "postgres://dev:dev@postgres:5432/backend_db?sslmode=disable&search_path=openfga"
|
|
||||||
},
|
|
||||||
"grpc": {
|
|
||||||
"addr": ":8081"
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"addr": ":8080"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: cnpg-app
|
|
||||||
namespace: argocd
|
|
||||||
spec:
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
syncOptions:
|
|
||||||
- ServerSideApply=true
|
|
||||||
project: default
|
|
||||||
source:
|
|
||||||
chart: cloudnative-pg
|
|
||||||
repoURL: 'https://cloudnative-pg.github.io/charts'
|
|
||||||
targetRevision: 0.26.1
|
|
||||||
destination:
|
|
||||||
namespace: default
|
|
||||||
server: 'https://kubernetes.default.svc'
|
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
# ==============================
|
||||||
|
# Kratos Application
|
||||||
|
# ==============================
|
||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata:
|
metadata:
|
||||||
@ -15,6 +18,10 @@ spec:
|
|||||||
targetRevision: 0.58.0
|
targetRevision: 0.58.0
|
||||||
helm:
|
helm:
|
||||||
valuesObject:
|
valuesObject:
|
||||||
|
statefulSet:
|
||||||
|
extraArgs:
|
||||||
|
- --config
|
||||||
|
- /etc/config/custom/kratos.yaml
|
||||||
deployment:
|
deployment:
|
||||||
extraVolumes:
|
extraVolumes:
|
||||||
- name: kratos-custom-config
|
- name: kratos-custom-config
|
||||||
@ -30,77 +37,17 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: postgres-kratos-app
|
name: postgres-kratos-app
|
||||||
key: uri
|
key: uri
|
||||||
|
extraArgs:
|
||||||
|
- --config
|
||||||
|
- /etc/config/custom/kratos.yaml
|
||||||
kratos:
|
kratos:
|
||||||
# config db
|
log:
|
||||||
|
leak_sensitive_values: true
|
||||||
automigration:
|
automigration:
|
||||||
enabled: true
|
enabled: true
|
||||||
config:
|
|
||||||
dsn:
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgres-kratos-app
|
|
||||||
key: uri
|
|
||||||
selfservice:
|
|
||||||
methods:
|
|
||||||
code:
|
|
||||||
enabled: true
|
|
||||||
passwordless_enabled: true
|
|
||||||
lifespan: 2m
|
|
||||||
password:
|
|
||||||
enabled: true
|
|
||||||
default_browser_return_url: "http://localhost:4433"
|
|
||||||
flows:
|
|
||||||
login:
|
|
||||||
ui_url: "http://localhost:5200/auth/login"
|
|
||||||
registration:
|
|
||||||
after:
|
|
||||||
password:
|
|
||||||
hooks:
|
|
||||||
- hook: session
|
|
||||||
code:
|
|
||||||
hooks:
|
|
||||||
- hook: session
|
|
||||||
ui_url: "http://localhost:5200/auth/register"
|
|
||||||
verification:
|
|
||||||
ui_url: "http://localhost:5200/auth/verify"
|
|
||||||
enabled: true
|
|
||||||
identity:
|
|
||||||
default_schema_id: default
|
|
||||||
schemas:
|
|
||||||
- id: default
|
|
||||||
url: file:///etc/config/custom/identity.schema.json
|
|
||||||
courier:
|
|
||||||
channels:
|
|
||||||
- id: sms
|
|
||||||
type: http
|
|
||||||
request_config:
|
|
||||||
method: POST
|
|
||||||
url: https://webhook.site/b9e137a6-f184-47b5-ac43-50c95a95cd14
|
|
||||||
body: base64://ZnVuY3Rpb24oY3R4KSB7CiAgcmVjaXBpZW50OiBjdHgucmVjaXBpZW50LAogIHRlbXBsYXRlX3R5cGU6IGN0eC50ZW1wbGF0ZV90eXBlLAogIHRvOiBpZiAidGVtcGxhdGVfZGF0YSIgaW4gY3R4ICYmICJ0byIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS50byBlbHNlIG51bGwsCiAgcmVjb3ZlcnlfY29kZTogaWYgInRlbXBsYXRlX2RhdGEiIGluIGN0eCAmJiAicmVjb3ZlcnlfY29kZSIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV9jb2RlIGVsc2UgbnVsbCwKICByZWNvdmVyeV91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInJlY292ZXJ5X3VybCIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV91cmwgZWxzZSBudWxsLAogIHZlcmlmaWNhdGlvbl91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl91cmwiIGluIGN0eC50ZW1wbGF0ZV9kYXRhIHRoZW4gY3R4LnRlbXBsYXRlX2RhdGEudmVyaWZpY2F0aW9uX3VybCBlbHNlIG51bGwsCiAgdmVyaWZpY2F0aW9uX2NvZGU6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl9jb2RlIiBpbiBjdHgudGVtcGxhdGVfZGF0YSB0aGVuIGN0eC50ZW1wbGF0ZV9kYXRhLnZlcmlmaWNhdGlvbl9jb2RlIGVsc2UgbnVsbCwKICBzdWJqZWN0OiBjdHguc3ViamVjdCwKICBib2R5OiBjdHguYm9keQp9Cg==
|
|
||||||
headers:
|
|
||||||
content-type: application/json
|
|
||||||
templates:
|
|
||||||
verification_code:
|
|
||||||
valid:
|
|
||||||
sms:
|
|
||||||
body:
|
|
||||||
plaintext: "base64://WW91ciB2ZXJpZmljYXRpb24gY29kZSBpczoge3sgLlZlcmlmaWNhdGlvbkNvZGUgfX0="
|
|
||||||
email:
|
|
||||||
body:
|
|
||||||
plaintext: "base64://WW91ciB2ZXJpZmljYXRpb24gY29kZSBpczoge3sgLlZlcmlmaWNhdGlvbkNvZGUgfX0="
|
|
||||||
login_code:
|
|
||||||
valid:
|
|
||||||
sms:
|
|
||||||
body:
|
|
||||||
plaintext: "base64://WW91ciBsb2dpbiBjb2RlIGlzOiB7eyAuTG9naW5Db2RlIH19"
|
|
||||||
email:
|
|
||||||
body:
|
|
||||||
plaintext: "base64://WW91ciB2ZXJpZmljYXRpb24gY29kZSBpczoge3sgLlZlcmlmaWNhdGlvbkNvZGUgfX0="
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: shadowlegend/ory-kratos
|
repository: shadowlegend/ory-kratos
|
||||||
tag: master-arm64
|
tag: master-arm64
|
||||||
|
|
||||||
destination:
|
destination:
|
||||||
namespace: default
|
namespace: default
|
||||||
server: 'https://kubernetes.default.svc'
|
server: 'https://kubernetes.default.svc'
|
||||||
@ -1,80 +0,0 @@
|
|||||||
# ==============================
|
|
||||||
# Ory Kratos Deployment + Service
|
|
||||||
# ==============================
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: kratos
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: kratos
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: kratos
|
|
||||||
spec:
|
|
||||||
volumes:
|
|
||||||
- name: kratos-config
|
|
||||||
configMap:
|
|
||||||
name: kratos-config
|
|
||||||
initContainers:
|
|
||||||
- name: kratos-migrate
|
|
||||||
image: shadowlegend/ory-kratos@sha256:1e2a07fa1406c90eb2a16df5da509d163db394262c1a91baf73cc32009dcccd6
|
|
||||||
env:
|
|
||||||
- name: DSN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgres-kratos-app
|
|
||||||
key: uri
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- kratos migrate sql -e --yes
|
|
||||||
containers:
|
|
||||||
- name: kratos
|
|
||||||
image: shadowlegend/ory-kratos@sha256:1e2a07fa1406c90eb2a16df5da509d163db394262c1a91baf73cc32009dcccd6
|
|
||||||
env:
|
|
||||||
- name: DSN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: postgres-kratos-app
|
|
||||||
key: uri
|
|
||||||
- name: KRATOS_LOG_LEVEL
|
|
||||||
value: "info"
|
|
||||||
command:
|
|
||||||
- kratos
|
|
||||||
- serve
|
|
||||||
- --watch-courier
|
|
||||||
- --config
|
|
||||||
- /etc/config/kratos.yml
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: "128Mi"
|
|
||||||
cpu: "500m"
|
|
||||||
requests:
|
|
||||||
memory: 64Mi
|
|
||||||
cpu: 20m
|
|
||||||
ports:
|
|
||||||
- containerPort: 4433
|
|
||||||
- containerPort: 4434
|
|
||||||
volumeMounts:
|
|
||||||
- name: kratos-config
|
|
||||||
mountPath: /etc/config
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: kratos
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: kratos
|
|
||||||
ports:
|
|
||||||
- name: public
|
|
||||||
port: 4433
|
|
||||||
targetPort: 4433
|
|
||||||
- name: admin
|
|
||||||
port: 4434
|
|
||||||
targetPort: 4434
|
|
||||||
@ -3,20 +3,16 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
|||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- app-kratos.yml
|
|
||||||
- app-cnpg.yml
|
|
||||||
- app-envoy.yml
|
|
||||||
- envoy.yml
|
- envoy.yml
|
||||||
# - kratos.yml
|
# - deployment.yml
|
||||||
|
- app-kratos.yml
|
||||||
- postgres-dotnet.yml
|
- postgres-dotnet.yml
|
||||||
- postgres-kratos.yml
|
- postgres-kratos.yml
|
||||||
- deployment.yml
|
|
||||||
# - app-khmereid.yml
|
|
||||||
|
|
||||||
configMapGenerator:
|
configMapGenerator:
|
||||||
- name: kratos-config
|
- name: kratos-config
|
||||||
files:
|
files:
|
||||||
- kratos.yml=../../config/kratos/kratos.yml
|
- kratos.yaml=../../config/kratos/kratos.yml
|
||||||
- identity.schema.json=../../config/kratos/identity.schema.json
|
- identity.schema.json=../../config/kratos/identity.schema.json
|
||||||
generatorOptions:
|
generatorOptions:
|
||||||
disableNameSuffixHash: true
|
disableNameSuffixHash: true
|
||||||
|
|||||||
@ -2,7 +2,7 @@ apiVersion: postgresql.cnpg.io/v1
|
|||||||
kind: Cluster
|
kind: Cluster
|
||||||
metadata:
|
metadata:
|
||||||
name: postgres-dotnet
|
name: postgres-dotnet
|
||||||
namespace: argocd
|
namespace: default
|
||||||
annotations:
|
annotations:
|
||||||
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
|
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@ -3,7 +3,6 @@ kind: Cluster
|
|||||||
metadata:
|
metadata:
|
||||||
name: postgres-kratos
|
name: postgres-kratos
|
||||||
namespace: default
|
namespace: default
|
||||||
# namespace: argocd
|
|
||||||
annotations:
|
annotations:
|
||||||
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
|
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@ -1,5 +1,26 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: cnpg-app
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
enabled: true
|
||||||
|
syncOptions:
|
||||||
|
- ServerSideApply=true
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
chart: cloudnative-pg
|
||||||
|
repoURL: 'https://cloudnative-pg.github.io/charts'
|
||||||
|
targetRevision: 0.26.1
|
||||||
|
destination:
|
||||||
|
namespace: default
|
||||||
|
server: 'https://kubernetes.default.svc'
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
metadata:
|
metadata:
|
||||||
name: envoy-app
|
name: envoy-app
|
||||||
namespace: argocd
|
namespace: argocd
|
||||||
@ -22,6 +43,5 @@ spec:
|
|||||||
agent:
|
agent:
|
||||||
enabled: true
|
enabled: true
|
||||||
destination:
|
destination:
|
||||||
# namespace: default
|
|
||||||
namespace: envoy-gateway-system
|
namespace: envoy-gateway-system
|
||||||
server: 'https://kubernetes.default.svc'
|
server: 'https://kubernetes.default.svc'
|
||||||
17
k8s/bootstrap/kustomization.yml
Normal file
17
k8s/bootstrap/kustomization.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: argocd
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespaces.yml
|
||||||
|
- https://raw.githubusercontent.com/argoproj/argo-cd/v3.1.9/manifests/install.yaml
|
||||||
|
|
||||||
|
patches:
|
||||||
|
- patch: |-
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: argocd-cm
|
||||||
|
data:
|
||||||
|
kustomize.buildOptions: --enable-helm --load-restrictor LoadRestrictionsNone
|
||||||
4
k8s/bootstrap/namespaces.yml
Normal file
4
k8s/bootstrap/namespaces.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: argocd
|
||||||
@ -3,15 +3,3 @@ kind: Kustomization
|
|||||||
resources:
|
resources:
|
||||||
- ../base
|
- ../base
|
||||||
- nginx-temp.yml
|
- nginx-temp.yml
|
||||||
|
|
||||||
patches:
|
|
||||||
- target:
|
|
||||||
kind: Deployment
|
|
||||||
name: argocd-repo-server
|
|
||||||
namespace: argocd
|
|
||||||
patch: |-
|
|
||||||
- op: add
|
|
||||||
path: /spec/template/spec/containers/0/env/-
|
|
||||||
value:
|
|
||||||
name: ARGOCD_APP_SOURCE_PATH_RESTRICTION
|
|
||||||
value: repo
|
|
||||||
18
k8s/uat/app.yml
Normal file
18
k8s/uat/app.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: khmereid-app
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
enabled: true
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: 'https://gitea.internal.ekycsolutions.com/xadminx/khmereid-backend-temp.git'
|
||||||
|
path: k8s/uat
|
||||||
|
targetRevision: main
|
||||||
|
destination:
|
||||||
|
namespace: default
|
||||||
|
server: 'https://kubernetes.default.svc'
|
||||||
@ -2,3 +2,16 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
|||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- ../base
|
- ../base
|
||||||
|
|
||||||
|
# secretGenerator:
|
||||||
|
# - name: crime-management-system-deployment-repo
|
||||||
|
# namespace: argocd
|
||||||
|
# envs:
|
||||||
|
# - .env.cms-deploy-repo-cred
|
||||||
|
# literals:
|
||||||
|
# - type=git
|
||||||
|
# - project=default
|
||||||
|
# - url=https://github.com/EKYCSolutions/crime-management-system.git
|
||||||
|
# options:
|
||||||
|
# labels:
|
||||||
|
# argocd.argoproj.io/secret-type: repository
|
||||||
@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.5.2.0
|
VisualStudioVersion = 17.5.2.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "khmer_eid_backend", "khmer_eid_backend\khmer_eid_backend.csproj", "{6D4F6FF4-86EE-BD7D-7691-D7FD0BC6AFAB}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "khmereid_backend", "khmereid_backend\khmereid_backend.csproj", "{6D4F6FF4-86EE-BD7D-7691-D7FD0BC6AFAB}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using khmer_eid_backend.Integrations.Ory;
|
|
||||||
using khmer_eid_backend.Requests;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace khmer_eid_backend.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("auth")]
|
|
||||||
public class AuthController(KratosIntegration _kratos) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpPost("request-signup-otp")]
|
|
||||||
public async Task<IActionResult> RequestSignupOtp([FromForm] SignupRequest request)
|
|
||||||
{
|
|
||||||
var data = await _kratos.CreateOtpRegistrationFlowAsync(phone: request.Phone);
|
|
||||||
return Ok(new { Message = "OTP sent if the phone number is valid.", data });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("verify-signup-otp")]
|
|
||||||
public async Task<IActionResult> VerifySignupOtp([FromForm] string phone,[FromForm] string otp, [FromForm] string flowId)
|
|
||||||
{
|
|
||||||
var data = await _kratos.CompleteOtpRegistrationFlowAsync(flowId, phone, otp);
|
|
||||||
return Ok(new { Message = "OTP verified successfully.", data });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("request-login-otp")]
|
|
||||||
public async Task<IActionResult> RequestLoginOtp([FromForm] string phone)
|
|
||||||
{
|
|
||||||
var data = await _kratos.CreateOtpLoginFlowAsync(phone: phone);
|
|
||||||
return Ok(new { Message = "OTP sent if the phone number is valid.", data });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("verify-login-otp")]
|
|
||||||
public async Task<IActionResult> VerifyLoginOtp([FromForm] string phone, [FromForm] string otp, [FromForm] string flowId)
|
|
||||||
{
|
|
||||||
var data = await _kratos.CompleteOtpLoginFlowAsync(flowId, phone, otp);
|
|
||||||
return Ok(new { Message = "OTP verified successfully.", data });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("logout")]
|
|
||||||
[Authorize(AuthenticationSchemes = "Kratos")]
|
|
||||||
public async Task<IActionResult> Logout()
|
|
||||||
{
|
|
||||||
var data = await _kratos.Logout(Request.Headers.Authorization.ToString().Replace("Bearer ",""));
|
|
||||||
return Ok(new { Message = "Logged out successfully."});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using khmer_eid_backend.Integrations.Ory;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace khmer_eid_backend.Controllers;
|
|
||||||
|
|
||||||
public class UserController(KratosIntegration _kratos) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet("me")]
|
|
||||||
[Authorize(AuthenticationSchemes = "Kratos")]
|
|
||||||
// public Task<IActionResult> Me()
|
|
||||||
// {
|
|
||||||
// var id = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
||||||
// var phone = User.FindFirstValue("phone");
|
|
||||||
// // return Ok(new { id, phone });
|
|
||||||
// return Task.FromResult<IActionResult>(Ok(new { id, phone }));
|
|
||||||
// }
|
|
||||||
public async Task<IActionResult> Me()
|
|
||||||
{
|
|
||||||
var data = await _kratos.GetMe(Request.Headers.Authorization.ToString().Replace("Bearer ",""));
|
|
||||||
return Ok(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
using Ory.Kratos.Client.Api;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using Ory.Kratos.Client.Client;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
|
|
||||||
namespace khmer_eid_backend.Integrations.Ory;
|
|
||||||
|
|
||||||
public class KratosHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
|
||||||
{
|
|
||||||
private readonly FrontendApi _frontendApi;
|
|
||||||
|
|
||||||
public KratosHandler(
|
|
||||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
|
||||||
ILoggerFactory logger,
|
|
||||||
UrlEncoder encoder,
|
|
||||||
IConfiguration config)
|
|
||||||
: base(options, logger, encoder)
|
|
||||||
{
|
|
||||||
var cfg = new Configuration { BasePath = config["Ory:Kratos:PublicUrl"]! };
|
|
||||||
_frontendApi = new FrontendApi(cfg);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
||||||
{
|
|
||||||
var authHeader = Request.Headers["Authorization"].ToString();
|
|
||||||
if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer "))
|
|
||||||
return AuthenticateResult.NoResult();
|
|
||||||
|
|
||||||
var token = authHeader.Substring("Bearer ".Length).Trim();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var session = await _frontendApi.ToSessionAsync(xSessionToken: token);
|
|
||||||
|
|
||||||
var identity = new ClaimsIdentity(new[]
|
|
||||||
{
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, session.Identity.Id),
|
|
||||||
new Claim("phone", session.Identity.Traits.ToString() ?? "")
|
|
||||||
}, Scheme.Name);
|
|
||||||
|
|
||||||
var principal = new ClaimsPrincipal(identity);
|
|
||||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Failed to authenticate Kratos session");
|
|
||||||
return AuthenticateResult.Fail("Invalid Kratos session token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Ory.Kratos.Client.Api;
|
|
||||||
using Ory.Kratos.Client.Model;
|
|
||||||
using Ory.Kratos.Client.Client;
|
|
||||||
|
|
||||||
namespace khmer_eid_backend.Integrations.Ory;
|
|
||||||
|
|
||||||
public class KratosIntegration
|
|
||||||
{
|
|
||||||
private readonly FrontendApi _frontendApi;
|
|
||||||
private readonly IdentityApi _identityApi;
|
|
||||||
private readonly ILogger<KratosIntegration> _logger;
|
|
||||||
|
|
||||||
public KratosIntegration(IConfiguration config, ILogger<KratosIntegration> logger)
|
|
||||||
{
|
|
||||||
var publicCfg = new Configuration { BasePath = config["Ory:Kratos:PublicUrl"]! };
|
|
||||||
var adminCfg = new Configuration { BasePath = config["Ory:Kratos:AdminUrl"]! };
|
|
||||||
|
|
||||||
_frontendApi = new FrontendApi(publicCfg);
|
|
||||||
_identityApi = new IdentityApi(adminCfg);
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<dynamic> CreateOtpRegistrationFlowAsync(string phone)
|
|
||||||
{
|
|
||||||
var flow = await _frontendApi.CreateNativeRegistrationFlowAsync(returnSessionTokenExchangeCode: true);
|
|
||||||
_logger.LogInformation("Started registration flow: {FlowId}", flow.Id);
|
|
||||||
|
|
||||||
phone = phone.Trim();
|
|
||||||
var method = new KratosUpdateRegistrationFlowWithCodeMethod(
|
|
||||||
traits: new { phone = phone },
|
|
||||||
method: "code"
|
|
||||||
);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var body = new KratosUpdateRegistrationFlowBody(method);
|
|
||||||
_logger.LogInformation("Updating OTP registration flow for {Phone}, flow {FlowId}", phone, flow.Id);
|
|
||||||
var updatedFlow = _frontendApi.UpdateRegistrationFlow(flow.Id, body);
|
|
||||||
|
|
||||||
return flow;
|
|
||||||
}
|
|
||||||
catch (ApiException ex)
|
|
||||||
{
|
|
||||||
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent.ToString()!);
|
|
||||||
return ex.ErrorCode switch
|
|
||||||
{
|
|
||||||
400 => res.GetProperty("state").ToString() == "sent_email" ?
|
|
||||||
new
|
|
||||||
{
|
|
||||||
FlowId = res.GetProperty("id").GetString(),
|
|
||||||
State = res.GetProperty("state").GetString(),
|
|
||||||
ExpiredAt = res.GetProperty("expires_at").GetDateTime()
|
|
||||||
} : throw new Exception("Unhandled Kratos API exception: " + ex.Message),
|
|
||||||
_ => throw new Exception("Unhandled Kratos API exception: " + ex.Message),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<dynamic> CompleteOtpRegistrationFlowAsync(string flowId, string phone,string otp)
|
|
||||||
{
|
|
||||||
phone = phone.Trim();
|
|
||||||
var method = new KratosUpdateRegistrationFlowWithCodeMethod(
|
|
||||||
code: otp,
|
|
||||||
traits: new { phone = phone},
|
|
||||||
method: "code"
|
|
||||||
);
|
|
||||||
var body = new KratosUpdateRegistrationFlowBody(method);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body);
|
|
||||||
|
|
||||||
_logger.LogInformation("Completed OTP registration flow for flow {FlowId}", flowId);
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
accessToken = result.SessionToken,
|
|
||||||
expiredAt = result.Session?.ExpiresAt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (ApiException ex)
|
|
||||||
{
|
|
||||||
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent.ToString()!);
|
|
||||||
return ex.ErrorCode switch
|
|
||||||
{
|
|
||||||
400 => res.GetProperty("ui").GetProperty("messages").EnumerateArray().First().GetProperty("text").GetString()!,
|
|
||||||
404 => throw new Exception("Registration flow not found."),
|
|
||||||
_ => throw new Exception("Unhandled Kratos API exception: " + ex.Message),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<dynamic> CreateOtpLoginFlowAsync(string phone)
|
|
||||||
{
|
|
||||||
var flow = await _frontendApi.CreateNativeLoginFlowAsync(returnSessionTokenExchangeCode: true);
|
|
||||||
_logger.LogInformation("Started login flow: {FlowId}", flow.Id);
|
|
||||||
|
|
||||||
phone = phone.Trim();
|
|
||||||
var method = new KratosUpdateLoginFlowWithCodeMethod(
|
|
||||||
method: "code",
|
|
||||||
csrfToken: "dfdfdfde",
|
|
||||||
identifier: phone
|
|
||||||
);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var body = new KratosUpdateLoginFlowBody(method);
|
|
||||||
_logger.LogInformation("Updating OTP registration flow for {Phone}, flow {FlowId}", phone, flow.Id);
|
|
||||||
var updatedFlow = await _frontendApi.UpdateLoginFlowAsync(flow.Id, body);
|
|
||||||
|
|
||||||
return flow;
|
|
||||||
}
|
|
||||||
catch (ApiException ex)
|
|
||||||
{
|
|
||||||
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent.ToString()!);
|
|
||||||
return ex.ErrorCode switch
|
|
||||||
{
|
|
||||||
400 => res.GetProperty("state").ToString() == "sent_email" ?
|
|
||||||
new
|
|
||||||
{
|
|
||||||
FlowId = res.GetProperty("id").GetString(),
|
|
||||||
State = res.GetProperty("state").GetString(),
|
|
||||||
ExpiredAt = res.GetProperty("expires_at").GetDateTime()
|
|
||||||
} : throw new Exception("Unhandled Kratos API exception: " + ex.Message),
|
|
||||||
_ => throw new Exception("Unhandled Kratos API exception: " + ex.Message),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<dynamic> CompleteOtpLoginFlowAsync(string flowId, string phone, string otp)
|
|
||||||
{
|
|
||||||
phone = "+" + phone.Trim();
|
|
||||||
var method = new KratosUpdateLoginFlowWithCodeMethod(
|
|
||||||
code: otp,
|
|
||||||
method: "code",
|
|
||||||
csrfToken: "dfdfdfde",
|
|
||||||
identifier: phone
|
|
||||||
);
|
|
||||||
var body = new KratosUpdateLoginFlowBody(method);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _frontendApi.UpdateLoginFlowAsync(flowId, body);
|
|
||||||
|
|
||||||
_logger.LogInformation("Completed OTP login flow for flow {FlowId}", flowId);
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
accessToken = result.SessionToken,
|
|
||||||
expiredAt = result.Session?.ExpiresAt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (ApiException ex)
|
|
||||||
{
|
|
||||||
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent.ToString()!);
|
|
||||||
return ex.ErrorCode switch
|
|
||||||
{
|
|
||||||
400 => res,
|
|
||||||
404 => throw new Exception("Login flow not found."),
|
|
||||||
_ => throw new Exception("Unhandled Kratos API exception: " + ex.Message),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<dynamic> Logout(string sessionToken)
|
|
||||||
{
|
|
||||||
var body = new KratosPerformNativeLogoutBody(sessionToken: sessionToken);
|
|
||||||
await _frontendApi.PerformNativeLogoutAsync(body);
|
|
||||||
_logger.LogInformation("Logged out session with token {SessionToken}", sessionToken);
|
|
||||||
return new { Message = "Logged out successfully." };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<dynamic> GetMe(string sessionToken)
|
|
||||||
{
|
|
||||||
var session = await _frontendApi.ToSessionAsync(xSessionToken: sessionToken);
|
|
||||||
_logger.LogInformation("Fetched session for identity {IdentityId}", session.Identity.Id);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<KratosSuccessfulNativeRegistration> PasswordRegistrationFlowAsync(string flowId, string phone)
|
|
||||||
{
|
|
||||||
var method = new KratosUpdateRegistrationFlowWithPasswordMethod(
|
|
||||||
password: "add3ae4d8ae8",
|
|
||||||
traits: new { phone = phone, identifier = phone },
|
|
||||||
method: "password"
|
|
||||||
);
|
|
||||||
var body = new KratosUpdateRegistrationFlowBody(method);
|
|
||||||
var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body);
|
|
||||||
|
|
||||||
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
|
|
||||||
_logger.LogInformation("Completed registration flow for {Phone}", phone);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
//todo obsolete
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
|
|
||||||
namespace khmer_eid_backend.Integrations.Ory;
|
|
||||||
|
|
||||||
public sealed class OryAuthSchemeOptions : AuthenticationSchemeOptions
|
|
||||||
{
|
|
||||||
public string? BasePath { get; set; }
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// // validates token & attaches user info
|
|
||||||
|
|
||||||
// public class KratosAuthMiddleware
|
|
||||||
// {
|
|
||||||
// private readonly RequestDelegate _next;
|
|
||||||
|
|
||||||
// public KratosAuthMiddleware(RequestDelegate next) => _next = next;
|
|
||||||
|
|
||||||
// public async Task InvokeAsync(HttpContext context, KratosIntegration kratos)
|
|
||||||
// {
|
|
||||||
// var cookie = context.Request.Headers["Cookie"].ToString();
|
|
||||||
// var bearer = context.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
|
|
||||||
|
|
||||||
// var session = await kratos.ValidateSessionAsync(cookie, bearer);
|
|
||||||
|
|
||||||
// if (session == null)
|
|
||||||
// {
|
|
||||||
// context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
||||||
// await context.Response.WriteAsync("Unauthorized");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // attach identity to HttpContext
|
|
||||||
// context.Items["user"] = session.Identity;
|
|
||||||
// await _next(context);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
// Data models that map Kratos JSON responses
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace khmer_eid_backend.Models
|
|
||||||
{
|
|
||||||
// Represents user traits stored in Kratos
|
|
||||||
public class KratosTraits
|
|
||||||
{
|
|
||||||
public string Phone { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Represents an identity in Kratos
|
|
||||||
public class KratosIdentity
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
public KratosTraits Traits { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Represents a session returned by Kratos
|
|
||||||
public class KratosSession
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
public KratosIdentity Identity { get; set; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
public class UserProfile
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string KratosId { get; set; } = ""; // link to Kratos user
|
|
||||||
public string FullName { get; set; } = "";
|
|
||||||
public string NationalId { get; set; } = "";
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
using khmer_eid_backend.Integrations.Ory;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<KratosIntegration>();
|
|
||||||
|
|
||||||
builder.Services.AddAuthentication("Kratos")
|
|
||||||
.AddScheme<AuthenticationSchemeOptions, KratosHandler>("Kratos", _ => { });
|
|
||||||
|
|
||||||
builder.Services.AddAuthorization();
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
app.MapControllers();
|
|
||||||
app.Run();
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
namespace khmer_eid_backend.Resources;
|
|
||||||
|
|
||||||
public class SignupResource
|
|
||||||
{
|
|
||||||
public string Phone { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Debug",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AppSettings": {
|
|
||||||
"ApplicationName": "Khmer eID Backend - Development"
|
|
||||||
},
|
|
||||||
"Ory": {
|
|
||||||
"Kratos": {
|
|
||||||
"PublicUrl": "http://localhost:4433",
|
|
||||||
"AdminUrl": "http://localhost:4434"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Consul": {
|
|
||||||
"Address": "http://consul:8500"
|
|
||||||
},
|
|
||||||
"Grpc": {
|
|
||||||
"GDIOnline": {
|
|
||||||
"Address": "https://gdi.dev.ekycsolutions.com"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Kyc": {
|
|
||||||
"BaseUrl": "https://uat.ekycsolutions.com",
|
|
||||||
"EKYC_SOLUTION_PRIVATE_KEY": "d4e5840f352d448a49f296e2344cffd406535969e5b7af84b8cb867dc6ab8258"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Consul" Version="1.7.14.9" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
|
|
||||||
<PackageReference Include="Ory.Kratos.Client" Version="1.3.8" />
|
|
||||||
<Protobuf Include="Protos\gdi_online\user.proto" GrpcServices="Client" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
@khmer_eid_backend_HostAddress = http://localhost:5103
|
|
||||||
|
|
||||||
GET {{khmer_eid_backend_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
62
khmereid_backend/Controllers/AuthController.cs
Normal file
62
khmereid_backend/Controllers/AuthController.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using khmereid_backend.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using khmereid_backend.Services;
|
||||||
|
using khmereid_backend.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("auth")]
|
||||||
|
public class AuthController(AuthService _authService) : ControllerBase
|
||||||
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("request-signup-otp")]
|
||||||
|
public async Task<IActionResult> RequestSignupOtp([FromForm] SignupRequest request)
|
||||||
|
{
|
||||||
|
var response = await _authService.StartRegistrationAsync(request.Phone);
|
||||||
|
|
||||||
|
return this.ToActionResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("verify-signup-otp")]
|
||||||
|
public async Task<IActionResult> VerifySignupOtp([FromForm] VerifyOtpRequest request)
|
||||||
|
{
|
||||||
|
var response = await _authService.CompleteRegistrationAsync(request.FlowId, request.Phone, request.Otp);
|
||||||
|
|
||||||
|
return this.ToActionResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("request-login-otp")]
|
||||||
|
public async Task<IActionResult> RequestLoginOtp([FromForm] LoginRequest request)
|
||||||
|
{
|
||||||
|
var response = await _authService.StartLoginAsync(request.Phone);
|
||||||
|
return this.ToActionResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("verify-login-otp")]
|
||||||
|
public async Task<IActionResult> VerifyLoginOtp([FromForm] VerifyOtpRequest request)
|
||||||
|
{
|
||||||
|
var response = await _authService.CompleteLoginAsync(request.FlowId, request.Phone, request.Otp);
|
||||||
|
|
||||||
|
return this.ToActionResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("logout")]
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||||
|
var response = await _authService.LogoutAsync(token);
|
||||||
|
return this.ToActionResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("options")]
|
||||||
|
public IActionResult GetAuthOptions()
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<string>.Ok(null, "Not implemented."));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
khmereid_backend/Controllers/UserController.cs
Normal file
17
khmereid_backend/Controllers/UserController.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using khmereid_backend.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using khmereid_backend.Services;
|
||||||
|
using khmereid_backend.Extensions;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Controllers;
|
||||||
|
|
||||||
|
public class UserController(AuthService _authService) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("me")]
|
||||||
|
public IActionResult Me()
|
||||||
|
{
|
||||||
|
var user = HttpContext.GetUser();
|
||||||
|
|
||||||
|
return this.ToActionResult(ApiResponse<UserDto>.Ok(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
10
khmereid_backend/Data/AppDbContext.cs
Normal file
10
khmereid_backend/Data/AppDbContext.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using khmereid_backend.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Data
|
||||||
|
{
|
||||||
|
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
29
khmereid_backend/Dtos/ApiResponse.cs
Normal file
29
khmereid_backend/Dtos/ApiResponse.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
public class ApiResponse<T>
|
||||||
|
{
|
||||||
|
public bool Success { get; set; } = true;
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public T? Data { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
|
||||||
|
// public static ApiResponse<T> Ok(T? data = default, string? message = null)
|
||||||
|
// => new() { Success = true, Data = data, Message = message};
|
||||||
|
|
||||||
|
// public static ApiResponse<T> Fail(string error, string? message = null, string? code = null)
|
||||||
|
// => new() { Success = false, Error = error, Message = message, Code = code };
|
||||||
|
|
||||||
|
public static ApiResponse<T> Ok(string message)
|
||||||
|
=> new() { Success = true, Message = message };
|
||||||
|
public static ApiResponse<T> Ok(T? data = default, string? message = null)
|
||||||
|
=> new() { Success = true, Data = data, Message = message };
|
||||||
|
|
||||||
|
public static ApiResponse<T> Fail(string error)
|
||||||
|
=> new() { Success = false, Error = error };
|
||||||
|
|
||||||
|
public static ApiResponse<T> Fail(string error, string code)
|
||||||
|
=> new() { Success = false, Error = error, Code = code };
|
||||||
|
|
||||||
|
public static ApiResponse<T> Fail(string error, string? message, string? code = null)
|
||||||
|
=> new() { Success = false, Error = error, Message = message, Code = code };
|
||||||
|
}
|
||||||
10
khmereid_backend/Dtos/FlowDto.cs
Normal file
10
khmereid_backend/Dtos/FlowDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
public class FlowDto
|
||||||
|
{
|
||||||
|
public string FlowId { get; set; } = null!;
|
||||||
|
public string State { get; set; } = null!;
|
||||||
|
public DateTimeOffset? ExpiresAt { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
10
khmereid_backend/Dtos/LoginRequest.cs
Normal file
10
khmereid_backend/Dtos/LoginRequest.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "Phone number is required")]
|
||||||
|
[Phone(ErrorMessage = "Invalid phone number format")]
|
||||||
|
public string Phone { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
13
khmereid_backend/Dtos/LoginResultDto.cs
Normal file
13
khmereid_backend/Dtos/LoginResultDto.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
public class LoginResultDto
|
||||||
|
{
|
||||||
|
// [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||||
|
public Guid? IdentityId { get; set; }
|
||||||
|
public string? AccessToken { get; set; }
|
||||||
|
public DateTime? ExpiredAt { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public string? UseFlowId { get; set; }
|
||||||
|
}
|
||||||
11
khmereid_backend/Dtos/SessionDto.cs
Normal file
11
khmereid_backend/Dtos/SessionDto.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
|
||||||
|
public class SessionDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = null!;
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
// public string? Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public DateTimeOffset? ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
namespace khmer_eid_backend.Requests;
|
|
||||||
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
|
||||||
public class SignupRequest
|
public class SignupRequest
|
||||||
{
|
{
|
||||||
[Required]
|
[Required(ErrorMessage = "Phone number is required")]
|
||||||
[RegularExpression(@"^\+855\d{8,9}$", ErrorMessage = "Phone number must be in the format +855XXXXXXXX or +855XXXXXXXXX")]
|
[RegularExpression(@"^\+855\d{8,9}$", ErrorMessage = "Phone number must be in the format +855XXXXXXXX or +855XXXXXXXXX")]
|
||||||
public string Phone { get; set; } = default!;
|
public string Phone { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
13
khmereid_backend/Dtos/SignupResultDto.cs
Normal file
13
khmereid_backend/Dtos/SignupResultDto.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
public class SignupResultDto
|
||||||
|
{
|
||||||
|
// [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||||
|
public Guid? IdentityId { get; set; }
|
||||||
|
public string? AccessToken { get; set; }
|
||||||
|
public DateTime? ExpiredAt { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public string? UseFlowId { get; set; }
|
||||||
|
}
|
||||||
10
khmereid_backend/Dtos/UserDto.cs
Normal file
10
khmereid_backend/Dtos/UserDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace khmereid_backend.Dtos
|
||||||
|
{
|
||||||
|
public class UserDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid IdentityId { get; set; } = Guid.Empty;
|
||||||
|
public string Phone { get; set; } = default!;
|
||||||
|
public string NationalId { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
khmereid_backend/Dtos/VerifyOtpRequest.cs
Normal file
10
khmereid_backend/Dtos/VerifyOtpRequest.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Dtos;
|
||||||
|
|
||||||
|
public class VerifyOtpRequest
|
||||||
|
{
|
||||||
|
[Required] public string Phone { get; set; } = string.Empty;
|
||||||
|
[Required] public string Otp { get; set; } = string.Empty;
|
||||||
|
[Required] public string FlowId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
20
khmereid_backend/Extensions/ControllerExtensions.cs
Normal file
20
khmereid_backend/Extensions/ControllerExtensions.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using khmereid_backend.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Extensions;
|
||||||
|
public static class ControllerExtensions
|
||||||
|
{
|
||||||
|
public static IActionResult ToActionResult<T>(this ControllerBase controller, ApiResponse<T> response)
|
||||||
|
{
|
||||||
|
if (response == null)
|
||||||
|
return controller.StatusCode(500, new { success = false, error = "Internal error: response is null" });
|
||||||
|
|
||||||
|
return response.Success switch
|
||||||
|
{
|
||||||
|
true => controller.Ok(response),
|
||||||
|
false when response.Code?.Contains("not found", StringComparison.OrdinalIgnoreCase) == true =>
|
||||||
|
controller.NotFound(response),
|
||||||
|
false => controller.BadRequest(response)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
khmereid_backend/Extensions/HttpContextExtensions.cs
Normal file
10
khmereid_backend/Extensions/HttpContextExtensions.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using khmereid_backend.Dtos;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Extensions
|
||||||
|
{
|
||||||
|
public static class HttpContextExtensions
|
||||||
|
{
|
||||||
|
public static UserDto? GetUser(this HttpContext context)
|
||||||
|
=> context.Items["UserDto"] as UserDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
khmereid_backend/Integrations/IAuthnApi.cs
Normal file
13
khmereid_backend/Integrations/IAuthnApi.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using khmereid_backend.Dtos;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Integrations;
|
||||||
|
|
||||||
|
public interface IAuthnApi
|
||||||
|
{
|
||||||
|
public Task<dynamic> Logout(string sessionToken);
|
||||||
|
public Task<SessionDto> GetMeAsync(string sessionToken);
|
||||||
|
public Task<FlowDto> CreateOtpLoginFlowAsync(string phone);
|
||||||
|
public Task<FlowDto> CreateOtpRegistrationFlowAsync(string phone);
|
||||||
|
public Task<LoginResultDto> CompleteOtpLoginFlowAsync(string flowId, string phone, string otp);
|
||||||
|
public Task<SignupResultDto> CompleteOtpRegistrationFlowAsync(string flowId, string phone, string otp);
|
||||||
|
}
|
||||||
260
khmereid_backend/Integrations/KratosApi.cs
Normal file
260
khmereid_backend/Integrations/KratosApi.cs
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Ory.Kratos.Client.Api;
|
||||||
|
using khmereid_backend.Dtos;
|
||||||
|
using Ory.Kratos.Client.Model;
|
||||||
|
using Ory.Kratos.Client.Client;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Integrations;
|
||||||
|
|
||||||
|
public class KratosApi : IAuthnApi
|
||||||
|
{
|
||||||
|
private readonly FrontendApi _frontendApi;
|
||||||
|
private readonly IdentityApi _identityApi;
|
||||||
|
private readonly ILogger<KratosApi> _logger;
|
||||||
|
|
||||||
|
public KratosApi(IConfiguration config, ILogger<KratosApi> logger)
|
||||||
|
{
|
||||||
|
var publicCfg = new Configuration { BasePath = config["Ory:Kratos:PublicUrl"]! };
|
||||||
|
var adminCfg = new Configuration { BasePath = config["Ory:Kratos:AdminUrl"]! };
|
||||||
|
|
||||||
|
_frontendApi = new FrontendApi(publicCfg);
|
||||||
|
_identityApi = new IdentityApi(adminCfg);
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Registration Flows ---------------------------
|
||||||
|
public async Task<FlowDto> CreateOtpRegistrationFlowAsync(string phone)
|
||||||
|
{
|
||||||
|
var flow = await _frontendApi.CreateNativeRegistrationFlowAsync(returnSessionTokenExchangeCode: true);
|
||||||
|
_logger.LogInformation("Started registration flow: {FlowId}", flow.Id);
|
||||||
|
|
||||||
|
phone = phone.Trim();
|
||||||
|
var method = new KratosUpdateRegistrationFlowWithCodeMethod(method: "code", traits: new { phone });
|
||||||
|
var body = new KratosUpdateRegistrationFlowBody(method);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updatedFlow = await _frontendApi.UpdateRegistrationFlowAsync(flow.Id, body);
|
||||||
|
return new FlowDto{};
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return HandleFlowException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SignupResultDto> CompleteOtpRegistrationFlowAsync(string otp, string phone, string flowId)
|
||||||
|
{
|
||||||
|
phone = phone.Trim();
|
||||||
|
var method = new KratosUpdateRegistrationFlowWithCodeMethod(code: otp, method: "code", traits: new { phone });
|
||||||
|
var body = new KratosUpdateRegistrationFlowBody(method);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body);
|
||||||
|
_logger.LogInformation("Completed OTP registration flow for flow {FlowId}", flowId);
|
||||||
|
|
||||||
|
return new SignupResultDto
|
||||||
|
{
|
||||||
|
IdentityId = Guid.Parse(result.Identity.Id),
|
||||||
|
AccessToken = result.SessionToken,
|
||||||
|
ExpiredAt = result.Session?.ExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return HandleRegistrationException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Login Flows ---------------------------
|
||||||
|
public async Task<FlowDto> CreateOtpLoginFlowAsync(string phone)
|
||||||
|
{
|
||||||
|
var flow = await _frontendApi.CreateNativeLoginFlowAsync(returnSessionTokenExchangeCode: true);
|
||||||
|
_logger.LogInformation("Started login flow: {FlowId}", flow.Id);
|
||||||
|
|
||||||
|
phone = phone.Trim();
|
||||||
|
var method = new KratosUpdateLoginFlowWithCodeMethod(method: "code", csrfToken: "dfdfdfde", identifier: phone);
|
||||||
|
var body = new KratosUpdateLoginFlowBody(method);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updatedFlow = await _frontendApi.UpdateLoginFlowAsync(flow.Id, body);
|
||||||
|
return new FlowDto{};
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return HandleFlowException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LoginResultDto> CompleteOtpLoginFlowAsync(string flowId, string phone, string otp)
|
||||||
|
{
|
||||||
|
phone = "+" + phone.Trim();
|
||||||
|
var method = new KratosUpdateLoginFlowWithCodeMethod(code: otp, method: "code", csrfToken: "dfdfdfde", identifier: phone);
|
||||||
|
var body = new KratosUpdateLoginFlowBody(method);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _frontendApi.UpdateLoginFlowAsync(flowId, body);
|
||||||
|
_logger.LogInformation("Completed OTP login flow for flow {FlowId}", flowId);
|
||||||
|
|
||||||
|
return new LoginResultDto
|
||||||
|
{
|
||||||
|
AccessToken = result.SessionToken,
|
||||||
|
ExpiredAt = result.Session?.ExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return HandleLoginException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Session & Logout ---------------------------
|
||||||
|
public async Task<SessionDto> GetMeAsync(string sessionToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = await _frontendApi.ToSessionAsync(xSessionToken: sessionToken);
|
||||||
|
return new SessionDto
|
||||||
|
{
|
||||||
|
Id = session.Identity.Id,
|
||||||
|
ExpiresAt = session.ExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (ApiException ex) when (ex.ErrorCode == 401 || ex.ErrorCode == 403)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("Invalid or expired Kratos session token", ex);
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to fetch session from Kratos: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<dynamic> Logout(string sessionToken)
|
||||||
|
{
|
||||||
|
var body = new KratosPerformNativeLogoutBody(sessionToken: sessionToken);
|
||||||
|
await _frontendApi.PerformNativeLogoutAsync(body);
|
||||||
|
_logger.LogInformation("Logged out session with token {SessionToken}", sessionToken);
|
||||||
|
return new { Message = "Logged out successfully." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Password Flow ---------------------------
|
||||||
|
public async Task<dynamic> PasswordRegistrationFlowAsync(string flowId, string phone)
|
||||||
|
{
|
||||||
|
var method = new KratosUpdateRegistrationFlowWithPasswordMethod(
|
||||||
|
password: "add3ae4d8ae8",
|
||||||
|
traits: new { phone, identifier = phone },
|
||||||
|
method: "password"
|
||||||
|
);
|
||||||
|
|
||||||
|
var body = new KratosUpdateRegistrationFlowBody(method);
|
||||||
|
var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body);
|
||||||
|
|
||||||
|
_logger.LogInformation("Completed password registration flow for {Phone}", phone);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Private Helpers ---------------------------
|
||||||
|
private static FlowDto HandleFlowException(ApiException ex)
|
||||||
|
{
|
||||||
|
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent?.ToString() ?? "{}");
|
||||||
|
|
||||||
|
if (ex.ErrorCode == 400 && res.TryGetProperty("state", out var stateProp) && stateProp.GetString() == "sent_email")
|
||||||
|
{
|
||||||
|
return new FlowDto
|
||||||
|
{
|
||||||
|
FlowId = res.GetProperty("id").GetString() ?? "",
|
||||||
|
State = "sent_otp",
|
||||||
|
ExpiresAt = res.TryGetProperty("expires_at", out var expiresAtProp) ? expiresAtProp.GetDateTime() : (DateTimeOffset?)null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ex.ErrorCode == 400)
|
||||||
|
return new FlowDto
|
||||||
|
{
|
||||||
|
Error = "BadRequest",
|
||||||
|
Code = ex.ErrorCode.ToString(),
|
||||||
|
Message = GetKratosErrorMessage(res)
|
||||||
|
};
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Kratos API returned {ex.ErrorCode}: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignupResultDto HandleRegistrationException(ApiException ex)
|
||||||
|
{
|
||||||
|
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent?.ToString() ?? "{}");
|
||||||
|
|
||||||
|
if (ex.ErrorCode == 410 && res.TryGetProperty("use_flow_id", out var useFlowProp))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Registration flow expired. Use new flow: {FlowId}", useFlowProp.GetString());
|
||||||
|
return new SignupResultDto
|
||||||
|
{
|
||||||
|
Error = "FlowExpired",
|
||||||
|
Code = ex.ErrorCode.ToString(),
|
||||||
|
Message = "Registration flow expired. Please restart registration.",
|
||||||
|
UseFlowId = useFlowProp.GetString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ex.ErrorCode == 400)
|
||||||
|
return new SignupResultDto
|
||||||
|
{
|
||||||
|
Code = ex.ErrorCode.ToString(),
|
||||||
|
Error = "BadRequest",
|
||||||
|
Message = GetKratosErrorMessage(res)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ex.ErrorCode == 404)
|
||||||
|
return new SignupResultDto
|
||||||
|
{
|
||||||
|
Code = ex.ErrorCode.ToString(),
|
||||||
|
Error = "NotFound",
|
||||||
|
Message = "Registration flow not found."
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SignupResultDto
|
||||||
|
{
|
||||||
|
Code = ex.ErrorCode.ToString(),
|
||||||
|
Error = "UnhandledException",
|
||||||
|
Message = $"Unhandled Kratos API exception: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoginResultDto HandleLoginException(ApiException ex)
|
||||||
|
{
|
||||||
|
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent?.ToString() ?? "{}");
|
||||||
|
|
||||||
|
if (ex.ErrorCode == 404)
|
||||||
|
return new LoginResultDto
|
||||||
|
{
|
||||||
|
Error = "NotFound",
|
||||||
|
Code = ex.ErrorCode.ToString(),
|
||||||
|
Message = "Login flow not found."
|
||||||
|
};
|
||||||
|
|
||||||
|
return new LoginResultDto
|
||||||
|
{
|
||||||
|
Error = "BadRequest",
|
||||||
|
Code = ex.ErrorCode.ToString(),
|
||||||
|
Message = GetKratosErrorMessage(res)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetKratosErrorMessage(JsonElement res)
|
||||||
|
{
|
||||||
|
if (res.TryGetProperty("error", out var errProp) && errProp.TryGetProperty("reason", out var reasonProp))
|
||||||
|
return reasonProp.GetString() ?? "Bad request";
|
||||||
|
|
||||||
|
if (res.TryGetProperty("ui", out var uiProp) &&
|
||||||
|
uiProp.TryGetProperty("messages", out var messagesProp) &&
|
||||||
|
messagesProp.ValueKind == JsonValueKind.Array &&
|
||||||
|
messagesProp.GetArrayLength() > 0 &&
|
||||||
|
messagesProp[0].TryGetProperty("text", out var textProp))
|
||||||
|
return textProp.GetString() ?? "Bad request";
|
||||||
|
|
||||||
|
return "Bad request";
|
||||||
|
}
|
||||||
|
}
|
||||||
102
khmereid_backend/Integrations/MockAuthnApi.cs
Normal file
102
khmereid_backend/Integrations/MockAuthnApi.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
using khmereid_backend.Dtos;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Integrations;
|
||||||
|
|
||||||
|
public class MockAuthnApi : IAuthnApi
|
||||||
|
{
|
||||||
|
private readonly ILogger<MockAuthnApi> _logger;
|
||||||
|
|
||||||
|
private static readonly Guid StaticIdentityId = Guid.Parse("c9ac4dab-cb86-4e5b-8a59-8ecf6a5479a2");
|
||||||
|
private const string StaticOtp = "000000";
|
||||||
|
private const string StaticToken = "ory_st_gCk1btBYn0RUCdLV31N60jZlzLgcAoS8";
|
||||||
|
|
||||||
|
public MockAuthnApi(ILogger<MockAuthnApi> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Registration Flows ---------------------------
|
||||||
|
public Task<FlowDto> CreateOtpRegistrationFlowAsync(string phone)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[Mock] Created registration flow for {Phone}, OTP={Otp}", phone, StaticOtp);
|
||||||
|
|
||||||
|
return Task.FromResult(new FlowDto
|
||||||
|
{
|
||||||
|
FlowId = "mock_flow_id",
|
||||||
|
State = "sent_otp",
|
||||||
|
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(5)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SignupResultDto> CompleteOtpRegistrationFlowAsync(string otp, string phone, string flowId)
|
||||||
|
{
|
||||||
|
if (otp != StaticOtp)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new SignupResultDto
|
||||||
|
{
|
||||||
|
Error = "BadRequest",
|
||||||
|
Message = "Invalid OTP."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("[Mock] Completed registration for {Phone}, IdentityId={IdentityId}", phone, StaticIdentityId);
|
||||||
|
|
||||||
|
return Task.FromResult(new SignupResultDto
|
||||||
|
{
|
||||||
|
IdentityId = StaticIdentityId,
|
||||||
|
AccessToken = StaticToken,
|
||||||
|
ExpiredAt = DateTimeOffset.UtcNow.AddHours(1).UtcDateTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Login Flows ---------------------------
|
||||||
|
public Task<FlowDto> CreateOtpLoginFlowAsync(string phone)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[Mock] Created login flow for {Phone}, OTP={Otp}", phone, StaticOtp);
|
||||||
|
|
||||||
|
return Task.FromResult(new FlowDto
|
||||||
|
{
|
||||||
|
FlowId = "mock_login_flow_id",
|
||||||
|
State = "sent_otp",
|
||||||
|
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(5)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<LoginResultDto> CompleteOtpLoginFlowAsync(string flowId, string phone, string otp)
|
||||||
|
{
|
||||||
|
if (otp != StaticOtp)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new LoginResultDto
|
||||||
|
{
|
||||||
|
Error = "BadRequest",
|
||||||
|
Message = "Invalid OTP."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("[Mock] Completed login for {Phone}, IdentityId={IdentityId}", phone, StaticIdentityId);
|
||||||
|
|
||||||
|
return Task.FromResult(new LoginResultDto
|
||||||
|
{
|
||||||
|
IdentityId = StaticIdentityId,
|
||||||
|
AccessToken = StaticToken,
|
||||||
|
ExpiredAt = DateTimeOffset.UtcNow.AddHours(1).UtcDateTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------- Session & Logout ---------------------------
|
||||||
|
public Task<SessionDto> GetMeAsync(string sessionToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new SessionDto
|
||||||
|
{
|
||||||
|
Id = StaticIdentityId.ToString(),
|
||||||
|
ExpiresAt = DateTimeOffset.UtcNow.AddHours(1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<dynamic> Logout(string sessionToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[Mock] Logged out session {Token}", sessionToken);
|
||||||
|
return Task.FromResult<dynamic>(new { Message = "Logged out successfully." });
|
||||||
|
}
|
||||||
|
}
|
||||||
73
khmereid_backend/Middlewares/KratosMiddleware.cs
Normal file
73
khmereid_backend/Middlewares/KratosMiddleware.cs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
using khmereid_backend.Data;
|
||||||
|
using khmereid_backend.Dtos;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using khmereid_backend.Services;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Ory.Kratos.Client.Client;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Integrations;
|
||||||
|
|
||||||
|
public class KratosMiddleware : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly AuthService _authService;
|
||||||
|
|
||||||
|
public KratosMiddleware(
|
||||||
|
AppDbContext db,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
AuthService authService,
|
||||||
|
IOptionsMonitor<AuthenticationSchemeOptions> options
|
||||||
|
)
|
||||||
|
: base(options, logger, encoder)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
var authHeader = Request.Headers["Authorization"].ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer "))
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
|
||||||
|
var token = authHeader.Substring("Bearer ".Length).Trim();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = await _authService.GetMeAsync(token);
|
||||||
|
Console.WriteLine("---------------");
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(session, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
|
||||||
|
var identityId = Guid.Parse(session.Data!.Id);
|
||||||
|
var localUser = _db.Users.FirstOrDefault(u => u.IdentityId == identityId);
|
||||||
|
|
||||||
|
if (localUser == null)
|
||||||
|
return AuthenticateResult.Fail("User not found in local database");
|
||||||
|
|
||||||
|
var userDto = new UserDto
|
||||||
|
{
|
||||||
|
Id = localUser.Id,
|
||||||
|
Phone = localUser.Phone,
|
||||||
|
IdentityId = localUser.IdentityId
|
||||||
|
};
|
||||||
|
Context.Items["UserDto"] = userDto;
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(
|
||||||
|
[new Claim(ClaimTypes.NameIdentifier, userDto.Id.ToString())],
|
||||||
|
Scheme.Name
|
||||||
|
);
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
|
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Failed to authenticate Kratos session");
|
||||||
|
return AuthenticateResult.Fail("Invalid Kratos session token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
khmereid_backend/Migrations/20251117033748_init.Designer.cs
generated
Normal file
69
khmereid_backend/Migrations/20251117033748_init.Designer.cs
generated
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using khmereid_backend.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace khmereid_backend.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20251117033748_init")]
|
||||||
|
partial class init
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("khmereid_backend.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("IdentityId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("identity_id");
|
||||||
|
|
||||||
|
b.Property<string>("NationalId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("national_id");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("phone");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_users");
|
||||||
|
|
||||||
|
b.HasIndex("NationalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_users_national_id");
|
||||||
|
|
||||||
|
b.HasIndex("Phone")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_users_phone");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
khmereid_backend/Migrations/20251117033748_init.cs
Normal file
49
khmereid_backend/Migrations/20251117033748_init.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace khmereid_backend.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class init : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
identity_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
phone = table.Column<string>(type: "text", nullable: false),
|
||||||
|
national_id = table.Column<string>(type: "text", nullable: false),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_users", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_users_national_id",
|
||||||
|
table: "users",
|
||||||
|
column: "national_id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_users_phone",
|
||||||
|
table: "users",
|
||||||
|
column: "phone",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
khmereid_backend/Migrations/AppDbContextModelSnapshot.cs
Normal file
66
khmereid_backend/Migrations/AppDbContextModelSnapshot.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using khmereid_backend.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace khmereid_backend.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("khmereid_backend.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Guid>("IdentityId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("identity_id");
|
||||||
|
|
||||||
|
b.Property<string>("NationalId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("national_id");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("phone");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_users");
|
||||||
|
|
||||||
|
b.HasIndex("NationalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_users_national_id");
|
||||||
|
|
||||||
|
b.HasIndex("Phone")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_users_phone");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
khmereid_backend/Models/User.cs
Normal file
20
khmereid_backend/Models/User.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Models;
|
||||||
|
|
||||||
|
[Index(nameof(Phone), IsUnique = true)]
|
||||||
|
[Index(nameof(NationalId), IsUnique = true)]
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Guid IdentityId { get; set; } = default!; // Identity id from the provider
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Phone { get; set; } = default!;
|
||||||
|
public string NationalId { get; set; } = "";
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
68
khmereid_backend/Program.cs
Normal file
68
khmereid_backend/Program.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using DotNetEnv;
|
||||||
|
using khmereid_backend.Data;
|
||||||
|
using khmereid_backend.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using khmereid_backend.Integrations;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
if (builder.Environment.IsDevelopment() && File.Exists("../.env"))
|
||||||
|
{
|
||||||
|
Env.Load("../.env");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Configuration
|
||||||
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||||
|
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: false)
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
var dbHost = builder.Configuration["DB_HOST"];
|
||||||
|
var dbPort = builder.Configuration["DB_PORT"];
|
||||||
|
var dbUser = builder.Configuration["DB_USER"];
|
||||||
|
var dbPass = builder.Configuration["DB_PASS"];
|
||||||
|
var dbName = builder.Configuration["DB_NAME"];
|
||||||
|
var dbSsl = builder.Configuration["DB_SSL_MODE"] ?? "Disable";
|
||||||
|
|
||||||
|
var connectionString = $"Host={dbHost};Port={dbPort};Username={dbUser};Password={dbPass};Database={dbName};Ssl Mode={dbSsl}";
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseNpgsql(connectionString).UseSnakeCaseNamingConvention();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IAuthnApi, KratosApi>();
|
||||||
|
builder.Services.AddScoped<AuthService>();
|
||||||
|
builder.Services.AddScoped<UserService>();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = "Kratos";
|
||||||
|
options.DefaultChallengeScheme = "Kratos";
|
||||||
|
options.DefaultScheme = "Kratos";
|
||||||
|
}).AddScheme<AuthenticationSchemeOptions, KratosMiddleware>("Kratos", _ => { });
|
||||||
|
|
||||||
|
builder.Services.AddAuthorizationBuilder()
|
||||||
|
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
|
||||||
|
.RequireAuthenticatedUser()
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.DefaultIgnoreCondition =
|
||||||
|
System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault;
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await using var scope = app.Services.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
var canConnect = await db.Database.CanConnectAsync();
|
||||||
|
app.Logger.LogInformation("Can connect to database: {CanConnect}", canConnect);
|
||||||
|
|
||||||
|
app.MapGet("/health", () => Results.Ok("I'm good.")).AllowAnonymous().WithOpenApi();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
90
khmereid_backend/Services/AuthService.cs
Normal file
90
khmereid_backend/Services/AuthService.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using khmereid_backend.Dtos;
|
||||||
|
using khmereid_backend.Integrations;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Services;
|
||||||
|
|
||||||
|
public class AuthService(
|
||||||
|
IAuthnApi authnApi,
|
||||||
|
ILogger<AuthService> logger,
|
||||||
|
UserService userService)
|
||||||
|
{
|
||||||
|
private readonly UserService _userService = userService;
|
||||||
|
private readonly ILogger<AuthService> _logger = logger;
|
||||||
|
private readonly IAuthnApi _authnApi = authnApi;
|
||||||
|
|
||||||
|
public async Task<ApiResponse<object>> StartRegistrationAsync(string phone)
|
||||||
|
{
|
||||||
|
if (await _userService.IsUserExistsAsync(phone))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("User {Phone} already exists", phone);
|
||||||
|
return ApiResponse<object>.Fail("User already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting OTP registration for phone {Phone}", phone);
|
||||||
|
var result = await _authnApi.CreateOtpRegistrationFlowAsync(phone);
|
||||||
|
|
||||||
|
return ApiResponse<object>.Ok(result, "OTP sent if the phone number is valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<SignupResultDto>> CompleteRegistrationAsync(string flowId, string phone, string otp)
|
||||||
|
{
|
||||||
|
var data = await _authnApi.CompleteOtpRegistrationFlowAsync(otp, phone, flowId);
|
||||||
|
|
||||||
|
if (data.Error != null)
|
||||||
|
return ApiResponse<SignupResultDto>.Fail(data.Message ?? "Registration failed.", null, data.Code);
|
||||||
|
|
||||||
|
var user = await _userService.CreateUserAsync(phone, (Guid)data.IdentityId!);
|
||||||
|
return ApiResponse<SignupResultDto>.Ok(data, "Registration completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<FlowDto>> StartLoginAsync(string phone)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting OTP login for phone {Phone}", phone);
|
||||||
|
var data = await _authnApi.CreateOtpLoginFlowAsync(phone);
|
||||||
|
|
||||||
|
if (data.Error != null)
|
||||||
|
return ApiResponse<FlowDto>.Fail(data.Message ?? "Request failed.", null, data.Code);
|
||||||
|
return ApiResponse<FlowDto>.Ok(data, "Request completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<LoginResultDto>> CompleteLoginAsync(string flowId, string phone, string otp)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Completing OTP login for phone {Phone}, Flow: {FlowId}", phone, flowId);
|
||||||
|
|
||||||
|
var data = await _authnApi.CompleteOtpLoginFlowAsync(flowId, phone, otp);
|
||||||
|
|
||||||
|
if (data.Error != null)
|
||||||
|
return ApiResponse<LoginResultDto>.Fail(data.Message ?? "Login failed.", null, data.Code);
|
||||||
|
return ApiResponse<LoginResultDto>.Ok(data, "Login completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<string>> LogoutAsync(string sessionToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Logging out session token {Token}", sessionToken);
|
||||||
|
await _authnApi.Logout(sessionToken);
|
||||||
|
return ApiResponse<string>.Ok("Logout completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<SessionDto>> GetMeAsync(string sessionToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Fetching current session for token {Token}", sessionToken);
|
||||||
|
var response = await _authnApi.GetMeAsync(sessionToken);
|
||||||
|
|
||||||
|
if (response.Error != null)
|
||||||
|
return ApiResponse<SessionDto>.Fail(response.Message ?? "Failed.");
|
||||||
|
return ApiResponse<SessionDto>.Ok(response, "Session returned.");
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to fetch session from Kratos");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
khmereid_backend/Services/UserService.cs
Normal file
31
khmereid_backend/Services/UserService.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using khmereid_backend.Data;
|
||||||
|
using khmereid_backend.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace khmereid_backend.Services
|
||||||
|
{
|
||||||
|
public class UserService(AppDbContext context)
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context = context;
|
||||||
|
|
||||||
|
public async Task<bool> IsUserExistsAsync(string phone)
|
||||||
|
{
|
||||||
|
phone = phone.Trim();
|
||||||
|
return await _context.Users.AnyAsync(u => u.Phone == phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> CreateUserAsync(string phone, Guid identityId)
|
||||||
|
{
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Phone = phone.Trim(),
|
||||||
|
IdentityId = identityId
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Users.Add(user);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
khmereid_backend/appsettings.Development.json
Normal file
21
khmereid_backend/appsettings.Development.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Ory": {
|
||||||
|
"Kratos": {
|
||||||
|
"PublicUrl": "http://localhost:4433",
|
||||||
|
"AdminUrl": "http://localhost:4434"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Kyc": {
|
||||||
|
"EKYC_Solutions": {
|
||||||
|
"BaseUrl": "",
|
||||||
|
"PrivateKey": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,8 +5,5 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*"
|
||||||
"AppSettings": {
|
|
||||||
"ApplicationName": "Khmer eID Backend"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
26
khmereid_backend/khmereid_backend.csproj
Normal file
26
khmereid_backend/khmereid_backend.csproj
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<RootNamespace>khmereid_backend</RootNamespace>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Consul" Version="1.7.14.9" />
|
||||||
|
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||||
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Ory.Kratos.Client" Version="1.3.8" />
|
||||||
|
<Protobuf Include="Protos\gdi_online\user.proto" GrpcServices="Client" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
5
khmereid_backend/test.http
Normal file
5
khmereid_backend/test.http
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@baseUrl = http://localhost:5200
|
||||||
|
|
||||||
|
GET {{baseUrl}}/health
|
||||||
|
Accept: application/json
|
||||||
|
###
|
||||||
@ -14,14 +14,16 @@ manifests:
|
|||||||
- k8s/local/
|
- k8s/local/
|
||||||
buildArgs:
|
buildArgs:
|
||||||
- --load-restrictor=LoadRestrictionsNone
|
- --load-restrictor=LoadRestrictionsNone
|
||||||
|
deploy:
|
||||||
|
statusCheck: true
|
||||||
portForward:
|
portForward:
|
||||||
# - resourceType: service
|
|
||||||
# resourceName: envoy-default-my-gateway-1c7c06f0
|
|
||||||
# namespace: envoy-gateway-system
|
|
||||||
# port: 80
|
|
||||||
# localPort: 8888
|
|
||||||
- resourceType: service
|
- resourceType: service
|
||||||
resourceName: kratos
|
resourceName: kratos-app-public
|
||||||
namespace: default
|
namespace: default
|
||||||
port: 4433
|
port: 80
|
||||||
localPort: 4433
|
localPort: 4433
|
||||||
|
- resourceType: service
|
||||||
|
resourceName: postgres-dotnet-rw
|
||||||
|
namespace: default
|
||||||
|
port: 5432
|
||||||
|
localPort: 5600
|
||||||
Loading…
x
Reference in New Issue
Block a user