Compare commits

...

4 Commits

Author SHA1 Message Date
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
12 changed files with 83 additions and 197 deletions

View File

@ -1,28 +1,40 @@
APP_NAME=NounozCommunity # Environment variables for application
APP_ENV=development 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_DOMAIN=
APP_PROTOCOL= APP_PROTOCOL=
APP_PORT= APP_PORT=
API_KEY=87e20de621fe18930dfbe714d8684bd5ada376903c3092fa3b9aa4a2db10cfba APP_URL=
API_KEY=
ALLOWED_ORIGINS=www.nounoz.com,nounoz.com,localhost ALLOWED_ORIGINS=www.nounoz.com,nounoz.com,localhost
# Admin user configuration
DEFAULT_ADMIN_EMAIL= DEFAULT_ADMIN_EMAIL=
DEFAULT_ADMIN_USERNAME= DEFAULT_ADMIN_USERNAME=
DEFAULT_ADMIN_PASSWORD= DEFAULT_ADMIN_PASSWORD=
# Application features
ENABLE_REGISTRATION=
ENABLE_HERO_BANNER=
# Auth Service configuration
SALT_ROUNDS= SALT_ROUNDS=
JWT_SECRET= JWT_SECRET=
SESSION_EXPIRE= SESSION_EXPIRE=
SESSION_CACHE_EXPIRE=
# MinIO configuration
MINIO_HOST= MINIO_HOST=
MINIO_PORT= MINIO_PORT=
MINIO_ACCESS_KEY= MINIO_ACCESS_KEY=
MINIO_SECRET_KEY= MINIO_SECRET_KEY=
MINIO_BUCKET= MINIO_BUCKET=
# MyAnimeList API credentials
MAL_CLIENT_ID= MAL_CLIENT_ID=
MAL_CLIENT_SECRET= MAL_CLIENT_SECRET=
# SMTP configuration
SMTP_HOST= SMTP_HOST=
SMTP_PORT= SMTP_PORT=
SMTP_SECURE= SMTP_SECURE=
@ -30,14 +42,18 @@ SMTP_USER=
SMTP_PASS= SMTP_PASS=
SMTP_FROM= SMTP_FROM=
# Caching configuration
REDIS_HOST=localhost REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_PASSWORD= REDIS_PASSWORD=
# Database configuration
DATABASE_URL= DATABASE_URL=
SHADOW_DATABASE_URL=
ENABLE_PRISMA_LOG=
# OAuth configuration
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET= GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { email } from "zod";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUserHeaderInformation/types"; import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUserHeaderInformation/types";
import { GithubCallbackUserData } from "../../auth.types"; import { GithubCallbackUserData } from "../../auth.types";
@ -6,7 +7,7 @@ import { OAuthUserProvisionService } from "../internal/OAuthUserProvision.servic
export const githubCallbackService = async ( export const githubCallbackService = async (
query: { code: string; callbackURI: string }, query: { code: string; callbackURI: string },
userHeaderInfo: UserHeaderInformation userHeaderInfo: UserHeaderInformation,
) => { ) => {
try { try {
// Initialize GitHub provider // Initialize GitHub provider
@ -37,21 +38,18 @@ export const githubCallbackService = async (
// Provision or authenticate the user in the system // Provision or authenticate the user in the system
return await OAuthUserProvisionService( return await OAuthUserProvisionService(
{ {
provider: "github", fullname: userPayload.user_data.name || userPayload.user_data.login,
providerId: userPayload.user_data.id.toString(), username: `gh_${userPayload.user_data.id}`,
providerToken: accessToken, email: userPayload.user_email.find((email) => email.primary)?.email || userPayload.user_email[0]?.email,
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,
avatar: userPayload.user_data.avatar_url, avatar: userPayload.user_data.avatar_url,
password: Math.random() bio: userPayload.user_data.bio || undefined,
.toString(36) oauthProvider: {
.slice(2, 16), providerName: "github",
sub: userPayload.user_data.id.toString(),
token: accessToken,
},
}, },
userHeaderInfo userHeaderInfo,
); );
} catch (error) { } catch (error) {
ErrorForwarder(error, 500, "Authentication service 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 { createUserSessionService } from "../../../userSession/services/createUserSession.service";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { createUserViaOauth } from "../../../user/user.types"; import { createUserViaOauth } from "../../../user/user.types";
import { createUserService } from "../../../user/services/internal/createUser.service";
import { AppError } from "../../../../helpers/error/instances/app"; import { AppError } from "../../../../helpers/error/instances/app";
import { findAuthIdentityByEmailAndProviderRepository } from "../../repositories/READ/findAuthIdentityByEmailAndProvider.repository"; import { findAuthIdentityByEmailAndProviderRepository } from "../../repositories/READ/findAuthIdentityByEmailAndProvider.repository";
import { createUserWithOAuthCredentialsRepository } from "../../repositories/WRITE/createUserWithOAuthCredentials.repository"; import { createUserWithOAuthCredentialsRepository } from "../../repositories/WRITE/createUserWithOAuthCredentials.repository";

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 { AppError } from "../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { tokenValidationService } from "../../auth/services/http/tokenValidation.service"; import { showHeroBannerToHomePageRepository } from "../repositories/READ/showHeroBannerToHomePage.repository";
import { findSystemPreferenceService } from "../../systemPreference/services/internal/findSystemPreference.service";
import { findAllActiveHeroBannerRepository } from "../repositories/GET/findAllActiveHeroBanner.repository";
export const getActiveHeroBannerService = async ({ cookie }: { cookie?: string }) => { export const getActiveHeroBannerService = async ({ cookie }: { cookie?: string }) => {
try { 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 if (!isHeroBannerEnabled) throw new AppError(403, "Hero banner feature is disabled");
const isHeroBannerEnabled = await findSystemPreferenceService("HERO_BANNER_ENABLED", "boolean");
if (!isHeroBannerEnabled) throw new AppError(403, "Hero Banner is disabled");
// Dont implement caching just yet; implement collection caching first, then implement banner caching. return await showHeroBannerToHomePageRepository();
// 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;
} catch (error) { } catch (error) {
ErrorForwarder(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, id: sessionId,
}, },
data: { data: {
isAuthenticated: false, logout_at: new Date(),
deletedAt: new Date(),
}, },
}); });
} catch (error) { } catch (error) {