Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
APP_NAME="Auth API"
APP_URL="http://localhost:3000"
APP_KEY="change-me"
NODE_ENV="local"
APP_NAME="Node Starter API"
PORT="3000"
APP_URL="http://127.0.0.1:3000"
APP_KEY=

JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=

DATABASE_URL="postgresql://postgres:@localhost:5432/DATABASE?schema=public"
PORT=3000
LOG_LEVEL="info"

DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
REDIS_URL="redis://localhost:6379"

GOOGLE_CLIENT_ID="your-google-client-id"
JWT_ACCESS_SECRET="change-me"
JWT_REFRESH_SECRET="change-me"
JWT_ACCESS_EXPIRES_IN="120m"
JWT_REFRESH_EXPIRES_IN="7d"

MAIL_HOST=0.0.0.0
MAIL_PORT=1025
MAIL_FROM="API Demo <noreply@demo.com>"
SWAGGER_ENABLED=false

GOOGLE_CLIENT_ID=

SWAGGER_ENABLED=
MAIL_HOST="localhost"
MAIL_PORT=1025
MAIL_USERNAME=""
MAIL_PASSWORD=""
MAIL_FROM="noreply@example.com"
55 changes: 52 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,61 @@

## Environment Config

API is configured to use PostgreSQL.
API is configured to use PostgreSQL. Copy `.env.example` to `.env` and adjust values for your environment.

| Variable | Description | Default |
| --- | --- | --- |
| `APP_NAME` | Display name for docs and emails | `Auth API` |
| `APP_URL` | Base URL used in emails and Swagger docs | Required |
| `APP_KEY` | Application signing key for verification tokens | Required |
| `NODE_ENV` | Runtime environment (`local`, `development`, `test`, `production`) | `local` |
| `PORT` | Port the API listens on | `3000` |
| `LOG_LEVEL` | Pino log level | `info` |
| `DATABASE_URL` | PostgreSQL connection string | Required |
| `REDIS_URL` | Redis connection string | `redis://localhost:6379` |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | Required |
| `JWT_ACCESS_SECRET` | JWT access token secret | Required |
| `JWT_REFRESH_SECRET` | JWT refresh token secret | Required |
| `JWT_ACCESS_EXPIRES_IN` | Access token TTL (e.g. `120m`) | `120m` |
| `JWT_REFRESH_EXPIRES_IN` | Refresh token TTL (e.g. `7d`) | `7d` |
| `SWAGGER_ENABLED` | Enable Swagger docs | `false` |
| `MAIL_HOST` | SMTP host | `localhost` |
| `MAIL_PORT` | SMTP port | `1025` |
| `MAIL_USERNAME` | SMTP username | empty |
| `MAIL_PASSWORD` | SMTP password | empty |
| `MAIL_FROM` | Default sender email | `noreply@example.com` |

### Example `.env`

```env
APP_NAME="Auth API"
APP_URL="http://localhost:3000"
APP_KEY="change-me"
NODE_ENV="local"
PORT=3000
LOG_LEVEL="info"

DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
REDIS_URL="redis://localhost:6379"

GOOGLE_CLIENT_ID="your-google-client-id"
JWT_ACCESS_SECRET="change-me"
JWT_REFRESH_SECRET="change-me"
JWT_ACCESS_EXPIRES_IN="120m"
JWT_REFRESH_EXPIRES_IN="7d"

SWAGGER_ENABLED=false

MAIL_HOST="localhost"
MAIL_PORT=1025
MAIL_USERNAME=""
MAIL_PASSWORD=""
MAIL_FROM="noreply@example.com"
```

## Setup

