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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@ android/local.properties
docker-data/
.obsidian
.excalidraw
.gocache
.gocache
apps/web/.jest-cache/
35 changes: 35 additions & 0 deletions apps/server/cmd/cors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/labstack/echo/v5"
"github.com/stretchr/testify/require"
)

func TestCORSMiddlewareAllowsWebClientHeaders(t *testing.T) {
e := echo.New()
e.Use(corsMiddleware("http://localhost:3000"))
e.POST("/api/v1/auth/login", func(c *echo.Context) error {
return c.NoContent(http.StatusOK)
})

req := httptest.NewRequest(http.MethodOptions, "/api/v1/auth/login", nil)
req.Header.Set("Origin", "http://localhost:3000")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
req.Header.Set("Access-Control-Request-Headers", "content-type,x-requested-with")
rec := httptest.NewRecorder()

e.ServeHTTP(rec, req)

require.Equal(t, http.StatusNoContent, rec.Code)
require.Equal(t, "http://localhost:3000", rec.Header().Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", rec.Header().Get("Access-Control-Allow-Credentials"))

allowedHeaders := strings.ToLower(rec.Header().Get("Access-Control-Allow-Headers"))
require.Contains(t, allowedHeaders, "content-type")
require.Contains(t, allowedHeaders, "x-requested-with")
}
41 changes: 30 additions & 11 deletions apps/server/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"time"

"github.com/coderz-space/coderz.space/internal/common/logger"
"github.com/coderz-space/coderz.space/internal/common/middleware"
appMiddleware "github.com/coderz-space/coderz.space/internal/common/middleware"
"github.com/coderz-space/coderz.space/internal/common/middleware/timeout"
config "github.com/coderz-space/coderz.space/internal/config"
"github.com/coderz-space/coderz.space/internal/container"
Expand Down Expand Up @@ -47,8 +47,33 @@ import (

// @tag.name Bootcamp Enrollments
// @tag.description Bootcamp enrollment management endpoints
func main() {
func corsConfig(frontendOrigin string) echoMiddleware.CORSConfig {
return echoMiddleware.CORSConfig{
AllowOrigins: []string{frontendOrigin},
AllowMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowHeaders: []string{
"Origin",
"Content-Type",
"Accept",
"Authorization",
"X-Requested-With",
},
ExposeHeaders: []string{"Content-Length", "Content-Type", "X-Request-Id"},
AllowCredentials: true,
}
}

func corsMiddleware(frontendOrigin string) echo.MiddlewareFunc {
return echoMiddleware.CORSWithConfig(corsConfig(frontendOrigin))
}

func main() {
cfg := config.LoadConfig()
logger.Initialize(cfg)
defer func() {
Expand All @@ -64,15 +89,9 @@ func main() {
e := echo.New()

// middleware
e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{
AllowOrigins: []string{cfg.FrontendOrigin},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
ExposeHeaders: []string{"Content-Length", "Content-Type", "X-Request-Id"},
AllowCredentials: true,
}))
e.Use(middleware.ZapLogger())
e.Use(middleware.Recovery())
e.Use(corsMiddleware(cfg.FrontendOrigin))
e.Use(appMiddleware.ZapLogger())
e.Use(appMiddleware.Recovery())
e.Use(timeout.TimeoutMiddleware(30 * time.Second)) // 30 second timeout to prevent resource exhaustion

// swagger docs
Expand Down
27 changes: 23 additions & 4 deletions apps/server/internal/modules/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@ import (
"encoding/hex"
"errors"
"fmt"
"strings"
"time"

"github.com/coderz-space/coderz.space/internal/common/email"
"github.com/coderz-space/coderz.space/internal/common/logger"
"github.com/coderz-space/coderz.space/internal/common/utils"
"github.com/coderz-space/coderz.space/internal/common/email"
"github.com/coderz-space/coderz.space/internal/config"
db "github.com/coderz-space/coderz.space/internal/db/sqlc"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)

const errEmailAlreadyExists = "EMAIL_ALREADY_EXISTS"

type Service struct {
queries *db.Queries
config *config.Config
queries *db.Queries
config *config.Config
emailService email.Service
}

Expand All @@ -46,12 +50,27 @@ func (s *Service) Signup(ctx context.Context, req SignupRequest) (*AuthResponseD
Role: db.UserRoleUser,
})
if err != nil {
return nil, err
return nil, normalizeSignupError(err)
}

return s.generateAuthData(ctx, &user)
}

func normalizeSignupError(err error) error {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" && pgErr.ConstraintName == "users_email_key" {
return errors.New(errEmailAlreadyExists)
}

errMsg := err.Error()
if strings.Contains(errMsg, "users_email_key") ||
strings.Contains(errMsg, "duplicate key value violates unique constraint") {
return errors.New(errEmailAlreadyExists)
}

return err
}

func (s *Service) Login(ctx context.Context, req LoginRequest) (*AuthResponseData, error) {
user, err := s.queries.GetUserByEmail(ctx, pgtype.Text{String: req.Email, Valid: true})
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions apps/server/internal/modules/auth/signup_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ func TestHandler_Signup(t *testing.T) {
checkResponse: func(t *testing.T, rec *httptest.ResponseRecorder) {
var resp map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &resp)
if resp["message"] != "duplicate key value violates unique constraint" {
t.Errorf("Expected database error message, got %v", resp["message"])
if resp["message"] != "EMAIL_ALREADY_EXISTS" {
t.Errorf("Expected EMAIL_ALREADY_EXISTS, got %v", resp["message"])
}
},
},
Expand Down
3 changes: 2 additions & 1 deletion apps/web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# testing
/coverage
/.jest-cache/

# next.js
/.next/
Expand Down Expand Up @@ -40,4 +41,4 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts

.kiro
.kiro
3 changes: 0 additions & 3 deletions apps/web/__tests__/example.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

describe('Example test', () => {
it('should pass', () => {
expect(1 + 1).toBe(2);
Expand Down
10 changes: 4 additions & 6 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import Script from "next/script";
import "./globals.css";
import ThemeToggle from "@/components/ThemeToggle";

Expand All @@ -15,13 +16,10 @@ export default function RootLayout({
}>) {
return (
<html lang="en" className="h-full antialiased" suppressHydrationWarning>
<head>
{/* Blocking script: sets dark class before first paint to avoid flash */}
<script suppressHydrationWarning>
{`(function(){try{var t=localStorage.getItem('coderz_theme');var d=t?t==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark');}catch(e){}})();`}
</script>
</head>
<body className="flex min-h-full flex-col bg-white text-gray-900 transition-colors dark:bg-gray-950 dark:text-gray-100">
<Script id="theme-bootstrap" strategy="beforeInteractive">
{`(function(){try{var t=localStorage.getItem('coderz_theme');var d=t?t==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){}})();`}
</Script>
<ThemeToggle />
{children}
</body>
Expand Down
48 changes: 35 additions & 13 deletions apps/web/components/HeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,41 @@ import { HeroSectionProps } from "@/types";

export default function HeroSection({ onGetStarted }: HeroSectionProps) {
return (
<section className="min-h-screen flex flex-col items-center justify-center text-center dark:bg-black bg-white px-4">
<h1 className="text-4xl font-bold dark:text-purple-400 text-purple-700 mb-4">
Welcome to Algo Buddy
</h1>
<p className="text-lg dark:text-gray-300 text-gray-700 max-w-xl mb-8">
A collaborative platform where developers master DSA and tech stacks through peer learning, problem-solving, and real progress tracking.
</p>
<button
onClick={onGetStarted}
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-3 rounded-lg font-semibold"
>
Get Started
</button>
<section className="relative isolate flex min-h-screen items-center overflow-hidden bg-white px-4 py-20 text-center dark:bg-black">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_50%_20%,rgba(124,58,237,0.14),transparent_32%),linear-gradient(180deg,rgba(255,255,255,0),rgba(124,58,237,0.05))] dark:bg-[radial-gradient(circle_at_50%_20%,rgba(168,85,247,0.2),transparent_34%),linear-gradient(180deg,rgba(0,0,0,0),rgba(88,28,135,0.14))]" />
<div className="mx-auto flex w-full max-w-5xl flex-col items-center gap-8">
<div className="flex max-w-3xl flex-col items-center">
<p className="mb-4 rounded-full border border-purple-200 bg-purple-50 px-4 py-1 text-sm font-medium text-purple-700 dark:border-purple-800 dark:bg-purple-950/40 dark:text-purple-300">
Peer learning for serious DSA practice
</p>
<h1 className="mb-5 text-4xl font-bold text-purple-800 sm:text-5xl dark:text-purple-300">
Welcome to Algo Buddy
</h1>
<p className="mb-8 max-w-2xl text-base leading-7 text-gray-700 sm:text-lg dark:text-gray-300">
A collaborative platform where developers master DSA and tech stacks through peer learning, problem-solving, and real progress tracking.
</p>
<button
onClick={onGetStarted}
className="rounded-lg bg-purple-600 px-6 py-3 font-semibold text-white shadow-lg shadow-purple-600/20 transition hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-black"
>
Get Started
</button>
</div>

<div className="grid w-full max-w-3xl grid-cols-1 overflow-hidden rounded-lg border border-purple-100 bg-white/80 text-left shadow-xl shadow-purple-900/5 backdrop-blur sm:grid-cols-3 dark:border-purple-900/70 dark:bg-gray-950/80">
{[
["01", "Choose your role", "Mentor and mentee paths stay focused from the first click."],
["02", "Practice with structure", "Assignments and sheets keep daily progress visible."],
["03", "Track real growth", "Profiles and leaderboards make outcomes easy to scan."],
].map(([step, title, text]) => (
<div key={step} className="border-b border-purple-100 p-5 last:border-b-0 sm:border-b-0 sm:border-r sm:last:border-r-0 dark:border-purple-900/70">
<p className="text-sm font-semibold text-purple-600 dark:text-purple-400">{step}</p>
<h2 className="mt-2 text-base font-semibold text-gray-950 dark:text-white">{title}</h2>
<p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">{text}</p>
</div>
))}
</div>
</div>
</section>
);
}
25 changes: 15 additions & 10 deletions apps/web/components/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useCallback, useState } from "react";
import type { Role } from "@/types";
import HeroSection from "@/components/HeroSection";
import RoleCard from "@/components/RoleCard";
Expand All @@ -14,33 +14,38 @@ export default function LandingPage() {
const [view, setView] = useState<View>("none");
const [activeRole, setActiveRole] = useState<Role | null>(null);

const handleSelectRole = async (role: Role) => {
const closeDialog = useCallback(() => setView("none"), []);
const showRoleSelect = useCallback(() => setView("roleSelect"), []);
const showSignUp = useCallback(() => setView("signUp"), []);
const showLogin = useCallback(() => setView("login"), []);

const handleSelectRole = useCallback(async (role: Role) => {
await selectRole(role);
setActiveRole(role);
setView("login");
};
}, []);

return (
<main className="min-h-screen dark:bg-black bg-white">
<HeroSection onGetStarted={() => setView("roleSelect")} />
<main className="min-h-screen bg-white dark:bg-black">
<HeroSection onGetStarted={showRoleSelect} />

{view === "roleSelect" && (
<RoleCard onSelectRole={handleSelectRole} onClose={() => setView("none")} />
<RoleCard onSelectRole={handleSelectRole} onClose={closeDialog} />
)}

{view === "login" && activeRole && (
<MenteeLoginCard
role={activeRole}
onClose={() => setView("none")}
onSignUp={() => setView("signUp")}
onClose={closeDialog}
onSignUp={showSignUp}
/>
)}

{view === "signUp" && activeRole && (
<MenteeSignUpCard
role={activeRole}
onClose={() => setView("none")}
onBackToLogin={() => setView("login")}
onClose={closeDialog}
onBackToLogin={showLogin}
/>
)}
</main>
Expand Down
Loading
Loading