refactor: centralize configuration
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
BASANGO_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app?serverVersion=16&charset=utf8"
|
||||
|
||||
BASANGO_SOURCE_DATABASE_HOST="localhost"
|
||||
BASANGO_SOURCE_DATABASE_PASS="root"
|
||||
BASANGO_SOURCE_DATABASE_NAME="app"
|
||||
BASANGO_SOURCE_DATABASE_USER="root"
|
||||
@@ -1,10 +1,9 @@
|
||||
import { config } from "@basango/domain/config";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
import { env } from "./src/config";
|
||||
|
||||
export default defineConfig({
|
||||
dbCredentials: {
|
||||
url: env("BASANGO_DATABASE_URL"),
|
||||
url: config.database.url,
|
||||
},
|
||||
dialect: "postgresql",
|
||||
out: "./migrations",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "article" drop column "tsv";--> statement-breakpoint
|
||||
ALTER TABLE "article" ADD COLUMN "tsv" "tsvector" GENERATED ALWAYS AS (setweight(to_tsvector('french'::regconfig, COALESCE(title, '')::text), 'A'::"char")) STORED;--> statement-breakpoint
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,13 @@
|
||||
"tag": "0001_init",
|
||||
"version": "7",
|
||||
"when": 1762775267679
|
||||
},
|
||||
{
|
||||
"breakpoints": true,
|
||||
"idx": 2,
|
||||
"tag": "0002_modern_joseph",
|
||||
"version": "7",
|
||||
"when": 1763920009482
|
||||
}
|
||||
],
|
||||
"version": "7"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"@basango/encryption": "workspace:*",
|
||||
"@basango/logger": "workspace:*",
|
||||
"@date-fns/utc": "^2.1.1",
|
||||
"@devscast/config": "catalog:",
|
||||
"date-fns": "catalog:",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"mysql2": "^3.15.3",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { config } from "@basango/domain/config";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import { env } from "#db/config";
|
||||
import * as schema from "#db/schema";
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
const pool = new Pool({
|
||||
allowExitOnIdle: true,
|
||||
connectionString: env("BASANGO_DATABASE_URL"),
|
||||
connectionString: config.database.url,
|
||||
connectionTimeoutMillis: 15_000,
|
||||
idleTimeoutMillis: isDevelopment ? 5_000 : 60_000,
|
||||
max: isDevelopment ? 8 : 12,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { loadConfig } from "@devscast/config";
|
||||
import { z } from "zod";
|
||||
|
||||
const PROJECT_DIR = path.resolve(__dirname, "../");
|
||||
|
||||
export const { env, config } = loadConfig({
|
||||
env: {
|
||||
knownKeys: [
|
||||
"BASANGO_DATABASE_URL",
|
||||
"BASANGO_SOURCE_DATABASE_HOST",
|
||||
"BASANGO_SOURCE_DATABASE_USER",
|
||||
"BASANGO_SOURCE_DATABASE_PASS",
|
||||
"BASANGO_SOURCE_DATABASE_NAME",
|
||||
] as const,
|
||||
path: path.join(PROJECT_DIR, ".env"),
|
||||
},
|
||||
schema: z.object({}),
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { md5 } from "@basango/encryption";
|
||||
import type { SQL } from "drizzle-orm";
|
||||
import { count, desc, eq, getTableColumns, sql } from "drizzle-orm";
|
||||
import { v7 as uuidV7 } from "uuid";
|
||||
import * as uuid from "uuid";
|
||||
|
||||
import { Database } from "#db/client";
|
||||
import { getSourceIdByName } from "#db/queries/sources";
|
||||
@@ -56,7 +56,7 @@ export async function createArticle(db: Database, params: CreateArticleParams) {
|
||||
|
||||
const [result] = await db
|
||||
.insert(articles)
|
||||
.values({ id: uuidV7(), ...data })
|
||||
.values({ id: uuid.v7(), ...data })
|
||||
.returning({
|
||||
id: articles.id,
|
||||
sourceId: articles.sourceId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DEFAULT_CATEGORY_SHARES_LIMIT, DEFAULT_TIMEZONE } from "@basango/domain/constants";
|
||||
import { ID, Publication, Publications } from "@basango/domain/models";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { v7 as uuidV7 } from "uuid";
|
||||
import * as uuid from "uuid";
|
||||
|
||||
import { Database } from "#db/client";
|
||||
import { NotFoundError } from "#db/errors";
|
||||
@@ -32,7 +32,7 @@ export async function getSources(db: Database) {
|
||||
export async function createSource(db: Database, params: CreateSourceParams) {
|
||||
const [result] = await db
|
||||
.insert(sources)
|
||||
.values({ id: uuidV7(), ...params })
|
||||
.values({ id: uuid.v7(), ...params })
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
|
||||
@@ -114,10 +114,7 @@ export const articles = pgTable(
|
||||
title: varchar({ length: 1024 }).notNull(),
|
||||
tokenStatistics: jsonb("token_statistics").$type<TokenStatistics>(),
|
||||
tsv: tsvector("tsv").generatedAlwaysAs(
|
||||
sql`(
|
||||
setweight(to_tsvector('french'::regconfig, COALESCE(title, '')::text), 'A'::"char")
|
||||
|| setweight(to_tsvector('french'::regconfig, COALESCE(body, ''::text)), 'B'::"char")
|
||||
)`,
|
||||
sql`setweight(to_tsvector('french'::regconfig, COALESCE(title, '')::text), 'A'::"char")`,
|
||||
),
|
||||
updatedAt: timestamp("updated_at"),
|
||||
},
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
/** biome-ignore-all lint/correctness/noUnusedPrivateClassMembers: false positive */
|
||||
|
||||
import { config } from "@basango/domain/config";
|
||||
import { RowDataPacket } from "mysql2/promise";
|
||||
import { Pool, PoolClient } from "pg";
|
||||
|
||||
import { env } from "#db/config";
|
||||
import { computeReadingTime } from "#db/utils/computed";
|
||||
|
||||
type SourceOptions = {
|
||||
@@ -598,13 +598,13 @@ async function main() {
|
||||
|
||||
const engine = new Engine(
|
||||
{
|
||||
database: env("BASANGO_SOURCE_DATABASE_NAME"),
|
||||
host: env("BASANGO_SOURCE_DATABASE_HOST"),
|
||||
password: env("BASANGO_SOURCE_DATABASE_PASS"),
|
||||
user: env("BASANGO_SOURCE_DATABASE_USER"),
|
||||
database: config.database.legacy.name,
|
||||
host: config.database.legacy.host,
|
||||
password: config.database.legacy.password,
|
||||
user: config.database.legacy.user,
|
||||
},
|
||||
{
|
||||
database: env("BASANGO_DATABASE_URL"),
|
||||
database: config.database.url,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { config } from "@basango/domain/config";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import { env } from "#db/config";
|
||||
import { computeTokenStatistics } from "#db/utils/computed";
|
||||
|
||||
type ArticleRow = {
|
||||
@@ -114,7 +114,7 @@ class Engine {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const engine = new Engine(env("BASANGO_DATABASE_URL"));
|
||||
const engine = new Engine(config.database.url);
|
||||
|
||||
try {
|
||||
await engine.synchronize();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"api": {
|
||||
"cors": {
|
||||
"allowedHeaders": [
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"accept-language",
|
||||
"x-trpc-source",
|
||||
"x-user-locale",
|
||||
"x-user-timezone",
|
||||
"x-user-country"
|
||||
],
|
||||
"allowMethods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
|
||||
"exposeHeaders": ["Content-Length"],
|
||||
"maxAge": 86400
|
||||
},
|
||||
"security": {
|
||||
"accessTokenTtl": "15m",
|
||||
"audience": "basango_dashboard",
|
||||
"crawlerToken": "%env(BASANGO_API_CRAWLER_TOKEN)%",
|
||||
"issuer": "basango_api",
|
||||
"jwtSecret": "%env(BASANGO_API_JWT_SECRET)%",
|
||||
"refreshTokenTtl": "7d"
|
||||
},
|
||||
"server": {
|
||||
"host": "%env(BASANGO_API_HOST)%",
|
||||
"port": "%env(number:BASANGO_API_PORT)%",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
{
|
||||
"crawler": {
|
||||
"backend": {
|
||||
"endpoint": "%env(BASANGO_API_CRAWLER_ENDPOINT)%",
|
||||
"token": "%env(BASANGO_API_CRAWLER_TOKEN)%"
|
||||
},
|
||||
"fetch": {
|
||||
"async": {
|
||||
"prefix": "basango:crawler",
|
||||
"queues": {
|
||||
"details": "%env(BASANGO_CRAWLER_ASYNC_QUEUE_DETAILS)%",
|
||||
"listing": "%env(BASANGO_CRAWLER_ASYNC_QUEUE_LISTING)%",
|
||||
"processing": "%env(BASANGO_CRAWLER_ASYNC_QUEUE_PROCESSING)%"
|
||||
},
|
||||
"redisUrl": "%env(BASANGO_CRAWLER_ASYNC_REDIS_URL)%",
|
||||
"ttl": {
|
||||
"default": 600,
|
||||
"failure": "%env(number:BASANGO_CRAWLER_ASYNC_TTL_FAILURE)%",
|
||||
"result": "%env(number:BASANGO_CRAWLER_ASYNC_TTL_RESULT)%"
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"backoffInitial": 1,
|
||||
"backoffMax": 30,
|
||||
"backoffMultiplier": 2,
|
||||
"followRedirects": true,
|
||||
"maxRetries": "%env(number:BASANGO_CRAWLER_FETCH_MAX_RETRIES)%",
|
||||
"respectRetryAfter": "%env(boolean:BASANGO_CRAWLER_FETCH_RESPECT_RETRY_AFTER)%",
|
||||
"rotate": true,
|
||||
"timeout": 20,
|
||||
"userAgent": "%env(BASANGO_CRAWLER_FETCH_USER_AGENT)%",
|
||||
"verifySsl": true
|
||||
},
|
||||
"crawler": {
|
||||
"direction": "%env(BASANGO_CRAWLER_UPDATE_DIRECTION)%",
|
||||
"maxWorkers": 5,
|
||||
"notify": false,
|
||||
"useMultiThreading": false
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"data": "%env(BASANGO_CRAWLER_DATA_PATH)%",
|
||||
"root": "%env(BASANGO_CRAWLER_ROOT_PATH)%"
|
||||
},
|
||||
"sources": {
|
||||
"html": [
|
||||
{
|
||||
"paginationTemplate": "actualite",
|
||||
"requiresDetails": true,
|
||||
"requiresRateLimit": false,
|
||||
"sourceDate": {},
|
||||
"sourceId": "radiookapi.net",
|
||||
"sourceKind": "html",
|
||||
"sourceSelectors": {
|
||||
"articleBody": ".field-name-body",
|
||||
"articleCategories": ".views-field-field-cat-gorie a",
|
||||
"articleDate": "head > meta[property=\"article:published_time\"]",
|
||||
"articleLink": ".views-field-title a",
|
||||
"articles": ".view-content > .views-row.content-row",
|
||||
"articleTitle": "h1.page-header",
|
||||
"pagination": "ul.pagination > li.pager-last > a"
|
||||
},
|
||||
"sourceUrl": "https://www.radiookapi.net",
|
||||
"supportsCategories": false
|
||||
},
|
||||
{
|
||||
"categories": ["politique", "economie", "culture", "sport", "societe"],
|
||||
"paginationTemplate": "index.php/category/{category}",
|
||||
"requiresDetails": true,
|
||||
"requiresRateLimit": false,
|
||||
"sourceDate": {},
|
||||
"sourceId": "7sur7.cd",
|
||||
"sourceKind": "html",
|
||||
"sourceSelectors": {
|
||||
"articleBody": "div[property=\"schema:text\"].field.field--name-body",
|
||||
"articleDate": "head > meta[property=\"article:published_time\"]",
|
||||
"articleLink": ".views-field-title a",
|
||||
"articles": ".view-content > .row.views-row",
|
||||
"articleTitle": ".views-field-title a",
|
||||
"pagination": "ul.pagination > li.pager__item.pager__item--last > a"
|
||||
},
|
||||
"sourceUrl": "https://7sur7.cd",
|
||||
"supportsCategories": true
|
||||
},
|
||||
{
|
||||
"paginationTemplate": "articles.html",
|
||||
"requiresDetails": true,
|
||||
"requiresRateLimit": false,
|
||||
"sourceDate": {
|
||||
"format": "dd.MM.yyyy"
|
||||
},
|
||||
"sourceId": "mediacongo.net",
|
||||
"sourceKind": "html",
|
||||
"sourceSelectors": {
|
||||
"articleBody": ".article_ttext",
|
||||
"articleCategories": "a.color_link",
|
||||
"articleDate": ".article_other_about",
|
||||
"articleLink": "a:first-child",
|
||||
"articles": ".for_aitems > .article_other_item",
|
||||
"articleTitle": "h1",
|
||||
"pagination": "div.pagination > div > a:last-child"
|
||||
},
|
||||
"sourceUrl": "https://www.mediacongo.net",
|
||||
"supportsCategories": false
|
||||
},
|
||||
{
|
||||
"paginationTemplate": "actualite",
|
||||
"requiresDetails": true,
|
||||
"requiresRateLimit": false,
|
||||
"sourceDate": {},
|
||||
"sourceId": "actualite.cd",
|
||||
"sourceKind": "html",
|
||||
"sourceSelectors": {
|
||||
"articleBody": ".views-field.views-field-body .field-content",
|
||||
"articleCategories": "#actu-cat",
|
||||
"articleDate": "head > meta[property=\"article:published_time\"]",
|
||||
"articleLink": "#actu-titre a",
|
||||
"articles": "#views-bootstrap-taxonomy-term-page-2 > div > div",
|
||||
"articleTitle": "h1.page-title"
|
||||
},
|
||||
"sourceUrl": "https://actualite.cd",
|
||||
"supportsCategories": false
|
||||
}
|
||||
],
|
||||
"wordpress": [
|
||||
{
|
||||
"requiresRateLimit": true,
|
||||
"sourceId": "beto.cd",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://beto.cd"
|
||||
},
|
||||
{ "sourceId": "newscd.net", "sourceKind": "wordpress", "sourceUrl": "https://newscd.net" },
|
||||
{
|
||||
"sourceId": "africanewsrdc.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://www.africanewsrdc.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "angazainstitute.ac.cd",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://angazainstitute.ac.cd"
|
||||
},
|
||||
{ "sourceId": "b-onetv.cd", "sourceKind": "wordpress", "sourceUrl": "https://b-onetv.cd" },
|
||||
{
|
||||
"sourceId": "bukavufm.com",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://bukavufm.com"
|
||||
},
|
||||
{
|
||||
"sourceId": "changement7.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://changement7.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "congoactu.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://congoactu.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "congoindependant.com",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://www.congoindependant.com"
|
||||
},
|
||||
{
|
||||
"sourceId": "congoquotidien.com",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://www.congoquotidien.com"
|
||||
},
|
||||
{
|
||||
"sourceId": "cumulard.cd",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://www.cumulard.cd"
|
||||
},
|
||||
{
|
||||
"sourceId": "environews-rdc.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://environews-rdc.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "freemediardc.info",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://www.freemediardc.info"
|
||||
},
|
||||
{
|
||||
"sourceId": "geopolismagazine.org",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://geopolismagazine.org"
|
||||
},
|
||||
{
|
||||
"sourceId": "habarirdc.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://habarirdc.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "infordc.com",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://infordc.com"
|
||||
},
|
||||
{
|
||||
"sourceId": "kilalopress.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://kilalopress.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "laprosperiteonline.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://laprosperiteonline.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "laprunellerdc.cd",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://laprunellerdc.cd"
|
||||
},
|
||||
{
|
||||
"sourceId": "lesmedias.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://lesmedias.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "lesvolcansnews.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://lesvolcansnews.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "netic-news.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://www.netic-news.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "objectif-infos.cd",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://objectif-infos.cd"
|
||||
},
|
||||
{
|
||||
"sourceId": "scooprdc.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://scooprdc.net"
|
||||
},
|
||||
{
|
||||
"sourceId": "journaldekinshasa.com",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://www.journaldekinshasa.com"
|
||||
},
|
||||
{
|
||||
"sourceId": "lepotentiel.cd",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://lepotentiel.cd"
|
||||
},
|
||||
{
|
||||
"sourceId": "acturdc.com",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://acturdc.com"
|
||||
},
|
||||
{
|
||||
"sourceId": "matininfos.net",
|
||||
"sourceKind": "wordpress",
|
||||
"sourceUrl": "https://matininfos.net"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"database": {
|
||||
"legacy": {
|
||||
"host": "%env(BASANGO_DATABASE_LEGACY_HOST)%",
|
||||
"name": "%env(BASANGO_DATABASE_LEGACY_NAME)%",
|
||||
"password": "%env(BASANGO_DATABASE_LEGACY_PASSWORD)%",
|
||||
"port": "%env(number:BASANGO_DATABASE_LEGACY_PORT)%",
|
||||
"user": "%env(BASANGO_DATABASE_LEGACY_USER)%"
|
||||
},
|
||||
"url": "%env(BASANGO_DATABASE_URL)%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"encryption": {
|
||||
"algorithm": "aes-256-gcm",
|
||||
"authTagLength": 16,
|
||||
"bcryptSaltRounds": 12,
|
||||
"ivLength": 16,
|
||||
"key": "%env(BASANGO_ENCRYPTION_KEY)%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"logger": {
|
||||
"level": "%env(BASANGO_LOGGER_LEVEL)%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"shared": {
|
||||
"categorySharesLimit": 10,
|
||||
"dateFormat": "yyyy-LL-dd",
|
||||
"dateTimeFormat": "yyyy-LL-dd'T'HH:mm:ss",
|
||||
"name": "Basango",
|
||||
"pagination": {
|
||||
"defaultLimit": 20,
|
||||
"maxLimit": 100,
|
||||
"page": 1
|
||||
},
|
||||
"publicationGraphDays": 30,
|
||||
"timezone": "Africa/Lubumbashi"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"@basango/tsconfig": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
"./config": "./src/config/index.ts",
|
||||
"./constants": "./src/constants.ts",
|
||||
"./crawler": "./src/crawler/index.ts",
|
||||
"./models": "./src/models/index.ts"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ApiConfigurationSchema = z.object({
|
||||
cors: z.object({
|
||||
allowedHeaders: z.array(z.string()).default([]),
|
||||
allowMethods: z.array(z.string()).default([]),
|
||||
exposeHeaders: z.array(z.string()).default([]),
|
||||
maxAge: z.number().int().min(0).optional(),
|
||||
origin: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default(["http://localhost:3000", "http://127.0.0.1:3000", "https://dashboard.basango.io"]),
|
||||
}),
|
||||
security: z.object({
|
||||
accessTokenTtl: z.string(),
|
||||
audience: z.string(),
|
||||
crawlerToken: z.string(),
|
||||
issuer: z.string(),
|
||||
jwtSecret: z.string(),
|
||||
refreshTokenTtl: z.string(),
|
||||
}),
|
||||
server: z.object({
|
||||
host: z.string().default("localhost"),
|
||||
port: z.number().int().min(1).max(65535).default(3080),
|
||||
version: z.string().default("1.0.0"),
|
||||
}),
|
||||
});
|
||||
|
||||
export type ApiConfiguration = z.infer<typeof ApiConfigurationSchema>;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SOURCE_KINDS } from "../constants";
|
||||
import { PageRangeSchema, TimestampRangeSchema, UpdateDirectionSchema } from "../models";
|
||||
|
||||
export const SourceKindSchema = z.enum(SOURCE_KINDS);
|
||||
|
||||
export const SourceDateSchema = z.object({
|
||||
format: z.string().default("yyyy-LL-dd HH:mm"),
|
||||
});
|
||||
|
||||
const SourceOptionsSchema = z.object({
|
||||
categories: z.array(z.string()).default([]),
|
||||
requiresDetails: z.boolean().default(false),
|
||||
requiresRateLimit: z.boolean().default(false),
|
||||
sourceDate: SourceDateSchema,
|
||||
sourceId: z.string(),
|
||||
sourceKind: SourceKindSchema,
|
||||
sourceUrl: z.url(),
|
||||
supportsCategories: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const HtmlSourceOptionsSchema = SourceOptionsSchema.extend({
|
||||
paginationTemplate: z.string(),
|
||||
sourceKind: z.literal("html"),
|
||||
sourceSelectors: z.object({
|
||||
articleBody: z.string(),
|
||||
articleCategories: z.string().optional(),
|
||||
articleDate: z.string(),
|
||||
articleLink: z.string(),
|
||||
articles: z.string(),
|
||||
articleTitle: z.string(),
|
||||
pagination: z.string().default("ul.pagination > li a"),
|
||||
}),
|
||||
});
|
||||
|
||||
export const WordPressSourceOptionsSchema = SourceOptionsSchema.extend({
|
||||
sourceDate: SourceDateSchema.default(SourceDateSchema.parse({ format: "yyyy-LL-dd'T'HH:mm:ss" })),
|
||||
sourceKind: z.literal("wordpress"),
|
||||
});
|
||||
|
||||
export const CrawlerConfigurationSchema = z.object({
|
||||
backend: z.object({
|
||||
endpoint: z.url(),
|
||||
token: z.string(),
|
||||
}),
|
||||
fetch: z.object({
|
||||
async: z.object({
|
||||
prefix: z.string().default("basango:crawler:queue"),
|
||||
queues: z.object({
|
||||
details: z.string().default("details"),
|
||||
listing: z.string().default("listing"),
|
||||
processing: z.string().default("processing"),
|
||||
}),
|
||||
redisUrl: z.string().default("redis://localhost:6379/0"),
|
||||
ttl: z.object({
|
||||
default: z.number().int().positive().default(600),
|
||||
failure: z.number().int().nonnegative().default(3600),
|
||||
result: z.number().int().nonnegative().default(3600),
|
||||
}),
|
||||
}),
|
||||
client: z.object({
|
||||
backoffInitial: z.number().nonnegative().default(1),
|
||||
backoffMax: z.number().nonnegative().default(30),
|
||||
backoffMultiplier: z.number().positive().default(2),
|
||||
followRedirects: z.boolean().default(true),
|
||||
maxRetries: z.number().int().nonnegative().default(3),
|
||||
respectRetryAfter: z.boolean().default(true),
|
||||
rotate: z.boolean().default(true),
|
||||
timeout: z.number().positive().default(20),
|
||||
userAgent: z.string().default("Basango/0.1 (+https://github.com/bernard-ng/basango)"),
|
||||
verifySsl: z.boolean().default(true),
|
||||
}),
|
||||
crawler: z.object({
|
||||
category: z.string().optional(),
|
||||
dateRange: TimestampRangeSchema.optional(),
|
||||
direction: UpdateDirectionSchema.default("forward"),
|
||||
isUpdate: z.boolean().default(false),
|
||||
maxWorkers: z.number().int().positive().default(5),
|
||||
notify: z.boolean().default(false),
|
||||
pageRange: PageRangeSchema.optional(),
|
||||
source: z.union([HtmlSourceOptionsSchema, WordPressSourceOptionsSchema]).optional(),
|
||||
useMultiThreading: z.boolean().default(false),
|
||||
}),
|
||||
}),
|
||||
paths: z.object({
|
||||
data: z.string(),
|
||||
root: z.string(),
|
||||
}),
|
||||
sources: z.object({
|
||||
html: z.array(HtmlSourceOptionsSchema).default([]),
|
||||
wordpress: z.array(WordPressSourceOptionsSchema).default([]),
|
||||
}),
|
||||
});
|
||||
|
||||
// types
|
||||
export type SourceKind = z.infer<typeof SourceKindSchema>;
|
||||
export type SourceDate = z.infer<typeof SourceDateSchema>;
|
||||
export type HtmlSourceOptions = z.infer<typeof HtmlSourceOptionsSchema>;
|
||||
export type WordPressSourceOptions = z.infer<typeof WordPressSourceOptionsSchema>;
|
||||
export type AnySourceOptions = HtmlSourceOptions | WordPressSourceOptions;
|
||||
|
||||
export type CrawlerConfiguration = z.infer<typeof CrawlerConfigurationSchema>;
|
||||
export type CrawlerHttpOptions = CrawlerConfiguration["fetch"]["client"];
|
||||
export type CrawlerFetchingOptions = CrawlerConfiguration["fetch"]["crawler"];
|
||||
export type CrawlerAsyncOptions = CrawlerConfiguration["fetch"]["async"];
|
||||
export type CrawlerBackendOptions = CrawlerConfiguration["backend"];
|
||||
@@ -0,0 +1,15 @@
|
||||
import z from "zod";
|
||||
|
||||
export const DatabaseConfigurationSchema = z.object({
|
||||
legacy: z.object({
|
||||
host: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
port: z.number().optional(),
|
||||
user: z.string().min(1),
|
||||
}),
|
||||
url: z.string().min(1),
|
||||
});
|
||||
|
||||
// types
|
||||
export type DatabaseConfiguration = z.infer<typeof DatabaseConfigurationSchema>;
|
||||
@@ -0,0 +1,18 @@
|
||||
import z from "zod";
|
||||
|
||||
import {
|
||||
DEFAULT_AUTH_TAG_LENGTH,
|
||||
DEFAULT_BCRYPT_SALT_ROUNDS,
|
||||
DEFAULT_IV_LENGTH,
|
||||
} from "../constants";
|
||||
|
||||
export const EncryptionConfigurationSchema = z.object({
|
||||
algorithm: z.enum(["aes-128-gcm", "aes-192-gcm", "aes-256-gcm"]),
|
||||
authTagLength: z.number().nonnegative().default(DEFAULT_AUTH_TAG_LENGTH),
|
||||
bcryptSaltRounds: z.number().nonnegative().default(DEFAULT_BCRYPT_SALT_ROUNDS),
|
||||
ivLength: z.number().nonnegative().default(DEFAULT_IV_LENGTH),
|
||||
key: z.string(),
|
||||
});
|
||||
|
||||
// types
|
||||
export type EncryptionConfiguration = z.infer<typeof EncryptionConfigurationSchema>;
|
||||
@@ -0,0 +1,72 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { defineConfig } from "@devscast/config";
|
||||
import z from "zod";
|
||||
|
||||
import { ApiConfigurationSchema } from "./api";
|
||||
import { CrawlerConfigurationSchema } from "./crawler";
|
||||
import { DatabaseConfigurationSchema } from "./database";
|
||||
import { EncryptionConfigurationSchema } from "./encryption";
|
||||
import { LoggerConfigurationSchema } from "./logger";
|
||||
import { SharedConfigurationSchema } from "./shared";
|
||||
|
||||
export * from "./api";
|
||||
export * from "./crawler";
|
||||
export * from "./database";
|
||||
export * from "./encryption";
|
||||
export * from "./logger";
|
||||
export * from "./shared";
|
||||
|
||||
const root = path.resolve(__dirname, "../../../../");
|
||||
const domain = path.join(root, "packages", "domain", "config");
|
||||
|
||||
export const { env, config } = defineConfig({
|
||||
env: {
|
||||
knownKeys: [
|
||||
"NODE_ENV",
|
||||
"BASANGO_API_HOST",
|
||||
"BASANGO_API_PORT",
|
||||
"BASANGO_API_ALLOWED_ORIGINS",
|
||||
"BASANGO_API_KEY",
|
||||
"BASANGO_API_CRAWLER_TOKEN",
|
||||
"BASANGO_API_JWT_SECRET",
|
||||
"BASANGO_DATABASE_URL",
|
||||
"BASANGO_DATABASE_LEGACY_HOST",
|
||||
"BASANGO_DATABASE_LEGACY_PASSWORD",
|
||||
"BASANGO_DATABASE_LEGACY_NAME",
|
||||
"BASANGO_DATABASE_LEGACY_USER",
|
||||
"BASANGO_CRAWLER_ROOT_PATH",
|
||||
"BASANGO_CRAWLER_DATA_PATH",
|
||||
"BASANGO_CRAWLER_LOGS_PATH",
|
||||
"BASANGO_CRAWLER_CONFIG_PATH",
|
||||
"BASANGO_CRAWLER_UPDATE_DIRECTION",
|
||||
"BASANGO_CRAWLER_FETCH_USER_AGENT",
|
||||
"BASANGO_CRAWLER_FETCH_MAX_RETRIES",
|
||||
"BASANGO_CRAWLER_FETCH_RESPECT_RETRY_AFTER",
|
||||
"BASANGO_CRAWLER_ASYNC_REDIS_URL",
|
||||
"BASANGO_CRAWLER_ASYNC_TTL_RESULT",
|
||||
"BASANGO_CRAWLER_ASYNC_TTL_FAILURE",
|
||||
"BASANGO_CRAWLER_ASYNC_QUEUE_LISTING",
|
||||
"BASANGO_CRAWLER_ASYNC_QUEUE_DETAILS",
|
||||
"BASANGO_CRAWLER_ASYNC_QUEUE_PROCESSING",
|
||||
"BASANGO_ENCRYPTION_KEY",
|
||||
] as const,
|
||||
path: path.join(root, ".env"),
|
||||
},
|
||||
schema: z.object({
|
||||
api: ApiConfigurationSchema,
|
||||
crawler: CrawlerConfigurationSchema,
|
||||
database: DatabaseConfigurationSchema,
|
||||
encryption: EncryptionConfigurationSchema,
|
||||
logger: LoggerConfigurationSchema,
|
||||
shared: SharedConfigurationSchema,
|
||||
}),
|
||||
sources: [
|
||||
path.join(domain, "api.json"),
|
||||
path.join(domain, "crawler.json"),
|
||||
path.join(domain, "database.json"),
|
||||
path.join(domain, "encryption.json"),
|
||||
path.join(domain, "logger.json"),
|
||||
path.join(domain, "shared.json"),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import z from "zod";
|
||||
|
||||
export const LoggerConfigurationSchema = z.object({
|
||||
level: z.string().default("info"),
|
||||
});
|
||||
|
||||
// types
|
||||
export type LoggerConfiguration = z.infer<typeof LoggerConfigurationSchema>;
|
||||
@@ -0,0 +1,17 @@
|
||||
import z from "zod";
|
||||
|
||||
export const SharedConfigurationSchema = z.object({
|
||||
categorySharesLimit: z.number().int().min(1).default(10),
|
||||
dateFormat: z.string(),
|
||||
dateTimeFormat: z.string(),
|
||||
name: z.string().default("Basango"),
|
||||
pagination: z.object({
|
||||
defaultLimit: z.number().int().min(1).max(100),
|
||||
maxLimit: z.number().int().min(1).max(100),
|
||||
page: z.number().int().min(1),
|
||||
}),
|
||||
publicationGraphDays: z.number().int().min(1),
|
||||
timezone: z.string(),
|
||||
});
|
||||
|
||||
export type SharedConfiguration = z.infer<typeof SharedConfigurationSchema>;
|
||||
@@ -1,10 +1,8 @@
|
||||
// Domain-specific constants and types
|
||||
export const BIAS = ["neutral", "slightly", "partisan", "extreme"] as const;
|
||||
export const RELIABILITY = ["trusted", "reliable", "average", "low_trust", "unreliable"] as const;
|
||||
export const TRANSPARENCY = ["high", "medium", "low"] as const;
|
||||
export const SENTIMENT = ["positive", "neutral", "negative"] as const;
|
||||
|
||||
// Crawler-related constants and types
|
||||
export const UPDATE_DIRECTIONS = ["forward", "backward"] as const;
|
||||
export const SOURCE_KINDS = ["wordpress", "html"] as const;
|
||||
|
||||
@@ -32,5 +30,5 @@ export const DEFAULT_AUTH_TAG_LENGTH = 16;
|
||||
export const DEFAULT_BCRYPT_SALT_ROUNDS = 12;
|
||||
export const DEFAULT_TOKEN_AUDIENCE = "basango_dashboard";
|
||||
export const DEFAULT_TOKEN_ISSUER = "basango_api";
|
||||
export const DEFAULT_ACCESS_TOKEN_TTL = "15m";
|
||||
export const DEFAULT_ACCESS_TOKEN_TTL = "35m";
|
||||
export const DEFAULT_REFRESH_TOKEN_TTL = "7d";
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SOURCE_KINDS } from "#domain/constants";
|
||||
|
||||
// schemas
|
||||
export const SourceKindSchema = z.enum(SOURCE_KINDS);
|
||||
|
||||
export const SourceDateSchema = z.object({
|
||||
format: z.string().default("yyyy-LL-dd HH:mm"),
|
||||
});
|
||||
|
||||
const SourceConfigSchema = z.object({
|
||||
categories: z.array(z.string()).default([]),
|
||||
requiresDetails: z.boolean().default(false),
|
||||
requiresRateLimit: z.boolean().default(false),
|
||||
sourceDate: SourceDateSchema,
|
||||
sourceId: z.string(),
|
||||
sourceKind: SourceKindSchema,
|
||||
sourceUrl: z.url(),
|
||||
supportsCategories: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const HtmlSourceConfigSchema = SourceConfigSchema.extend({
|
||||
paginationTemplate: z.string(),
|
||||
sourceKind: z.literal("html"),
|
||||
sourceSelectors: z.object({
|
||||
articleBody: z.string(),
|
||||
articleCategories: z.string().optional(),
|
||||
articleDate: z.string(),
|
||||
articleLink: z.string(),
|
||||
articles: z.string(),
|
||||
articleTitle: z.string(),
|
||||
pagination: z.string().default("ul.pagination > li a"),
|
||||
}),
|
||||
});
|
||||
|
||||
export const WordPressSourceConfigSchema = SourceConfigSchema.extend({
|
||||
sourceDate: SourceDateSchema.default(SourceDateSchema.parse({ format: "yyyy-LL-dd'T'HH:mm:ss" })),
|
||||
sourceKind: z.literal("wordpress"),
|
||||
});
|
||||
|
||||
// types
|
||||
export type SourceKind = z.infer<typeof SourceKindSchema>;
|
||||
export type SourceDate = z.infer<typeof SourceDateSchema>;
|
||||
export type HtmlSourceConfig = z.infer<typeof HtmlSourceConfigSchema>;
|
||||
export type WordPressSourceConfig = z.infer<typeof WordPressSourceConfigSchema>;
|
||||
export type AnySourceConfig = HtmlSourceConfig | WordPressSourceConfig;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./config";
|
||||
export * from "./schemas";
|
||||
@@ -1,185 +1,65 @@
|
||||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
import { idSchema, sentimentSchema } from "#domain/models/shared";
|
||||
import z from "zod";
|
||||
|
||||
import { idSchema, sentimentSchema } from "./shared";
|
||||
import { sourceSchema } from "./sources";
|
||||
|
||||
// schemas
|
||||
export const articleMetadataSchema = z.object({
|
||||
author: z.string().optional().openapi({
|
||||
description: "The author of the article.",
|
||||
example: "John Doe",
|
||||
}),
|
||||
description: z.string().optional().openapi({
|
||||
description: "A brief description or summary of the article.",
|
||||
example: "This article discusses the latest advancements in AI technology.",
|
||||
}),
|
||||
image: z.url().optional().openapi({
|
||||
description: "The URL of the main image associated with the article.",
|
||||
example: "https://example.com/image.jpg",
|
||||
}),
|
||||
publishedAt: z.date().optional().openapi({
|
||||
description: "The publication date of the article as a Date object.",
|
||||
example: "2023-01-01T00:00:00Z",
|
||||
}),
|
||||
title: z.string().optional().openapi({
|
||||
description: "The title of the article for metadata purposes.",
|
||||
example: "The Rise of AI",
|
||||
}),
|
||||
updatedAt: z.date().optional().openapi({
|
||||
description: "The last updated date of the article as a Date object.",
|
||||
example: "2023-01-02T12:00:00Z",
|
||||
}),
|
||||
url: z.url().optional().openapi({
|
||||
description: "The canonical URL of the article.",
|
||||
example: "https://example.com/article",
|
||||
}),
|
||||
author: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
image: z.url().optional(),
|
||||
publishedAt: z.date().optional(),
|
||||
title: z.string().optional(),
|
||||
updatedAt: z.date().optional(),
|
||||
url: z.url().optional(),
|
||||
});
|
||||
|
||||
export const tokenStatisticsSchema = z.object({
|
||||
body: z.number().optional().default(0).openapi({
|
||||
description: "The number of tokens in the article body.",
|
||||
example: 250,
|
||||
}),
|
||||
categories: z.number().optional().default(0).openapi({
|
||||
description: "The number of tokens in the article categories.",
|
||||
example: 3,
|
||||
}),
|
||||
excerpt: z.number().optional().default(0).openapi({
|
||||
description: "The number of tokens in the article excerpt.",
|
||||
example: 50,
|
||||
}),
|
||||
title: z.number().optional().default(0).openapi({
|
||||
description: "The number of tokens in the article title.",
|
||||
example: 10,
|
||||
}),
|
||||
total: z.number().optional().default(0).openapi({
|
||||
description: "The total number of tokens in the article.",
|
||||
example: 313,
|
||||
}),
|
||||
body: z.number().optional().default(0),
|
||||
categories: z.number().optional().default(0),
|
||||
excerpt: z.number().optional().default(0),
|
||||
title: z.number().optional().default(0),
|
||||
total: z.number().optional().default(0),
|
||||
});
|
||||
|
||||
export const articleSchema = z.object({
|
||||
body: z.string().min(1).openapi({
|
||||
description: "The main content of the article.",
|
||||
example: "This is the body of the article...",
|
||||
}),
|
||||
categories: z.array(z.string()).openapi({
|
||||
description: "The categories or tags associated with the article.",
|
||||
example: ["Technology", "AI"],
|
||||
}),
|
||||
createdAt: z.date().openapi({
|
||||
description: "The date and time when the article was created in the system.",
|
||||
example: "2023-01-01T12:00:00Z",
|
||||
}),
|
||||
excerpt: z.string().optional().openapi({
|
||||
description: "A brief excerpt or summary of the article.",
|
||||
example: "This article discusses the latest advancements in AI technology.",
|
||||
}),
|
||||
hash: z.string().min(1).openapi({
|
||||
description: "The unique hash of the article link.",
|
||||
example: "d41d8cd98f00b204e9800998ecf8427e",
|
||||
}),
|
||||
body: z.string().min(1),
|
||||
categories: z.array(z.string()),
|
||||
createdAt: z.date(),
|
||||
excerpt: z.string().optional(),
|
||||
hash: z.string().min(1),
|
||||
id: idSchema,
|
||||
image: z.url().optional().openapi({
|
||||
description: "The URL of the main image associated with the article.",
|
||||
example: "https://example.com/image.jpg",
|
||||
}),
|
||||
link: z.string().url().openapi({
|
||||
description: "The URL of the article.",
|
||||
example: "https://example.com/article",
|
||||
}),
|
||||
image: z.url().optional(),
|
||||
link: z.url(),
|
||||
metadata: articleMetadataSchema.optional(),
|
||||
publishedAt: z.date().openapi({
|
||||
description: "The publication date of the article as a Date object.",
|
||||
example: "2023-01-01T00:00:00Z",
|
||||
}),
|
||||
readingTime: z.number().int().min(1).openapi({
|
||||
description: "Estimated reading time of the article in minutes.",
|
||||
example: 5,
|
||||
}),
|
||||
publishedAt: z.date(),
|
||||
readingTime: z.number().int().min(1),
|
||||
source: sourceSchema.optional(),
|
||||
sourceId: z.union([z.uuid(), z.string().min(1)]).openapi({
|
||||
description: "The unique identifier of the source from which the article was crawled.",
|
||||
example: "b3e1c8f4-5d6a-4c9e-8f1e-2d3c4b5a6f7g",
|
||||
}),
|
||||
title: z.string().min(1).openapi({
|
||||
description: "The title of the article.",
|
||||
example: "The Rise of AI",
|
||||
}),
|
||||
sourceId: z.union([z.uuid(), z.string().min(1)]),
|
||||
title: z.string().min(1),
|
||||
tokenStatistics: tokenStatisticsSchema.optional(),
|
||||
updatedAt: z.date().optional().openapi({
|
||||
description: "The date and time when the article was last updated in the system.",
|
||||
example: "2023-01-02T12:00:00Z",
|
||||
}),
|
||||
updatedAt: z.date().optional(),
|
||||
});
|
||||
|
||||
// API
|
||||
export const createArticleSchema = z
|
||||
.object({
|
||||
body: z.string().min(1).openapi({
|
||||
description: "The main content of the article.",
|
||||
example: "This is the body of the article...",
|
||||
}),
|
||||
categories: z
|
||||
.array(z.string())
|
||||
.openapi({
|
||||
description: "The categories or tags associated with the article.",
|
||||
example: ["Technology", "AI"],
|
||||
})
|
||||
.optional()
|
||||
.default([]),
|
||||
hash: z.string().min(1).openapi({
|
||||
description: "The unique hash of the article link.",
|
||||
example: "d41d8cd98f00b204e9800998ecf8427e",
|
||||
}),
|
||||
link: z.string().url().openapi({
|
||||
description: "The URL of the article.",
|
||||
example: "https://example.com/article",
|
||||
}),
|
||||
metadata: articleMetadataSchema.optional(),
|
||||
publishedAt: z
|
||||
.string()
|
||||
.refine((value) => !Number.isNaN(Date.parse(value)), {
|
||||
message: "Invalid date format",
|
||||
})
|
||||
.transform((value) => new Date(value))
|
||||
.openapi({
|
||||
description: "The publication date of the article in ISO 8601 format.",
|
||||
example: "2023-01-01T00:00:00Z",
|
||||
}),
|
||||
sourceId: z.string().openapi({
|
||||
description: "The unique identifier of the source from which the article was crawled.",
|
||||
example: "radiookapi.net",
|
||||
}),
|
||||
title: z.string().min(1).openapi({
|
||||
description: "The title of the article.",
|
||||
example: "The Rise of AI",
|
||||
}),
|
||||
})
|
||||
.openapi("CreateArticle");
|
||||
export const createArticleSchema = z.object({
|
||||
body: z.string().min(1),
|
||||
categories: z.array(z.string()).optional().default([]),
|
||||
hash: z.string().min(1),
|
||||
link: z.url(),
|
||||
metadata: articleMetadataSchema.optional(),
|
||||
publishedAt: z.coerce.date(),
|
||||
sourceId: z.string(),
|
||||
title: z.string().min(1),
|
||||
});
|
||||
|
||||
export const createArticleResponseSchema = z
|
||||
.object({ id: idSchema, sourceId: idSchema })
|
||||
.openapi("CreateArticleResponse");
|
||||
export const createArticleResponseSchema = z.object({ id: idSchema, sourceId: idSchema });
|
||||
|
||||
export const getArticlesSchema = z.object({
|
||||
category: z.string().min(1).max(255).optional().openapi({
|
||||
description: "Filter articles by a specific category.",
|
||||
example: "Technology",
|
||||
}),
|
||||
cursor: z.string().nullable().optional().openapi({
|
||||
description: "Optional cursor for fetching the next page of articles.",
|
||||
}),
|
||||
limit: z.number().int().min(1).max(100).optional().openapi({
|
||||
default: 10,
|
||||
description: "Maximum number of articles to return per page.",
|
||||
example: 20,
|
||||
}),
|
||||
search: z.string().max(512).optional().openapi({
|
||||
description: "Full-text search query applied to article titles and bodies.",
|
||||
example: "gouvernement congolais",
|
||||
}),
|
||||
category: z.string().min(1).max(255).optional(),
|
||||
cursor: z.string().nullable().optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
search: z.string().max(512).optional(),
|
||||
sentiment: sentimentSchema.optional(),
|
||||
sourceId: idSchema.optional(),
|
||||
});
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { z } from "@hono/zod-openapi";
|
||||
import z from "zod";
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.email().openapi({
|
||||
description: "Email address used to authenticate the user.",
|
||||
example: "user@example.com",
|
||||
}),
|
||||
password: z.string().min(8).openapi({
|
||||
description: "Account password.",
|
||||
example: "••••••••",
|
||||
}),
|
||||
email: z.email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
export const refreshSessionSchema = z.object({
|
||||
refreshToken: z.string().min(1).openapi({
|
||||
description: "Refresh token returned when logging in.",
|
||||
}),
|
||||
refreshToken: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import z from "zod";
|
||||
|
||||
import { UPDATE_DIRECTIONS } from "#domain/constants";
|
||||
import { UPDATE_DIRECTIONS } from "../constants";
|
||||
|
||||
// schemas
|
||||
export const UpdateDirectionSchema = z.enum(UPDATE_DIRECTIONS);
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./articles";
|
||||
export * from "./auth";
|
||||
export * from "./crawler";
|
||||
export * from "./reports";
|
||||
export * from "./shared";
|
||||
export * from "./sources";
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
import { z } from "@hono/zod-openapi";
|
||||
import z from "zod";
|
||||
|
||||
import { deltaSchema } from "#domain/models/shared";
|
||||
import { deltaSchema } from "./shared";
|
||||
|
||||
export const overviewMetricSchema = z
|
||||
.object({
|
||||
delta: deltaSchema.openapi({
|
||||
description: "Change measured over the last 30 days compared to the previous 30-day window.",
|
||||
}),
|
||||
total: z.number().int().nonnegative().openapi({
|
||||
description: "Total count across the entire dataset.",
|
||||
example: 12584,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Aggregated metric with total count and delta metadata.",
|
||||
});
|
||||
export const overviewMetricSchema = z.object({
|
||||
delta: deltaSchema,
|
||||
total: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const dashboardOverviewSchema = z
|
||||
.object({
|
||||
articles: overviewMetricSchema,
|
||||
sources: overviewMetricSchema,
|
||||
users: overviewMetricSchema,
|
||||
})
|
||||
.openapi({
|
||||
description: "Dashboard overview metrics for key entities.",
|
||||
});
|
||||
export const dashboardOverviewSchema = z.object({
|
||||
articles: overviewMetricSchema,
|
||||
sources: overviewMetricSchema,
|
||||
users: overviewMetricSchema,
|
||||
});
|
||||
|
||||
export type OverviewMetric = z.infer<typeof overviewMetricSchema>;
|
||||
export type DashboardOverview = z.infer<typeof dashboardOverviewSchema>;
|
||||
|
||||
@@ -1,138 +1,50 @@
|
||||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
import { BIAS, RELIABILITY, SENTIMENT, TRANSPARENCY } from "#domain/constants";
|
||||
import { BIAS, RELIABILITY, SENTIMENT, TRANSPARENCY } from "../constants";
|
||||
|
||||
// schemas
|
||||
export const idSchema = z.uuid().openapi({
|
||||
description: "The unique identifier of the resource.",
|
||||
example: "b3e1c8f4-5d6a-4c9e-8f1e-2d3c4b5a6f7g",
|
||||
export const idSchema = z.uuid();
|
||||
|
||||
export const dateRangeSchema = z.object({
|
||||
end: z.coerce.date(),
|
||||
start: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const dateRangeSchema = z
|
||||
.object({
|
||||
end: z.date().openapi({
|
||||
description: "The end date of the range.",
|
||||
example: "2023-01-30T23:59:59Z",
|
||||
}),
|
||||
start: z.date().openapi({
|
||||
description: "The start date of the range.",
|
||||
example: "2023-01-01T00:00:00Z",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Inclusive date range for publication metrics.",
|
||||
});
|
||||
export const limitSchema = z.number().int().min(1).max(100);
|
||||
export const sentimentSchema = z.enum(SENTIMENT);
|
||||
export const biasSchema = z.enum(BIAS);
|
||||
export const reliabilitySchema = z.enum(RELIABILITY);
|
||||
export const transparencySchema = z.enum(TRANSPARENCY);
|
||||
|
||||
export const limitSchema = z.number().int().min(1).max(100).openapi({
|
||||
default: 10,
|
||||
description: "The maximum number of items to return.",
|
||||
example: 10,
|
||||
export const credibilitySchema = z.object({
|
||||
bias: biasSchema.default("neutral"),
|
||||
reliability: reliabilitySchema.default("average"),
|
||||
transparency: transparencySchema.default("medium"),
|
||||
});
|
||||
|
||||
export const sentimentSchema = z.enum(SENTIMENT).openapi({
|
||||
description: "Sentiment detected for the article.",
|
||||
example: "positive",
|
||||
export const deviceSchema = z.object({
|
||||
client: z.string().optional(),
|
||||
device: z.string().optional(),
|
||||
isBot: z.boolean(),
|
||||
operatingSystem: z.string().optional(),
|
||||
});
|
||||
|
||||
export const biasSchema = z.enum(BIAS).openapi({
|
||||
description: "The bias level of the source.",
|
||||
example: "neutral",
|
||||
export const geoLocationSchema = z.object({
|
||||
accuracyRadius: z.number().optional(),
|
||||
city: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
latitude: z.number().optional(),
|
||||
longitude: z.number().optional(),
|
||||
timeZone: z.string().optional(),
|
||||
});
|
||||
|
||||
export const reliabilitySchema = z.enum(RELIABILITY).openapi({
|
||||
description: "The reliability level of the source.",
|
||||
example: "trusted",
|
||||
export const distrubtionSchema = z.object({
|
||||
count: z.number().int(),
|
||||
id: idSchema,
|
||||
name: z.string(),
|
||||
percentage: z.number(),
|
||||
});
|
||||
|
||||
export const transparencySchema = z.enum(TRANSPARENCY).openapi({
|
||||
description: "The transparency level of the source.",
|
||||
example: "high",
|
||||
});
|
||||
|
||||
export const credibilitySchema = z
|
||||
.object({
|
||||
bias: biasSchema.default("neutral"),
|
||||
reliability: reliabilitySchema.default("average"),
|
||||
transparency: transparencySchema.default("medium"),
|
||||
})
|
||||
.openapi({
|
||||
description: "Credibility information about the resource.",
|
||||
});
|
||||
|
||||
export const deviceSchema = z
|
||||
.object({
|
||||
client: z.string().optional().openapi({
|
||||
description: "The client software of the device.",
|
||||
example: "Chrome 90",
|
||||
}),
|
||||
device: z.string().optional().openapi({
|
||||
description: "The device model.",
|
||||
example: "Dell XPS 13",
|
||||
}),
|
||||
isBot: z.boolean().openapi({
|
||||
description: "Indicates if the device is a bot.",
|
||||
example: false,
|
||||
}),
|
||||
operatingSystem: z.string().optional().openapi({
|
||||
description: "The operating system of the device.",
|
||||
example: "Windows 10",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Information about the user's device.",
|
||||
});
|
||||
|
||||
export const geoLocationSchema = z
|
||||
.object({
|
||||
accuracyRadius: z.number().optional().openapi({
|
||||
description: "The accuracy radius in kilometers.",
|
||||
example: 50,
|
||||
}),
|
||||
city: z.string().optional().openapi({
|
||||
description: "The city of the user.",
|
||||
example: "San Francisco",
|
||||
}),
|
||||
country: z.string().optional().openapi({
|
||||
description: "The country of the user.",
|
||||
example: "United States",
|
||||
}),
|
||||
latitude: z.number().optional().openapi({
|
||||
description: "The latitude of the user's location.",
|
||||
example: 37.7749,
|
||||
}),
|
||||
longitude: z.number().optional().openapi({
|
||||
description: "The longitude of the user's location.",
|
||||
example: -122.4194,
|
||||
}),
|
||||
timeZone: z.string().optional().openapi({
|
||||
description: "The time zone of the user.",
|
||||
example: "America/Los_Angeles",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Geolocation information about the user.",
|
||||
});
|
||||
|
||||
export const distrubtionSchema = z
|
||||
.object({
|
||||
count: z.number().int().openapi({
|
||||
description: "The count of items in the distribution.",
|
||||
example: 42,
|
||||
}),
|
||||
id: idSchema,
|
||||
name: z.string().openapi({
|
||||
description: "The name of the distribution.",
|
||||
example: "Technology",
|
||||
}),
|
||||
percentage: z.number().openapi({
|
||||
description: "The percentage of items in the distribution.",
|
||||
example: 12.5,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Distribution information.",
|
||||
});
|
||||
|
||||
export const getDistributionsSchema = z.object({
|
||||
id: idSchema.optional(),
|
||||
limit: limitSchema.optional(),
|
||||
@@ -143,172 +55,60 @@ export const getPublicationsSchema = z.object({
|
||||
range: dateRangeSchema.optional(),
|
||||
});
|
||||
|
||||
export const distributionsSchema = z
|
||||
.object({
|
||||
items: z.array(distrubtionSchema).openapi({
|
||||
description: "List of distributions.",
|
||||
}),
|
||||
total: z.number().int().openapi({
|
||||
description: "Total number of distributions.",
|
||||
example: 100,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Distributions data.",
|
||||
});
|
||||
export const distributionsSchema = z.object({
|
||||
items: z.array(distrubtionSchema),
|
||||
total: z.number().int(),
|
||||
});
|
||||
|
||||
export const publicationSchema = z
|
||||
.object({
|
||||
count: z.number().int().openapi({
|
||||
description: "The number of articles published on that date.",
|
||||
example: 42,
|
||||
}),
|
||||
date: z.string().openapi({
|
||||
description: "The date of the publication.",
|
||||
example: "2023-01-15",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Publication metrics for a specific date.",
|
||||
});
|
||||
export const publicationSchema = z.object({
|
||||
count: z.number().int(),
|
||||
date: z.string(),
|
||||
});
|
||||
|
||||
export const deltaSchema = z
|
||||
.object({
|
||||
delta: z.number().openapi({
|
||||
description: "The absolute change in value.",
|
||||
example: 10,
|
||||
}),
|
||||
percentage: z.number().openapi({
|
||||
description: "The percentage change in value.",
|
||||
example: 25.0,
|
||||
}),
|
||||
sign: z.enum(["+", "-"]).openapi({
|
||||
description: "The sign of the change.",
|
||||
example: "+",
|
||||
}),
|
||||
variant: z.enum(["increase", "decrease", "positive"]).openapi({
|
||||
description: "The variant of the change.",
|
||||
example: "increase",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Delta information representing change over time.",
|
||||
});
|
||||
export const deltaSchema = z.object({
|
||||
delta: z.number(),
|
||||
percentage: z.number(),
|
||||
sign: z.enum(["+", "-"]),
|
||||
variant: z.enum(["increase", "decrease", "positive"]),
|
||||
});
|
||||
|
||||
export const publicationMetaSchema = z
|
||||
.object({
|
||||
current: z.number().openapi({
|
||||
description: "The current total value.",
|
||||
example: 150,
|
||||
}),
|
||||
delta: deltaSchema,
|
||||
previous: z.number().openapi({
|
||||
description: "The previous total value.",
|
||||
example: 120,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Metadata for publication metrics.",
|
||||
});
|
||||
export const publicationMetaSchema = z.object({
|
||||
current: z.number(),
|
||||
delta: deltaSchema,
|
||||
previous: z.number(),
|
||||
});
|
||||
|
||||
export const publicationsSchema = z
|
||||
.object({
|
||||
items: z.array(publicationSchema).openapi({
|
||||
description: "List of publication metrics for the source.",
|
||||
}),
|
||||
meta: publicationMetaSchema.optional(),
|
||||
})
|
||||
.openapi({
|
||||
description: "Publication metrics for the source.",
|
||||
});
|
||||
export const publicationsSchema = z.object({
|
||||
items: z.array(publicationSchema),
|
||||
meta: publicationMetaSchema.optional(),
|
||||
});
|
||||
|
||||
export const paginationCursorSchema = z
|
||||
.object({
|
||||
date: z.string().openapi({
|
||||
description: "The date associated with the last item in the current page.",
|
||||
example: "2023-01-15",
|
||||
}),
|
||||
id: z.string().openapi({
|
||||
description: "The unique identifier of the last item in the current page.",
|
||||
example: "b3e1c8f4-5d6a-4c9e-8f1e-2d3c4b5a6f7g",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Cursor information for pagination.",
|
||||
});
|
||||
export const paginationCursorSchema = z.object({
|
||||
date: z.string(),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export const paginationRequestSchema = z
|
||||
.object({
|
||||
cursor: z.string().nullable().optional().openapi({
|
||||
description: "The pagination cursor for cursor-based pagination.",
|
||||
example:
|
||||
"eyJkYXRlIjoiMjAyMy0wMS0xNSIsImlkIjoiYjNlMWM4ZjQtNWQ2YS00YzllLThmMWUtMmQzYzRiNWE2ZjdifQ==",
|
||||
}),
|
||||
limit: limitSchema.optional(),
|
||||
page: z.number().int().min(1).optional().openapi({
|
||||
description: "The page number to retrieve.",
|
||||
example: 1,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Pagination request parameters.",
|
||||
});
|
||||
export const paginationRequestSchema = z.object({
|
||||
cursor: z.string().nullable().optional(),
|
||||
limit: limitSchema.optional(),
|
||||
page: z.number().nonnegative().default(1).optional(),
|
||||
});
|
||||
|
||||
export const paginationStateSchema = z
|
||||
.object({
|
||||
cursor: z.string().nullable().openapi({
|
||||
description: "The current pagination cursor.",
|
||||
example:
|
||||
"eyJkYXRlIjoiMjAyMy0wMS0xNSIsImlkIjoiYjNlMWM4ZjQtNWQ2YS00YzllLThmMWUtMmQzYzRiNWE2ZjdifQ==",
|
||||
}),
|
||||
limit: z.number().int().openapi({
|
||||
description: "The number of items per page.",
|
||||
example: 10,
|
||||
}),
|
||||
offset: z.number().int().openapi({
|
||||
description: "The offset for the current page.",
|
||||
example: 0,
|
||||
}),
|
||||
page: z.number().int().openapi({
|
||||
description: "The current page number.",
|
||||
example: 1,
|
||||
}),
|
||||
payload: paginationCursorSchema.nullable().openapi({
|
||||
description: "The decoded payload from the pagination cursor.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Internal pagination state.",
|
||||
});
|
||||
export const paginationStateSchema = z.object({
|
||||
cursor: z.string().nullable(),
|
||||
limit: z.number().int(),
|
||||
offset: z.number().int(),
|
||||
page: z.number().int(),
|
||||
payload: paginationCursorSchema.nullable(),
|
||||
});
|
||||
|
||||
export const paginationMetaSchema = z
|
||||
.object({
|
||||
current: z.number().int().openapi({
|
||||
description: "The current page number or offset.",
|
||||
example: 1,
|
||||
}),
|
||||
cursor: z.string().nullable().openapi({
|
||||
description: "The current pagination cursor.",
|
||||
example:
|
||||
"eyJkYXRlIjoiMjAyMy0wMS0xNSIsImlkIjoiYjNlMWM4ZjQtNWQ2YS00YzllLThmMWUtMmQzYzRiNWE2ZjdifQ==",
|
||||
}),
|
||||
hasNext: z.boolean().openapi({
|
||||
description: "Indicates if there is a next page available.",
|
||||
example: true,
|
||||
}),
|
||||
limit: z.number().int().openapi({
|
||||
description: "The number of items per page.",
|
||||
example: 10,
|
||||
}),
|
||||
nextCursor: z.string().nullable().openapi({
|
||||
description: "The next pagination cursor, if available.",
|
||||
example:
|
||||
"eyJkYXRlIjoiMjAyMy0wMS0yMCIsImlkIjoiZDRmNWU2ZTAtNzY4Ny00Y2E3LTg5ZTItYjY0ZGI3Y2E3ZGIifQ==",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Pagination metadata.",
|
||||
});
|
||||
export const paginationMetaSchema = z.object({
|
||||
current: z.number().int(),
|
||||
cursor: z.string().nullable(),
|
||||
hasNext: z.boolean(),
|
||||
limit: z.number().int(),
|
||||
nextCursor: z.string().nullable(),
|
||||
});
|
||||
|
||||
// types
|
||||
export type PaginatedResult<T> = {
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
import { z } from "@hono/zod-openapi";
|
||||
import z from "zod";
|
||||
|
||||
import {
|
||||
credibilitySchema,
|
||||
idSchema,
|
||||
limitSchema,
|
||||
publicationsSchema,
|
||||
} from "#domain/models/shared";
|
||||
import { credibilitySchema, idSchema, limitSchema, publicationsSchema } from "./shared";
|
||||
|
||||
// schemas
|
||||
export const sourceSchema = z.object({
|
||||
articles: z.number().int().min(0).optional().openapi({
|
||||
description: "The total number of articles from this source.",
|
||||
example: 1250,
|
||||
}),
|
||||
articles: z.number().int().min(0).optional(),
|
||||
credibility: credibilitySchema.optional(),
|
||||
description: z.string().max(1024).optional().openapi({
|
||||
description: "A brief description of the source.",
|
||||
example: "Radio Okapi is a Congolese radio station that provides news and information.",
|
||||
}),
|
||||
displayName: z.string().min(1).max(255).optional().openapi({
|
||||
description: "The display name of the source.",
|
||||
example: "Radio Okapi",
|
||||
}),
|
||||
description: z.string().max(1024).optional(),
|
||||
displayName: z.string().min(1).max(255).optional(),
|
||||
id: idSchema,
|
||||
name: z.string().min(1).max(255).openapi({
|
||||
description: "The name of the source.",
|
||||
example: "radiookapi.com",
|
||||
}),
|
||||
name: z.string().min(1).max(255),
|
||||
publications: publicationsSchema.optional(),
|
||||
url: z.url().max(255).openapi({
|
||||
description: "The URL of the source.",
|
||||
example: "https://techcrunch.com",
|
||||
}),
|
||||
url: z.url().max(255),
|
||||
});
|
||||
|
||||
export const createSourceSchema = sourceSchema.pick({
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
BASANGO_ENCRYPTION_KEY=testkey
|
||||
@@ -1,18 +1,10 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import {
|
||||
DEFAULT_AUTH_TAG_LENGTH,
|
||||
DEFAULT_BCRYPT_SALT_ROUNDS,
|
||||
DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
DEFAULT_IV_LENGTH,
|
||||
} from "@basango/domain/constants";
|
||||
import { createEnvAccessor } from "@devscast/config";
|
||||
import { config } from "@basango/domain/config";
|
||||
import * as bcrypt from "bcrypt";
|
||||
|
||||
export const env = createEnvAccessor(["BASANGO_ENCRYPTION_KEY"] as const);
|
||||
|
||||
function getKey(): Buffer {
|
||||
const key = env("BASANGO_ENCRYPTION_KEY");
|
||||
const key = config.encryption.key;
|
||||
|
||||
if (Buffer.from(key, "hex").length !== 32) {
|
||||
throw new Error("BASANGO_ENCRYPTION_KEY must be a 64-character hex string (32 bytes).");
|
||||
@@ -20,6 +12,12 @@ function getKey(): Buffer {
|
||||
return Buffer.from(key, "hex");
|
||||
}
|
||||
|
||||
const getEncryptionSettings = () => ({
|
||||
algorithm: config.encryption.algorithm as crypto.CipherGCMTypes,
|
||||
authTagLength: config.encryption.authTagLength,
|
||||
ivLength: config.encryption.ivLength,
|
||||
});
|
||||
|
||||
/**
|
||||
* Encrypts a plaintext string using AES-256-GCM.
|
||||
* @param text The plaintext string to encrypt.
|
||||
@@ -27,8 +25,9 @@ function getKey(): Buffer {
|
||||
*/
|
||||
export function encrypt(text: string): string {
|
||||
const key = getKey();
|
||||
const iv = crypto.randomBytes(DEFAULT_IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(DEFAULT_ENCRYPTION_ALGORITHM, key, iv);
|
||||
const { algorithm, ivLength } = getEncryptionSettings();
|
||||
const iv = crypto.randomBytes(ivLength);
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
|
||||
let encrypted = cipher.update(text, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
@@ -50,17 +49,15 @@ export function encrypt(text: string): string {
|
||||
*/
|
||||
export function decrypt(encryptedPayload: string): string {
|
||||
const key = getKey();
|
||||
const { algorithm, authTagLength, ivLength } = getEncryptionSettings();
|
||||
const dataBuffer = Buffer.from(encryptedPayload, "base64");
|
||||
|
||||
// Extract IV, auth tag, and encrypted data
|
||||
const iv = dataBuffer.subarray(0, DEFAULT_IV_LENGTH);
|
||||
const authTag = dataBuffer.subarray(
|
||||
DEFAULT_IV_LENGTH,
|
||||
DEFAULT_IV_LENGTH + DEFAULT_AUTH_TAG_LENGTH,
|
||||
);
|
||||
const encryptedText = dataBuffer.subarray(DEFAULT_IV_LENGTH + DEFAULT_AUTH_TAG_LENGTH);
|
||||
const iv = dataBuffer.subarray(0, ivLength);
|
||||
const authTag = dataBuffer.subarray(ivLength, ivLength + authTagLength);
|
||||
const encryptedText = dataBuffer.subarray(ivLength + authTagLength);
|
||||
|
||||
const decipher = crypto.createDecipheriv(DEFAULT_ENCRYPTION_ALGORITHM, key, iv);
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encryptedText.toString("hex"), "hex", "utf8");
|
||||
@@ -82,7 +79,8 @@ export function generateRandomBytes(size: number): string {
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, DEFAULT_BCRYPT_SALT_ROUNDS);
|
||||
const rounds = config.encryption.bcryptSaltRounds;
|
||||
return bcrypt.hash(password, rounds);
|
||||
}
|
||||
|
||||
export async function verifyPassword(password: string, hashed: string): Promise<boolean> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@devscast/config": "catalog:",
|
||||
"@basango/domain": "workspace:*",
|
||||
"pino": "^10.1.0",
|
||||
"pino-pretty": "^13.1.2"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { createEnvAccessor } from "@devscast/config";
|
||||
import { config } from "@basango/domain/config";
|
||||
import pino from "pino";
|
||||
|
||||
const env = createEnvAccessor(["LOG_LEVEL", "NODE_ENV"] as const);
|
||||
|
||||
export const logger = pino({
|
||||
level: env("LOG_LEVEL", { default: "info" }),
|
||||
// Use pretty printing in development, structured JSON in production
|
||||
...(env("NODE_ENV") !== "production" && {
|
||||
level: config.logger.level,
|
||||
...(process.env.NODE_ENV !== "production" && {
|
||||
transport: {
|
||||
options: {
|
||||
colorize: true,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"#domain/*": ["../domain/src/*"],
|
||||
"#logger/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"extends": "@basango/tsconfig/base.json",
|
||||
"include": ["src"]
|
||||
|
||||
Reference in New Issue
Block a user