init
This commit is contained in:
parent
3f044867e9
commit
03296b4ae9
319
.drone.yml
Normal file
319
.drone.yml
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
host:
|
||||||
|
path: /tmp/drone/cache
|
||||||
|
|
||||||
|
environment:
|
||||||
|
DB_HOST: mariadb
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_USERNAME: root
|
||||||
|
DB_PASSWORD: productzilla
|
||||||
|
DB_NAME: productzilla
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: mariadb
|
||||||
|
image: mariadb:10.3.10
|
||||||
|
environment:
|
||||||
|
MYSQL_USER: productzilla
|
||||||
|
MYSQL_ROOT_PASSWORD: productzilla
|
||||||
|
MYSQL_DATABASE: productzilla
|
||||||
|
MYSQL_PASSWORD: productzilla
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: prepare mariadb
|
||||||
|
image: mariadb:10.3.10
|
||||||
|
commands:
|
||||||
|
- until mysql -u root -p'productzilla' -e 'select version()' -h mariadb; do sleep 1; done;
|
||||||
|
- mysql -u root -p'productzilla' -h mariadb -e 'CREATE DATABASE IF NOT EXISTS identity'
|
||||||
|
|
||||||
|
- name: restore build cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
restore: 'true'
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
- ./.pkg
|
||||||
|
- ./.scannerwork
|
||||||
|
- ./.sonar
|
||||||
|
- ./.nexe
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
|
||||||
|
- name: build web-pz
|
||||||
|
image: node:18.19.0-alpine
|
||||||
|
commands:
|
||||||
|
- yarn install
|
||||||
|
- yarn build
|
||||||
|
|
||||||
|
- name: migrate web-pz
|
||||||
|
image: node:18.19.0-alpine
|
||||||
|
commands:
|
||||||
|
- export DB_HOST=$DB_HOST
|
||||||
|
- export DB_PORT=$DB_PORT
|
||||||
|
- export DB_USERNAME=$DB_USERNAME
|
||||||
|
- export DB_PASSWORD=$DB_PASSWORD
|
||||||
|
- export DB_NAME=$DB_NAME
|
||||||
|
- yarn typeorm:run
|
||||||
|
|
||||||
|
- name: analyze code using sonarqube
|
||||||
|
image: oeoen/drone-sonar-plugin:5.0.1-pr-enabled
|
||||||
|
settings:
|
||||||
|
url:
|
||||||
|
from_secret: sonar_url
|
||||||
|
token:
|
||||||
|
from_secret: sonar_token
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
- name: rebuild build cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
rebuild: 'true'
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
- ./.scannerwork
|
||||||
|
- ./.sonar
|
||||||
|
- ./.nexe
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
exclude:
|
||||||
|
# - develop
|
||||||
|
- master
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- custom
|
||||||
|
include:
|
||||||
|
- push
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: semantic_versioning
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: semantic-release
|
||||||
|
image: ilhamfadhilah/drone-semantic-release
|
||||||
|
settings:
|
||||||
|
semantic_release: true # enable or disable semantic release
|
||||||
|
mode: release
|
||||||
|
git_method: cr
|
||||||
|
git_user_email: hello@productzillaacademy.com
|
||||||
|
git_host:
|
||||||
|
from_secret: gitea_host
|
||||||
|
git_host_proto:
|
||||||
|
from_secret: gitea_host_proto
|
||||||
|
git_login:
|
||||||
|
from_secret: gitea_login
|
||||||
|
git_password:
|
||||||
|
from_secret: gitea_password
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- custom
|
||||||
|
include:
|
||||||
|
- push
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: publish develop image to docker registry
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
host:
|
||||||
|
path: /tmp/drone/cache
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: restore build cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
restore: 'true'
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
- ./.pkg
|
||||||
|
- ./.scannerwork
|
||||||
|
- ./.sonar
|
||||||
|
- ./.nexe
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
|
||||||
|
- name: build and publish image
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
repo: 'productzilla/web-pz'
|
||||||
|
username:
|
||||||
|
from_secret: ci_registry_user
|
||||||
|
password:
|
||||||
|
from_secret: ci_registry_password
|
||||||
|
dockerfile: misc/docker/dockerfile
|
||||||
|
tag: dev
|
||||||
|
|
||||||
|
- name: rebuild build cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
rebuild: 'true'
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
- ./.scannerwork
|
||||||
|
- ./.sonar
|
||||||
|
- ./.nexe
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
- name: deploy to staging
|
||||||
|
image: alpine:latest
|
||||||
|
environment:
|
||||||
|
SERVER_HOST:
|
||||||
|
from_secret: k8s_staging_server_host
|
||||||
|
CONTEXT:
|
||||||
|
from_secret: k8s_staging_context
|
||||||
|
USER:
|
||||||
|
from_secret: k8s_staging_user
|
||||||
|
TOKEN:
|
||||||
|
from_secret: k8s_staging_token
|
||||||
|
NAMESPACE:
|
||||||
|
from_secret: k8s_staging_namespace
|
||||||
|
commands:
|
||||||
|
- apk add curl
|
||||||
|
- curl -LO https://dl.k8s.io/release/v1.22.0/bin/linux/amd64/kubectl
|
||||||
|
- chmod u+x kubectl && mv kubectl /bin/kubectl
|
||||||
|
- mkdir -p /root/.kube
|
||||||
|
- cat misc/deployment/config.yml | sed "s#{{server_host}}#$SERVER_HOST#g" | sed "s#{{context}}#$CONTEXT#g" | sed "s#{{user}}#$USER#g" | sed "s#{{token}}#$TOKEN#g" > '/root/.kube/config'
|
||||||
|
- cat misc/deployment/k8s.template.staging.yml | sed 's/{{tags}}/dev/g' | sed "s/{{namespace}}/$NAMESPACE/g" > 'misc/deployment/dev.template.yml'
|
||||||
|
- kubectl -n $NAMESPACE apply -f 'misc/deployment/dev.template.yml'
|
||||||
|
- kubectl -n $NAMESPACE rollout restart deployment/productzilla-web-pz
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- develop
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- default
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build production image
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
host:
|
||||||
|
path: /tmp/drone/cache
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: restore build cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
restore: 'true'
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
- ./.pkg
|
||||||
|
- ./.scannerwork
|
||||||
|
- ./.sonar
|
||||||
|
- ./.nexe
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
|
||||||
|
- name: generate tag for image publish
|
||||||
|
image: alpine:3.13.6
|
||||||
|
commands:
|
||||||
|
- echo -n "${DRONE_TAG}" > .tags
|
||||||
|
|
||||||
|
- name: build and publish image
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
repo: 'productzilla/web-pz'
|
||||||
|
username:
|
||||||
|
from_secret: ci_registry_user
|
||||||
|
password:
|
||||||
|
from_secret: ci_registry_password
|
||||||
|
dockerfile: misc/docker/dockerfile
|
||||||
|
|
||||||
|
- name: rebuild build cache
|
||||||
|
image: drillster/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
rebuild: 'true'
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
- ./.scannerwork
|
||||||
|
- ./.sonar
|
||||||
|
- ./.nexe
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
trigger:
|
||||||
|
ref:
|
||||||
|
- refs/tags/*
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: deploy build production image to kubernetes
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: generate tag for image publish
|
||||||
|
image: alpine:3.13.6
|
||||||
|
commands:
|
||||||
|
- if [ "$IMAGE_VERSION" == "" ]; then exit 1; fi;
|
||||||
|
- echo -n "${IMAGE_VERSION}" > .tags
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
image: alpine:latest
|
||||||
|
environment:
|
||||||
|
SERVER_HOST:
|
||||||
|
from_secret: k8s_staging_server_host
|
||||||
|
CONTEXT:
|
||||||
|
from_secret: k8s_staging_context
|
||||||
|
USER:
|
||||||
|
from_secret: k8s_staging_user
|
||||||
|
TOKEN:
|
||||||
|
from_secret: k8s_staging_token
|
||||||
|
NAMESPACE:
|
||||||
|
from_secret: k8s_staging_namespace
|
||||||
|
commands:
|
||||||
|
- apk add curl
|
||||||
|
- curl -LO https://dl.k8s.io/release/v1.22.0/bin/linux/amd64/kubectl
|
||||||
|
- chmod u+x kubectl && mv kubectl /bin/kubectl
|
||||||
|
- mkdir -p /root/.kube
|
||||||
|
- cat misc/deployment/config.yml | sed "s#{{server_host}}#$SERVER_HOST#g" | sed "s#{{context}}#$CONTEXT#g" | sed "s#{{user}}#$USER#g" | sed "s#{{token}}#$TOKEN#g" > '/root/.kube/config'
|
||||||
|
- export IMAGE_VERSION=$(cat .tags)
|
||||||
|
- cat misc/deployment/k8s.template.yml | sed "s/{{tags}}/$IMAGE_VERSION/g" | sed "s/{{namespace}}/$NAMESPACE/g" > "misc/deployment/$IMAGE_VERSION.template.yml"
|
||||||
|
- kubectl -n $NAMESPACE apply -f "misc/deployment/$IMAGE_VERSION.template.yml"
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
event:
|
||||||
|
- custom
|
||||||
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=31360
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=productzilla
|
||||||
|
DB_NAME=productzilla
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -34,3 +34,9 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
misc/docker/volumes
|
||||||
|
.certs
|
||||||
|
.vscode/launch.json
|
||||||
|
.next
|
||||||
|
.scannerwork
|
||||||
3
@types/generic.ts
Normal file
3
@types/generic.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface Generic {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
7
@types/next.d.ts
vendored
Normal file
7
@types/next.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { UserEntity } from '../infrastructure/database/user/user.entity';
|
||||||
|
|
||||||
|
declare module 'next' {
|
||||||
|
interface NextApiRequest {
|
||||||
|
session: { user?: UserEntity | null };
|
||||||
|
}
|
||||||
|
}
|
||||||
19
@types/pagination.ts
Normal file
19
@types/pagination.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { IsNumberString } from 'class-validator';
|
||||||
|
|
||||||
|
export class PaginationParam<T = any> {
|
||||||
|
@IsNumberString()
|
||||||
|
page!: number;
|
||||||
|
|
||||||
|
@IsNumberString()
|
||||||
|
size!: number;
|
||||||
|
|
||||||
|
search?: T | Partial<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Paginated<T> {
|
||||||
|
items: T[];
|
||||||
|
totalSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
12
config/index.ts
Normal file
12
config/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const config = {
|
||||||
|
db: {
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST ?? 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT ?? '') ?? 3306,
|
||||||
|
username: process.env.DB_USERNAME ?? 'root',
|
||||||
|
password: process.env.DB_PASSWORD ?? '',
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
maxPoolConnection: process.env.DB_MAX_POOL_CONNECTION ?? 10,
|
||||||
|
},
|
||||||
|
jwtSecret: process.env.JWT_SECRET ?? 'super-secret-jwt-key',
|
||||||
|
};
|
||||||
18
misc/deployment/config.yml
Normal file
18
misc/deployment/config.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
insecure-skip-tls-verify: true
|
||||||
|
server: {{server_host}}/api/endpoints/1/kubernetes
|
||||||
|
name: {{context}}
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: {{context}}
|
||||||
|
user: {{user}}
|
||||||
|
name: {{context}}
|
||||||
|
current-context: {{context}}
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: {{user}}
|
||||||
|
user:
|
||||||
|
token: {{token}}
|
||||||
68
misc/deployment/k8s.template.staging.yml
Normal file
68
misc/deployment/k8s.template.staging.yml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: productzilla-web-pz
|
||||||
|
namespace: {{namespace}}
|
||||||
|
labels:
|
||||||
|
app: web-pz
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: web-pz
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: web-pz
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: web-pz
|
||||||
|
image: productzilla/web-pz:{{tags}}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: '64Mi'
|
||||||
|
cpu: '250m'
|
||||||
|
limits:
|
||||||
|
memory: '256Mi'
|
||||||
|
cpu: '500m'
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
env:
|
||||||
|
- name: NODE_ENV
|
||||||
|
value: production
|
||||||
|
- name: DB_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-secret
|
||||||
|
key: mariadb-host
|
||||||
|
- name: DB_PORT
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-secret
|
||||||
|
key: mariadb-port
|
||||||
|
- name: DB_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-password-secret
|
||||||
|
key: mariadb-username
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-password-secret
|
||||||
|
key: mariadb-password
|
||||||
|
- name: DB_NAME
|
||||||
|
value: productzilla_web-pz_staging
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: web-pz-svc
|
||||||
|
namespace: {{namespace}}
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: web-pz
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3000
|
||||||
|
targetPort: 3000 # should match containerPort
|
||||||
68
misc/deployment/k8s.template.yml
Normal file
68
misc/deployment/k8s.template.yml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: productzilla-web-pz
|
||||||
|
namespace: {{namespace}}
|
||||||
|
labels:
|
||||||
|
app: web-pz
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: web-pz
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: web-pz
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: web-pz
|
||||||
|
image: productzilla/web-pz:{{tags}}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: '64Mi'
|
||||||
|
cpu: '250m'
|
||||||
|
limits:
|
||||||
|
memory: '256Mi'
|
||||||
|
cpu: '500m'
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
env:
|
||||||
|
- name: NODE_ENV
|
||||||
|
value: production
|
||||||
|
- name: DB_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-secret
|
||||||
|
key: mariadb-host
|
||||||
|
- name: DB_PORT
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-secret
|
||||||
|
key: mariadb-port
|
||||||
|
- name: DB_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-password-secret
|
||||||
|
key: mariadb-username
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: productzilla-password-secret
|
||||||
|
key: mariadb-password
|
||||||
|
- name: DB_NAME
|
||||||
|
value: web-pz
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: web-pz-svc
|
||||||
|
namespace: {{namespace}}
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: web-pz
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3000
|
||||||
|
targetPort: 3000 # should match containerPort
|
||||||
33
misc/docker/docker-compose.yml
Normal file
33
misc/docker/docker-compose.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
version: '3.4'
|
||||||
|
services:
|
||||||
|
########################
|
||||||
|
# SERVICE DEPENDENCIES #
|
||||||
|
########################
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:10.3.10
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=productzilla
|
||||||
|
- MYSQL_DATABASE=productzilla
|
||||||
|
- MYSQL_USER=productzilla
|
||||||
|
- MYSQL_PASSWORD=productzilla
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'mysqladmin', 'ping', '--silent']
|
||||||
|
start_period: 30s
|
||||||
|
ports:
|
||||||
|
- 31360:3306
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
volumes:
|
||||||
|
- ./volumes/mariadb:/var/lib/mysql
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin/phpmyadmin
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 31361:80
|
||||||
|
environment:
|
||||||
|
- PMA_HOST=mariadb
|
||||||
|
- UPLOAD_LIMIT=100M
|
||||||
1726
package-lock.json
generated
1726
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -6,21 +6,41 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"docker-compose": "docker-compose -f misc/docker/docker-compose.yml",
|
||||||
|
"typeorm": "cross-env DOTENV_CONFIG_PATH=./.env node -r tsconfig-paths/register -r ts-node/register -r dotenv/config ./node_modules/typeorm/cli",
|
||||||
|
"typeorm:create": "npm run typeorm migration:create -o ./src/backend/infrastructure/database/provider/migrations/$NAME",
|
||||||
|
"typeorm:generate": "npm run typeorm migration:generate -- -d ./src/backend/infrastructure/database/provider/index.ts ./src/backend/infrastructure/database/provider/migrations/$NAME",
|
||||||
|
"typeorm:run": "npm run typeorm migration:run -- -d ./src/backend/infrastructure/database/provider/index.ts",
|
||||||
|
"typeorm:revert": "npm run typeorm migration:revert -- -d ./src/backend/infrastructure/database/provider/index.ts",
|
||||||
|
"typeorm:show": "npm run typeorm migration:show -- -d ./src/backend/infrastructure/database/provider/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"next": "14.2.15",
|
||||||
|
"next-api-decorators": "^2.0.2",
|
||||||
|
"path-to-regexp": "^8.2.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"next": "14.2.15"
|
"ts-node": "^10.9.2",
|
||||||
|
"typeorm": "^0.3.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.2.15",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"eslint": "^8",
|
"typescript": "^5.6.3"
|
||||||
"eslint-config-next": "14.2.15"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/app/components/Features.tsx
Normal file
34
src/app/components/Features.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: 'path/to/icon1.svg',
|
||||||
|
title: 'Mentor Praktisi Profesional',
|
||||||
|
description: 'Dapatkan arahan dari mentor profesional di berbagai bidang industri.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'path/to/icon2.svg',
|
||||||
|
title: 'Sertifikat Diakui Industri',
|
||||||
|
description: 'Dapatkan sertifikat yang diakui oleh perusahaan besar.',
|
||||||
|
},
|
||||||
|
// Add other features here...
|
||||||
|
];
|
||||||
|
|
||||||
|
const Features = () => {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto py-12">
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-8">Kenapa Memilih Productzilla?</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<div key={index} className="text-center">
|
||||||
|
<img className="mx-auto h-16" src={feature.icon} alt={feature.title} />
|
||||||
|
<h3 className="mt-4 text-lg font-semibold">{feature.title}</h3>
|
||||||
|
<p className="mt-2 text-gray-600">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Features;
|
||||||
30
src/app/components/Footer.tsx
Normal file
30
src/app/components/Footer.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<footer className="bg-gray-900 text-white py-12">
|
||||||
|
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Productzilla Academy</h3>
|
||||||
|
<p className="mt-4">Jalan Abdul Regency, Bandung, Indonesia</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Perusahaan</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#" className="hover:underline">Tentang Kami</a></li>
|
||||||
|
<li><a href="#" className="hover:underline">Kontak Kami</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Ikuti Kami</h3>
|
||||||
|
<ul className="flex space-x-4 mt-4">
|
||||||
|
<li><a href="#"><img src="/facebook.svg" alt="Facebook" /></a></li>
|
||||||
|
<li><a href="#"><img src="/instagram.svg" alt="Instagram" /></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
25
src/app/components/Header.tsx
Normal file
25
src/app/components/Header.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
return (
|
||||||
|
<header className="bg-white shadow-lg">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center h-16">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<img className="h-8 w-8" src="/logo.png" alt="Productzilla" />
|
||||||
|
</div>
|
||||||
|
<nav className="flex space-x-4">
|
||||||
|
<a href="#" className="text-gray-900">Kursus</a>
|
||||||
|
<a href="#" className="text-gray-900">Bootcamps</a>
|
||||||
|
<a href="#" className="text-gray-900">For Company</a>
|
||||||
|
<a href="#" className="text-gray-900">Tentang Kami</a>
|
||||||
|
</nav>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<a href="#" className="text-gray-900">Login</a>
|
||||||
|
<a href="#" className="bg-blue-500 text-white px-4 py-2 rounded">Register</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
22
src/app/components/Hero.tsx
Normal file
22
src/app/components/Hero.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Hero = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-500 text-white py-24">
|
||||||
|
<div className="max-w-7xl mx-auto text-center">
|
||||||
|
<h1 className="text-4xl font-bold">Tingkatkan Karirmu di Dunia Industri</h1>
|
||||||
|
<p className="mt-4 text-lg">
|
||||||
|
Pelajari keterampilan baru yang dibutuhkan oleh perusahaan dari expert dengan sertifikat yang diakui.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="mt-8 inline-block bg-white text-blue-500 px-6 py-3 rounded-md font-medium"
|
||||||
|
>
|
||||||
|
Explore our program
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Hero;
|
||||||
36
src/app/components/Program.tsx
Normal file
36
src/app/components/Program.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const Programs = () => {
|
||||||
|
const programs = [
|
||||||
|
{
|
||||||
|
title: "Bootcamp Fullstack Software Development",
|
||||||
|
description: "Masyarakat tentang...",
|
||||||
|
price: "Rp 10,000,000",
|
||||||
|
date: "September 2024 - December 2024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bootcamp Digital Marketing From Scratch",
|
||||||
|
description: "Menguasai tentang konsep pemasaran...",
|
||||||
|
price: "Rp 15,000,000",
|
||||||
|
date: "September 2024 - December 2024"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-gray-100">
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-10">Our Programs</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
{programs.map((program, index) => (
|
||||||
|
<div key={index} className="bg-white p-6 shadow rounded">
|
||||||
|
<h3 className="text-xl font-bold">{program.title}</h3>
|
||||||
|
<p className="mt-2">{program.description}</p>
|
||||||
|
<p className="mt-4 font-bold">{program.price}</p>
|
||||||
|
<p>{program.date}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Programs;
|
||||||
@ -1,27 +1,3 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--background);
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.text-balance {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
106
src/app/page.tsx
106
src/app/page.tsx
@ -1,101 +1,17 @@
|
|||||||
import Image from "next/image";
|
import Features from "./components/Features";
|
||||||
|
import Footer from "./components/Footer";
|
||||||
|
import Header from "./components/Header";
|
||||||
|
import Hero from "./components/Hero";
|
||||||
|
import Programs from "./components/Program";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
<div>
|
||||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
<Header />
|
||||||
<Image
|
<Hero />
|
||||||
className="dark:invert"
|
<Programs />
|
||||||
src="https://nextjs.org/icons/next.svg"
|
<Features />
|
||||||
alt="Next.js logo"
|
<Footer />
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
|
||||||
<li className="mb-2">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li>Save and see your changes instantly.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="https://nextjs.org/icons/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="https://nextjs.org/icons/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="https://nextjs.org/icons/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="https://nextjs.org/icons/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/backend/domains/user.service.ts
Normal file
61
src/backend/domains/user.service.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { NotFoundException, UnauthorizedException } from 'next-api-decorators';
|
||||||
|
import { CreateUserRequest } from 'src/pages/api/users/request';
|
||||||
|
import { UserRepository } from 'src/backend/infrastructure/database/user/user.repository';
|
||||||
|
import { hashString } from 'src/utils/hash';
|
||||||
|
import { encode, validate } from 'src/utils/jwt';
|
||||||
|
import { UserEntity } from 'src/backend/infrastructure/database/user/user.entity';
|
||||||
|
import { LoginRequest } from '@/src/pages/api/auth/request';
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
public static readonly service: UserService = new UserService();
|
||||||
|
static getService(): UserService {
|
||||||
|
return UserService.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(loginRequest: LoginRequest) {
|
||||||
|
const user = await UserRepository.getRepository().findOne({
|
||||||
|
where: { email: loginRequest.email },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
if (user.password !== hashString(loginRequest.password)) {
|
||||||
|
throw new UnauthorizedException('Incorrect Credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: encode({ id: user.id, email: user.email }),
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async me(token: string) {
|
||||||
|
const userData = validate(token) as UserEntity;
|
||||||
|
const user = await UserRepository.getRepository().findOne({
|
||||||
|
where: { id: userData.id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
phone: true,
|
||||||
|
lastSignInAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(user: CreateUserRequest) {
|
||||||
|
const u = new UserEntity({
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
password: hashString(user.password),
|
||||||
|
phone: user.phoneNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
return UserRepository.getRepository().save(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
EntityTarget,
|
||||||
|
FindManyOptions,
|
||||||
|
ObjectLiteral,
|
||||||
|
Repository,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { DatabaseProvider } from '.';
|
||||||
|
import { Paginated, PaginationParam } from '@/@types/pagination';
|
||||||
|
|
||||||
|
export class BaseRepository<E extends ObjectLiteral> extends Repository<E> {
|
||||||
|
constructor(entity: EntityTarget<E>) {
|
||||||
|
const baseRepository =
|
||||||
|
DatabaseProvider.getDatasource().getRepository<E>(entity);
|
||||||
|
super(
|
||||||
|
baseRepository.target,
|
||||||
|
baseRepository.manager,
|
||||||
|
baseRepository.queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async getPaginated(
|
||||||
|
{ page, size }: PaginationParam = { page: 1, size: 10 },
|
||||||
|
options: FindManyOptions<E> = {},
|
||||||
|
): Promise<Paginated<E>> {
|
||||||
|
page = parseInt(page.toString()) || 1;
|
||||||
|
size = parseInt(size.toString()) || 10;
|
||||||
|
const [items, totalItems] = await this.findAndCount({
|
||||||
|
skip: (page - 1) * size,
|
||||||
|
take: size,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const totalPages = Math.ceil(totalItems / size);
|
||||||
|
const totalSize = totalItems;
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
totalPages,
|
||||||
|
totalSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/backend/infrastructure/database/provider/index.ts
Normal file
25
src/backend/infrastructure/database/provider/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { dataSourceOptions } from './typeorm.config';
|
||||||
|
|
||||||
|
export class DatabaseProvider {
|
||||||
|
static datasource: NonNullable<DataSource>;
|
||||||
|
static getDatasource = () => {
|
||||||
|
if (!DatabaseProvider.datasource) {
|
||||||
|
DatabaseProvider.datasource = new DataSource(dataSourceOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DatabaseProvider.datasource;
|
||||||
|
};
|
||||||
|
static async initialize() {
|
||||||
|
const datasource = DatabaseProvider.getDatasource();
|
||||||
|
if (!datasource.isInitialized) {
|
||||||
|
await DatabaseProvider.getDatasource().initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async close() {
|
||||||
|
const datasource = DatabaseProvider.getDatasource();
|
||||||
|
datasource.manager.connection.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrmConfig = DatabaseProvider.getDatasource();
|
||||||
46
src/backend/infrastructure/database/provider/inject-db.ts
Normal file
46
src/backend/infrastructure/database/provider/inject-db.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { ObjectLiteral, Repository } from 'typeorm';
|
||||||
|
import { DatabaseProvider } from '.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* these functions do not related to the database connection
|
||||||
|
*/
|
||||||
|
const excludeFunctions = [
|
||||||
|
'createQueryBuilder',
|
||||||
|
'hasId',
|
||||||
|
'getId',
|
||||||
|
'create',
|
||||||
|
'merge',
|
||||||
|
'preload',
|
||||||
|
'recover',
|
||||||
|
'extend',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* since we are using typeorm, we need to initialize the database before
|
||||||
|
* any repository method is called. This decorator will inject the
|
||||||
|
* initialize method to all repository methods.
|
||||||
|
* @param target Repository class
|
||||||
|
*/
|
||||||
|
export const InjectInitializeDatabaseOnAllProps = <T extends ObjectLiteral>(
|
||||||
|
target: typeof Repository<T>,
|
||||||
|
) => {
|
||||||
|
const parentClass = Object.getPrototypeOf(target);
|
||||||
|
if (parentClass.name) {
|
||||||
|
InjectInitializeDatabaseOnAllProps(parentClass);
|
||||||
|
}
|
||||||
|
const properties = Object.getOwnPropertyDescriptors(target.prototype);
|
||||||
|
for (const name of Object.keys(properties)) {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(target.prototype, name);
|
||||||
|
const isMethod = descriptor?.value instanceof Function;
|
||||||
|
if (!isMethod) continue;
|
||||||
|
if (excludeFunctions.includes(name)) continue;
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
descriptor.value = async function (...args: any[]) {
|
||||||
|
await DatabaseProvider.initialize();
|
||||||
|
const result = await originalMethod.apply(this, args);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(target.prototype, name, descriptor);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class InitUsersDatabase1713768656950 implements MigrationInterface {
|
||||||
|
name = 'InitUsersDatabase1713768656950';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`email\` varchar(255) NOT NULL, \`username\` varchar(255) NOT NULL, \`password\` varchar(255) NOT NULL, \`emailConfirmedAt\` timestamp NULL, \`confirmationToken\` varchar(255) NOT NULL DEFAULT '', \`confirmationSentAt\` timestamp NULL, \`confirmedAt\` timestamp NULL, \`recoveryToken\` varchar(255) NULL, \`recoverySentAt\` timestamp NULL, \`lastSignInAt\` timestamp NULL, \`rawUserMetadata\` text NOT NULL DEFAULT '{}', \`phone\` varchar(255) NULL, \`phoneConfirmedAt\` timestamp NULL, \`createdAt\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`deletedAt\` timestamp(6) NULL, UNIQUE INDEX \`IDX_97672ac88f789774dd47f7c8be\` (\`email\`), UNIQUE INDEX \`IDX_fe0bb3f6520ee0469504521e71\` (\`username\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_fe0bb3f6520ee0469504521e71\` ON \`users\``,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX \`IDX_97672ac88f789774dd47f7c8be\` ON \`users\``,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE \`users\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/backend/infrastructure/database/provider/seeder.ts
Normal file
7
src/backend/infrastructure/database/provider/seeder.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { DatabaseProvider } from '.';
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
await DatabaseProvider.initialize();
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { DataSourceOptions } from 'typeorm';
|
||||||
|
import { config } from '@/config';
|
||||||
|
import { UserEntity } from '../user/user.entity';
|
||||||
|
import { join, resolve } from 'path';
|
||||||
|
|
||||||
|
export const dataSourceOptions: DataSourceOptions = {
|
||||||
|
type: config.db.type as any,
|
||||||
|
host: config.db.host,
|
||||||
|
port: config.db.port,
|
||||||
|
username: config.db.username,
|
||||||
|
password: config.db.password,
|
||||||
|
database: config.db.database,
|
||||||
|
entities: [UserEntity],
|
||||||
|
migrations: [resolve(join(__dirname, 'migrations/*.{ts,js}'))],
|
||||||
|
synchronize: false,
|
||||||
|
logger: 'simple-console',
|
||||||
|
poolSize: config.db.maxPoolConnection as number,
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
};
|
||||||
66
src/backend/infrastructure/database/user/user.entity.ts
Normal file
66
src/backend/infrastructure/database/user/user.entity.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Profile } from '@/src/backend/models/profile';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ name: 'users' })
|
||||||
|
export class UserEntity<T = Profile> {
|
||||||
|
constructor(partial: Partial<UserEntity>) {
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, unique: true })
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, unique: true })
|
||||||
|
username!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255 })
|
||||||
|
password!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
emailConfirmedAt?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, default: '' })
|
||||||
|
confirmationToken?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
confirmationSentAt?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
confirmedAt?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true})
|
||||||
|
recoveryToken!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
recoverySentAt?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
lastSignInAt?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '{}' })
|
||||||
|
rawUserMetadata?: T;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
phoneConfirmedAt?: Date;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamp' })
|
||||||
|
createdAt?: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamp' })
|
||||||
|
updatedAt?: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn({ type: 'timestamp', select: false })
|
||||||
|
deletedAt?: Date;
|
||||||
|
}
|
||||||
15
src/backend/infrastructure/database/user/user.repository.ts
Normal file
15
src/backend/infrastructure/database/user/user.repository.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { type EntityTarget } from 'typeorm';
|
||||||
|
import { UserEntity } from './user.entity';
|
||||||
|
import { BaseRepository } from '../provider/base.repository';
|
||||||
|
import { InjectInitializeDatabaseOnAllProps } from '../provider/inject-db';
|
||||||
|
|
||||||
|
@InjectInitializeDatabaseOnAllProps
|
||||||
|
export class UserRepository extends BaseRepository<UserEntity> {
|
||||||
|
static repository = new UserRepository();
|
||||||
|
static getRepository(): UserRepository {
|
||||||
|
return UserRepository.repository;
|
||||||
|
}
|
||||||
|
constructor(target: EntityTarget<UserEntity> = UserEntity) {
|
||||||
|
super(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/backend/middlewares/AuthGuard.ts
Normal file
20
src/backend/middlewares/AuthGuard.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { NextFunction, createMiddlewareDecorator } from 'next-api-decorators';
|
||||||
|
import { UserService } from '../domains/user.service';
|
||||||
|
import { COOKIE_SESSION_NAME } from '@/src/constants/auth';
|
||||||
|
|
||||||
|
export const SessionGuard = createMiddlewareDecorator(
|
||||||
|
async (req: NextApiRequest, res: NextApiResponse, next: NextFunction) => {
|
||||||
|
const session = req.cookies[COOKIE_SESSION_NAME];
|
||||||
|
|
||||||
|
const user = await UserService.getService().me(session as string);
|
||||||
|
if (!req.session) {
|
||||||
|
req.session = {} as any;
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
req.session.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
);
|
||||||
18
src/backend/models/profile.ts
Normal file
18
src/backend/models/profile.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export class Profile {
|
||||||
|
fullname?: string;
|
||||||
|
birthdate?: string;
|
||||||
|
familyName?: string;
|
||||||
|
gender?: string;
|
||||||
|
givenName?: string;
|
||||||
|
locale?: string;
|
||||||
|
middleName?: string;
|
||||||
|
name?: string;
|
||||||
|
nickname?: string;
|
||||||
|
picture?: string;
|
||||||
|
preferredUsername?: string;
|
||||||
|
website?: string;
|
||||||
|
address?: string;
|
||||||
|
avatar?: string;
|
||||||
|
description?: string;
|
||||||
|
job?: string;
|
||||||
|
}
|
||||||
1
src/constants/auth.ts
Normal file
1
src/constants/auth.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const COOKIE_SESSION_NAME = 'pz-fr-session'
|
||||||
47
src/pages/api/auth/[[...params]].ts
Normal file
47
src/pages/api/auth/[[...params]].ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
createHandler,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
Post,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
} from 'next-api-decorators';
|
||||||
|
import { LoginRequest } from './request';
|
||||||
|
import { UserService } from 'src/backend/domains/user.service';
|
||||||
|
import { type NextApiRequest, type NextApiResponse } from 'next';
|
||||||
|
import { SessionGuard } from 'src/backend/middlewares/AuthGuard';
|
||||||
|
import { COOKIE_SESSION_NAME } from 'src/constants/auth';
|
||||||
|
|
||||||
|
class AuthHandler {
|
||||||
|
@Post('/login')
|
||||||
|
@HttpCode(200)
|
||||||
|
async login(@Body() body: LoginRequest, @Res() res: NextApiResponse) {
|
||||||
|
const response = await UserService.getService().login(body);
|
||||||
|
res.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
`${COOKIE_SESSION_NAME}=${response.token}; Path=/; HttpOnly`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/me')
|
||||||
|
@SessionGuard()
|
||||||
|
@HttpCode(200)
|
||||||
|
async me(@Req() req: NextApiRequest) {
|
||||||
|
return req.session.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/logout')
|
||||||
|
@SessionGuard()
|
||||||
|
@HttpCode(200)
|
||||||
|
async logout(@Req() req: NextApiRequest, @Res() res: NextApiResponse) {
|
||||||
|
req.session.user = null;
|
||||||
|
res.setHeader('Set-Cookie', `${COOKIE_SESSION_NAME}=; Path=/; HttpOnly`);
|
||||||
|
|
||||||
|
return { message: 'Logout success' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createHandler(AuthHandler);
|
||||||
9
src/pages/api/auth/request.ts
Normal file
9
src/pages/api/auth/request.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginRequest {
|
||||||
|
@IsString()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
password!: string;
|
||||||
|
}
|
||||||
15
src/pages/api/users/[[...params]].ts
Normal file
15
src/pages/api/users/[[...params]].ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Body, createHandler, HttpCode, Post } from 'next-api-decorators';
|
||||||
|
import { UserService } from 'src/backend/domains/user.service';
|
||||||
|
import { CreateUserRequest } from './request';
|
||||||
|
|
||||||
|
class UserHandler {
|
||||||
|
@Post('/')
|
||||||
|
@HttpCode(200)
|
||||||
|
async create(@Body() body: CreateUserRequest) {
|
||||||
|
const response = await UserService.getService().create(body);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createHandler(UserHandler);
|
||||||
18
src/pages/api/users/request.ts
Normal file
18
src/pages/api/users/request.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateUserRequest {
|
||||||
|
@IsString()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
username!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
password!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
companyName?: string;
|
||||||
|
}
|
||||||
11
src/utils/await-to-error.ts
Normal file
11
src/utils/await-to-error.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const awaitToError = async <E = Error, T = any>(
|
||||||
|
p: Promise<T>,
|
||||||
|
): Promise<[E | null, T | null]> => {
|
||||||
|
try {
|
||||||
|
const r = await Promise.resolve<T>(p);
|
||||||
|
|
||||||
|
return [null, r];
|
||||||
|
} catch (e: any) {
|
||||||
|
return [e, null];
|
||||||
|
}
|
||||||
|
};
|
||||||
8
src/utils/hash.ts
Normal file
8
src/utils/hash.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
export const hashString = (inputString: string) => {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(inputString);
|
||||||
|
|
||||||
|
return hash.digest('hex');
|
||||||
|
};
|
||||||
12
src/utils/jwt.ts
Normal file
12
src/utils/jwt.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { config } from '@/config';
|
||||||
|
|
||||||
|
export const encode = (data: any) => {
|
||||||
|
return jwt.sign(data, config.jwtSecret, {
|
||||||
|
expiresIn: '1d',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validate = (token: string) => {
|
||||||
|
return jwt.verify(token, config.jwtSecret);
|
||||||
|
};
|
||||||
5
src/utils/request.ts
Normal file
5
src/utils/request.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const apiRequest = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
});
|
||||||
@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"ts-node": {
|
||||||
|
// these options are overrides used only by ts-node
|
||||||
|
// same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
@ -17,9 +24,13 @@
|
|||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./*"],
|
||||||
}
|
"src/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user