diff --git a/wasp-ai/main.wasp b/wasp-ai/main.wasp index d58fa712f..32c279ef3 100644 --- a/wasp-ai/main.wasp +++ b/wasp-ai/main.wasp @@ -25,13 +25,24 @@ app waspAi { ("@visx/scale", "3.2.0"), ("@visx/responsive", "3.0.0"), ("@visx/gradient", "3.0.0"), - ("@visx/axis", "3.2.0") + ("@visx/axis", "3.2.0"), ], client: { rootComponent: import { RootComponent } from "@client/RootComponent.jsx", }, db: { system: PostgreSQL + }, + auth: { + userEntity: User, + externalAuthEntity: SocialLogin, + methods: { + google: { + configFn: import { getGoogleAuthConfig } from "@server/auth.js", + getUserFieldsFn: import { getUserFields } from "@server/auth.js", + } + }, + onAuthFailedRedirectTo: "/login" } } @@ -47,7 +58,13 @@ page ResultPage { route StatsRoute { path: "/stats", to: StatsPage } page StatsPage { - component: import { Stats } from "@client/pages/StatsPage.jsx" + component: import { Stats } from "@client/pages/StatsPage.jsx", + authRequired: true +} + +route LoginRoute { path: "/login", to: LoginPage } +page LoginPage { + component: import { LoginPage } from "@client/pages/LoginPage.jsx", } action startGeneratingNewApp { @@ -73,6 +90,23 @@ query getStats { ] } +entity User {=psl + id Int @id @default(autoincrement()) + + email String @unique + externalAuthAssociations SocialLogin[] +psl=} + +entity SocialLogin {=psl + id String @id @default(uuid()) + + provider String + providerId String + + userId Int + user User @relation(fields: [userId], references: [id]) +psl=} + entity Project {=psl id String @id @default(uuid()) name String diff --git a/wasp-ai/migrations/20230703150042_add_user/migration.sql b/wasp-ai/migrations/20230703150042_add_user/migration.sql new file mode 100644 index 000000000..492cb40f5 --- /dev/null +++ b/wasp-ai/migrations/20230703150042_add_user/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SocialLogin" ( + "id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerId" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "SocialLogin_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "SocialLogin" ADD CONSTRAINT "SocialLogin_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/wasp-ai/migrations/20230703151150_add_email_to_user/migration.sql b/wasp-ai/migrations/20230703151150_add_email_to_user/migration.sql new file mode 100644 index 000000000..d8f1f48d1 --- /dev/null +++ b/wasp-ai/migrations/20230703151150_add_email_to_user/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - Added the required column `email` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "email" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/wasp-ai/src/client/components/BarChart.jsx b/wasp-ai/src/client/components/BarChart.jsx index 6b7c98225..02f6e22b7 100644 --- a/wasp-ai/src/client/components/BarChart.jsx +++ b/wasp-ai/src/client/components/BarChart.jsx @@ -28,7 +28,6 @@ function generateLast24HoursData(projects) { buckets[reverseBucketIndex].count++; } }); - console.log(buckets); return buckets; } diff --git a/wasp-ai/src/client/pages/LoginPage.jsx b/wasp-ai/src/client/pages/LoginPage.jsx new file mode 100644 index 000000000..40257e316 --- /dev/null +++ b/wasp-ai/src/client/pages/LoginPage.jsx @@ -0,0 +1,9 @@ +import { LoginForm } from "@wasp/auth/forms/Login"; + +export function LoginPage() { + return ( +
+ +
+ ); +} diff --git a/wasp-ai/src/client/pages/StatsPage.jsx b/wasp-ai/src/client/pages/StatsPage.jsx index 9202de025..a65ba6dde 100644 --- a/wasp-ai/src/client/pages/StatsPage.jsx +++ b/wasp-ai/src/client/pages/StatsPage.jsx @@ -8,6 +8,7 @@ import { StatusPill } from "../components/StatusPill"; import { BarChart } from "../components/BarChart"; import ParentSize from "@visx/responsive/lib/components/ParentSize"; import { poolOfExampleIdeas } from "../examples"; +import logout from "@wasp/auth/logout"; export function Stats() { const [filterOutExampleApps, setFilterOutExampleApps] = useState(true); @@ -64,13 +65,24 @@ export function Stats() { return (
-

Stats

+
+

Stats

+
+ +
+
{isLoading &&

Loading...

} {error &&

Error: {error.message}

} - {stats && ( + {stats && filteredStats.length === 0 && ( +

+ No projects created yet. +

+ )} + + {stats && filteredStats.length > 0 && ( <>

Number of projects created in the last 24 hours:{" "} diff --git a/wasp-ai/src/server/auth.ts b/wasp-ai/src/server/auth.ts new file mode 100644 index 000000000..683b821bf --- /dev/null +++ b/wasp-ai/src/server/auth.ts @@ -0,0 +1,15 @@ +import { GetUserFieldsFn } from "@wasp/types"; + +export const getUserFields: GetUserFieldsFn = async (_context, args) => { + return { + email: args.profile.emails[0].value, + }; +}; + +export const getGoogleAuthConfig = () => { + return { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + scope: ["profile", "email"], + }; +}; diff --git a/wasp-ai/src/server/operations.ts b/wasp-ai/src/server/operations.ts index e175e982f..f25b47d80 100644 --- a/wasp-ai/src/server/operations.ts +++ b/wasp-ai/src/server/operations.ts @@ -198,6 +198,11 @@ export const getAppGenerationResult = (async (args, context) => { }>; export const getStats = (async (args, context) => { + const emailsWhitelist = process.env.ADMIN_EMAILS_WHITELIST?.split(",") || []; + if (!context.user || !emailsWhitelist.includes(context.user.email)) { + throw new HttpError(401, "Only admins can access stats."); + } + const { Project } = context.entities; const projects = await Project.findMany({ orderBy: {