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
|
||||
|
||||
# Copy the project files
|
||||
COPY khmer_eid_backend/*.csproj ./
|
||||
COPY khmereid_backend/*.csproj ./
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY khmer_eid_backend/ ./
|
||||
COPY khmereid_backend/ ./
|
||||
|
||||
# Build the application
|
||||
# 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
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||
@ -23,11 +23,11 @@ WORKDIR /app
|
||||
COPY --from=build /app/out ./
|
||||
|
||||
# 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
|
||||
RUN apt-get update && apt-get install -y vim && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Expose the port and run the application
|
||||
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:
|
||||
docker compose -f docker-compose.dev.yml up -d --remove-orphans
|
||||
bootstrap:
|
||||
@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:
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
@echo "Applying Kubernetes bootstrap manifests..."
|
||||
@kubectl apply -k k8s/bootstrap
|
||||
@kubectl apply -f k8s/bootstrap/bootstrap.yml
|
||||
|
||||
dev-restore:
|
||||
docker compose -f docker-compose.dev.yml run khmer-eid-api dotnet restore
|
||||
@echo "Waiting for namespace 'envoy-gateway-system' to appear..."
|
||||
@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:
|
||||
docker compose -f docker-compose.dev.yml build
|
||||
@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
|
||||
@echo "All pods are ready!"
|
||||
|
||||
dev-list-packages:
|
||||
docker exec -it khmer-eid-api-dev dotnet list package
|
||||
|
||||
dev-exec:
|
||||
docker exec -it khmer-eid-api-dev /bin/bash
|
||||
|
||||
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-migrate:
|
||||
@skaffold run --cleanup=false
|
||||
@sleep 5
|
||||
@cd khmereid_backend && dotnet ef database update
|
||||
@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
|
||||
|
||||
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
|
||||
1. dotnet add package <PackageName>
|
||||
2. dotnet resotre
|
||||
3. docker compose build
|
||||
4. docker compose up
|
||||
## Quick Start (Local Development)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Make sure you have:
|
||||
|
||||
### config kratos
|
||||
create schema kratos
|
||||
- [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download)
|
||||
- [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
|
||||
create schema hanko
|
||||
1. Clone the repository and navigate into it:
|
||||
```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
|
||||
dsn: "postgres://dev:dev@postgres:5432/backend_db?sslmode=disable&search_path=kratos"
|
||||
dsn: ${DSN}
|
||||
log:
|
||||
leak_sensitive_values: true
|
||||
|
||||
@ -31,14 +31,14 @@ identity:
|
||||
default_schema_id: default
|
||||
schemas:
|
||||
- id: default
|
||||
url: file:///etc/config/identity.schema.json
|
||||
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
|
||||
url: https://webhook.site/d67a9ea6-e152-4d7f-8432-7ab92b2ace1f
|
||||
body: base64://ZnVuY3Rpb24oY3R4KSB7CiAgcmVjaXBpZW50OiBjdHgucmVjaXBpZW50LAogIHRlbXBsYXRlX3R5cGU6IGN0eC50ZW1wbGF0ZV90eXBlLAogIHRvOiBpZiAidGVtcGxhdGVfZGF0YSIgaW4gY3R4ICYmICJ0byIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS50byBlbHNlIG51bGwsCiAgcmVjb3ZlcnlfY29kZTogaWYgInRlbXBsYXRlX2RhdGEiIGluIGN0eCAmJiAicmVjb3ZlcnlfY29kZSIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV9jb2RlIGVsc2UgbnVsbCwKICByZWNvdmVyeV91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInJlY292ZXJ5X3VybCIgaW4gY3R4LnRlbXBsYXRlX2RhdGEgdGhlbiBjdHgudGVtcGxhdGVfZGF0YS5yZWNvdmVyeV91cmwgZWxzZSBudWxsLAogIHZlcmlmaWNhdGlvbl91cmw6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl91cmwiIGluIGN0eC50ZW1wbGF0ZV9kYXRhIHRoZW4gY3R4LnRlbXBsYXRlX2RhdGEudmVyaWZpY2F0aW9uX3VybCBlbHNlIG51bGwsCiAgdmVyaWZpY2F0aW9uX2NvZGU6IGlmICJ0ZW1wbGF0ZV9kYXRhIiBpbiBjdHggJiYgInZlcmlmaWNhdGlvbl9jb2RlIiBpbiBjdHgudGVtcGxhdGVfZGF0YSB0aGVuIGN0eC50ZW1wbGF0ZV9kYXRhLnZlcmlmaWNhdGlvbl9jb2RlIGVsc2UgbnVsbCwKICBzdWJqZWN0OiBjdHguc3ViamVjdCwKICBib2R5OiBjdHguYm9keQp9Cg==
|
||||
headers:
|
||||
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
|
||||
kind: Application
|
||||
metadata:
|
||||
@ -15,6 +18,10 @@ spec:
|
||||
targetRevision: 0.58.0
|
||||
helm:
|
||||
valuesObject:
|
||||
statefulSet:
|
||||
extraArgs:
|
||||
- --config
|
||||
- /etc/config/custom/kratos.yaml
|
||||
deployment:
|
||||
extraVolumes:
|
||||
- name: kratos-custom-config
|
||||
@ -30,77 +37,17 @@ spec:
|
||||
secretKeyRef:
|
||||
name: postgres-kratos-app
|
||||
key: uri
|
||||
extraArgs:
|
||||
- --config
|
||||
- /etc/config/custom/kratos.yaml
|
||||
kratos:
|
||||
# config db
|
||||
log:
|
||||
leak_sensitive_values: true
|
||||
automigration:
|
||||
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:
|
||||
repository: shadowlegend/ory-kratos
|
||||
tag: master-arm64
|
||||
|
||||
destination:
|
||||
namespace: default
|
||||
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
|
||||
|
||||
resources:
|
||||
- app-kratos.yml
|
||||
- app-cnpg.yml
|
||||
- app-envoy.yml
|
||||
- envoy.yml
|
||||
# - kratos.yml
|
||||
# - deployment.yml
|
||||
- app-kratos.yml
|
||||
- postgres-dotnet.yml
|
||||
- postgres-kratos.yml
|
||||
- deployment.yml
|
||||
# - app-khmereid.yml
|
||||
|
||||
configMapGenerator:
|
||||
- name: kratos-config
|
||||
files:
|
||||
- kratos.yml=../../config/kratos/kratos.yml
|
||||
- kratos.yaml=../../config/kratos/kratos.yml
|
||||
- identity.schema.json=../../config/kratos/identity.schema.json
|
||||
generatorOptions:
|
||||
disableNameSuffixHash: true
|
||||
|
||||
@ -2,7 +2,7 @@ apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: postgres-dotnet
|
||||
namespace: argocd
|
||||
namespace: default
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
|
||||
spec:
|
||||
|
||||
@ -3,7 +3,6 @@ kind: Cluster
|
||||
metadata:
|
||||
name: postgres-kratos
|
||||
namespace: default
|
||||
# namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
|
||||
spec:
|
||||
|
||||
@ -1,5 +1,26 @@
|
||||
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'
|
||||
|
||||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: envoy-app
|
||||
namespace: argocd
|
||||
@ -22,6 +43,5 @@ spec:
|
||||
agent:
|
||||
enabled: true
|
||||
destination:
|
||||
# namespace: default
|
||||
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
|
||||
@ -2,16 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../base
|
||||
- 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
|
||||
- nginx-temp.yml
|
||||
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'
|
||||
@ -1,4 +1,17 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
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
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
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
|
||||
Global
|
||||
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;
|
||||
|
||||
namespace khmereid_backend.Dtos;
|
||||
|
||||
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")]
|
||||
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"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AppSettings": {
|
||||
"ApplicationName": "Khmer eID Backend"
|
||||
}
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
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/
|
||||
buildArgs:
|
||||
- --load-restrictor=LoadRestrictionsNone
|
||||
deploy:
|
||||
statusCheck: true
|
||||
portForward:
|
||||
# - resourceType: service
|
||||
# resourceName: envoy-default-my-gateway-1c7c06f0
|
||||
# namespace: envoy-gateway-system
|
||||
# port: 80
|
||||
# localPort: 8888
|
||||
- resourceType: service
|
||||
resourceName: kratos
|
||||
resourceName: kratos-app-public
|
||||
namespace: default
|
||||
port: 4433
|
||||
localPort: 4433
|
||||
port: 80
|
||||
localPort: 4433
|
||||
- resourceType: service
|
||||
resourceName: postgres-dotnet-rw
|
||||
namespace: default
|
||||
port: 5432
|
||||
localPort: 5600
|
||||
Loading…
x
Reference in New Issue
Block a user