From a2f4c0040fce93071332a7c5f0a50ffa186bd69a Mon Sep 17 00:00:00 2001 From: Michael Maliar Date: Tue, 5 Dec 2023 22:34:02 +0200 Subject: [PATCH] fix: revert --- server/Docker/app.e2e.Dockerfile | 40 ++++ server/Docker/app.prod.Dockerfile | 39 ++++ server/Docker/cache.Dockerfile | 17 ++ server/Docker/db-prepare.Dockerfile | 24 +++ server/Docker/docker-compose.yaml | 170 ++++++++++++++++ server/Docker/maildev.Dockerfile | 5 + server/sh-scripts/compose.sh | 117 +++++++++++ server/sh-scripts/startup.sh | 35 ++++ server/sh-scripts/wait-for-it.sh | 182 ++++++++++++++++++ .../migrations/1696830395691-CreateUser.ts | 117 +++++++++++ .../modules/forgot/entities/forgot.entity.ts | 35 ++++ server/src/modules/forgot/forgot.module.ts | 11 ++ server/src/modules/forgot/forgot.service.ts | 34 ++++ server/src/modules/users/dto/find-user.dto.ts | 48 +++++ 14 files changed, 874 insertions(+) create mode 100644 server/Docker/app.e2e.Dockerfile create mode 100644 server/Docker/app.prod.Dockerfile create mode 100644 server/Docker/cache.Dockerfile create mode 100644 server/Docker/db-prepare.Dockerfile create mode 100644 server/Docker/docker-compose.yaml create mode 100644 server/Docker/maildev.Dockerfile create mode 100644 server/sh-scripts/compose.sh create mode 100644 server/sh-scripts/startup.sh create mode 100755 server/sh-scripts/wait-for-it.sh create mode 100644 server/src/libs/database/migrations/1696830395691-CreateUser.ts create mode 100644 server/src/modules/forgot/entities/forgot.entity.ts create mode 100644 server/src/modules/forgot/forgot.module.ts create mode 100644 server/src/modules/forgot/forgot.service.ts create mode 100644 server/src/modules/users/dto/find-user.dto.ts diff --git a/server/Docker/app.e2e.Dockerfile b/server/Docker/app.e2e.Dockerfile new file mode 100644 index 000000000..55384671e --- /dev/null +++ b/server/Docker/app.e2e.Dockerfile @@ -0,0 +1,40 @@ +ARG COMPOSE_PROJECT_NAME +FROM ${COMPOSE_PROJECT_NAME}-cache as cache + +FROM node:18 as build +ARG WORKDIR_FLOW +ARG COMPOSE_PROJECT_NAME +WORKDIR ${WORKDIR_FLOW} + +COPY --from=cache /tmp/node_modules ./node_modules +COPY . . + +RUN if [ ! -f .env ]; then cp ./env-example .env; fi +RUN sed -i -e 's/^DATABASE_HOST.*/DATABASE_HOST='${COMPOSE_PROJECT_NAME}'-postgres/' -e 's/^MAIL_HOST.*/MAIL_HOST='${COMPOSE_PROJECT_NAME}'-maildev/' .env + +COPY ./package*.json . +COPY ./tsconfig.build.json . + +RUN yarn run build && ls dist + +FROM node:18-alpine as release +MAINTAINER Pikj [Jreydman] Reyderman + +ARG WORKDIR_FLOW +ARG APP_PORT +WORKDIR ${WORKDIR_FLOW} + +COPY --from=build ${WORKDIR_FLOW} . +#COPY --from=build ${WORKDIR_FLOW}/node_modules ./node_modules +#COPY --from=build ${WORKDIR_FLOW}/.env . +#COPY --from=build ${WORKDIR_FLOW}/dist ./dist +#COPY --from=build ${WORKDIR_FLOW}/package*.json . + +EXPOSE ${APP_PORT} + +RUN apk add --no-cache bash +COPY ./sh-scripts/startup.sh /opt/startup.sh +COPY ./sh-scripts/wait-for-it.sh /opt/wait-for-it.sh +RUN chmod +x /opt/wait-for-it.sh && chmod +x /opt/startup.sh + +RUN sed -i 's/\r//g' /opt/wait-for-it.sh && sed -i 's/\r//g' /opt/startup.sh diff --git a/server/Docker/app.prod.Dockerfile b/server/Docker/app.prod.Dockerfile new file mode 100644 index 000000000..2012bca11 --- /dev/null +++ b/server/Docker/app.prod.Dockerfile @@ -0,0 +1,39 @@ +ARG COMPOSE_PROJECT_NAME +FROM ${COMPOSE_PROJECT_NAME}-cache as cache + +FROM node:18 as build +ARG WORKDIR_FLOW +WORKDIR ${WORKDIR_FLOW} + +COPY --from=cache /tmp/node_modules ./node_modules +COPY . . + +RUN if [ ! -f .env ]; then cp ./env-example .env; fi +RUN sed -i -e 's/^DATABASE_HOST.*/DATABASE_HOST='${COMPOSE_PROJECT_NAME}'-postgres/' -e 's/^MAIL_HOST.*/MAIL_HOST='${COMPOSE_PROJECT_NAME}'-maildev/' .env + + +COPY ./package*.json . +COPY ./tsconfig.build.json . + +RUN yarn run build && ls dist + +FROM node:18-alpine as release +MAINTAINER Pikj [Jreydman] Reyderman + +ARG WORKDIR_FLOW +ARG APP_PORT +WORKDIR ${WORKDIR_FLOW} + +COPY --from=build ${WORKDIR_FLOW}/node_modules ./node_modules +COPY --from=build ${WORKDIR_FLOW}/.env . +COPY --from=build ${WORKDIR_FLOW}/dist ./dist +COPY --from=build ${WORKDIR_FLOW}/package*.json . + +EXPOSE ${APP_PORT} + +RUN apk add --no-cache bash +COPY ./sh-scripts/startup.sh /opt/startup.sh +COPY ./sh-scripts/wait-for-it.sh /opt/wait-for-it.sh +RUN chmod +x /opt/wait-for-it.sh && chmod +x /opt/startup.sh + +RUN sed -i 's/\r//g' /opt/wait-for-it.sh && sed -i 's/\r//g' /opt/startup.sh diff --git a/server/Docker/cache.Dockerfile b/server/Docker/cache.Dockerfile new file mode 100644 index 000000000..292fbc68d --- /dev/null +++ b/server/Docker/cache.Dockerfile @@ -0,0 +1,17 @@ +FROM node:18 as build +WORKDIR /tmp +MAINTAINER Pikj [Jreydman] Reyderman + +COPY ["package*.json", "yarn.lock", ".yarnrc.yml", "./"] +COPY .yarn ./.yarn + +RUN yarn install --immutable + +FROM alpine as release +WORKDIR /tmp + +COPY --from=build /tmp/node_modules ./node_modules + + + + diff --git a/server/Docker/db-prepare.Dockerfile b/server/Docker/db-prepare.Dockerfile new file mode 100644 index 000000000..6e24abff3 --- /dev/null +++ b/server/Docker/db-prepare.Dockerfile @@ -0,0 +1,24 @@ +ARG COMPOSE_PROJECT_NAME +FROM ${COMPOSE_PROJECT_NAME}-cache as cache + +FROM node:18-alpine as release +ARG COMPOSE_PROJECT_NAME +MAINTAINER Pikj [Jreydman] Reyderman +ARG WORKDIR_FLOW +WORKDIR ${WORKDIR_FLOW} +ARG APP_PORT + +COPY --from=cache /tmp/node_modules ./node_modules + +COPY ["package*.json", "tsconfig*.json", ".env", "./"] + +RUN sed -i -e 's/^DATABASE_HOST.*/DATABASE_HOST='${COMPOSE_PROJECT_NAME}'-postgres/' -e 's/^MAIL_HOST.*/MAIL_HOST='${COMPOSE_PROJECT_NAME}'-maildev/' .env + +COPY ./src ./src + +EXPOSE ${APP_PORT} + +RUN apk add --no-cache bash +COPY ./sh-scripts/startup.sh /opt/startup.sh +COPY ./sh-scripts/wait-for-it.sh /opt/wait-for-it.sh +RUN chmod +x /opt/wait-for-it.sh && chmod +x /opt/startup.sh diff --git a/server/Docker/docker-compose.yaml b/server/Docker/docker-compose.yaml new file mode 100644 index 000000000..5cba4f48a --- /dev/null +++ b/server/Docker/docker-compose.yaml @@ -0,0 +1,170 @@ +version: '3.9' + +# profiles: +# - virtual-dev +# - local-dev +# - virtual-prod +# - local-prod +# - virtual-ci +# - local-ci + +x-common-context: &common-context + context: ../ + +x-common-args: &common-args + args: + - WORKDIR_FLOW=$SERVER_PATH_CONTAINER + - APP_PORT=$APP_PORT + - COMPOSE_PROJECT_NAME=$COMPOSE_PROJECT_NAME + +x-common-config: &common-config + env_file: + - ../.env + +x-common-healthcheck: &common-healthcheck + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +x-common-api: &common-api + container_name: ${COMPOSE_PROJECT_NAME}-api + <<: *common-config + restart: unless-stopped + expose: + - ${APP_PORT} + ports: + - ${APP_PORT}:${APP_PORT} + depends_on: + db-prepare: + condition: service_completed_successfully + postgres: + condition: service_healthy + maildev: + condition: service_healthy + + +services: + + postgres: #service + profiles: + - virtual-dev + - local-dev + - virtual-prod + - local-prod + - virtual-ci + - local-ci + container_name: ${COMPOSE_PROJECT_NAME}-postgres + image: postgres:16.0-alpine + <<: *common-config + expose: + - ${DATABASE_PORT} + restart: unless-stopped + environment: + POSTGRES_USER: ${DATABASE_USERNAME} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} +# volumes: #production-toggle +# - db-data:${DATABASE_PATH_CONTAINER}/data #production-toggle + healthcheck: + <<: *common-healthcheck + test: [ "CMD-SHELL", "pg_isready -d ${DATABASE_NAME} -U ${DATABASE_USERNAME}" ] + ports: #stage-toggle + - ${DATABASE_PORT}:${DATABASE_PORT} #stage-toggle + #endservice + + maildev: #service + container_name: ${COMPOSE_PROJECT_NAME}-maildev + profiles: + - virtual-dev + - local-dev + - virtual-prod + - local-prod + - virtual-ci + - local-ci + restart: unless-stopped + build: + <<: *common-context + dockerfile: ${DOCKER_PATH_LOCAL}/maildev.Dockerfile + healthcheck: + <<: *common-healthcheck + test: [ "CMD-SHELL", "wget -O - http://localhost:${MAIL_CLIENT_PORT}/healthz || exit 1" ] + expose: + - ${MAIL_CLIENT_PORT} + - ${MAIL_PORT} + ports: + - ${MAIL_PORT}:${MAIL_PORT} #stage-toggle + - ${MAIL_CLIENT_PORT}:${MAIL_CLIENT_PORT} + #endservice + + db-prepare: + profiles: + - virtual-dev + - local-dev + - virtual-prod + - local-prod + - virtual-ci + - local-ci + container_name: ${COMPOSE_PROJECT_NAME}-db-prepare + entrypoint: [ "/opt/startup.sh", "db-prepare" ] + <<: *common-config + build: + <<: [*common-context, *common-args] + dockerfile: ${DOCKER_PATH_LOCAL}/db-prepare.Dockerfile + environment: + - NODE_ENV=development + - DATABASE_HOST=${COMPOSE_PROJECT_NAME}-postgres + depends_on: + postgres: + condition: service_healthy + + api-development: + profiles: + - virtual-dev + <<: *common-api + image: node:18-alpine + working_dir: ${SERVER_PATH_CONTAINER} + command: [ "yarn", "start:dev" ] + environment: + - NODE_ENV=development + - DATABASE_HOST=${COMPOSE_PROJECT_NAME}-postgres + - MAIL_HOST=${COMPOSE_PROJECT_NAME}-maildev + volumes: + - ../:${SERVER_PATH_CONTAINER} + + + api-production: + profiles: + - virtual-prod + <<: [*common-api, *common-config] + restart: on-failure + command: ["/opt/startup.sh", "prod"] + build: + <<: [*common-context, *common-args] + dockerfile: ${DOCKER_PATH_LOCAL}/app.prod.Dockerfile + environment: + - NODE_ENV=production + - DATABASE_HOST=${COMPOSE_PROJECT_NAME}-postgres + - MAIL_HOST=${COMPOSE_PROJECT_NAME}-maildev + + api-ci: + profiles: + - virtual-ci + <<: [ *common-api, *common-config ] + restart: on-failure + command: [ "/opt/startup.sh", "ci" ] + build: + <<: [ *common-context, *common-args ] + dockerfile: ${DOCKER_PATH_LOCAL}/app.e2e.Dockerfile + environment: + - NODE_ENV=production + - DATABASE_HOST=${COMPOSE_PROJECT_NAME}-postgres + - MAIL_HOST=${COMPOSE_PROJECT_NAME}-maildev + +volumes: + db-data: + driver: local + driver_opts: + type: none + o: bind + device: ../../database/data diff --git a/server/Docker/maildev.Dockerfile b/server/Docker/maildev.Dockerfile new file mode 100644 index 000000000..4b4035d31 --- /dev/null +++ b/server/Docker/maildev.Dockerfile @@ -0,0 +1,5 @@ +FROM node:18.17.1-alpine + +RUN npm i -g maildev@2.0.5 + +CMD maildev diff --git a/server/sh-scripts/compose.sh b/server/sh-scripts/compose.sh new file mode 100644 index 000000000..91bb55d22 --- /dev/null +++ b/server/sh-scripts/compose.sh @@ -0,0 +1,117 @@ +#!/bin/bash +set -e + +if sed --version > /dev/null 2>&1; then + echo "Info! Setting [sed] command style: GNU" + sed_command="sed -i" +else + echo "Info! Setting [sed] command style: BSD" + sed_command="sed -i ''" +fi + +if [ -f /.dockerenv ]; then + PARENT_DIR=$SERVER_PATH_CONTAINER +else + PARENT_DIR=$(dirname "$(dirname "$(realpath "$0")")") +fi + +if [ ! -f .env ]; then + echo "Error! .env file wasn't fount in project [server] directory" \ + "$PARENT_DIR"/.env + return 0 +fi +source "$PARENT_DIR"/.env + +if [ -f /.dockerenv ]; then + export DATABASE_HOST="$COMPOSE_PROJECT_NAME-postgres" + export MAIL_HOST="$COMPOSE_PROJECT_NAME-maildev" +else + export DATABASE_HOST="localhost" + export MAIL_HOST="localhost" +fi + +get_arg_value() { + for arg in "$@"; do + case $arg in + "$1="*) + echo "${arg#*=}" + return + ;; + esac + done +} + +stage=$(get_arg_value "stage" "$@") +if [ -z "$stage" ]; then echo "Warning! [stage] wasn't found, set default: localhost"; stage="local"; fi + +type=$(get_arg_value "type" "$@") +if [ -z "$type" ]; then echo "Warning! [type] wasn't found, set default: dev"; type="dev"; fi + +cache=$(get_arg_value "cache" "$@") +if [ -z "$cache" ] && [ "$stage" = "docker" ] && [ "$type" != "dev" ]; then echo "Warning! [cache] wasn't found, set default: true (preferred for virtual override)"; cache=true; fi +if [ -z "$cache" ] && [ "$stage" = "docker" ] && [ "$type" = "dev" ]; then echo "Warning! [cache] wasn't found, set default: false (mount volumes don't need it)"; cache=false; fi +if [ -z "$cache" ] && [ "$stage" = "local" ]; then echo "Warning! [type] wasn't found, set default: false"; cache=false; fi + +echo "Info! Type: $type | Staging: $stage | Caching: $cache" + +if [ -z "$(docker images -q "$COMPOSE_PROJECT_NAME"-cache)" ]; then + echo "Info! Creating cache by file" "$PARENT_DIR/$DOCKER_PATH_LOCAL"/cache.Dockerfile "with tag: " "$COMPOSE_PROJECT_NAME"-cache + docker build . -f "$PARENT_DIR/$DOCKER_PATH_LOCAL"/cache.Dockerfile -t "$COMPOSE_PROJECT_NAME"-cache + echo "Info! Finish cache create" +else + echo "Info! CACHE IMAGE IS ALREADY EXISTS. ITS SEEMS YOU MAY REBASE IT" +fi + +sleep 3 +reverse_stage_toggle() { + if [ "$1" = "local" ]; then + echo "Info! Setup docker-compose stage-toggle " + $sed_command -e '/postgres: #service/,/#endservice/ {;s/# \(.*\)#stage-toggle/\1#stage-toggle/;}' -e '/maildev: #service/,/#endservice/ {;s/# \(.*\)#stage-toggle/\1#stage-toggle/;}' "$PARENT_DIR"/"$DOCKER_PATH_LOCAL"/docker-compose.yaml + echo "Info! Finish edit docker-compose" + + fi + if [ "$1" = "virtual" ]; then + echo "Info! Setup docker-compose stage-toggle " + $sed_command -e '/postgres: #service/,/#endservice/ {;s/\(.*\)#stage-toggle/# \1#stage-toggle/;}' -e '/maildev: #service/,/#endservice/ {;s/\(.*\)#stage-toggle/# \1#stage-toggle/;}' "$PARENT_DIR/$DOCKER_PATH_LOCAL"/docker-compose.yaml + echo "Info! Finish edit docker-compose" + fi + $sed_command -e 's/# # /# /' "$PARENT_DIR/$DOCKER_PATH_LOCAL"/docker-compose.yaml +} + +reverse_production_toggle() { + if [ "$1" = "prod" ]; then + echo "Info! Setup docker-compose production-toggle " + $sed_command -e '/postgres: #service/,/#endservice/ {;s/# \(.*\)#production-toggle/\1#production-toggle/;}' "$PARENT_DIR"/"$DOCKER_PATH_LOCAL"/docker-compose.yaml + echo "Info! Upload database dir into [] catalog: ./database/data:init" + mkdir -p ../database/data + mkdir -p ../database/init + echo "Info! Finish edit docker-compose" + else + echo "Info! Setup docker-compose production-toggle " + $sed_command -e '/postgres: #service/,/#endservice/ {;s/\(.*\)#production-toggle/# \1#production-toggle/;}' "$PARENT_DIR/$DOCKER_PATH_LOCAL"/docker-compose.yaml + echo "Info! Finish edit docker-compose" + fi + $sed_command -e 's/# # /# /' "$PARENT_DIR/$DOCKER_PATH_LOCAL"/docker-compose.yaml +} +reverse_production_toggle $type +case $stage in + local) + echo "Step! Running local staging..." + reverse_stage_toggle "local" + echo "Info! Running docker" + (docker-compose -f "$PARENT_DIR/$DOCKER_PATH_LOCAL"/docker-compose.yaml --env-file .env --profile $stage-$type up -d) & + process=$! + wait $process + /bin/bash "$PARENT_DIR/$SHSCRIPT_PATH_LOCAL"/wait-for-it.sh localhost:5432 + echo "Info! Prepare local values" + yarn install + echo "Info! Running server in local" + /bin/bash "$PARENT_DIR/$SHSCRIPT_PATH_LOCAL"/startup.sh $type + ;; + virtual) + echo "Step! Running docker staging..." + reverse_stage_toggle "virtual" + echo "Info! Running docker" + docker-compose -f "$PARENT_DIR/$DOCKER_PATH_LOCAL"/docker-compose.yaml --env-file .env --profile $stage-$type up -d + ;; +esac diff --git a/server/sh-scripts/startup.sh b/server/sh-scripts/startup.sh new file mode 100644 index 000000000..b2f75e1f9 --- /dev/null +++ b/server/sh-scripts/startup.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -e + +if [ -f /.dockerenv ]; then + PARENT_DIR=$SERVER_PATH_CONTAINER + SHSCRIPTS_PATH_LOCAL=/opt +else + PARENT_DIR=$(dirname "$(dirname "$(realpath "$0")")") + SHSCRIPTS_PATH_LOCAL=$PARENT_DIR/sh-scripts +fi + +echo "Info! startup: " "$PARENT_DIR" "$SHSCRIPTS_PATH_LOCAL" + +if [ "$1" == "ci" ] ; then + yarn run start:production > /dev/null 2>&1 & + /bin/bash "$SHSCRIPTS_PATH_LOCAL"/wait-for-it.sh localhost:3001 + yarn run lint + yarn run test:e2e --runInBand +fi + +if [ "$1" == "prod" ] ; then + yarn run start:prod +fi + +if [ "$1" == "dev" ] ; then + yarn run start:dev +fi + +if [ "$1" == "db-prepare" ] ; then + yarn migration:run + yarn seed:run +fi + +echo false diff --git a/server/sh-scripts/wait-for-it.sh b/server/sh-scripts/wait-for-it.sh new file mode 100755 index 000000000..d990e0d36 --- /dev/null +++ b/server/sh-scripts/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/server/src/libs/database/migrations/1696830395691-CreateUser.ts b/server/src/libs/database/migrations/1696830395691-CreateUser.ts new file mode 100644 index 000000000..49022791a --- /dev/null +++ b/server/src/libs/database/migrations/1696830395691-CreateUser.ts @@ -0,0 +1,117 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateUser1696830395691 implements MigrationInterface { + name = 'CreateUser1696830395691'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "role" ("id" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "status" ("id" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_e12743a7086ec826733f54e1d95" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "file" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "path" character varying NOT NULL, CONSTRAINT "PK_36b46d232307066b3a2c9ea3a1d" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "universities" ("id" SERIAL NOT NULL, "university" character varying NOT NULL, "degree" character varying NOT NULL, "major" character varying NOT NULL, "admissionDate" date NOT NULL, "graduationDate" date, "userId" integer, CONSTRAINT "PK_8da52f2cee6b407559fdbabf59e" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "jobs" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "company" character varying NOT NULL, "startDate" date NOT NULL, "endDate" date, "userId" integer, CONSTRAINT "PK_cf0a6c42b72fcc7f7c237def345" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "projects" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "link" character varying NOT NULL, "userId" integer, CONSTRAINT "PK_6271df0a7aed1d6c0691ce6ac50" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "links" ("id" SERIAL NOT NULL, "github" character varying, "linkedIn" character varying, "behance" character varying, "telegram" character varying, CONSTRAINT "PK_ecf17f4a741d3c5ba0b4c5ab4b6" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying, "password" character varying, "username" character varying, "provider" character varying NOT NULL DEFAULT 'email', "socialId" character varying, "fullName" character varying, "hash" character varying, "isLeader" boolean, "country" character varying, "dateOfBirth" date, "concentration" character varying, "description" character varying, "experience" character varying, "programmingLanguages" text array, "frameworks" text array, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "photoId" uuid, "roleId" integer, "statusId" integer, "linksId" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "REL_c5a79824fd8a241f5a7ec428b3" UNIQUE ("linksId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9bd2fe7a8e694dedc4ec2f666f" ON "user" ("socialId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_035190f70c9aff0ef331258d28" ON "user" ("fullName") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_e282acb94d2e3aec10f480e4f6" ON "user" ("hash") `); + await queryRunner.query( + `CREATE INDEX "IDX_8bceb9ec5c48c54f7a3f11f31b" ON "user" ("isLeader") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_5cb2b3e0419a73a360d327d497" ON "user" ("country") `); + await queryRunner.query( + `CREATE TABLE "session" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "userId" integer, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_3d2f174ef04fb312fdebd0ddc5" ON "session" ("userId") ` + ); + await queryRunner.query( + `CREATE TABLE "forgot" ("id" SERIAL NOT NULL, "hash" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "userId" integer, CONSTRAINT "PK_087959f5bb89da4ce3d763eab75" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "IDX_df507d27b0fb20cd5f7bef9b9a" ON "forgot" ("hash") `); + await queryRunner.query( + `ALTER TABLE "universities" ADD CONSTRAINT "FK_a8ad75b47a153c0d91f8360c9fb" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "jobs" ADD CONSTRAINT "FK_79ae682707059d5f7655db4212a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "projects" ADD CONSTRAINT "FK_361a53ae58ef7034adc3c06f09f" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "user" ADD CONSTRAINT "FK_75e2be4ce11d447ef43be0e374f" FOREIGN KEY ("photoId") REFERENCES "file"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "user" ADD CONSTRAINT "FK_c28e52f758e7bbc53828db92194" FOREIGN KEY ("roleId") REFERENCES "role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "user" ADD CONSTRAINT "FK_dc18daa696860586ba4667a9d31" FOREIGN KEY ("statusId") REFERENCES "status"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "user" ADD CONSTRAINT "FK_c5a79824fd8a241f5a7ec428b3e" FOREIGN KEY ("linksId") REFERENCES "links"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "session" ADD CONSTRAINT "FK_3d2f174ef04fb312fdebd0ddc53" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "forgot" ADD CONSTRAINT "FK_31f3c80de0525250f31e23a9b83" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "forgot" DROP CONSTRAINT "FK_31f3c80de0525250f31e23a9b83"` + ); + await queryRunner.query( + `ALTER TABLE "session" DROP CONSTRAINT "FK_3d2f174ef04fb312fdebd0ddc53"` + ); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_c5a79824fd8a241f5a7ec428b3e"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_dc18daa696860586ba4667a9d31"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_c28e52f758e7bbc53828db92194"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_75e2be4ce11d447ef43be0e374f"`); + await queryRunner.query( + `ALTER TABLE "projects" DROP CONSTRAINT "FK_361a53ae58ef7034adc3c06f09f"` + ); + await queryRunner.query(`ALTER TABLE "jobs" DROP CONSTRAINT "FK_79ae682707059d5f7655db4212a"`); + await queryRunner.query( + `ALTER TABLE "universities" DROP CONSTRAINT "FK_a8ad75b47a153c0d91f8360c9fb"` + ); + await queryRunner.query(`DROP INDEX "public"."IDX_df507d27b0fb20cd5f7bef9b9a"`); + await queryRunner.query(`DROP TABLE "forgot"`); + await queryRunner.query(`DROP INDEX "public"."IDX_3d2f174ef04fb312fdebd0ddc5"`); + await queryRunner.query(`DROP TABLE "session"`); + await queryRunner.query(`DROP INDEX "public"."IDX_5cb2b3e0419a73a360d327d497"`); + await queryRunner.query(`DROP INDEX "public"."IDX_8bceb9ec5c48c54f7a3f11f31b"`); + await queryRunner.query(`DROP INDEX "public"."IDX_e282acb94d2e3aec10f480e4f6"`); + await queryRunner.query(`DROP INDEX "public"."IDX_035190f70c9aff0ef331258d28"`); + await queryRunner.query(`DROP INDEX "public"."IDX_9bd2fe7a8e694dedc4ec2f666f"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`DROP TABLE "links"`); + await queryRunner.query(`DROP TABLE "projects"`); + await queryRunner.query(`DROP TABLE "jobs"`); + await queryRunner.query(`DROP TABLE "universities"`); + await queryRunner.query(`DROP TABLE "file"`); + await queryRunner.query(`DROP TABLE "status"`); + await queryRunner.query(`DROP TABLE "role"`); + } +} diff --git a/server/src/modules/forgot/entities/forgot.entity.ts b/server/src/modules/forgot/entities/forgot.entity.ts new file mode 100644 index 000000000..85f023866 --- /dev/null +++ b/server/src/modules/forgot/entities/forgot.entity.ts @@ -0,0 +1,35 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, + DeleteDateColumn, +} from 'typeorm'; +import { User } from 'src/modules/users/entities/user.entity'; +import { Allow } from 'class-validator'; +import { EntityHelper } from 'src/utils/entity-helper'; + +@Entity() +export class Forgot extends EntityHelper { + @PrimaryGeneratedColumn() + id: number; + + @Allow() + @Column() + @Index() + hash: string; + + @Allow() + @ManyToOne(() => User, { + eager: true, + }) + user: User; + + @CreateDateColumn() + createdAt: Date; + + @DeleteDateColumn() + deletedAt: Date; +} diff --git a/server/src/modules/forgot/forgot.module.ts b/server/src/modules/forgot/forgot.module.ts new file mode 100644 index 000000000..e2d6d99a8 --- /dev/null +++ b/server/src/modules/forgot/forgot.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Forgot } from './entities/forgot.entity'; +import { ForgotService } from './forgot.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Forgot])], + providers: [ForgotService], + exports: [ForgotService], +}) +export class ForgotModule {} diff --git a/server/src/modules/forgot/forgot.service.ts b/server/src/modules/forgot/forgot.service.ts new file mode 100644 index 000000000..9d50d9f5f --- /dev/null +++ b/server/src/modules/forgot/forgot.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FindOptions } from 'src/utils/types/find-options.type'; +import { DeepPartial, Repository } from 'typeorm'; +import { Forgot } from './entities/forgot.entity'; +import { NullableType } from 'src/utils/types/nullable.type'; + +@Injectable() +export class ForgotService { + constructor( + @InjectRepository(Forgot) + private readonly forgotRepository: Repository + ) {} + + async findOne(options: FindOptions): Promise> { + return this.forgotRepository.findOne({ + where: options.where, + }); + } + + async findMany(options: FindOptions): Promise { + return this.forgotRepository.find({ + where: options.where, + }); + } + + async create(data: DeepPartial): Promise { + return this.forgotRepository.save(this.forgotRepository.create(data)); + } + + async softDelete(id: Forgot['id']): Promise { + await this.forgotRepository.softDelete(id); + } +} diff --git a/server/src/modules/users/dto/find-user.dto.ts b/server/src/modules/users/dto/find-user.dto.ts new file mode 100644 index 000000000..e4c0c9a69 --- /dev/null +++ b/server/src/modules/users/dto/find-user.dto.ts @@ -0,0 +1,48 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayNotEmpty, IsIn, IsNotEmpty, IsOptional } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { lowerCaseTransformer } from '../../../utils/transformers/lower-case.transformer'; + +export class FindUserDto { + @ApiProperty() + @IsOptional() + @IsNotEmpty({ message: 'mustBeNotEmpty' }) + fullName?: string; + + @ApiProperty({ example: 'nmashchenko' }) + @Transform(lowerCaseTransformer) + @IsNotEmpty() + @IsOptional() + username?: string; + + @ApiProperty() + @IsOptional() + @IsNotEmpty({ message: 'mustBeNotEmpty' }) + isLeader?: boolean; + + @ApiProperty() + @IsOptional() + @IsNotEmpty({ message: 'mustBeNotEmpty' }) + country?: string; + + @ApiProperty() + @IsOptional() + @IsNotEmpty({ message: 'mustBeNotEmpty' }) + concentration?: string; + + @ApiProperty({ enum: ['0-1 years', '1-3 years', '3-5 years', '5+ years'] }) + @IsOptional() + @IsNotEmpty({ message: 'mustBeNotEmpty' }) + @IsIn(['beginner', 'intermediate', 'advanced'], { message: 'mustBeValidExperience' }) + experience?: '0-1 years' | '1-3 years' | '3-5 years' | '5+ years'; + + @ApiProperty() + @IsOptional() + @ArrayNotEmpty({ message: 'mustBeNotEmpty' }) + programmingLanguages?: string[]; + + @ApiProperty() + @IsOptional() + @ArrayNotEmpty({ message: 'mustBeNotEmpty' }) + frameworks?: string[]; +}