```bash

cp .env.example .env

# Install JavaScript dependencies
Expand Down Expand Up @@ -53,4 +102,4 @@ To test Google Auth Flow, use the following:
4. Top right, click Settings -> Use your own OAuth credentials
5. Enter Client ID and Client Secret
6. Click Authorize APIs and then Exchange authorization code for tokens
7. Copy the id_token and paste it in the request body
7. Copy the id_token and paste it in the request body
54 changes: 28 additions & 26 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,38 @@ import {setupSwagger} from "./src/config/swagger";
import {errorMiddleware} from "./src/middleware/errorMiddleware";
import {pinoHttp} from "pino-http";
import {logger} from "./src/lib/logger";
import {env} from "./src/utils/environment-variables";

export function createApp() {
const app = express();
const isProd = process.env.NODE_ENV === "production";

if (isProd) {
app.use(
pinoHttp({
logger,
autoLogging: {
ignore: (req) => req.url.startsWith("/docs"),
},
customLogLevel(req, res, err) {
if (res.statusCode >= 500) return "error";
if (res.statusCode >= 400) return "warn";
return "info";
},
customSuccessMessage(req, res) {
return `${req.method} ${req.url} → ${res.statusCode}`;
},
redact: {
paths: [
"req.headers.cookie",
"req.headers.authorization",
],
remove: true,
},
})
);
}
app.use(
pinoHttp({
logger,
autoLogging: {
ignore: (req) => req.url.startsWith("/docs"),
},
customLogLevel(req, res, err) {
if (err || res.statusCode >= 500) return "error";
if (res.statusCode >= 400) return "warn";
return "info";
},
customSuccessMessage(req, res) {
return `${req.method} ${req.url} → ${res.statusCode}`;
},
redact: {
paths: [
"req.headers.cookie",
"req.headers.authorization",
],
remove: true,
},
customAttributeKeys: env.NODE_ENV === "production"
? undefined
: {responseTime: "responseTime"},
})
);

app.use(express.json());
app.use(express.urlencoded({extended: true}));
app.use(cookieParser());
Expand Down
14 changes: 9 additions & 5 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,40 @@ import {createApp} from "./app";
import {connectDB, disconnectDB} from "./src/config/db";
import {initRedis} from "./src/config/redis";
import {logger} from "./src/lib/logger";
import {env} from "./src/utils/environment-variables";

(async () => {
try {
await connectDB();
await initRedis();

const app = createApp();
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
const port = env.PORT;

const server = app.listen(port, () => {
logger.info(`🚀 Server running on port ${port}`);
});

process.on("unhandledRejection", (err) => {
const isDev = process.env.NODE_ENV === "local";
const isDev = env.NODE_ENV === "local";
if (isDev) console.error("Unhandled Rejection:", err);
server.close(async () => {
await disconnectDB();
process.exit(1);
});
});

process.on("SIGTERM", () => {
const gracefulShutdown = () => {
server.close(async () => {
disconnectDB().finally(() => process.exit(0));
});
});
};

process.on("SIGTERM", gracefulShutdown);
process.on("SIGINT", gracefulShutdown);

} catch (err) {
const isDev = process.env.NODE_ENV === "local";
const isDev = env.NODE_ENV === "local";
if (isDev) console.error("Failed to start server:", err);
process.exit(1);
}
Expand Down
7 changes: 1 addition & 6 deletions src/config/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,9 @@ const schema = isTest ? "test" : "public";

let adapter: PrismaPg;

if (!process.env.DATABASE_URL) {
logger.fatal("[DB] DATABASE_URL is not set");
process.exit(1);
}

try {
adapter = new PrismaPg(
{connectionString: process.env.DATABASE_URL},
{connectionString: env.DATABASE_URL},
{schema}
);

Expand Down
4 changes: 2 additions & 2 deletions src/config/redis.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {createClient} from "redis";
import {logger} from "../lib/logger.js";
import {env} from "../utils/environment-variables";

export const redis = createClient({
url: process.env.REDIS_URL ?? "redis://localhost:6379",
url: env.REDIS_URL,
});

redis.on("error", (err) => {
Expand Down Expand Up @@ -30,4 +31,3 @@ export async function initRedis() {
process.exit(1);
}
}

8 changes: 4 additions & 4 deletions src/config/swagger.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import swaggerUi from "swagger-ui-express";
import swaggerJsdoc from "swagger-jsdoc";
import {Express, Request, Response} from "express";
import {env} from "../utils/environment-variables";

export function setupSwagger(app: Express) {
const enabled = (process.env.SWAGGER_ENABLED || "").toLowerCase() === "true";
if (!enabled)
if (!env.SWAGGER_ENABLED)
return;
const options = {
definition: {
openapi: "3.0.3",
info: {
title: process.env.APP_NAME ? `${process.env.APP_NAME} API` : "API Docs",
title: env.APP_NAME ? `${env.APP_NAME} API` : "API Docs",
version: "0.0.1",
description: "API with full Auth pipeline.",
},
servers: [
{url: process.env.APP_URL || `http://localhost:${process.env.PORT || 3000}`},
{url: env.APP_URL || `http://localhost:${env.PORT}`},
],
components: {
securitySchemes: {
Expand Down
23 changes: 15 additions & 8 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import pino from "pino";
import {env} from "../utils/environment-variables";

const isProduction = env.NODE_ENV === "production";

export const logger = pino({
level: process.env.LOG_LEVEL || "info",
transport: {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:standard",
},
},
level: env.LOG_LEVEL,
...(isProduction
? {}
: {
transport: {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:standard",
},
},
}),
});
13 changes: 7 additions & 6 deletions src/lib/mailer.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import "dotenv/config";
import nodemailer from "nodemailer";
import {env} from "../utils/environment-variables";

export const mailer = nodemailer.createTransport({
host: process.env.MAIL_HOST ?? "localhost",
port: Number(process.env.MAIL_PORT) ?? 1025,
host: env.MAIL_HOST,
port: env.MAIL_PORT,
secure: false,
auth: process.env.MAIL_USERNAME
auth: env.MAIL_USERNAME
? {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
user: env.MAIL_USERNAME,
pass: env.MAIL_PASSWORD,
}
: undefined,
});

export async function sendMail(to: string, subject: string, html: string) {
await mailer.sendMail({
from: process.env.MAIL_FROM ?? "noreply@example.com",
from: env.MAIL_FROM,
to,
subject,
html,
Expand Down
3 changes: 2 additions & 1 deletion src/services/auth/RegisterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {container} from "../../lib/container";
import {buildEmailTemplate, sendMail} from "../../lib/mailer";
import {toSafeUser} from "../../lib/safe-user";
import {EmailTakenError} from "../../lib/errors";
import {env} from "../../utils/environment-variables";

export class RegisterService {
async register(data: { email: string; password: string; first_name: string; last_name: string; }) {
Expand All @@ -25,7 +26,7 @@ export class RegisterService {
include: {profile: true},
});
const verificationToken = container.emailVerificationService.generateVerificationToken(user.id, user.email);
const verificationUrl = `${process.env.APP_URL}/auth/verify-email?token=${verificationToken}`;
const verificationUrl = `${env.APP_URL}/auth/verify-email?token=${verificationToken}`;

await sendMail(user.email, "Verify your email", buildEmailTemplate({
name: user.profile?.first_name ?? "there",
Expand Down
Loading