diff --git a/apps/api/package.json b/apps/api/package.json index 42da91d..6fd7208 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -13,9 +13,11 @@ "@glossa/db": "workspace:*", "@glossa/shared": "workspace:*", "better-auth": "^1.6.2", + "cors": "^2.8.6", "express": "^5.2.1" }, "devDependencies": { + "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/supertest": "^7.2.0", "supertest": "^7.2.2", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 79a45fd..276f0e2 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -4,10 +4,12 @@ import { toNodeHandler } from "better-auth/node"; import { auth } from "./lib/auth.js"; import { apiRouter } from "./routes/apiRouter.js"; import { errorHandler } from "./middleware/errorHandler.js"; +import cors from "cors"; export function createApp() { const app: Express = express(); + app.use(cors({ origin: "http://localhost:5173", credentials: true })); app.all("/api/auth/*splat", toNodeHandler(auth)); app.use(express.json()); app.use("/api/v1", apiRouter); diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts index c594f92..d3bc934 100644 --- a/apps/api/src/lib/auth.ts +++ b/apps/api/src/lib/auth.ts @@ -1,9 +1,11 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@glossa/db"; +import * as schema from "@glossa/db/schema"; export const auth = betterAuth({ - database: drizzleAdapter(db, { provider: "pg" }), + database: drizzleAdapter(db, { provider: "pg", schema }), + trustedOrigins: ["http://localhost:5173"], socialProviders: { google: { clientId: process.env["GOOGLE_CLIENT_ID"] as string, diff --git a/apps/api/src/middleware/authMiddleware.ts b/apps/api/src/middleware/authMiddleware.ts new file mode 100644 index 0000000..da18d01 --- /dev/null +++ b/apps/api/src/middleware/authMiddleware.ts @@ -0,0 +1,20 @@ +import type { Request, Response, NextFunction } from "express"; +import { fromNodeHeaders } from "better-auth/node"; +import { auth } from "../lib/auth.js"; + +export const requireAuth = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const session = await auth.api.getSession({ + headers: fromNodeHeaders(req.headers), + }); + + if (!session) { + res.status(401).json({ success: false, error: "Unauthorized" }); + return; + } + + next(); +}; diff --git a/apps/api/src/routes/gameRouter.ts b/apps/api/src/routes/gameRouter.ts index 664a640..f65bfb6 100644 --- a/apps/api/src/routes/gameRouter.ts +++ b/apps/api/src/routes/gameRouter.ts @@ -1,8 +1,10 @@ import express from "express"; import type { Router } from "express"; import { createGame, submitAnswer } from "../controllers/gameController.js"; +import { requireAuth } from "../middleware/authMiddleware.js"; export const gameRouter: Router = express.Router(); +gameRouter.use(requireAuth); gameRouter.post("/start", createGame); gameRouter.post("/answer", submitAnswer); diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts new file mode 100644 index 0000000..3addfad --- /dev/null +++ b/apps/web/src/lib/auth-client.ts @@ -0,0 +1,7 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: "http://localhost:3000", +}); + +export const { signIn, signOut, useSession } = authClient; diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 17e2c55..ce1cdf1 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as PlayRouteImport } from './routes/play' +import { Route as LoginRouteImport } from './routes/login' import { Route as AboutRouteImport } from './routes/about' import { Route as IndexRouteImport } from './routes/index' @@ -18,6 +19,11 @@ const PlayRoute = PlayRouteImport.update({ path: '/play', getParentRoute: () => rootRouteImport, } as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) const AboutRoute = AboutRouteImport.update({ id: '/about', path: '/about', @@ -32,30 +38,34 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/about': typeof AboutRoute + '/login': typeof LoginRoute '/play': typeof PlayRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/about': typeof AboutRoute + '/login': typeof LoginRoute '/play': typeof PlayRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/about': typeof AboutRoute + '/login': typeof LoginRoute '/play': typeof PlayRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/about' | '/play' + fullPaths: '/' | '/about' | '/login' | '/play' fileRoutesByTo: FileRoutesByTo - to: '/' | '/about' | '/play' - id: '__root__' | '/' | '/about' | '/play' + to: '/' | '/about' | '/login' | '/play' + id: '__root__' | '/' | '/about' | '/login' | '/play' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute AboutRoute: typeof AboutRoute + LoginRoute: typeof LoginRoute PlayRoute: typeof PlayRoute } @@ -68,6 +78,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PlayRouteImport parentRoute: typeof rootRouteImport } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } '/about': { id: '/about' path: '/about' @@ -88,6 +105,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AboutRoute: AboutRoute, + LoginRoute: LoginRoute, PlayRoute: PlayRoute, } export const routeTree = rootRouteImport diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index a05fa98..1448282 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,20 +1,51 @@ -import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; +import { + createRootRoute, + Link, + Outlet, + useNavigate, +} from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import { useSession, signOut } from "../lib/auth-client"; -const RootLayout = () => ( - <> -
- - Home - {" "} - - About - -
-
- - - -); +const RootLayout = () => { + const { data: session } = useSession(); + const navigate = useNavigate(); + + return ( + <> +
+ + Home + + + About + +
+ {session ? ( + + ) : ( + + Sign in + + )} +
+
+
+ + + + ); +}; export const Route = createRootRoute({ component: RootLayout }); diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx new file mode 100644 index 0000000..b14a734 --- /dev/null +++ b/apps/web/src/routes/login.tsx @@ -0,0 +1,44 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { signIn, useSession } from "../lib/auth-client"; + +const LoginPage = () => { + const { data: session, isPending } = useSession(); + const navigate = useNavigate(); + + if (isPending) return
Loading...
; + + if (session) { + navigate({ to: "/" }); + return null; + } + + return ( +
+

Sign in to Glossa

+ + +
+ ); +}; + +export const Route = createFileRoute("/login")({ component: LoginPage }); diff --git a/apps/web/src/routes/play.tsx b/apps/web/src/routes/play.tsx index 55a0051..92cb5e9 100644 --- a/apps/web/src/routes/play.tsx +++ b/apps/web/src/routes/play.tsx @@ -1,9 +1,10 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, redirect } from "@tanstack/react-router"; import { useState, useCallback } from "react"; import type { GameSession, GameRequest, AnswerResult } from "@glossa/shared"; import { QuestionCard } from "../components/game/QuestionCard"; import { ScoreScreen } from "../components/game/ScoreScreen"; import { GameSetup } from "../components/game/GameSetup"; +import { authClient } from "../lib/auth-client"; function Play() { const [gameSession, setGameSession] = useState(null); @@ -105,4 +106,12 @@ function Play() { ); } -export const Route = createFileRoute("/play")({ component: Play }); +export const Route = createFileRoute("/play")({ + component: Play, + beforeLoad: async () => { + const { data: session } = await authClient.getSession(); + if (!session) { + throw redirect({ to: "/login" }); + } + }, +}); diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index a2c8d96..0edb6c2 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -2,6 +2,9 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "allowImportingTsExtensions": true, + "composite": false, + "declaration": false, + "declarationMap": false, "lib": ["ES2023", "DOM", "DOM.Iterable"], "jsx": "react-jsx", "module": "ESNext", @@ -9,7 +12,7 @@ "noEmit": true, "target": "ES2023", "types": ["vite/client", "vitest/globals"], - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", }, - "include": ["src", "vitest.config.ts"] + "include": ["src", "vitest.config.ts"], } diff --git a/documentation/notes.md b/documentation/notes.md index a918c94..0d11637 100644 --- a/documentation/notes.md +++ b/documentation/notes.md @@ -7,6 +7,10 @@ ## problems+thoughts +### try now option + +there should be an option to try the app without an account so users can see what they would get when creating an account + ### resolve deps problem ﬌ pnpm --filter web add better-auth diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81172d3..184b74e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,10 +56,16 @@ importers: better-auth: specifier: ^1.6.2 version: 1.6.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.16)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@2.2.0))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) + cors: + specifier: ^2.8.6 + version: 2.8.6 express: specifier: ^5.2.1 version: 5.2.1 devDependencies: + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 '@types/express': specifier: ^5.0.6 version: 5.0.6 @@ -1243,6 +1249,9 @@ packages: '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1662,6 +1671,10 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -2397,6 +2410,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -3821,6 +3838,10 @@ snapshots: '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.19': + dependencies: + '@types/node': 24.12.0 + '@types/deep-eql@4.0.2': {} '@types/esrecurse@4.3.1': {} @@ -4306,6 +4327,11 @@ snapshots: cookiejar@2.1.4: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + crc-32@1.2.2: {} cross-spawn@7.0.6: @@ -4985,6 +5011,8 @@ snapshots: normalize-path@3.0.0: {} + object-assign@4.1.1: {} + object-inspect@1.13.4: {} obug@2.1.1: {}