Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
00434a1
feat: enhance security headers in Next.js config, add new dependencie…
JsCodeDevlopment May 5, 2026
7ad4e86
feat: update session navigation to use a button for sign-in and enhan…
JsCodeDevlopment May 5, 2026
a9548b8
feat: implement core application routes, billing pages, content loade…
JsCodeDevlopment May 7, 2026
bb11325
feat: implement authentication sign-in and sign-up forms with email a…
JsCodeDevlopment May 7, 2026
49315c6
feat: implement user profile system with public viewing, account mana…
JsCodeDevlopment May 7, 2026
6756217
feat: add SessionNav component for authenticated user profile and nav…
JsCodeDevlopment May 7, 2026
2a54d9c
feat: add legal documentation pages for privacy, refunds, terms, and …
JsCodeDevlopment May 7, 2026
eb767e8
feat: implement individual engineering work detail pages with author …
JsCodeDevlopment May 7, 2026
2cf2a72
feat: implement dynamic concept pages with paywall access control, co…
JsCodeDevlopment May 7, 2026
05326a7
feat: implement legal pages, user profile management, and expanded UI…
JsCodeDevlopment May 7, 2026
bd6eb06
feat: implement subscription billing system with Stripe, toast notifi…
JsCodeDevlopment May 7, 2026
9f6c82d
feat: implement changelog page and add initial content documentation
JsCodeDevlopment May 7, 2026
79c99cc
feat: implement comprehensive user profile management system with edi…
JsCodeDevlopment May 8, 2026
314a46c
feat: add project changelog file to track platform updates and features
JsCodeDevlopment May 8, 2026
4c98edf
feat: implement developer explorer page with sidebar navigation and s…
JsCodeDevlopment May 8, 2026
3fdf64e
fix: type fix to improve consistency with all project using typescript
JsCodeDevlopment May 8, 2026
357b245
feat: implement technical test framework including navigation, UI com…
JsCodeDevlopment May 8, 2026
b33541e
feat: implement course module runner with interactive lessons, exerci…
JsCodeDevlopment May 8, 2026
a202fa2
feat: implement technical assessment results tracking with visibility…
JsCodeDevlopment May 8, 2026
03a205d
feat: implement CourseModuleRunner component and route for interactiv…
JsCodeDevlopment May 8, 2026
801b23d
feat: implement assessment results storage and public profile display…
JsCodeDevlopment May 8, 2026
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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Algoria

