feat: observability — Winston logging, pagination admin, N+1 playlists
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
export interface AuthenticatedUser {
|
||||
id: string;
|
||||
@@ -40,7 +41,7 @@ export const requireAuth = async (
|
||||
|
||||
const superOAuthUrl = process.env.SUPER_OAUTH_URL;
|
||||
if (!superOAuthUrl) {
|
||||
console.error("SUPER_OAUTH_URL not configured");
|
||||
logger.error("SUPER_OAUTH_URL not configured");
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR", message: "Auth service not configured" });
|
||||
return;
|
||||
}
|
||||
@@ -65,7 +66,8 @@ export const requireAuth = async (
|
||||
|
||||
(req as AuthenticatedRequest).user = data.data.user;
|
||||
next();
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("requireAuth — auth service unreachable", { err });
|
||||
res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE", message: "Authentication service unreachable" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { UserSubscription } from "../entities/UserSubscription";
|
||||
import { SubscriptionPlan } from "../entities/SubscriptionPlan";
|
||||
import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware";
|
||||
import { requireAdmin } from "../middleware/admin.middleware";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR ?? path.join(process.cwd(), "uploads");
|
||||
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||
@@ -80,14 +81,27 @@ router.post(
|
||||
/**
|
||||
* GET /api/admin/videos
|
||||
* Liste toutes les vidéos (publiées et non publiées), tous les champs.
|
||||
* Query: ?page=1&limit=20
|
||||
*/
|
||||
router.get("/videos", async (_req: Request, res: Response): Promise<void> => {
|
||||
router.get("/videos", async (req: Request, res: Response): Promise<void> => {
|
||||
const rawPage = Number(req.query.page ?? 1);
|
||||
const rawLimit = Number(req.query.limit ?? 20);
|
||||
|
||||
if (!Number.isInteger(rawPage) || rawPage < 1 ||
|
||||
!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > 100) {
|
||||
res.status(400).json({ success: false, error: "INVALID_PAGINATION" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const videos = await AppDataSource.getRepository(Video).find({
|
||||
const [videos, total] = await AppDataSource.getRepository(Video).findAndCount({
|
||||
order: { createdAt: "DESC" },
|
||||
skip: (rawPage - 1) * rawLimit,
|
||||
take: rawLimit,
|
||||
});
|
||||
res.json({ success: true, data: { videos } });
|
||||
} catch {
|
||||
res.json({ success: true, data: videos, total, page: rawPage, limit: rawLimit });
|
||||
} catch (err) {
|
||||
logger.error("GET /admin/videos — failed to list videos", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -139,7 +153,8 @@ router.post("/videos", async (req: Request, res: Response): Promise<void> => {
|
||||
|
||||
await AppDataSource.getRepository(Video).save(video);
|
||||
res.status(201).json({ success: true, data: { video } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /admin/videos — failed to create video", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -179,7 +194,8 @@ router.patch("/videos/:id", async (req: Request, res: Response): Promise<void> =
|
||||
|
||||
await repo.save(video);
|
||||
res.json({ success: true, data: { video } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("PATCH /admin/videos/:id — failed to update video", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -200,7 +216,8 @@ router.delete("/videos/:id", async (req: Request, res: Response): Promise<void>
|
||||
|
||||
await repo.remove(video);
|
||||
res.json({ success: true, data: { deleted: req.params.id } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("DELETE /admin/videos/:id — failed to delete video", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -212,15 +229,27 @@ router.delete("/videos/:id", async (req: Request, res: Response): Promise<void>
|
||||
/**
|
||||
* GET /api/admin/users
|
||||
* Liste tous les utilisateurs avec leurs rôles et abonnement actif.
|
||||
* Query: ?page=1&limit=20
|
||||
*/
|
||||
router.get("/users", async (_req: Request, res: Response): Promise<void> => {
|
||||
router.get("/users", async (req: Request, res: Response): Promise<void> => {
|
||||
const rawPage = Number(req.query.page ?? 1);
|
||||
const rawLimit = Number(req.query.limit ?? 20);
|
||||
|
||||
if (!Number.isInteger(rawPage) || rawPage < 1 ||
|
||||
!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > 100) {
|
||||
res.status(400).json({ success: false, error: "INVALID_PAGINATION" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await AppDataSource.getRepository(User).find({
|
||||
const [users, total] = await AppDataSource.getRepository(User).findAndCount({
|
||||
relations: ["userRoles", "userRoles.role", "subscriptions", "subscriptions.plan"],
|
||||
order: { createdAt: "DESC" },
|
||||
skip: (rawPage - 1) * rawLimit,
|
||||
take: rawLimit,
|
||||
});
|
||||
|
||||
const result = users.map((u) => ({
|
||||
const data = users.map((u) => ({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
nickname: u.nickname,
|
||||
@@ -228,13 +257,14 @@ router.get("/users", async (_req: Request, res: Response): Promise<void> => {
|
||||
createdAt: u.createdAt,
|
||||
roles: u.userRoles.map((ur) => ({ id: ur.role.id, slug: ur.role.slug, name: ur.role.name })),
|
||||
activeSubscription: (() => {
|
||||
const sub = u.subscriptions.find((s) => s.status === "active");
|
||||
return sub ? { id: sub.id, status: sub.status, startsAt: sub.startsAt, endsAt: sub.endsAt, plan: sub.plan } : null;
|
||||
})(),
|
||||
const sub = u.subscriptions.find((s) => s.status === "active");
|
||||
return sub ? { id: sub.id, status: sub.status, startsAt: sub.startsAt, endsAt: sub.endsAt, plan: sub.plan } : null;
|
||||
})(),
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: { users: result } });
|
||||
} catch {
|
||||
res.json({ success: true, data, total, page: rawPage, limit: rawLimit });
|
||||
} catch (err) {
|
||||
logger.error("GET /admin/users — failed to list users", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -296,7 +326,8 @@ router.patch("/users/:id/roles", async (req: Request, res: Response): Promise<vo
|
||||
roles: roleEntities.map((r) => ({ id: r.id, slug: r.slug, name: r.name })),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("PATCH /admin/users/:id/roles — failed to assign roles", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -315,7 +346,8 @@ router.get("/plans", async (_req: Request, res: Response): Promise<void> => {
|
||||
order: { level: "ASC" },
|
||||
});
|
||||
res.json({ success: true, data: { plans } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("GET /admin/plans — failed to list plans", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -360,7 +392,8 @@ router.post("/plans", async (req: Request, res: Response): Promise<void> => {
|
||||
|
||||
await AppDataSource.getRepository(SubscriptionPlan).save(plan);
|
||||
res.status(201).json({ success: true, data: { plan } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /admin/plans — failed to create plan", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -390,7 +423,8 @@ router.patch("/plans/:id", async (req: Request, res: Response): Promise<void> =>
|
||||
|
||||
await repo.save(plan);
|
||||
res.json({ success: true, data: { plan } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("PATCH /admin/plans/:id — failed to update plan", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -445,7 +479,8 @@ router.post("/users/:id/subscriptions", async (req: Request, res: Response): Pro
|
||||
|
||||
await subRepo.save(sub);
|
||||
res.status(201).json({ success: true, data: { subscription: { ...sub, plan } } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /admin/users/:id/subscriptions — failed to assign subscription", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { AppDataSource } from "../config/data-source";
|
||||
import logger from "../utils/logger";
|
||||
import { User } from "../entities/User";
|
||||
import { UserSubscription } from "../entities/UserSubscription";
|
||||
import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware";
|
||||
@@ -79,7 +80,8 @@ router.post("/login", async (req: Request, res: Response): Promise<void> => {
|
||||
res.cookie(REFRESH_COOKIE_NAME, data.data.tokens.refreshToken, REFRESH_COOKIE_OPTIONS);
|
||||
}
|
||||
res.json({ success: true, data: { user: data.data.user } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /auth/login — auth service unavailable", { err });
|
||||
res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE" });
|
||||
}
|
||||
});
|
||||
@@ -125,7 +127,8 @@ router.post("/session", async (req: Request, res: Response): Promise<void> => {
|
||||
|
||||
res.cookie(COOKIE_NAME, token, COOKIE_OPTIONS);
|
||||
res.json({ success: true, data: { user: data.data.user } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /auth/session — auth service unavailable", { err });
|
||||
res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE" });
|
||||
}
|
||||
});
|
||||
@@ -173,7 +176,8 @@ router.post("/refresh", async (req: Request, res: Response): Promise<void> => {
|
||||
res.cookie(REFRESH_COOKIE_NAME, data.data.tokens.refreshToken, REFRESH_COOKIE_OPTIONS);
|
||||
}
|
||||
res.json({ success: true, data: { user: data.data.user ?? null } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /auth/refresh — auth service unavailable", { err });
|
||||
res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE" });
|
||||
}
|
||||
});
|
||||
@@ -275,7 +279,8 @@ router.get("/me/optional", async (req: Request, res: Response): Promise<void> =>
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { user: data.data.user } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("GET /auth/me/optional — auth service unavailable", { err });
|
||||
res.json({ success: true, data: { user: null } });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PlaylistShare } from "../entities/PlaylistShare";
|
||||
import { User } from "../entities/User";
|
||||
import { Video } from "../entities/Video";
|
||||
import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -42,7 +43,8 @@ router.get("/", requireAuth, async (req: Request, res: Response): Promise<void>
|
||||
shared: shared.map((s) => ({ ...s.playlist, permission: s.permission })),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("GET /playlists — failed to list playlists", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -78,7 +80,8 @@ router.post("/", requireAuth, async (req: Request, res: Response): Promise<void>
|
||||
|
||||
await AppDataSource.getRepository(Playlist).save(playlist);
|
||||
res.status(201).json({ success: true, data: { playlist } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /playlists — failed to create playlist", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -96,7 +99,7 @@ router.get("/:id", requireAuth, async (req: Request, res: Response): Promise<voi
|
||||
try {
|
||||
const playlist = await AppDataSource.getRepository(Playlist).findOne({
|
||||
where: { id: req.params.id },
|
||||
relations: ["playlistVideos", "playlistVideos.video", "shares"],
|
||||
relations: ["playlistVideos", "playlistVideos.video"],
|
||||
});
|
||||
|
||||
if (!playlist) {
|
||||
@@ -105,8 +108,12 @@ router.get("/:id", requireAuth, async (req: Request, res: Response): Promise<voi
|
||||
}
|
||||
|
||||
const isOwner = playlist.ownerId === dbUserId;
|
||||
const share = playlist.shares.find((s) => s.userId === dbUserId && s.status === "active");
|
||||
const isPublic = playlist.visibility === "public";
|
||||
const share = (!isOwner && !isPublic)
|
||||
? await AppDataSource.getRepository(PlaylistShare).findOne({
|
||||
where: { playlistId: playlist.id, userId: dbUserId, status: "active" },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!isOwner && !share && !isPublic) {
|
||||
res.status(403).json({ success: false, error: "FORBIDDEN" });
|
||||
@@ -125,7 +132,8 @@ router.get("/:id", requireAuth, async (req: Request, res: Response): Promise<voi
|
||||
permission: isOwner ? "owner" : share?.permission ?? "view",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("GET /playlists/:id — failed to fetch playlist", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -158,7 +166,8 @@ router.patch("/:id", requireAuth, async (req: Request, res: Response): Promise<v
|
||||
|
||||
await repo.save(playlist);
|
||||
res.json({ success: true, data: { playlist } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("PATCH /playlists/:id — failed to update playlist", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -181,7 +190,8 @@ router.delete("/:id", requireAuth, async (req: Request, res: Response): Promise<
|
||||
|
||||
await repo.remove(playlist);
|
||||
res.json({ success: true, data: { deleted: req.params.id } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("DELETE /playlists/:id — failed to delete playlist", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -200,15 +210,14 @@ router.post("/:id/videos", requireAuth, async (req: Request, res: Response): Pro
|
||||
if (!videoId) { res.status(400).json({ success: false, error: "MISSING_VIDEO_ID" }); return; }
|
||||
|
||||
try {
|
||||
const playlist = await AppDataSource.getRepository(Playlist).findOne({
|
||||
where: { id: req.params.id },
|
||||
relations: ["shares"],
|
||||
});
|
||||
const playlist = await AppDataSource.getRepository(Playlist).findOneBy({ id: req.params.id });
|
||||
|
||||
if (!playlist) { res.status(404).json({ success: false, error: "NOT_FOUND" }); return; }
|
||||
|
||||
const isOwner = playlist.ownerId === dbUserId;
|
||||
const canEdit = isOwner || playlist.shares.some((s) => s.userId === dbUserId && s.status === "active" && s.permission === "edit");
|
||||
const canEdit = isOwner || !!(await AppDataSource.getRepository(PlaylistShare).findOne({
|
||||
where: { playlistId: playlist.id, userId: dbUserId, status: "active", permission: "edit" },
|
||||
}));
|
||||
|
||||
if (!canEdit) { res.status(403).json({ success: false, error: "FORBIDDEN" }); return; }
|
||||
|
||||
@@ -226,7 +235,8 @@ router.post("/:id/videos", requireAuth, async (req: Request, res: Response): Pro
|
||||
|
||||
await AppDataSource.getRepository(PlaylistVideo).save(pv);
|
||||
res.status(201).json({ success: true, data: { playlistVideo: pv } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /playlists/:id/videos — failed to add video", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -241,15 +251,14 @@ router.delete("/:id/videos/:videoId", requireAuth, async (req: Request, res: Res
|
||||
if (!dbUserId) { res.status(401).json({ success: false, error: "USER_NOT_FOUND" }); return; }
|
||||
|
||||
try {
|
||||
const playlist = await AppDataSource.getRepository(Playlist).findOne({
|
||||
where: { id: req.params.id },
|
||||
relations: ["shares"],
|
||||
});
|
||||
const playlist = await AppDataSource.getRepository(Playlist).findOneBy({ id: req.params.id });
|
||||
|
||||
if (!playlist) { res.status(404).json({ success: false, error: "NOT_FOUND" }); return; }
|
||||
|
||||
const isOwner = playlist.ownerId === dbUserId;
|
||||
const canEdit = isOwner || playlist.shares.some((s) => s.userId === dbUserId && s.status === "active" && s.permission === "edit");
|
||||
const canEdit = isOwner || !!(await AppDataSource.getRepository(PlaylistShare).findOne({
|
||||
where: { playlistId: playlist.id, userId: dbUserId, status: "active", permission: "edit" },
|
||||
}));
|
||||
|
||||
if (!canEdit) { res.status(403).json({ success: false, error: "FORBIDDEN" }); return; }
|
||||
|
||||
@@ -262,7 +271,8 @@ router.delete("/:id/videos/:videoId", requireAuth, async (req: Request, res: Res
|
||||
|
||||
await AppDataSource.getRepository(PlaylistVideo).remove(pv);
|
||||
res.json({ success: true, data: { deleted: req.params.videoId } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("DELETE /playlists/:id/videos/:videoId — failed to remove video", { err, id: req.params.id, videoId: req.params.videoId });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -305,7 +315,8 @@ router.post("/:id/share", requireAuth, async (req: Request, res: Response): Prom
|
||||
|
||||
await AppDataSource.getRepository(PlaylistShare).save(share);
|
||||
res.status(201).json({ success: true, data: { share } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("POST /playlists/:id/share — failed to create share", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -343,7 +354,8 @@ router.patch("/:id/share/:shareId", requireAuth, async (req: Request, res: Respo
|
||||
|
||||
await AppDataSource.getRepository(PlaylistShare).save(share);
|
||||
res.json({ success: true, data: { share } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("PATCH /playlists/:id/share/:shareId — failed to update share", { err, id: req.params.id, shareId: req.params.shareId });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { AppDataSource } from "../config/data-source";
|
||||
import logger from "../utils/logger";
|
||||
import { Video } from "../entities/Video";
|
||||
import { User } from "../entities/User";
|
||||
import { requireAuth, AuthenticatedRequest, AuthenticatedUser } from "../middleware/auth.middleware";
|
||||
@@ -51,7 +52,8 @@ router.get("/", async (req: Request, res: Response): Promise<void> => {
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: { videos: result } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("GET /videos — failed to list videos", { err });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
@@ -84,7 +86,8 @@ router.get("/:id", async (req: Request, res: Response): Promise<void> => {
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { video } });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error("GET /videos/:id — failed to fetch video", { err, id: req.params.id });
|
||||
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
||||
}
|
||||
});
|
||||
|
||||
25
backend/src/utils/logger.ts
Normal file
25
backend/src/utils/logger.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import winston from "winston";
|
||||
|
||||
const { combine, timestamp, json, colorize, printf } = winston.format;
|
||||
|
||||
const devFormat = combine(
|
||||
colorize(),
|
||||
timestamp({ format: "HH:mm:ss" }),
|
||||
printf(({ level, message, timestamp: ts, ...meta }) => {
|
||||
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
||||
return `${ts} [${level}] ${message}${metaStr}`;
|
||||
})
|
||||
);
|
||||
|
||||
const prodFormat = combine(
|
||||
timestamp(),
|
||||
json()
|
||||
);
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL ?? "info",
|
||||
format: process.env.NODE_ENV === "production" ? prodFormat : devFormat,
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
|
||||
export default logger;
|
||||
Reference in New Issue
Block a user