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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/api/src/application/services/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ export class AccountsService {
pagination,
});
}
@Catch()
async listAccounts({ pagination }: { pagination: PaginationInput }) {
return this.accountRepository.listAccounts({ pagination });
}

@Catch()
async upsertRegistration(
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/application/services/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export class SessionService {
authenticated: true,
session: {
email: account.email,
type: account.type,
token: session.token,
},
};
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/application/usecases/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from "./editCharacter";
export * from "./findCharacter";
export * from "./generateEmailChange";
export * from "./generatePasswordReset";
export * from "./listAccounts";
export * from "./login";
export * from "./logout";
export * from "./permissioned";
Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/application/usecases/account/listAccounts/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type z from "zod";
import { ListAccountSchema } from "@/shared/schemas/ListAccounts";
import { createPaginateSchema, InputPageSchema } from "@/shared/utils/paginate";

export const ListAccountsContractSchema = {
input: InputPageSchema,
output: createPaginateSchema(ListAccountSchema),
};

export type ListAccountsContractInput = z.infer<
typeof ListAccountsContractSchema.input
>;

export type ListAccountsContractOutput = z.infer<
typeof ListAccountsContractSchema.output
>;
34 changes: 34 additions & 0 deletions apps/api/src/application/usecases/account/listAccounts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { inject, injectable } from "tsyringe";
import type { AccountsService } from "@/application/services";
import type { Pagination } from "@/domain/modules";
import { TOKENS } from "@/infra/di/tokens";
import type { UseCase } from "@/shared/interfaces/usecase";
import type {
ListAccountsContractInput,
ListAccountsContractOutput,
} from "./contract";

@injectable()
export class ListAccountsUseCase
implements UseCase<ListAccountsContractInput, ListAccountsContractOutput>
{
constructor(
@inject(TOKENS.AccountsService)
private readonly accountsService: AccountsService,
@inject(TOKENS.Pagination) private readonly pagination: Pagination,
) {}

async execute(
input: ListAccountsContractInput,
): Promise<ListAccountsContractOutput> {
const { storeHistory, total } = await this.accountsService.listAccounts({
pagination: input,
});

return this.pagination.paginate(storeHistory, {
page: input.page ?? 1,
size: input.size ?? 10,
total,
});
}
}
2 changes: 2 additions & 0 deletions apps/api/src/application/usecases/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export class UseCasesFactory {
const changePasswordWithToken = this.di.resolve(
TOKENS.AccountChangePasswordWithTokenUseCase,
);
const listAccounts = this.di.resolve(TOKENS.ListAccountsUseCase);
const changeEmailWithPassword = this.di.resolve(
TOKENS.AccountChangeEmailWithPasswordUseCase,
);
Expand Down Expand Up @@ -134,6 +135,7 @@ export class UseCasesFactory {
changePasswordWithOld,
generatePasswordReset,
changePasswordWithToken,
listAccounts,
changeEmailWithPassword,
generateEmailChange,
previewEmailChange,
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/application/usecases/session/info/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const SessionInfoContractSchema = {
.object({
token: z.string(),
email: z.email(),
type: z.number().nullable(),
})
.nullable(),
}),
Expand Down
21 changes: 21 additions & 0 deletions apps/api/src/domain/repositories/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,27 @@ export class AccountRepository {
};
}

async listAccounts(opts?: { pagination: PaginationInput }) {
const page = opts?.pagination.page ?? 1;
const size = opts?.pagination.size ?? 10;

const [storeHistory, total] = await Promise.all([
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name storeHistory is misleading and inconsistent with the function's purpose. This method lists accounts, not store history. Rename to accounts for clarity.

Copilot uses AI. Check for mistakes.
this.prisma.accounts.findMany({
orderBy: {
name: "desc",
},
skip: (page - 1) * size,
take: size,
}),
this.prisma.accounts.count(),
]);

return {
storeHistory,
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned property name storeHistory is misleading and inconsistent with the function's purpose. This method lists accounts, not store history. Rename to accounts for clarity.

Copilot uses AI. Check for mistakes.
total,
};
}

async details(email: string) {
return this.prisma.accounts.findFirst({
where: {
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/infra/di/containers/usecases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ChangePasswordWithTokenUseCase,
ConfigInfoUseCase,
ConfigUpdateUseCase,
ListAccountsUseCase,
LostAccountFindByEmailOrCharacterNameUseCase,
LostAccountGeneratePasswordResetUseCase,
LostAccountResetPasswordWithRecoveryKeyUseCase,
Expand Down Expand Up @@ -128,6 +129,9 @@ export function registerUseCases() {
{ useClass: ChangePasswordWithTokenUseCase },
{ lifecycle: Lifecycle.ResolutionScoped },
);
container.register(TOKENS.ListAccountsUseCase, {
useClass: ListAccountsUseCase,
});
container.register(
TOKENS.AccountTwoFactorSetupUseCase,
{ useClass: AccountTwoFactorSetupUseCase },
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/infra/di/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
ChangePasswordWithTokenUseCase,
ConfigInfoUseCase,
ConfigUpdateUseCase,
ListAccountsUseCase,
LostAccountFindByEmailOrCharacterNameUseCase,
LostAccountGeneratePasswordResetUseCase,
LostAccountResetPasswordWithRecoveryKeyUseCase,
Expand All @@ -53,6 +54,7 @@ import type {
TibiaLoginUseCase,
WorldsListUseCase,
} from "@/application/usecases";

import type { Mailer, OtsServerClient, Prisma, Redis } from "@/domain/clients";
import type { AppLivePublisher } from "@/domain/clients/live/types";
import type { ExecutionContext } from "@/domain/context";
Expand Down Expand Up @@ -214,6 +216,7 @@ export const TOKENS = {
AccountChangePasswordWithTokenUseCase: token<ChangePasswordWithTokenUseCase>(
"AccountChangePasswordWithTokenUseCase",
),
ListAccountsUseCase: token<ListAccountsUseCase>("ListAccountsUseCase"),
AccountChangeEmailWithPasswordUseCase:
token<AccountChangeEmailWithPasswordUseCase>(
"AccountChangeEmailWithPasswordUseCase",
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/presentation/v1/routes/admin/accounts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { base } from "@/infra/rpc/base";
import { listAccountsRouter } from "./list";

export const adminAccountsRouter = base.prefix("/accounts").router({
list: listAccountsRouter,
});
22 changes: 22 additions & 0 deletions apps/api/src/presentation/v1/routes/admin/accounts/list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ListAccountsContractSchema } from "@/application/usecases/account/listAccounts/contract";
import { isPermissionedProcedure } from "@/presentation/procedures/isPermissioned";

export const listAccountsRouter = isPermissionedProcedure
.meta({
permission: {
type: "GAME_MASTER",
},
})
.route({
method: "GET",
path: "/list",
summary: "List Accounts",
successStatus: 200,
description:
"Retrieves a list of accounts registered on the server. Only GAME_MASTER and ADMIN users are allowed to perform this action",
})
.input(ListAccountsContractSchema.input)
.output(ListAccountsContractSchema.output)
.handler(async ({ context, input }) => {
return await context.usecases.account.listAccounts.execute(input);
});
6 changes: 6 additions & 0 deletions apps/api/src/presentation/v1/routes/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { base } from "@/infra/rpc/base";
import { adminAccountsRouter } from "./accounts";

export const adminRouter = base.prefix("/admin").tag("Admin").router({
accounts: adminAccountsRouter,
});
2 changes: 2 additions & 0 deletions apps/api/src/presentation/v1/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { base } from "@/infra/rpc/base";
import { accountsRouter } from "./accounts";
import { adminRouter } from "./admin";
import { clientRouter } from "./client";
import { configRouter } from "./config";
import { lostAccountRouter } from "./lost";
Expand All @@ -8,6 +9,7 @@ import { sessionRouter } from "./session";
import { worldsRouter } from "./worlds";

export const router = base.router({
admin: adminRouter,
ping: pingRoute,
client: clientRouter,
accounts: accountsRouter,
Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/shared/schemas/ListAccounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import z from "zod";

export const ListAccountSchema = z.object({
id: z.number(),
name: z.string().nullable(),
email: z.email(),
type: z.number(),
});
5 changes: 5 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
VITE_SHOW_DEVTOOLS=true

# ==== RPC ==== #
VITE_MIFORGE_RPC_URL="http://localhost:4000"
VITE_MIFORGE_RPC_PATH="/v1/rpc"
16 changes: 6 additions & 10 deletions apps/web/src/components/Menu/Item/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { Link, type LinkProps, useRouterState } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { cn } from "@/sdk/utils/cn";

const Icons = {
news: "/assets/icons/32/news-menu.gif",
sphere: "/assets/icons/32/armillary_sphere.gif",
munster: "/assets/icons/32/baby_munster.gif",
};
import { Icons } from "..";

type Props = {
label: string;
Expand Down Expand Up @@ -38,7 +33,8 @@ export const MenuItem = ({ icon, label, menus = [] }: Props) => {

return (
<div>
<div
<button
type="button"
className={cn(
"hover:filter-hover relative flex h-8 w-[170px] cursor-pointer items-center gap-1 bg-no-repeat px-1 transition-all",
)}
Expand All @@ -56,7 +52,7 @@ export const MenuItem = ({ icon, label, menus = [] }: Props) => {
<span className="fondamento-title flex-1 text-center text-base capitalize">
{label}
</span>
</div>
</button>

<div
className="relative flex h-0 w-[170px] flex-col overflow-hidden px-0.5 transition-all duration-300"
Expand All @@ -65,12 +61,12 @@ export const MenuItem = ({ icon, label, menus = [] }: Props) => {
}}
>
<div
className="absolute top-0 right-0 h-full w-[7px] bg-[url('/assets/borders/chain.webp')] bg-repeat-y transition-all duration-700"
className="absolute top-0 right-0 h-full w-[7px] bg-[url('/assets/borders/chain.webp')] bg-repeat-y transition-all duration-300"
style={{ height: show ? `${heightTotal}px` : "0px" }}
/>
<div
style={{ height: show ? `${heightTotal}px` : "0px" }}
className="absolute top-0 left-0 h-full w-[7px] bg-[url('/assets/borders/chain.webp')] bg-repeat-y transition-all duration-700"
className="absolute top-0 left-0 h-full w-[7px] bg-[url('/assets/borders/chain.webp')] bg-repeat-y transition-all duration-300"
/>
{menus.map((subMenu) => {
const isActive = routerState.location.pathname === subMenu.to;
Expand Down
35 changes: 24 additions & 11 deletions apps/web/src/components/Menu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import type { LinkProps } from "@tanstack/react-router";
import { MenuBox } from "@/components/Box/Menu";
import { MenuItem } from "./Item";

export const Menu = () => {
export const Icons = {
management: "/assets/icons/32/loremaster_doll.gif",
news: "/assets/icons/32/news-menu.gif",
sphere: "/assets/icons/32/armillary_sphere.gif",
munster: "/assets/icons/32/baby_munster.gif",
};

interface MenuProps {
items: Array<{
label: string;
icon: keyof typeof Icons;
menus: Array<{
label: string;
to: LinkProps["to"];
hot?: boolean;
}>;
}>;
}

export const Menu = ({ items }: MenuProps) => {
return (
<div className="flex">
<MenuBox>
<MenuItem
label="News"
icon="news"
menus={[{ label: "Latest News", to: "/terms" }]}
/>
<MenuItem
label="Sphere"
icon="sphere"
menus={[{ label: "Updates", to: "/", hot: true }]}
/>
{items.map((item) => (
<MenuItem label={item.label} icon={item.icon} menus={item.menus} />

Check warning on line 29 in apps/web/src/components/Menu/index.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Missing "key" prop for element in iterator

See more on https://sonarcloud.io/project/issues?id=mitgdev_mitg.forge&issues=AZrsEq4P4M8lZEo7Fmkl&open=AZrsEq4P4M8lZEo7Fmkl&pullRequest=24
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing key prop in mapped MenuItem component. Add key={item.label} or a unique identifier to avoid React reconciliation issues.

Suggested change
<MenuItem label={item.label} icon={item.icon} menus={item.menus} />
<MenuItem key={item.label} label={item.label} icon={item.icon} menus={item.menus} />

Copilot uses AI. Check for mistakes.
))}
</MenuBox>
</div>
);
Expand Down
29 changes: 28 additions & 1 deletion apps/web/src/layout/Navigation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { BoxDownload } from "@/components/Box/Download";
import { BoxLogin } from "@/components/Box/Login";
import { useSession } from "@/sdk/contexts/session";
import { Menu } from "../../components/Menu";

export const Navigation = () => {
const role: number | null = useSession().session?.type || null;

return (
<nav
className="relative hidden flex-col items-center gap-4 xl:flex"
Expand All @@ -12,7 +15,31 @@ export const Navigation = () => {
>
<BoxLogin />
<BoxDownload />
<Menu />
{role !== null && role >= 4 && (
<Menu
items={[
{
label: "Management",
icon: "management",
menus: [{ label: "List accounts", to: "/admin/accounts/list" }],
},
]}
/>
)}
<Menu
items={[
{
label: "News",
icon: "news",
menus: [{ label: "Latest News", to: "/terms" }],
},
{
label: "Sphere",
icon: "sphere",
menus: [{ label: "Updates", to: "/", hot: true }],
},
]}
/>
</nav>
);
};
Loading