![](https://img.shields.io/badge/Versão-1.0.0-black?style=for-the-badge)

Plataforma em português para estudar **algoritmos e decisões em código** através de leitura guiada: catálogo de problemas com várias soluções (brute-force, óptima, alternativa), **code player** linha-a-linha com três níveis de explicação, mini-guias em **Conceitos**, **curso modular** com avaliações locais, hub de **inglês técnico para entrevistas** (conteúdo em inglês) e guias de **engenharia aplicada** (front, back, DevOps).

<h1 align="center">
Expand All @@ -14,6 +16,9 @@ Plataforma em português para estudar **algoritmos e decisões em código** atra
- 🎓 **Curso guiado** — trilha modular com exemplos, MCQs e certificado por capítulo (progresso no browser)
- 🌍 **Interview English** — hub `/interview-en` com vocabulário e scripts 100% em inglês para entrevistas
- 💼 **Engenharia no trabalho** — guias didáticos `/engineering-work` (frontend, backend, DevOps)
- 🧪 **Testes Técnicos** — hub `/tests` com simulados reais (escolha múltipla + live coding) por trilha e nível
- 👤 **Perfis Profissionais** — portfólio público `/profile` com experiência detalhada, projetos e tecnologias
- 🔍 **Explorador de Talentos** — hub `/explorer` para descobrir e conectar-te com outros engenheiros da comunidade
- 🌓 **Tema claro/escuro** (`next-themes`)
- 📊 **Analytics opcional** — PostHog quando `NEXT_PUBLIC_POSTHOG_KEY` está definido
- 🔍 **SEO** — `sitemap.ts` e `robots.ts` com gate por ambiente (`NEXT_PUBLIC_ENVIRONMENT` + `NODE_ENV`)
Expand All @@ -33,7 +38,10 @@ algoria/
│ ├── concepts/ # Índice + página por conceito
│ ├── curso/ # Programas e módulos guiados
│ ├── interview-en/ # Hub EN + artigos
│ └── engenharia-trabalho/ # Hub + guias por slug
│ ├── engenharia-trabalho/ # Hub + guias por slug
│ ├── tests/ # Hub de testes + execução por slug
│ ├── explorer/ # Explorador de talentos / engenheiros
│ └── profile/ # Perfil profissional (pessoal e público)
├── components/
│ ├── ui/ # Button, Card, Tabs, Badge, …
│ ├── layout/ # Site header / footer
Expand All @@ -43,6 +51,9 @@ algoria/
│ ├── problem/ # Tabs / barras de estudo
│ ├── solution/ # Seletor de linguagem, trackers
│ ├── course/ # Runner do curso, MCQ, certificado
│ ├── tests/ # Motor de testes técnicos, avaliação de código
│ ├── profile/ # Componentes de portfólio e editor de perfil
│ ├── explorer/ # Filtros e cards de talentos
│ ├── concepts/ # Tracker de visitas
│ ├── complexity/ # Badges Big O
│ ├── analytics/ # PostHog provider
Expand Down
4 changes: 4 additions & 0 deletions app/api/auth/[...all]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';

export const { GET, POST, PATCH, PUT, DELETE } = toNextJsHandler(auth);
16 changes: 16 additions & 0 deletions app/api/billing/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { headers } from 'next/headers';

import { auth } from '@/lib/auth';
import { userHasPro } from '@/lib/billing/entitlements';

export async function GET() {
const session = await auth.api.getSession({ headers: await headers() });
const uid = session?.user?.id;
const pro = await userHasPro(uid);
return Response.json({
authenticated: Boolean(uid),
pro,
email: session?.user?.email ?? null,
userId: uid ?? null,
});
}
65 changes: 65 additions & 0 deletions app/api/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { headers } from 'next/headers';

import { auth } from '@/lib/auth';
import { getStripe } from '@/lib/billing/stripe';
import { rateLimit } from '@/lib/security/rate-limit';

const CHECKOUT_LIMIT = 15;
const CHECKOUT_WINDOW_MS = 900_000;

export async function POST(req: Request) {
const stripe = getStripe();
if (!stripe) {
return Response.json({ error: 'Stripe não configurado.' }, { status: 501 });
}
const priceId = process.env.STRIPE_PRICE_PRO_MONTHLY;
if (!priceId) {
return Response.json({ error: 'STRIPE_PRICE_PRO_MONTHLY em falta.' }, { status: 501 });
}

const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) {
return Response.json({ error: 'Inicia sessão para continuar.' }, { status: 401 });
}

if (
!rateLimit(
`checkout:${session.user.id}`,
CHECKOUT_LIMIT,
CHECKOUT_WINDOW_MS,
)
) {
return Response.json(
{ error: 'Demasiadas tentativas de checkout. Aguarda alguns minutos.' },
{ status: 429 },
);
}

const origin =
process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') || new URL(req.url).origin;

try {
const checkoutSession = await stripe.checkout.sessions.create({
mode: 'subscription',
customer_email: session.user.email ?? undefined,
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${origin}/pricing?checkout=success`,
cancel_url: `${origin}/pricing?checkout=cancel`,
metadata: { userId: session.user.id },
client_reference_id: session.user.id,
});

if (!checkoutSession.url) {
return Response.json({ error: 'URL de checkout indisponível.' }, { status: 500 });
}

return Response.json({ url: checkoutSession.url });
} catch (err) {
const error = err as Error;
console.error('Checkout error:', error);
return Response.json(
{ error: error.message ?? 'Erro ao criar sessão de checkout.' },
{ status: 500 },
);
}
}
58 changes: 58 additions & 0 deletions app/api/customer-portal/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { headers } from 'next/headers';
import { eq } from 'drizzle-orm';

import { auth } from '@/lib/auth';
import { getStripe } from '@/lib/billing/stripe';
import { db } from '@/lib/db';
import { subscription } from '@/lib/db/schema';

export async function POST(req: Request) {
const stripe = getStripe();
if (!stripe) {
return Response.json({ error: 'Stripe não configurado.' }, { status: 501 });
}

const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) {
return Response.json({ error: 'Inicia sessão para continuar.' }, { status: 401 });
}

// Busca o stripeCustomerId do banco
const subRows = await db
.select({ stripeCustomerId: subscription.stripeCustomerId })
.from(subscription)
.where(eq(subscription.userId, session.user.id))
.limit(1);

const sub = subRows[0];
if (!sub) {
return Response.json({ error: 'Não tens uma assinatura ativa.' }, { status: 404 });
}

// Verifica se é um ID fictício do script de dev
if (sub.stripeCustomerId.startsWith('cus_mock_')) {
return Response.json(
{ error: 'Não é possível abrir o portal com um ID fictício. Faz um checkout real para testar.' },
{ status: 400 }
);
}

const origin =
process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') || new URL(req.url).origin;

try {
const portalSession = await stripe.billingPortal.sessions.create({
customer: sub.stripeCustomerId,
return_url: `${origin}/pricing`,
});

return Response.json({ url: portalSession.url });
} catch (err) {
const error = err as Error;
console.error('Portal error:', error);
return Response.json(
{ error: error.message ?? 'Erro ao abrir o portal de gestão.' },
{ status: 500 },
);
}
}
81 changes: 81 additions & 0 deletions app/api/progress/merge/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { eq } from 'drizzle-orm';
import { headers } from 'next/headers';

import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { userProgress } from '@/lib/db/schema';
import { ProgressBlobSchema, type ProgressBlob } from '@/lib/progress/local-progress-schema';
import { mergeProgressBlobs } from '@/lib/progress/merge-blobs';
import { rateLimit } from '@/lib/security/rate-limit';

const MAX_BODY_BYTES = 512 * 1024;
const MERGE_LIMIT = 40;
const MERGE_WINDOW_MS = 60_000;

export async function POST(req: Request) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) {
return Response.json({ error: 'Não autenticado.' }, { status: 401 });
}

if (
!rateLimit(`progress-merge:${session.user.id}`, MERGE_LIMIT, MERGE_WINDOW_MS)
) {
return Response.json(
{ error: 'Demasiados pedidos. Tenta daqui a um minuto.' },
{ status: 429 },
);
}

const len = req.headers.get('content-length');
if (len && Number(len) > MAX_BODY_BYTES) {
return Response.json({ error: 'Payload demasiado grande.' }, { status: 413 });
}

const raw = await req.text();
if (raw.length > MAX_BODY_BYTES) {
return Response.json({ error: 'Payload demasiado grande.' }, { status: 413 });
}

let parsed: unknown;
try {
parsed = JSON.parse(raw) as unknown;
} catch {
return Response.json({ error: 'JSON inválido.' }, { status: 400 });
}

const body = parsed as { blob?: unknown };
const local = ProgressBlobSchema.parse(body.blob ?? { version: 1, problems: {} });

const rows = await db
.select()
.from(userProgress)
.where(eq(userProgress.userId, session.user.id))
.limit(1);

let server: ProgressBlob = { version: 1, problems: {} };
if (rows[0]) {
server = ProgressBlobSchema.parse(JSON.parse(rows[0].data));
}

const merged = mergeProgressBlobs(local, server);
const now = new Date();

if (rows[0]) {
await db
.update(userProgress)
.set({
data: JSON.stringify(merged),
updatedAt: now,
})
.where(eq(userProgress.userId, session.user.id));
} else {
await db.insert(userProgress).values({
userId: session.user.id,
data: JSON.stringify(merged),
updatedAt: now,
});
}

return Response.json({ blob: merged });
}
27 changes: 27 additions & 0 deletions app/api/progress/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { eq } from 'drizzle-orm';
import { headers } from 'next/headers';

import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { userProgress } from '@/lib/db/schema';
import { ProgressBlobSchema } from '@/lib/progress/local-progress-schema';

export async function GET() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) {
return Response.json({ error: 'Não autenticado.' }, { status: 401 });
}

const rows = await db
.select()
.from(userProgress)
.where(eq(userProgress.userId, session.user.id))
.limit(1);

if (!rows[0]) {
return Response.json({ blob: { version: 1 as const, problems: {} } });
}

const blob = ProgressBlobSchema.parse(JSON.parse(rows[0].data));
return Response.json({ blob });
}
Loading
Loading