Compare commits

7 Commits

Author SHA1 Message Date
2faaeabf92 Merge pull request 'feature/adapt-to-new-database' (#34) from feature/adapt-to-new-database into main
All checks were successful
Sync to GitHub / sync (push) Successful in 9s
Reviewed-on: #34
2026-05-29 15:47:46 +07:00
e527aa0655 🚨 fix: resolve linting type error
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 34s
2026-05-29 15:46:09 +07:00
aea16ad35a 🦖 fix: update payload to match new schema
Some checks failed
Integration Tests / integration-tests (pull_request) Failing after 1m37s
2026-05-29 15:42:02 +07:00
6215e14420 🐛 fix: resolve session deletion issue 2026-05-29 14:27:29 +07:00
cfb9b61c8a 🔧 chore: update env configuration 2026-05-29 14:14:10 +07:00
e92d996621 ♻️ refactor: update hero banner code for new schema 2026-05-29 14:07:40 +07:00
7da662f7ba Merge pull request 'refactor-db' (#33) from refactor-db into main
All checks were successful
Sync to GitHub / sync (push) Successful in 9s
Reviewed-on: #33
2026-05-26 21:33:20 +07:00
13 changed files with 84 additions and 199 deletions

View File

@ -1,28 +1,40 @@
APP_NAME=NounozCommunity
APP_ENV=development
# Environment variables for application
APP_NAME=AstofoTV
APP_ENV=development # Change to "production" when deploying to production environment, this will disable some development features and enable production optimizations.
APP_DOMAIN=
APP_PROTOCOL=
APP_PORT=
API_KEY=87e20de621fe18930dfbe714d8684bd5ada376903c3092fa3b9aa4a2db10cfba
APP_URL=
API_KEY=
ALLOWED_ORIGINS=www.nounoz.com,nounoz.com,localhost
# Admin user configuration
DEFAULT_ADMIN_EMAIL=
DEFAULT_ADMIN_USERNAME=
DEFAULT_ADMIN_PASSWORD=
# Application features
ENABLE_REGISTRATION=
ENABLE_HERO_BANNER=
# Auth Service configuration
SALT_ROUNDS=
JWT_SECRET=
SESSION_EXPIRE=
SESSION_CACHE_EXPIRE=
# MinIO configuration
MINIO_HOST=
MINIO_PORT=
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=
# MyAnimeList API credentials
MAL_CLIENT_ID=
MAL_CLIENT_SECRET=
# SMTP configuration
SMTP_HOST=
SMTP_PORT=
SMTP_SECURE=
@ -30,14 +42,18 @@ SMTP_USER=
SMTP_PASS=
SMTP_FROM=
# Caching configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# Database configuration
DATABASE_URL=
SHADOW_DATABASE_URL=
ENABLE_PRISMA_LOG=
# OAuth configuration
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

View File

@ -2,15 +2,7 @@ import fs from "fs";
import path from "path";
// These keys will not be cleared in the .env.example file
const PRESERVED_KEYS = [
"APP_NAME",
"APP_ENV",
"PORT",
"API_KEY",
"ALLOWED_ORIGINS",
"REDIS_HOST",
"REDIS_PORT",
];
const PRESERVED_KEYS = ["APP_NAME", "APP_ENV", "PORT", "ALLOWED_ORIGINS", "REDIS_HOST", "REDIS_PORT"];
/**
* Script to create or update the .env.example file based on the .env file.

View File

@ -16,8 +16,8 @@ interface GithubUserData {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
avatar_url?: string;
gravatar_id?: string;
url: string;
html_url: string;
followers_url: string;

View File

@ -6,7 +6,7 @@ import { OAuthUserProvisionService } from "../internal/OAuthUserProvision.servic
export const githubCallbackService = async (
query: { code: string; callbackURI: string },
userHeaderInfo: UserHeaderInformation
userHeaderInfo: UserHeaderInformation,
) => {
try {
// Initialize GitHub provider
@ -37,21 +37,18 @@ export const githubCallbackService = async (
// Provision or authenticate the user in the system
return await OAuthUserProvisionService(
{
provider: "github",
providerId: userPayload.user_data.id.toString(),
providerToken: accessToken,
providerPayload: userPayload,
email:
userPayload.user_email.find((email) => email.primary === true)
?.email || userPayload.user_email[0].email,
username: `git_${userPayload.user_data.id}`,
name: userPayload.user_data.name ?? userPayload.user_data.login,
fullname: userPayload.user_data.name || userPayload.user_data.login,
username: `gh_${userPayload.user_data.id}`,
email: userPayload.user_email.find((email) => email.primary)?.email || userPayload.user_email[0]?.email,
avatar: userPayload.user_data.avatar_url,
password: Math.random()
.toString(36)
.slice(2, 16),
bio: userPayload.user_data.bio || undefined,
oauthProvider: {
providerName: "github",
sub: userPayload.user_data.id.toString(),
token: accessToken,
},
},
userHeaderInfo
userHeaderInfo,
);
} catch (error) {
ErrorForwarder(error, 500, "Authentication service error");

View File

@ -2,7 +2,6 @@ import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUs
import { createUserSessionService } from "../../../userSession/services/createUserSession.service";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { createUserViaOauth } from "../../../user/user.types";
import { createUserService } from "../../../user/services/internal/createUser.service";
import { AppError } from "../../../../helpers/error/instances/app";
import { findAuthIdentityByEmailAndProviderRepository } from "../../repositories/READ/findAuthIdentityByEmailAndProvider.repository";
import { createUserWithOAuthCredentialsRepository } from "../../repositories/WRITE/createUserWithOAuthCredentials.repository";

View File

@ -9,7 +9,7 @@ export const getActiveHeroBannerController = async (ctx: {
header: Static<typeof getActiveHeroBannerSchema.headers>;
}) => {
try {
const response = await getActiveHeroBannerService({ cookie: ctx.header?.cookie });
const response = await getActiveHeroBannerService();
return returnReadResponse(ctx.set, 200, "Active hero banners fetched successfully", response);
} catch (error) {
return mainErrorHandler(ctx.set, error);

View File

@ -1,57 +0,0 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { prisma } from "../../../../utils/databases/prisma/connection";
export const findAllActiveHeroBannerRepository = async (userId?: string) => {
try {
return await prisma.heroBanner.findMany({
where: {
startDate: {
lte: new Date(),
},
endDate: {
gte: new Date(),
},
},
orderBy: [
{
orderPriority: "asc",
},
{
startDate: "asc",
},
],
select: {
orderPriority: true,
imageUrl: true,
media: {
select: {
id: true,
title: true,
slug: true,
pictureLarge: true,
synopsis: true,
genres: {
select: {
slug: true,
name: true,
},
},
_count: {
select: {
inCollections: {
where: {
collection: {
ownerId: userId,
},
},
},
},
},
},
},
},
});
} catch (error) {
throw new AppError(500, "Failed to fetch active hero banners", error);
}
};

View File

@ -0,0 +1,43 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { prisma } from "../../../../utils/databases/prisma/connection";
export const showHeroBannerToHomePageRepository = async () => {
try {
return await prisma.homeMediaBanner.findMany({
where: {
start_show: {
lte: new Date(),
},
end_show: {
gte: new Date(),
},
},
orderBy: {
priority: "asc",
created_at: "desc",
},
select: {
id: true,
media: {
select: {
title: true,
synopsis: true,
large_image_url: true,
genres: {
select: {
genre: {
select: {
name: true,
slug: true,
},
},
},
},
},
},
},
});
} catch (error) {
throw new AppError(500, "Error fetching hero banner data", error);
}
};

View File

@ -1,44 +1,14 @@
import { AppError } from "../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { tokenValidationService } from "../../auth/services/http/tokenValidation.service";
import { findSystemPreferenceService } from "../../systemPreference/services/internal/findSystemPreference.service";
import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllActiveHeroBanner.repository";
import { showHeroBannerToHomePageRepository } from "../repositories/READ/showHeroBannerToHomePage.repository";
export const getActiveHeroBannerService = async ({ cookie }: { cookie?: string }) => {
export const getActiveHeroBannerService = async () => {
try {
const userId = cookie ? (await tokenValidationService(cookie)).user.id : undefined;
const isHeroBannerEnabled = process.env.ENABLE_HERO_BANNER === "true";
// Check if Hero Banner is enabled in system preferences
const isHeroBannerEnabled = await findSystemPreferenceService("HERO_BANNER_ENABLED", "boolean");
if (!isHeroBannerEnabled) throw new AppError(403, "Hero Banner is disabled");
if (!isHeroBannerEnabled) throw new AppError(403, "Hero banner feature is disabled");
// Dont implement caching just yet; implement collection caching first, then implement banner caching.
// Please note that currently, a database query is still required to check the collections.
// // Try to get active banners from Redis cache
// const cachedBanners = await redis.get(`${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`);
// if (cachedBanners) return JSON.parse(cachedBanners);
// If not in cache, fetch from database and cache the result
const activeBanners = await findAllActiveHeroBannerRepository(userId);
const constructedBanners = activeBanners.map((banner) => ({
id: banner.media.id,
title: banner.media.title,
slug: banner.media.slug,
imageUrl: banner.imageUrl || banner.media.pictureLarge,
synopsis: banner.media.synopsis,
genres: banner.media.genres.map((genre) => ({
slug: genre.slug,
name: genre.name,
})),
isInCollection: banner.media._count.inCollections > 0,
}));
// await redis.set(
// `${redisKey.filter((key) => key.name === "HERO_BANNER")[0].key}`,
// JSON.stringify(constructedBanners),
// );
return constructedBanners;
return await showHeroBannerToHomePageRepository();
} catch (error) {
ErrorForwarder(error);
}

View File

@ -1,12 +0,0 @@
import Elysia, { Context } from "elysia";
import { returnWriteResponse } from "../../helpers/callback/httpResponse";
export const systemPreferenceModule = new Elysia({
prefix: "/system-preference",
}).get("/", (ctx: Context) => {
return returnWriteResponse(
ctx.set,
200,
"System Preference module is up and running",
);
});

View File

@ -1,47 +0,0 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { redis } from "../../../../utils/databases/redis/connection";
import { findSystemPreferenceRepository } from "../repositories/findSystemPreference.repository";
export const findSystemPreferenceService = async (
key: string,
type: "boolean" | "string" | "number" = "string",
) => {
try {
// First, check if the system preference is exists in redis cache
const cachedValue = await redis.get(
`${process.env.APP_NAME}:configs:${key}`,
);
if (!cachedValue) {
// If not exists in cache, fetch from database. If found, return the value and set it to cache, if not found, throw an error
const systemPreference = await findSystemPreferenceRepository(key);
if (!systemPreference)
throw new AppError(404, "System preference not found");
// and set it to cache for future requests
await redis.set(
`${process.env.APP_NAME}:configs:${key}`,
systemPreference.value,
);
// Return the value from database
return parseValue(systemPreference.value, type);
} else {
return parseValue(cachedValue, type);
}
} catch (error) {
ErrorForwarder(error, 500, "Failed to find system preference");
}
};
const parseValue = (value: string, type: "boolean" | "string" | "number") => {
switch (type) {
case "boolean":
return value === "true";
case "number":
return Number(value);
default:
return value;
}
};

View File

@ -1,15 +0,0 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { prisma } from "../../../../utils/databases/prisma/connection";
export const findSystemPreferenceRepository = async (key: string) => {
try {
return await prisma.systemPreference.findUnique({
where: {
key,
deletedAt: null,
},
});
} catch (error) {
throw new AppError(500, "Failed to find system preference", error);
}
};

View File

@ -8,8 +8,7 @@ export const deleteUserSessionRepository = async (sessionId: string) => {
id: sessionId,
},
data: {
isAuthenticated: false,
deletedAt: new Date(),
logout_at: new Date(),
},
});
} catch (error) {