initial commit
This commit is contained in:
commit
5e5aff990d
16 changed files with 1737 additions and 0 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
9
.env
Normal file
9
.env
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Этот файл содержит переменные окружения для локального запуска.
|
||||||
|
# Реальные пароли и другие секреты заменяются при развертывании.
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_DB=motd
|
||||||
|
POSTGRES_USER=user
|
||||||
|
POSTGRES_PASSWORD=super_secure_password
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
.npmrc
|
||||||
|
.ssl/ca
|
||||||
|
.ssl/certs
|
||||||
|
dist/
|
||||||
42
.ssl/gen-ca.sh
Executable file
42
.ssl/gen-ca.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CA_DIR="${CA_DIR:-$(dirname $0)/ca}"
|
||||||
|
CA_KEY="${CA_DIR}/ca.key"
|
||||||
|
CA_CERT="${CA_DIR}/ca.pem"
|
||||||
|
|
||||||
|
CA_SUBJECT="${CA_SUBJECT:-/C=UZ/O=Local CA/CN=Local Root CA}"
|
||||||
|
CA_DAYS="${CA_DAYS:-3650}"
|
||||||
|
CA_KEY_BITS="${CA_KEY_BITS:-4096}"
|
||||||
|
|
||||||
|
mkdir -p "${CA_DIR}"
|
||||||
|
chmod 700 "${CA_DIR}"
|
||||||
|
|
||||||
|
if [[ -f "${CA_KEY}" || -f "${CA_CERT}" ]]; then
|
||||||
|
echo "CA already exists:"
|
||||||
|
echo " ${CA_KEY}"
|
||||||
|
echo " ${CA_CERT}"
|
||||||
|
echo "Nothing to do."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
openssl genrsa -out "${CA_KEY}" "${CA_KEY_BITS}"
|
||||||
|
chmod 600 "${CA_KEY}"
|
||||||
|
|
||||||
|
openssl req \
|
||||||
|
-x509 \
|
||||||
|
-new \
|
||||||
|
-nodes \
|
||||||
|
-key "${CA_KEY}" \
|
||||||
|
-sha256 \
|
||||||
|
-days "${CA_DAYS}" \
|
||||||
|
-subj "${CA_SUBJECT}" \
|
||||||
|
-out "${CA_CERT}"
|
||||||
|
|
||||||
|
chmod 644 "${CA_CERT}"
|
||||||
|
|
||||||
|
echo "CA created:"
|
||||||
|
echo " key: ${CA_KEY}"
|
||||||
|
echo " cert: ${CA_CERT}"
|
||||||
|
|
||||||
|
openssl x509 -in "${CA_CERT}" -noout -subject -issuer -dates
|
||||||
116
.ssl/gen-cert.sh
Executable file
116
.ssl/gen-cert.sh
Executable file
|
|
@ -0,0 +1,116 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
CA_DIR="${CA_DIR:-$(dirname $0)/ca}"
|
||||||
|
CERTS_DIR="${CERTS_DIR:-$(dirname $0)/certs}"
|
||||||
|
|
||||||
|
CA_KEY="${CA_DIR}/ca.key"
|
||||||
|
CA_CERT="${CA_DIR}/ca.pem"
|
||||||
|
|
||||||
|
NAME="${1:-}"
|
||||||
|
DAYS="${DAYS:-825}"
|
||||||
|
KEY_BITS="${KEY_BITS:-2048}"
|
||||||
|
|
||||||
|
if [ -z "${NAME}" ]; then
|
||||||
|
echo "Usage:"
|
||||||
|
echo " $0 <name> [san1 san2 ...]"
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 api.local api.local localhost 127.0.0.1"
|
||||||
|
echo " $0 client-1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${CA_KEY}" ] || [ ! -f "${CA_CERT}" ]; then
|
||||||
|
echo "CA files not found. Run gen-ca.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
if [ -e "${CERTS_DIR}/${NAME}" ]; then
|
||||||
|
echo "Certificate for ${NAME} already exists."
|
||||||
|
echo "Nothing to do."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${CERTS_DIR}/${NAME}"
|
||||||
|
|
||||||
|
KEY="${CERTS_DIR}/${NAME}/${NAME}.key.pem"
|
||||||
|
CSR="${CERTS_DIR}/${NAME}/${NAME}.csr.pem"
|
||||||
|
CERT="${CERTS_DIR}/${NAME}/${NAME}.cert.pem"
|
||||||
|
EXT="${CERTS_DIR}/${NAME}/${NAME}.ext.cnf"
|
||||||
|
FULLCHAIN="${CERTS_DIR}/${NAME}/${NAME}.fullchain.pem"
|
||||||
|
|
||||||
|
SUBJECT="${SUBJECT:-/C=US/O=Localhost LLC/CN=${NAME}}"
|
||||||
|
|
||||||
|
cat > "${EXT}" <<EOF
|
||||||
|
[req]
|
||||||
|
distinguished_name = req_distinguished_name
|
||||||
|
req_extensions = v3_req
|
||||||
|
prompt = no
|
||||||
|
|
||||||
|
[req_distinguished_name]
|
||||||
|
CN = ${NAME}
|
||||||
|
|
||||||
|
[v3_req]
|
||||||
|
basicConstraints = CA:FALSE
|
||||||
|
keyUsage = critical, digitalSignature, keyEncipherment
|
||||||
|
extendedKeyUsage = serverAuth, clientAuth
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
|
||||||
|
[alt_names]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
i=1
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
for san in "$@"; do
|
||||||
|
case "${san}" in
|
||||||
|
*[!0-9.]* | .* | *.)
|
||||||
|
echo "DNS.${i} = ${san}" >> "${EXT}"
|
||||||
|
;;
|
||||||
|
*.*.*.*)
|
||||||
|
echo "IP.${i} = ${san}" >> "${EXT}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "DNS.${i} = ${san}" >> "${EXT}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "DNS.1 = ${NAME}" >> "${EXT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
openssl genrsa -out "${KEY}" "${KEY_BITS}"
|
||||||
|
chmod 600 "${KEY}"
|
||||||
|
|
||||||
|
openssl req \
|
||||||
|
-new \
|
||||||
|
-key "${KEY}" \
|
||||||
|
-subj "${SUBJECT}" \
|
||||||
|
-out "${CSR}"
|
||||||
|
|
||||||
|
openssl x509 \
|
||||||
|
-req \
|
||||||
|
-in "${CSR}" \
|
||||||
|
-CA "${CA_CERT}" \
|
||||||
|
-CAkey "${CA_KEY}" \
|
||||||
|
-CAcreateserial \
|
||||||
|
-out "${CERT}" \
|
||||||
|
-days "${DAYS}" \
|
||||||
|
-sha256 \
|
||||||
|
-extfile "${EXT}" \
|
||||||
|
-extensions v3_req
|
||||||
|
|
||||||
|
cat "${CERT}" "${CA_CERT}" > "${FULLCHAIN}"
|
||||||
|
|
||||||
|
chmod 644 "${CSR}" "${CERT}" "${EXT}" "${FULLCHAIN}"
|
||||||
|
|
||||||
|
echo "Certificate issued:"
|
||||||
|
echo " key: ${KEY}"
|
||||||
|
echo " csr: ${CSR}"
|
||||||
|
echo " cert: ${CERT}"
|
||||||
|
echo " fullchain: ${FULLCHAIN}"
|
||||||
|
|
||||||
|
openssl x509 -in "${CERT}" -noout -subject -issuer -dates -ext subjectAltName
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# --- Stage 1: Build ---
|
||||||
|
FROM node:22.22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# --- Stage 2: Run ---
|
||||||
|
FROM node:22.22-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/package.json ./package.json
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
54
README.md
Normal file
54
README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Simple MOTD Web App
|
||||||
|
Простенькое MOTD бекенд-приложение на веб-технологиях.
|
||||||
|
|
||||||
|
Предназначено для получения, передачи и сохранения MOTD (Message-of-the-Day).
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
|
||||||
|
Приложение и обвязка написаны на скорую руку, примерно за час. **Нюансы**:
|
||||||
|
- И базы, и приложение, и балансировщик записаны в одной композиции
|
||||||
|
- .env файл используется как для приложения, так и для контейнера с базой данных
|
||||||
|
- Генерация CA + пары сертификат-ключ происходят при сборке контейнера с приложением, один раз. При пересборке используются уже существующие сертификаты - в CI их можно подсунуть перед сборкой.
|
||||||
|
- Само приложение (только приложение - директория `src/`) сгенерировано нейронкой
|
||||||
|
- Никакого CI/CD
|
||||||
|
|
||||||
|
Чтобы этих нюансов не было - полагается трудоустройство, делать что-то полноценное за бесплатно я не собираюсь.
|
||||||
|
|
||||||
|
Стек включает в себя:
|
||||||
|
- Nginx
|
||||||
|
- PostgreSQL
|
||||||
|
- Redis
|
||||||
|
- Node.js (TypeScript)
|
||||||
|
|
||||||
|
## Сборка и запуск
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
- Подключение к интернету
|
||||||
|
- Незанятый порт 443
|
||||||
|
- Docker
|
||||||
|
|
||||||
|
1. Выполним команды:
|
||||||
|
```bash
|
||||||
|
docker run --rm -v ./.ssl:/ssl --entrypoint /bin/sh alpine/openssl -c "/ssl/gen-ca.sh && /ssl/gen-cert.sh localhost"
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
2. Перейдём к разделу [Использование](#использование).
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
Получить MOTD:
|
||||||
|
```bash
|
||||||
|
curl -k https://localhost/motd
|
||||||
|
```
|
||||||
|
|
||||||
|
Установить MOTD:
|
||||||
|
```bash
|
||||||
|
curl -k -X POST https://localhost/motd \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message":"Hello from MOTD app"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## P.S.
|
||||||
|
Честно говоря, тестовые задания - это развод гоев на бесплатную рабочую силу, сомнительные конторы их очень любят.<br>
|
||||||
|
В вашем случае я хотя бы увидел живого человека - обычно своё время на такое я не трачу.
|
||||||
63
docker-compose.yml
Normal file
63
docker-compose.yml
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: motd-postgres
|
||||||
|
env_file: .env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 15
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
container_name: motd-redis
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 15
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
|
|
||||||
|
app:
|
||||||
|
image: artifacts.siverov.com/library/motd-web-app
|
||||||
|
build: .
|
||||||
|
env_file: .env
|
||||||
|
container_name: motd-app
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "nc", "-vz", "::1", "3000" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 15
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: motd-nginx
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
- '443:443'
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_started
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
- ./.ssl/certs:/etc/nginx/certs:ro
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "nc", "-vz", "::1", "80" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 15
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data: {}
|
||||||
25
nginx.conf
Normal file
25
nginx.conf
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=10r/s;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/localhost/localhost.cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/localhost/localhost.key.pem;
|
||||||
|
ssl_trusted_certificate /etc/nginx/certs/localhost/localhost.fullchain.pem;
|
||||||
|
|
||||||
|
client_max_body_size 10m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://app:3000;
|
||||||
|
|
||||||
|
limit_req zone=per_ip nodelay;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "motd-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Simple MOTD app with TypeScript, Redis, and Postgres",
|
||||||
|
"author": "Roman Siverov",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsx src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"pg": "^8.12.0",
|
||||||
|
"redis": "^4.6.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/node": "^22.7.4",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
|
"tsx": "^4.19.1",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.32.1"
|
||||||
|
}
|
||||||
1201
pnpm-lock.yaml
generated
Normal file
1201
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
15
reqs.txt
Normal file
15
reqs.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Подготовьте docker-compose.yml и конфигурационные файлы для локального стенда веб-приложения:
|
||||||
|
|
||||||
|
Состав:
|
||||||
|
1. Nginx — reverse proxy, TLS termination (self-signed cert), слушает :443.
|
||||||
|
2. Любое веб-приложение (например, httpbin, Grafana или простой Node.js/Python HTTP-сервер) — backend на :8080.
|
||||||
|
3. PostgreSQL 15 — база данных.
|
||||||
|
4. Redis — кэш сессий.
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
- Nginx проксирует на backend с правильными заголовками X-Forwarded-For/Proto/Host.
|
||||||
|
- healthcheck для всех четырёх сервисов.
|
||||||
|
- .env файл для паролей и переменных (не хардкодить в compose).
|
||||||
|
- nginx.conf с rate limiting (10 req/s на IP) и ограничением размера тела запроса (10MB).
|
||||||
|
- Сгенерировать self-signed сертификат через скрипт gen-certs.sh (openssl).
|
||||||
|
- README.md с инструкцией по запуску (3–5 команд).
|
||||||
22
src/db.ts
Normal file
22
src/db.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export const pgPool = new Pool({
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
port: Number(process.env.POSTGRES_PORT || 5432),
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
user: process.env.POSTGRES_USER,
|
||||||
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function initDb(): Promise<void> {
|
||||||
|
await pgPool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS motd (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
85
src/http/motd.ts
Normal file
85
src/http/motd.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Express } from 'express';
|
||||||
|
import { redisClient } from '../index.js';
|
||||||
|
import { pgPool } from '../db.js';
|
||||||
|
|
||||||
|
|
||||||
|
export async function httpMotd(app: Express) {
|
||||||
|
|
||||||
|
app.get('/motd', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const cached = await redisClient.get('motd:current');
|
||||||
|
if (cached) {
|
||||||
|
return res.json({
|
||||||
|
source: 'redis',
|
||||||
|
message: cached,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pgPool.query(
|
||||||
|
`
|
||||||
|
SELECT message
|
||||||
|
FROM motd
|
||||||
|
ORDER BY created_at DESC, id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'No MOTD set',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = result.rows[0].message as string;
|
||||||
|
await redisClient.set('motd:current', message, {
|
||||||
|
EX: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
source: 'postgres',
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/motd', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const message = req.body?.message;
|
||||||
|
|
||||||
|
if (typeof message !== 'string' || message.trim() === '') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'message must be a non-empty string',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedMessage = message.trim();
|
||||||
|
|
||||||
|
await pgPool.query(
|
||||||
|
`
|
||||||
|
INSERT INTO motd (message)
|
||||||
|
VALUES ($1)
|
||||||
|
`,
|
||||||
|
[trimmedMessage]
|
||||||
|
);
|
||||||
|
|
||||||
|
await redisClient.set('motd:current', trimmedMessage, {
|
||||||
|
EX: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
message: 'MOTD updated',
|
||||||
|
motd: trimmedMessage,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
36
src/index.ts
Normal file
36
src/index.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import express from 'express';
|
||||||
|
import { createClient } from 'redis';
|
||||||
|
import { initDb } from './db.js';
|
||||||
|
import { httpMotd } from './http/motd.js';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const port = Number(3000);
|
||||||
|
|
||||||
|
export const redisClient = createClient({
|
||||||
|
url: process.env.REDIS_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.on('error', (err) => {
|
||||||
|
console.error('Redis error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
await redisClient.connect();
|
||||||
|
await initDb();
|
||||||
|
|
||||||
|
await httpMotd(app);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Listening on port ${port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Failed to start app:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue