)}
diff --git a/examples/tutorials/TodoApp/main.wasp b/examples/tutorials/TodoApp/main.wasp
index 821016f186..7389a43656 100644
--- a/examples/tutorials/TodoApp/main.wasp
+++ b/examples/tutorials/TodoApp/main.wasp
@@ -36,8 +36,6 @@ page LoginPage {
entity User {=psl
id Int @id @default(autoincrement())
- username String @unique
- password String
tasks Task[]
psl=}
diff --git a/examples/tutorials/TodoApp/migrations/20220113204751_init/migration.sql b/examples/tutorials/TodoApp/migrations/20220113204751_init/migration.sql
deleted file mode 100644
index 7a12b36244..0000000000
--- a/examples/tutorials/TodoApp/migrations/20220113204751_init/migration.sql
+++ /dev/null
@@ -1,18 +0,0 @@
--- CreateTable
-CREATE TABLE "User" (
- "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- "email" TEXT NOT NULL,
- "password" TEXT NOT NULL
-);
-
--- CreateTable
-CREATE TABLE "Task" (
- "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- "description" TEXT NOT NULL,
- "isDone" BOOLEAN NOT NULL DEFAULT false,
- "userId" INTEGER,
- FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
diff --git a/examples/tutorials/TodoApp/migrations/20220818151104_rename_email_to_username/migration.sql b/examples/tutorials/TodoApp/migrations/20220818151104_rename_email_to_username/migration.sql
deleted file mode 100644
index a3815987a5..0000000000
--- a/examples/tutorials/TodoApp/migrations/20220818151104_rename_email_to_username/migration.sql
+++ /dev/null
@@ -1,9 +0,0 @@
--- DropIndex
-DROP INDEX "User.email_unique";
-
--- AlterTable
-ALTER TABLE "User"
-RENAME COLUMN "email" TO "username";
-
--- CreateIndex
-CREATE UNIQUE INDEX "User.username_unique" ON "User"("username");
diff --git a/examples/tutorials/TodoApp/migrations/20220818170255_getting_up_to_date/migration.sql b/examples/tutorials/TodoApp/migrations/20220818170255_getting_up_to_date/migration.sql
deleted file mode 100644
index 215d194c3a..0000000000
--- a/examples/tutorials/TodoApp/migrations/20220818170255_getting_up_to_date/migration.sql
+++ /dev/null
@@ -1,3 +0,0 @@
--- RedefineIndex
-DROP INDEX "User.username_unique";
-CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
diff --git a/examples/tutorials/TodoApp/migrations/20231214131517_new_auth/migration.sql b/examples/tutorials/TodoApp/migrations/20231214131517_new_auth/migration.sql
new file mode 100644
index 0000000000..533c854ea7
--- /dev/null
+++ b/examples/tutorials/TodoApp/migrations/20231214131517_new_auth/migration.sql
@@ -0,0 +1,39 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "username" TEXT NOT NULL,
+ "password" TEXT NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "Task" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "description" TEXT NOT NULL,
+ "isDone" BOOLEAN NOT NULL DEFAULT false,
+ "userId" INTEGER,
+ CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Auth" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" INTEGER,
+ CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "providerName" TEXT NOT NULL,
+ "providerUserId" TEXT NOT NULL,
+ "providerData" TEXT NOT NULL DEFAULT '{}',
+ "authId" TEXT NOT NULL,
+
+ PRIMARY KEY ("providerName", "providerUserId"),
+ CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
diff --git a/examples/tutorials/TodoApp/migrations/20231214131607_remove_extra_auth_fields/migration.sql b/examples/tutorials/TodoApp/migrations/20231214131607_remove_extra_auth_fields/migration.sql
new file mode 100644
index 0000000000..969799968d
--- /dev/null
+++ b/examples/tutorials/TodoApp/migrations/20231214131607_remove_extra_auth_fields/migration.sql
@@ -0,0 +1,17 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
+
+*/
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
+);
+INSERT INTO "new_User" ("id") SELECT "id" FROM "User";
+DROP TABLE "User";
+ALTER TABLE "new_User" RENAME TO "User";
+PRAGMA foreign_key_check;
+PRAGMA foreign_keys=ON;
diff --git a/examples/tutorials/TodoAppTs/main.wasp b/examples/tutorials/TodoAppTs/main.wasp
index ffe78756cb..a6c25b7e62 100644
--- a/examples/tutorials/TodoAppTs/main.wasp
+++ b/examples/tutorials/TodoAppTs/main.wasp
@@ -36,8 +36,6 @@ page LoginPage {
entity User {=psl
id Int @id @default(autoincrement())
- username String @unique
- password String
tasks Task[]
psl=}
diff --git a/examples/tutorials/TodoAppTs/migrations/20231013154636_initial/migration.sql b/examples/tutorials/TodoAppTs/migrations/20231013154636_initial/migration.sql
deleted file mode 100644
index 32fc9d064f..0000000000
--- a/examples/tutorials/TodoAppTs/migrations/20231013154636_initial/migration.sql
+++ /dev/null
@@ -1,6 +0,0 @@
--- CreateTable
-CREATE TABLE "Task" (
- "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- "description" TEXT NOT NULL,
- "isDone" BOOLEAN NOT NULL DEFAULT false
-);
diff --git a/examples/tutorials/TodoAppTs/migrations/20231013155651_user/migration.sql b/examples/tutorials/TodoAppTs/migrations/20231013155651_user/migration.sql
deleted file mode 100644
index bcc4dd7d09..0000000000
--- a/examples/tutorials/TodoAppTs/migrations/20231013155651_user/migration.sql
+++ /dev/null
@@ -1,9 +0,0 @@
--- CreateTable
-CREATE TABLE "User" (
- "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- "username" TEXT NOT NULL,
- "password" TEXT NOT NULL
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
diff --git a/examples/tutorials/TodoAppTs/migrations/20231013160107_users_tasks/migration.sql b/examples/tutorials/TodoAppTs/migrations/20231013160107_users_tasks/migration.sql
deleted file mode 100644
index c800c980bd..0000000000
--- a/examples/tutorials/TodoAppTs/migrations/20231013160107_users_tasks/migration.sql
+++ /dev/null
@@ -1,14 +0,0 @@
--- RedefineTables
-PRAGMA foreign_keys=OFF;
-CREATE TABLE "new_Task" (
- "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- "description" TEXT NOT NULL,
- "isDone" BOOLEAN NOT NULL DEFAULT false,
- "userId" INTEGER,
- CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
-);
-INSERT INTO "new_Task" ("description", "id", "isDone") SELECT "description", "id", "isDone" FROM "Task";
-DROP TABLE "Task";
-ALTER TABLE "new_Task" RENAME TO "Task";
-PRAGMA foreign_key_check;
-PRAGMA foreign_keys=ON;
diff --git a/examples/tutorials/TodoAppTs/migrations/20231214131753_new_auth/migration.sql b/examples/tutorials/TodoAppTs/migrations/20231214131753_new_auth/migration.sql
new file mode 100644
index 0000000000..9e6e2345a3
--- /dev/null
+++ b/examples/tutorials/TodoAppTs/migrations/20231214131753_new_auth/migration.sql
@@ -0,0 +1,34 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
+);
+
+-- CreateTable
+CREATE TABLE "Task" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "description" TEXT NOT NULL,
+ "isDone" BOOLEAN NOT NULL DEFAULT false,
+ "userId" INTEGER,
+ CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Auth" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" INTEGER,
+ CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "providerName" TEXT NOT NULL,
+ "providerUserId" TEXT NOT NULL,
+ "providerData" TEXT NOT NULL DEFAULT '{}',
+ "authId" TEXT NOT NULL,
+
+ PRIMARY KEY ("providerName", "providerUserId"),
+ CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
diff --git a/examples/waspello/main.wasp b/examples/waspello/main.wasp
index 1bafe9b287..321d149480 100644
--- a/examples/waspello/main.wasp
+++ b/examples/waspello/main.wasp
@@ -11,7 +11,6 @@ app waspello {
auth: {
userEntity: User,
- externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
@@ -47,21 +46,9 @@ page Login {
entity User {=psl
id Int @id @default(autoincrement())
- username String @unique
- password String
+
lists List[]
cards Card[]
- externalAuthAssociations SocialLogin[]
-psl=}
-
-entity SocialLogin {=psl
- id Int @id @default(autoincrement())
- provider String
- providerId String
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- userId Int
- createdAt DateTime @default(now())
- @@unique([provider, providerId, userId])
psl=}
entity List {=psl
diff --git a/examples/waspello/migrations/20211002184633_added_waspello_entities/migration.sql b/examples/waspello/migrations/20211002184633_added_waspello_entities/migration.sql
deleted file mode 100644
index 203a88dfce..0000000000
--- a/examples/waspello/migrations/20211002184633_added_waspello_entities/migration.sql
+++ /dev/null
@@ -1,41 +0,0 @@
--- CreateTable
-CREATE TABLE "User" (
- "id" SERIAL NOT NULL,
- "email" TEXT NOT NULL,
- "password" TEXT NOT NULL,
-
- PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "List" (
- "id" SERIAL NOT NULL,
- "name" TEXT NOT NULL,
- "pos" DOUBLE PRECISION NOT NULL,
- "userId" INTEGER NOT NULL,
-
- PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Card" (
- "id" SERIAL NOT NULL,
- "title" TEXT NOT NULL,
- "pos" DOUBLE PRECISION NOT NULL,
- "listId" INTEGER NOT NULL,
- "authorId" INTEGER NOT NULL,
-
- PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
-
--- AddForeignKey
-ALTER TABLE "List" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Card" ADD FOREIGN KEY ("listId") REFERENCES "List"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Card" ADD FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/examples/waspello/migrations/20220211211659_upgrade_prisma_to_3_9_1/migration.sql b/examples/waspello/migrations/20220211211659_upgrade_prisma_to_3_9_1/migration.sql
deleted file mode 100644
index 893fbabf5a..0000000000
--- a/examples/waspello/migrations/20220211211659_upgrade_prisma_to_3_9_1/migration.sql
+++ /dev/null
@@ -1,20 +0,0 @@
--- DropForeignKey
-ALTER TABLE "Card" DROP CONSTRAINT "Card_authorId_fkey";
-
--- DropForeignKey
-ALTER TABLE "Card" DROP CONSTRAINT "Card_listId_fkey";
-
--- DropForeignKey
-ALTER TABLE "List" DROP CONSTRAINT "List_userId_fkey";
-
--- AddForeignKey
-ALTER TABLE "List" ADD CONSTRAINT "List_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Card" ADD CONSTRAINT "Card_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "Card" ADD CONSTRAINT "Card_listId_fkey" FOREIGN KEY ("listId") REFERENCES "List"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-
--- RenameIndex
-ALTER INDEX "User.email_unique" RENAME TO "User_email_key";
diff --git a/examples/waspello/migrations/20220818151104_rename_email_to_username/migration.sql b/examples/waspello/migrations/20220818151104_rename_email_to_username/migration.sql
deleted file mode 100644
index 10182cf688..0000000000
--- a/examples/waspello/migrations/20220818151104_rename_email_to_username/migration.sql
+++ /dev/null
@@ -1,9 +0,0 @@
--- DropIndex
-DROP INDEX "User_email_key";
-
--- AlterTable
-ALTER TABLE "User"
-RENAME COLUMN "email" TO "username";
-
--- CreateIndex
-CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
diff --git a/examples/waspello/migrations/20221122161320_added_social_login_with_google/migration.sql b/examples/waspello/migrations/20221122161320_added_social_login_with_google/migration.sql
deleted file mode 100644
index 0ae1754d7d..0000000000
--- a/examples/waspello/migrations/20221122161320_added_social_login_with_google/migration.sql
+++ /dev/null
@@ -1,16 +0,0 @@
--- CreateTable
-CREATE TABLE "SocialLogin" (
- "id" SERIAL NOT NULL,
- "provider" TEXT NOT NULL,
- "providerId" TEXT NOT NULL,
- "userId" INTEGER NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-
- CONSTRAINT "SocialLogin_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "SocialLogin_provider_providerId_userId_key" ON "SocialLogin"("provider", "providerId", "userId");
-
--- AddForeignKey
-ALTER TABLE "SocialLogin" ADD CONSTRAINT "SocialLogin_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/examples/waspello/migrations/20231214132020_new_auth/migration.sql b/examples/waspello/migrations/20231214132020_new_auth/migration.sql
new file mode 100644
index 0000000000..1649460c27
--- /dev/null
+++ b/examples/waspello/migrations/20231214132020_new_auth/migration.sql
@@ -0,0 +1,63 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "List" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "pos" DOUBLE PRECISION NOT NULL,
+ "userId" INTEGER NOT NULL,
+
+ CONSTRAINT "List_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Card" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "pos" DOUBLE PRECISION NOT NULL,
+ "listId" INTEGER NOT NULL,
+ "authorId" INTEGER NOT NULL,
+
+ CONSTRAINT "Card_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Auth" (
+ "id" TEXT NOT NULL,
+ "userId" INTEGER,
+
+ CONSTRAINT "Auth_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "providerName" TEXT NOT NULL,
+ "providerUserId" TEXT NOT NULL,
+ "providerData" TEXT NOT NULL DEFAULT '{}',
+ "authId" TEXT NOT NULL,
+
+ CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
+
+-- AddForeignKey
+ALTER TABLE "List" ADD CONSTRAINT "List_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Card" ADD CONSTRAINT "Card_listId_fkey" FOREIGN KEY ("listId") REFERENCES "List"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Card" ADD CONSTRAINT "Card_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/examples/waspello/src/client/Navbar.jsx b/examples/waspello/src/client/Navbar.jsx
index 8f109cf7d9..ca0a7de9bf 100644
--- a/examples/waspello/src/client/Navbar.jsx
+++ b/examples/waspello/src/client/Navbar.jsx
@@ -1,29 +1,34 @@
-import React from 'react'
+import React from "react";
-import logout from '@wasp/auth/logout'
+import logout from "@wasp/auth/logout";
-import logo from './waspello-logo-navbar.svg'
-import './Navbar.css'
+import logo from "./waspello-logo-navbar.svg";
+import "./Navbar.css";
+import { getName } from "./user";
const Navbar = ({ user }) => {
+ const name = getName(user);
return (
-
+
Home
-
+
- { user.username }
+ {name}
|
-
+
- )
-}
+ );
+};
-export default Navbar
+export default Navbar;
diff --git a/examples/waspello/src/client/user.ts b/examples/waspello/src/client/user.ts
new file mode 100644
index 0000000000..e0dcfa300d
--- /dev/null
+++ b/examples/waspello/src/client/user.ts
@@ -0,0 +1,19 @@
+import { User } from "@wasp/auth/types";
+import { findUserIdentity, getUsername } from "@wasp/auth/user";
+
+export function getName(user: User): string {
+ // We have two ways of authenticating users, so
+ // we have to check which one is used.
+ const googleIdentity = findUserIdentity(user, "google");
+ const usernameIdentity = findUserIdentity(user, "username");
+
+ if (usernameIdentity) {
+ return getUsername(user);
+ }
+
+ if (googleIdentity) {
+ return `Google user ${googleIdentity.providerUserId}`;
+ }
+
+ return "Unknown user";
+}
diff --git a/examples/websockets-realtime-voting/main.wasp b/examples/websockets-realtime-voting/main.wasp
index d0bad794f5..4ce3bf7ebc 100644
--- a/examples/websockets-realtime-voting/main.wasp
+++ b/examples/websockets-realtime-voting/main.wasp
@@ -24,8 +24,6 @@ app whereDoWeEat {
entity User {=psl
id Int @id @default(autoincrement())
- username String @unique
- password String
psl=}
route RootRoute { path: "/", to: MainPage }
diff --git a/examples/websockets-realtime-voting/migrations/20231214111940_new_auth/migration.sql b/examples/websockets-realtime-voting/migrations/20231214111940_new_auth/migration.sql
new file mode 100644
index 0000000000..fd930305e2
--- /dev/null
+++ b/examples/websockets-realtime-voting/migrations/20231214111940_new_auth/migration.sql
@@ -0,0 +1,30 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "username" TEXT NOT NULL,
+ "password" TEXT NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "Auth" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" INTEGER,
+ CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "providerName" TEXT NOT NULL,
+ "providerUserId" TEXT NOT NULL,
+ "providerData" TEXT NOT NULL DEFAULT '{}',
+ "authId" TEXT NOT NULL,
+
+ PRIMARY KEY ("providerName", "providerUserId"),
+ CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
diff --git a/examples/websockets-realtime-voting/migrations/20231214112306_remove_extra_user_fields/migration.sql b/examples/websockets-realtime-voting/migrations/20231214112306_remove_extra_user_fields/migration.sql
new file mode 100644
index 0000000000..969799968d
--- /dev/null
+++ b/examples/websockets-realtime-voting/migrations/20231214112306_remove_extra_user_fields/migration.sql
@@ -0,0 +1,17 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
+
+*/
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
+);
+INSERT INTO "new_User" ("id") SELECT "id" FROM "User";
+DROP TABLE "User";
+ALTER TABLE "new_User" RENAME TO "User";
+PRAGMA foreign_key_check;
+PRAGMA foreign_keys=ON;
diff --git a/examples/websockets-realtime-voting/migrations/migration_lock.toml b/examples/websockets-realtime-voting/migrations/migration_lock.toml
new file mode 100644
index 0000000000..e5e5c4705a
--- /dev/null
+++ b/examples/websockets-realtime-voting/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "sqlite"
\ No newline at end of file
diff --git a/examples/websockets-realtime-voting/src/client/Layout.jsx b/examples/websockets-realtime-voting/src/client/Layout.jsx
index dc3de690a6..ac42770a0d 100644
--- a/examples/websockets-realtime-voting/src/client/Layout.jsx
+++ b/examples/websockets-realtime-voting/src/client/Layout.jsx
@@ -4,6 +4,7 @@ import "./Main.css";
import { Flowbite, Dropdown, Navbar, Avatar } from "flowbite-react";
import Logo from "./logo.png";
import useAuth from "@wasp/auth/useAuth";
+import { getUsername } from "@wasp/auth/user";
import logout from "@wasp/auth/logout";
const customTheme = {
@@ -11,21 +12,18 @@ const customTheme = {
color: {
primary: "bg-red-500 hover:bg-red-600",
},
- }
+ },
};
export const Layout = ({ children }) => {
const { data: user } = useAuth();
+
return (
-
+
Undecisive Fox App
@@ -37,13 +35,15 @@ export const Layout = ({ children }) => {
label={
}
>
- {user.username}
+ {getUsername(user)}DashboardSettings
diff --git a/examples/websockets-realtime-voting/src/server/ws-server.ts b/examples/websockets-realtime-voting/src/server/ws-server.ts
index 07e2b1b60a..8a911f4006 100644
--- a/examples/websockets-realtime-voting/src/server/ws-server.ts
+++ b/examples/websockets-realtime-voting/src/server/ws-server.ts
@@ -1,33 +1,29 @@
-import { WebSocketDefinition } from "@wasp/webSocket"
-import { User } from "@wasp/entities"
+import { WebSocketDefinition } from "@wasp/webSocket";
+import { getUsername } from "@wasp/auth/user.js";
type PollState = {
- question: string
+ question: string;
options: {
- id: number
- text: string
- description: string
- votes: string[]
- }[]
-}
+ id: number;
+ text: string;
+ description: string;
+ votes: string[];
+ }[];
+};
interface ServerToClientEvents {
- updateState: (state: PollState) => void
+ updateState: (state: PollState) => void;
}
interface ClientToServerEvents {
vote: (optionId: number) => void;
askForStateUpdate: () => void;
}
interface InterServerEvents {}
-interface SocketData {
- user: User;
-}
export const webSocketFn: WebSocketDefinition<
-ClientToServerEvents,
-ServerToClientEvents,
-InterServerEvents,
-SocketData
+ ClientToServerEvents,
+ ServerToClientEvents,
+ InterServerEvents
> = (io, context) => {
const poll: PollState = {
question: "What are eating for lunch ✨ Let's order",
@@ -52,33 +48,40 @@ SocketData
},
],
};
- io.on('connection', (socket) => {
+ io.on("connection", (socket) => {
if (!socket.data.user) {
- console.log('Socket connected without user');
+ console.log("Socket connected without user");
return;
}
- console.log('Socket connected: ', socket.data.user?.username);
+ const connectionUsername = getUsername(socket.data.user);
+
+ console.log("Socket connected: ", connectionUsername);
socket.on("askForStateUpdate", () => {
- socket.emit('updateState', poll);
+ socket.emit("updateState", poll);
});
socket.on("vote", (optionId) => {
+ if (!connectionUsername) {
+ return;
+ }
// If user has already voted, remove their vote.
poll.options.forEach((option) => {
- option.votes = option.votes.filter((username) => username !== socket.data.user.username);
+ option.votes = option.votes.filter(
+ (username) => username !== connectionUsername
+ );
});
// And then add their vote to the new option.
const option = poll.options.find((o) => o.id === optionId);
if (!option) {
return;
}
- option.votes.push(socket.data.user.username);
- io.emit('updateState', poll);
+ option.votes.push(connectionUsername);
+ io.emit("updateState", poll);
});
- socket.on('disconnect', () => {
- console.log('Socket disconnected: ', socket.data.user?.username);
+ socket.on("disconnect", () => {
+ console.log("Socket disconnected: ", connectionUsername ?? "unknown");
});
});
-}
\ No newline at end of file
+};
diff --git a/waspc/data/Generator/templates/react-app/src/auth/types.ts b/waspc/data/Generator/templates/react-app/src/auth/types.ts
index b9614390c4..4405410cc7 100644
--- a/waspc/data/Generator/templates/react-app/src/auth/types.ts
+++ b/waspc/data/Generator/templates/react-app/src/auth/types.ts
@@ -1,2 +1,2 @@
// todo(filip): turn into a proper import/path
-export type { SanitizedUser as User } from '../../../server/src/_types/'
+export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../../../server/src/_types/'
diff --git a/waspc/data/Generator/templates/react-app/src/auth/user.ts b/waspc/data/Generator/templates/react-app/src/auth/user.ts
new file mode 100644
index 0000000000..5799c71ea7
--- /dev/null
+++ b/waspc/data/Generator/templates/react-app/src/auth/user.ts
@@ -0,0 +1,27 @@
+// We decided not to deduplicate these helper functions in the server and the client.
+// We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts
+// If you are changing the logic here, make sure to change it there as well.
+
+import type { User, ProviderName, DeserializedAuthEntity } from './types'
+
+export function getEmail(user: User): string | null {
+ return findUserIdentity(user, "email")?.providerUserId ?? null;
+}
+
+export function getUsername(user: User): string | null {
+ return findUserIdentity(user, "username")?.providerUserId ?? null;
+}
+
+export function getFirstProviderUserId(user?: User): string | null {
+ if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
+ return null;
+ }
+
+ return user.auth.identities[0].providerUserId ?? null;
+}
+
+export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined {
+ return user.auth.identities.find(
+ (identity) => identity.providerName === providerName
+ );
+}
diff --git a/waspc/data/Generator/templates/react-app/src/entities/index.ts b/waspc/data/Generator/templates/react-app/src/entities/index.ts
index a6b0269ac0..27d4ebc25e 100644
--- a/waspc/data/Generator/templates/react-app/src/entities/index.ts
+++ b/waspc/data/Generator/templates/react-app/src/entities/index.ts
@@ -9,6 +9,10 @@ export type {
{=# entities =}
{= name =},
{=/ entities =}
+ {=# isAuthEnabled =}
+ {= authEntityName =},
+ {= authIdentityEntityName =},
+ {=/ isAuthEnabled =}
} from '@prisma/client'
export type Entity =
diff --git a/waspc/data/Generator/templates/server/src/_types/index.ts b/waspc/data/Generator/templates/server/src/_types/index.ts
index 1c5562f5ab..2b3c6bf5ae 100644
--- a/waspc/data/Generator/templates/server/src/_types/index.ts
+++ b/waspc/data/Generator/templates/server/src/_types/index.ts
@@ -4,7 +4,16 @@ import { type Request, type Response } from 'express'
import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core'
import prisma from "../dbClient.js"
{=# isAuthEnabled =}
-import { type {= userEntityName =} } from "../entities"
+import {
+ type {= userEntityName =},
+ type {= authEntityName =},
+ type {= authIdentityEntityName =},
+} from "../entities"
+import {
+ type EmailProviderData,
+ type UsernameProviderData,
+ type OAuthProviderData,
+} from '../auth/utils.js'
{=/ isAuthEnabled =}
import { type _Entity } from "./taggedEntities"
import { type Payload } from "./serialization";
@@ -77,11 +86,22 @@ type Context = Expand<{
}>
{=# isAuthEnabled =}
-type ContextWithUser = Expand & { user?: SanitizedUser}>
+type ContextWithUser = Expand & { user?: SanitizedUser }>
// TODO: This type must match the logic in core/auth.js (if we remove the
// password field from the object there, we must do the same here). Ideally,
// these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
-export type SanitizedUser = Omit<{= userEntityName =}, 'password'>
+
+export type DeserializedAuthEntity = Expand & {
+ providerData: Omit | Omit | OAuthProviderData
+}>
+
+export type SanitizedUser = {= userEntityName =} & {
+ {= authFieldOnUserEntityName =}: {= authEntityName =} & {
+ {= identitiesFieldOnAuthEntityName =}: DeserializedAuthEntity[]
+ } | null
+}
+
+export type { ProviderName } from '../auth/utils.js'
{=/ isAuthEnabled =}
diff --git a/waspc/data/Generator/templates/server/src/auth/index.ts b/waspc/data/Generator/templates/server/src/auth/index.ts
index 374824b697..cf74b0773e 100644
--- a/waspc/data/Generator/templates/server/src/auth/index.ts
+++ b/waspc/data/Generator/templates/server/src/auth/index.ts
@@ -1,7 +1 @@
-{{={= =}=}}
-{=# isEmailAuthEnabled =}
-export { defineAdditionalSignupFields } from './providers/email/types.js';
-{=/ isEmailAuthEnabled =}
-{=# isLocalAuthEnabled =}
-export { defineAdditionalSignupFields } from './providers/local/types.js';
-{=/ isLocalAuthEnabled =}
\ No newline at end of file
+export { defineAdditionalSignupFields } from './providers/types.js';
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts b/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts
index 5d87d81976..a397bfa4be 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/config/_oauth.ts
@@ -11,7 +11,7 @@ import type { OAuthConfig } from "../oauth/types.js";
const _waspGetUserFieldsFn = {= userFieldsFn.importIdentifier =}
{=/ userFieldsFn.isDefined =}
{=^ userFieldsFn.isDefined =}
-import { getUserFieldsFn as _waspGetUserFieldsFn } from '../oauth/defaults.js'
+const _waspGetUserFieldsFn = undefined
{=/ userFieldsFn.isDefined =}
{=# configFn.isDefined =}
{=& configFn.importStatement =}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts b/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts
index 220857004c..ef327d20b5 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/config/email.ts
@@ -62,6 +62,7 @@ const config: ProviderConfig = {
fromField,
clientRoute: '{= emailVerificationClientRoute =}',
getVerificationEmailContent: _waspGetVerificationEmailContent,
+ allowUnverifiedLogin: {=# allowUnverifiedLogin =}true{=/ allowUnverifiedLogin =}{=^ allowUnverifiedLogin =}false{=/ allowUnverifiedLogin =},
}));
router.post('/signup', signupRoute);
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/config/local.ts b/waspc/data/Generator/templates/server/src/auth/providers/config/username.ts
similarity index 81%
rename from waspc/data/Generator/templates/server/src/auth/providers/config/local.ts
rename to waspc/data/Generator/templates/server/src/auth/providers/config/username.ts
index 92496bfb18..f7b78ed83b 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/config/local.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/config/username.ts
@@ -2,8 +2,8 @@
import { Router } from "express";
-import login from "../local/login.js";
-import signup from "../local/signup.js";
+import login from "../username/login.js";
+import signup from "../username/signup.js";
import { ProviderConfig } from "../types.js";
const config: ProviderConfig = {
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts
index fa53896468..b50422e3a6 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/email/login.ts
@@ -1,6 +1,12 @@
import { Request, Response } from 'express';
import { verifyPassword, throwInvalidCredentialsError } from "../../../core/auth.js";
-import { findUserBy, createAuthToken } from "../../utils.js";
+import {
+ createProviderId,
+ findAuthIdentity,
+ findAuthWithUserBy,
+ createAuthToken,
+ deserializeAndSanitizeProviderData,
+} from "../../utils.js";
import { ensureValidEmail, ensurePasswordIsPresent } from "../../validation.js";
export function getLoginRoute({
@@ -12,25 +18,27 @@ export function getLoginRoute({
req: Request<{ email: string; password: string; }>,
res: Response,
): Promise> {
- const userFields = req.body || {}
- ensureValidArgs(userFields)
+ const fields = req.body ?? {}
+ ensureValidArgs(fields)
- userFields.email = userFields.email.toLowerCase()
-
- const user = await findUserBy({ email: userFields.email })
- if (!user) {
+ const authIdentity = await findAuthIdentity(
+ createProviderId("email", fields.email)
+ )
+ if (!authIdentity) {
throwInvalidCredentialsError()
}
- if (!user.isEmailVerified && !allowUnverifiedLogin) {
+ const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData)
+ if (!providerData.isEmailVerified && !allowUnverifiedLogin) {
throwInvalidCredentialsError()
}
try {
- await verifyPassword(user.password, userFields.password);
+ await verifyPassword(providerData.hashedPassword, fields.password);
} catch(e) {
throwInvalidCredentialsError()
}
- const token = await createAuthToken(user)
+ const auth = await findAuthWithUserBy({ id: authIdentity.authId })
+ const token = await createAuthToken(auth.userId)
return res.json({ token })
};
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/requestPasswordReset.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/requestPasswordReset.ts
index e665240dd2..c0936bac59 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/email/requestPasswordReset.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/email/requestPasswordReset.ts
@@ -1,7 +1,9 @@
import { Request, Response } from 'express';
import {
- findUserBy,
+ createProviderId,
+ findAuthIdentity,
doFakeWork,
+ deserializeAndSanitizeProviderData,
} from "../../utils.js";
import {
createPasswordResetLink,
@@ -11,6 +13,7 @@ import {
import { ensureValidEmail } from "../../validation.js";
import type { EmailFromField } from '../../../email/core/types.js';
import { GetPasswordResetEmailContentFn } from './types.js';
+import HttpError from '../../../core/HttpError.js';
export function getRequestPasswordResetRoute({
fromField,
@@ -24,39 +27,47 @@ export function getRequestPasswordResetRoute({
return async function requestPasswordReset(
req: Request<{ email: string; }>,
res: Response,
- ): Promise> {
- const args = req.body || {};
+ ): Promise> {
+ const args = req.body ?? {};
ensureValidEmail(args);
- args.email = args.email.toLowerCase();
+ const authIdentity = await findAuthIdentity(
+ createProviderId("email", args.email),
+ );
- const user = await findUserBy({ email: args.email });
-
- // User not found or not verified - don't leak information
- if (!user || !user.isEmailVerified) {
+ /**
+ * By doing fake work, we make it harder to enumerate users by measuring
+ * the time it takes to respond. If we would respond immediately, an attacker
+ * could measure the time it takes to respond and figure out if the user exists.
+ */
+
+ if (!authIdentity) {
await doFakeWork();
return res.json({ success: true });
}
- if (!isEmailResendAllowed(user, 'passwordResetSentAt')) {
- return res.status(400).json({ success: false, message: "Please wait a minute before trying again." });
+ const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData);
+ const { isResendAllowed, timeLeft } = isEmailResendAllowed(providerData, 'passwordResetSentAt');
+ if (!isResendAllowed) {
+ throw new HttpError(400, `Please wait ${timeLeft} secs before trying again.`);
}
-
- const passwordResetLink = await createPasswordResetLink(user, clientRoute);
+
+ const passwordResetLink = await createPasswordResetLink(args.email, clientRoute);
try {
+ const email = authIdentity.providerUserId
await sendPasswordResetEmail(
- user.email,
+ email,
{
from: fromField,
- to: user.email,
+ to: email,
...getPasswordResetEmailContent({ passwordResetLink }),
- }
+ },
);
} catch (e: any) {
console.error("Failed to send password reset email:", e);
- return res.status(500).json({ success: false, message: "Failed to send password reset email." });
+ throw new HttpError(500, "Failed to send password reset email.");
}
- res.json({ success: true });
+ return res.json({ success: true });
};
}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/resetPassword.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/resetPassword.ts
index d7dce9f0e9..3f01d47c32 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/email/resetPassword.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/email/resetPassword.ts
@@ -1,31 +1,48 @@
import { Request, Response } from 'express';
-import { findUserBy, verifyToken } from "../../utils.js";
-import { updateUserPassword } from "./utils.js";
+import {
+ createProviderId,
+ findAuthIdentity,
+ updateAuthIdentityProviderData,
+ verifyToken,
+ deserializeAndSanitizeProviderData,
+} from "../../utils.js";
import { ensureTokenIsPresent, ensurePasswordIsPresent, ensureValidPassword } from "../../validation.js";
import { tokenVerificationErrors } from "./types.js";
+import HttpError from '../../../core/HttpError.js';
export async function resetPassword(
req: Request<{ token: string; password: string; }>,
res: Response,
-): Promise> {
- const args = req.body || {};
+): Promise> {
+ const args = req.body ?? {};
ensureValidArgs(args);
const { token, password } = args;
try {
- const { id: userId } = await verifyToken(token);
- const user = await findUserBy({ id: userId });
- if (!user) {
- return res.status(400).json({ success: false, message: 'Invalid token' });
+ const { email } = await verifyToken<{ email: string }>(token);
+
+ const providerId = createProviderId('email', email);
+ const authIdentity = await findAuthIdentity(providerId);
+ if (!authIdentity) {
+ throw new HttpError(400, "Password reset failed, invalid token");
}
- await updateUserPassword(userId, password);
+
+ const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData);
+
+ await updateAuthIdentityProviderData(providerId, providerData, {
+ // The act of resetting the password verifies the email
+ isEmailVerified: true,
+ // The password will be hashed when saving the providerData
+ // in the DB
+ hashedPassword: password,
+ });
} catch (e) {
const reason = e.name === tokenVerificationErrors.TokenExpiredError
? 'expired'
: 'invalid';
- return res.status(400).json({ success: false, message: `Password reset failed, ${reason} token`});
+ throw new HttpError(400, `Password reset failed, ${reason} token`);
}
- res.json({ success: true });
+ return res.json({ success: true });
};
function ensureValidArgs(args: unknown): void {
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts
index 3ad9d795a2..e6755e2b47 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/email/signup.ts
@@ -2,9 +2,13 @@ import { Request, Response } from 'express';
import { EmailFromField } from "../../../email/core/types.js";
import {
createUser,
- findUserBy,
- deleteUser,
+ createProviderId,
+ findAuthIdentity,
+ deleteUserByAuthId,
doFakeWork,
+ deserializeAndSanitizeProviderData,
+ sanitizeAndSerializeProviderData,
+ rethrowPossibleAuthError,
} from "../../utils.js";
import {
createEmailVerificationLink,
@@ -14,58 +18,123 @@ import {
import { ensureValidEmail, ensureValidPassword, ensurePasswordIsPresent } from "../../validation.js";
import { GetVerificationEmailContentFn } from './types.js';
import { validateAndGetAdditionalFields } from '../../utils.js'
+import HttpError from '../../../core/HttpError.js';
export function getSignupRoute({
fromField,
clientRoute,
getVerificationEmailContent,
+ allowUnverifiedLogin,
}: {
fromField: EmailFromField;
clientRoute: string;
getVerificationEmailContent: GetVerificationEmailContentFn;
+ allowUnverifiedLogin: boolean;
}) {
return async function signup(
req: Request<{ email: string; password: string; }>,
res: Response,
- ): Promise> {
- const userFields = req.body;
- ensureValidArgs(userFields);
+ ): Promise> {
+ const fields = req.body;
+ ensureValidArgs(fields);
- userFields.email = userFields.email.toLowerCase();
+ const providerId = createProviderId("email", fields.email);
+ const existingAuthIdentity = await findAuthIdentity(providerId);
- const existingUser = await findUserBy({ email: userFields.email });
- // User already exists and is verified - don't leak information
- if (existingUser && existingUser.isEmailVerified) {
- await doFakeWork();
- return res.json({ success: true });
- } else if (existingUser && !existingUser.isEmailVerified) {
- if (!isEmailResendAllowed(existingUser, 'emailVerificationSentAt')) {
- return res.status(400).json({ success: false, message: "Please wait a minute before trying again." });
+ /**
+ *
+ * There are two cases to consider in the case of an existing user:
+ * - if we allow unverified login
+ * - if the user is already verified
+ *
+ * Let's see what happens when we **don't** allow unverified login:
+ *
+ * We are handling the case of an existing auth identity in two ways:
+ *
+ * 1. If the user already exists and is verified, we don't want
+ * to leak that piece of info and instead we pretend that the user
+ * was created successfully.
+ * - This prevents the attacker from learning which emails already have
+ * an account created.
+ *
+ * 2. If the user is not verified:
+ * - We check when we last sent a verification email and if it was less than X seconds ago,
+ * we don't send another one.
+ * - If it was more than X seconds ago, we delete the user and create a new one.
+ * - This prevents the attacker from creating an account with somebody
+ * else's email address and therefore permanently making that email
+ * address unavailable for later account creation (by real owner).
+ */
+ if (existingAuthIdentity) {
+ if (allowUnverifiedLogin) {
+ /**
+ * This is the case where we allow unverified login.
+ *
+ * If we pretended that the user was created successfully that would bring
+ * us little value: the attacker would not be able to login and figure out
+ * if the user exists or not, anyway.
+ *
+ * So, we throw an error that says that the user already exists.
+ */
+ throw new HttpError(422, "User with that email already exists.")
+ }
+
+ const providerData = deserializeAndSanitizeProviderData<'email'>(existingAuthIdentity.providerData);
+
+ // TOOD: faking work makes sense if the time spent on faking the work matches the time
+ // it would take to send the email. Atm, the fake work takes obviously longer than sending
+ // the email!
+ if (providerData.isEmailVerified) {
+ await doFakeWork();
+ return res.json({ success: true });
+ }
+
+ // TODO: we are still leaking information here since when we are faking work
+ // we are not checking if the email was sent or not!
+ const { isResendAllowed, timeLeft } = isEmailResendAllowed(providerData, 'passwordResetSentAt');
+ if (!isResendAllowed) {
+ throw new HttpError(400, `Please wait ${timeLeft} secs before trying again.`);
+ }
+
+ try {
+ await deleteUserByAuthId(existingAuthIdentity.authId);
+ } catch (e: unknown) {
+ rethrowPossibleAuthError(e);
}
- await deleteUser(existingUser);
}
- const additionalFields = await validateAndGetAdditionalFields(userFields);
-
- const user = await createUser({
- ...additionalFields,
- email: userFields.email,
- password: userFields.password,
+ const userFields = await validateAndGetAdditionalFields(fields);
+
+ const newUserProviderData = await sanitizeAndSerializeProviderData<'email'>({
+ hashedPassword: fields.password,
+ isEmailVerified: false,
+ emailVerificationSentAt: null,
+ passwordResetSentAt: null,
});
- const verificationLink = await createEmailVerificationLink(user, clientRoute);
+ try {
+ await createUser(
+ providerId,
+ newUserProviderData,
+ userFields,
+ );
+ } catch (e: unknown) {
+ rethrowPossibleAuthError(e);
+ }
+
+ const verificationLink = await createEmailVerificationLink(fields.email, clientRoute);
try {
await sendEmailVerificationEmail(
- userFields.email,
+ fields.email,
{
from: fromField,
- to: userFields.email,
+ to: fields.email,
...getVerificationEmailContent({ verificationLink }),
}
);
- } catch (e: any) {
+ } catch (e: unknown) {
console.error("Failed to send email verification email:", e);
- return res.status(500).json({ success: false, message: "Failed to send email verification email." });
+ throw new HttpError(500, "Failed to send email verification email.");
}
return res.json({ success: true });
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/types.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/types.ts
index d1a7bc4a13..f213c80e93 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/email/types.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/email/types.ts
@@ -1,5 +1,3 @@
-import { createDefineAdditionalSignupFieldsFn } from '../types.js'
-
export type GetVerificationEmailContentFn = (params: { verificationLink: string }) => EmailContent;
export type GetPasswordResetEmailContentFn = (params: { passwordResetLink: string }) => EmailContent;
@@ -13,5 +11,3 @@ type EmailContent = {
export const tokenVerificationErrors = {
TokenExpiredError: 'TokenExpiredError',
};
-
-export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"email" | "password">()
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts
index 814325e181..a158b91482 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/email/utils.ts
@@ -1,99 +1,94 @@
{{={= =}=}}
-import { sign } from '../../../core/auth.js'
+import { signData } from '../../../core/auth.js'
import { emailSender } from '../../../email/index.js';
import { Email } from '../../../email/core/types.js';
-import { rethrowPossiblePrismaError } from '../../utils.js'
-import prisma from '../../../dbClient.js'
+import {
+ createProviderId,
+ updateAuthIdentityProviderData,
+ findAuthIdentity,
+ deserializeAndSanitizeProviderData,
+ type EmailProviderData,
+} from '../../utils.js';
import waspServerConfig from '../../../config.js';
-import { type {= userEntityUpper =} } from '../../../entities/index.js'
+import { type {= userEntityUpper =}, type {= authEntityUpper =} } from '../../../entities/index.js'
-type {= userEntityUpper =}Id = {= userEntityUpper =}['id']
-
-export async function updateUserEmailVerification(userId: {= userEntityUpper =}Id): Promise {
- try {
- await prisma.{= userEntityLower =}.update({
- where: { id: userId },
- data: { isEmailVerified: true },
- })
- } catch (e) {
- rethrowPossiblePrismaError(e);
- }
-}
-
-export async function updateUserPassword(userId: {= userEntityUpper =}Id, password: string): Promise {
- try {
- await prisma.{= userEntityLower =}.update({
- where: { id: userId },
- data: { password },
- })
- } catch (e) {
- rethrowPossiblePrismaError(e);
- }
-}
-
-export async function createEmailVerificationLink(user: {= userEntityUpper =}, clientRoute: string): Promise {
- const token = await createEmailVerificationToken(user);
- return `${waspServerConfig.frontendUrl}${clientRoute}?token=${token}`;
-}
-
-export async function createPasswordResetLink(user: {= userEntityUpper =}, clientRoute: string): Promise {
- const token = await createPasswordResetToken(user);
- return `${waspServerConfig.frontendUrl}${clientRoute}?token=${token}`;
+export async function createEmailVerificationLink(
+ email: string,
+ clientRoute: string,
+): Promise {
+ const { jwtToken } = await createEmailJwtToken(email);
+ return `${waspServerConfig.frontendUrl}${clientRoute}?token=${jwtToken}`;
}
-async function createEmailVerificationToken(user: {= userEntityUpper =}): Promise {
- return sign(user.id, { expiresIn: '30m' });
+export async function createPasswordResetLink(
+ email: string,
+ clientRoute: string,
+): Promise {
+ const { jwtToken } = await createEmailJwtToken(email);
+ return `${waspServerConfig.frontendUrl}${clientRoute}?token=${jwtToken}`;
}
-async function createPasswordResetToken(user: {= userEntityUpper =}): Promise {
- return sign(user.id, { expiresIn: '30m' });
+async function createEmailJwtToken(email: string): Promise<{ jwtToken: string; }> {
+ const jwtToken = await signData({ email }, { expiresIn: '30m' });
+ return { jwtToken };
}
export async function sendPasswordResetEmail(
email: string,
content: Email,
): Promise {
- return sendEmailAndLogTimestamp(email, content, 'passwordResetSentAt');
+ return sendEmailAndSaveMetadata(email, content, {
+ passwordResetSentAt: (new Date()).toISOString(),
+ });
}
export async function sendEmailVerificationEmail(
email: string,
content: Email,
): Promise {
- return sendEmailAndLogTimestamp(email, content, 'emailVerificationSentAt');
+ return sendEmailAndSaveMetadata(email, content, {
+ emailVerificationSentAt: (new Date()).toISOString(),
+ });
}
-async function sendEmailAndLogTimestamp(
+async function sendEmailAndSaveMetadata(
email: string,
content: Email,
- field: 'emailVerificationSentAt' | 'passwordResetSentAt',
+ metadata: Partial,
): Promise {
- // Set the timestamp first, and then send the email
- // so the user can't send multiple requests while
- // the email is being sent.
- try {
- await prisma.{= userEntityLower =}.update({
- where: { email },
- data: { [field]: new Date() },
- })
- } catch (e) {
- rethrowPossiblePrismaError(e);
- }
+ // Save the metadata (e.g. timestamp) first, and then send the email
+ // so the user can't send multiple requests while the email is being sent.
+ const providerId = createProviderId("email", email);
+ const authIdentity = await findAuthIdentity(providerId);
+ const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData);
+ await updateAuthIdentityProviderData<'email'>(providerId, providerData, metadata);
+
emailSender.send(content).catch((e) => {
- console.error(`Failed to send email for ${field}`, e);
+ console.error('Failed to send email', e);
});
}
-export function isEmailResendAllowed(
- user: {= userEntityUpper =},
- field: 'emailVerificationSentAt' | 'passwordResetSentAt',
+export function isEmailResendAllowed(
+ fields: {
+ [field in Field]: string | null
+ },
+ field: Field,
resendInterval: number = 1000 * 60,
-): boolean {
- const sentAt = user[field];
+): {
+ isResendAllowed: boolean;
+ timeLeft: number;
+} {
+ const sentAt = fields[field];
if (!sentAt) {
- return true;
+ return {
+ isResendAllowed: true,
+ timeLeft: 0,
+ };
}
const now = new Date();
- const diff = now.getTime() - sentAt.getTime();
- return diff > resendInterval;
+ const diff = now.getTime() - new Date(sentAt).getTime();
+ const isResendAllowed = diff > resendInterval;
+ // Time left in seconds
+ const timeLeft = isResendAllowed ? 0 : Math.round((resendInterval - diff) / 1000);
+ return { isResendAllowed, timeLeft };
}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/email/verifyEmail.ts b/waspc/data/Generator/templates/server/src/auth/providers/email/verifyEmail.ts
index e152591e41..7dc52d2576 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/email/verifyEmail.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/email/verifyEmail.ts
@@ -1,21 +1,35 @@
import { Request, Response } from 'express';
-import { updateUserEmailVerification } from './utils.js';
-import { verifyToken } from '../../utils.js';
+import {
+ verifyToken,
+ createProviderId,
+ findAuthIdentity,
+ updateAuthIdentityProviderData,
+ deserializeAndSanitizeProviderData,
+} from '../../utils.js';
import { tokenVerificationErrors } from './types.js';
+import HttpError from '../../../core/HttpError.js';
+
export async function verifyEmail(
req: Request<{ token: string }>,
res: Response,
-): Promise> {
+): Promise> {
try {
const { token } = req.body;
- const { id: userId } = await verifyToken(token);
- await updateUserEmailVerification(userId);
+ const { email } = await verifyToken<{ email: string }>(token);
+
+ const providerId = createProviderId('email', email);
+ const authIdentity = await findAuthIdentity(providerId);
+ const providerData = deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData);
+
+ await updateAuthIdentityProviderData(providerId, providerData, {
+ isEmailVerified: true,
+ });
} catch (e) {
const reason = e.name === tokenVerificationErrors.TokenExpiredError
? 'expired'
: 'invalid';
- return res.status(400).json({ success: false, message: `Token is ${reason}` });
+ throw new HttpError(400, `Token is ${reason}`);
}
return res.json({ success: true });
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/index.ts b/waspc/data/Generator/templates/server/src/auth/providers/index.ts
index 952abdb621..f3e3326fbe 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/index.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/index.ts
@@ -1,17 +1,18 @@
{{={= =}=}}
+import { join } from 'path'
import { Router } from "express";
-import { getDirFromFileUrl, importJsFilesFromDir } from "../../utils.js";
+import { getDirPathFromFileUrl, importJsFilesFromDir } from "../../utils.js";
import { ProviderConfig } from "./types";
-const allowedConfigs = [
+const whitelistedProviderConfigFileNames = [
{=# enabledProviderIds =}
"{= . =}.js",
{=/ enabledProviderIds =}
];
-const providers = await importProviders(allowedConfigs);
+const providers = await importProviders(whitelistedProviderConfigFileNames);
const router = Router();
@@ -27,8 +28,9 @@ for (const provider of providers) {
export default router;
-async function importProviders(providerConfigs: string[]): Promise {
- const currentExecutionDir = getDirFromFileUrl(import.meta.url);
- const providers = await importJsFilesFromDir(currentExecutionDir, "./config", providerConfigs);
+async function importProviders(whitelistedProviderConfigFileNames: string[]): Promise {
+ const currentExecutionDir = getDirPathFromFileUrl(import.meta.url);
+ const pathToDirWithConfigs = join(currentExecutionDir, "./config");
+ const providers = await importJsFilesFromDir(pathToDirWithConfigs, whitelistedProviderConfigFileNames);
return providers.map((provider) => provider.default);
}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/local/login.ts b/waspc/data/Generator/templates/server/src/auth/providers/local/login.ts
deleted file mode 100644
index af566844eb..0000000000
--- a/waspc/data/Generator/templates/server/src/auth/providers/local/login.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-{{={= =}=}}
-import { verifyPassword, throwInvalidCredentialsError } from '../../../core/auth.js'
-import { handleRejection } from '../../../utils.js'
-
-import { findUserBy, createAuthToken } from '../../utils.js'
-import { ensureValidUsername, ensurePasswordIsPresent } from '../../validation.js'
-
-export default handleRejection(async (req, res) => {
- const userFields = req.body || {}
- ensureValidArgs(userFields)
-
- const user = await findUserBy({ username: userFields.username })
- if (!user) {
- throwInvalidCredentialsError()
- }
-
- try {
- await verifyPassword(user.password, userFields.password)
- } catch(e) {
- throwInvalidCredentialsError()
- }
-
- // Username & password valid - generate token.
- const token = await createAuthToken(user)
-
- // NOTE(matija): Possible option - instead of explicitly returning token here,
- // we could add to response header 'Set-Cookie {token}' directive which would then make
- // browser automatically save cookie with token.
- // NOTE(shayne): Cross-domain cookies have serious limitations, which we recently explored.
-
- return res.json({ token })
-})
-
-function ensureValidArgs(args: unknown): void {
- ensureValidUsername(args);
- ensurePasswordIsPresent(args);
-}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/local/signup.ts b/waspc/data/Generator/templates/server/src/auth/providers/local/signup.ts
deleted file mode 100644
index 8705e411d1..0000000000
--- a/waspc/data/Generator/templates/server/src/auth/providers/local/signup.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-{{={= =}=}}
-import { handleRejection } from '../../../utils.js'
-import { createUser } from '../../utils.js'
-import { ensureValidUsername, ensurePasswordIsPresent, ensureValidPassword } from '../../validation.js'
-import { validateAndGetAdditionalFields } from '../../utils.js'
-
-export default handleRejection(async (req, res) => {
- const userFields = req.body || {}
- ensureValidArgs(userFields)
-
- const additionalFields = await validateAndGetAdditionalFields(userFields)
-
- await createUser({
- ...additionalFields,
- username: userFields.username,
- password: userFields.password,
- })
-
- return res.json({ success: true })
-})
-
-function ensureValidArgs(args: unknown): void {
- ensureValidUsername(args);
- ensurePasswordIsPresent(args);
- ensureValidPassword(args);
-}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/local/types.ts b/waspc/data/Generator/templates/server/src/auth/providers/local/types.ts
deleted file mode 100644
index 5d72e7d7ab..0000000000
--- a/waspc/data/Generator/templates/server/src/auth/providers/local/types.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { createDefineAdditionalSignupFieldsFn } from '../types.js'
-
-export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"username" | "password">()
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts
index aafb489ac5..d8a4fe0b62 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/oauth/createRouter.ts
@@ -2,21 +2,29 @@
import { Router } from "express"
import passport from "passport"
-import { v4 as uuidv4 } from 'uuid'
import prisma from '../../../dbClient.js'
import waspServerConfig from '../../../config.js'
-import { sign } from '../../../core/auth.js'
-import { authConfig, contextWithUserEntity, createUser } from "../../utils.js"
-
-import type { {= userEntityUpper =} } from '../../../entities';
+import {
+ type ProviderName,
+ type ProviderId,
+ createProviderId,
+ authConfig,
+ contextWithUserEntity,
+ createUser,
+ findAuthWithUserBy,
+ createAuthToken,
+ rethrowPossibleAuthError,
+ sanitizeAndSerializeProviderData,
+} from "../../utils.js"
+import { type {= userEntityUpper =} } from "../../../entities/index.js"
import type { ProviderConfig, RequestWithWasp } from "../types.js"
import type { GetUserFieldsFn } from "./types.js"
import { handleRejection } from "../../../utils.js"
// For oauth providers, we have an endpoint /login to get the auth URL,
// and the /callback endpoint which is used to get the actual access_token and the user info.
-export function createRouter(provider: ProviderConfig, initData: { passportStrategyName: string, getUserFieldsFn: GetUserFieldsFn }) {
+export function createRouter(provider: ProviderConfig, initData: { passportStrategyName: string, getUserFieldsFn?: GetUserFieldsFn }) {
const { passportStrategyName, getUserFieldsFn } = initData;
const router = Router();
@@ -42,48 +50,57 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat
throw new Error(`${provider.displayName} provider profile was missing required id property. This should not happen! Please contact Wasp.`);
}
- // Wrap call to getUserFieldsFn so we can invoke only if needed.
- const getUserFields = () => getUserFieldsFn(contextWithUserEntity, { profile: providerProfile });
- // TODO: In the future we could make this configurable, possibly associating an external account
- // with the currently logged in account, or by some DB lookup.
- const user = await findOrCreateUserByExternalAuthAssociation(provider.id, providerProfile.id, getUserFields);
+ const providerId = createProviderId(provider.id, providerProfile.id);
- const token = await sign(user.id);
- res.json({ token });
+ try {
+ const userId = await getUserIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn)
+ const token = await createAuthToken(userId)
+ res.json({ token })
+ } catch (e) {
+ rethrowPossibleAuthError(e)
+ }
})
)
return router;
}
-async function findOrCreateUserByExternalAuthAssociation(
- provider: string,
- providerId: string,
- getUserFields: () => ReturnType,
-): Promise<{= userEntityUpper =}> {
- // Attempt to find a User by an external auth association.
- const externalAuthAssociation = await prisma.{= externalAuthEntityLower =}.findFirst({
- where: { provider, providerId },
- include: { user: true }
+// We need a user id to create the auth token, so we either find an existing user
+// or create a new one if none exists for this provider.
+async function getUserIdFromProviderDetails(
+ providerId: ProviderId,
+ providerProfile: any,
+ getUserFieldsFn?: GetUserFieldsFn,
+): Promise<{= userEntityUpper =}['id']> {
+ const existingAuthIdentity = await prisma.{= authIdentityEntityLower =}.findUnique({
+ where: {
+ providerName_providerUserId: providerId,
+ },
+ include: {
+ {= authFieldOnAuthIdentityEntityName =}: {
+ include: {
+ {= userFieldOnAuthEntityName =}: true
+ }
+ }
+ }
})
- if (externalAuthAssociation) {
- return externalAuthAssociation.user
- }
+ if (existingAuthIdentity) {
+ return existingAuthIdentity.{= authFieldOnAuthIdentityEntityName =}.{= userFieldOnAuthEntityName =}.id
+ } else {
+ const userFields = getUserFieldsFn
+ ? await getUserFieldsFn(contextWithUserEntity, { profile: providerProfile })
+ : {};
- // No external auth association linkage found. Create a new User using details from
- // `getUserFields()`. Additionally, associate the externalAuthAssociations with the new User.
- const userFields = await getUserFields()
- const userAndExternalAuthAssociation = {
- ...userFields,
- {=# isPasswordOnUserEntity =}
- // TODO: Decouple social from usernameAndPassword auth.
- password: uuidv4(),
- {=/ isPasswordOnUserEntity =}
- externalAuthAssociations: {
- create: [{ provider, providerId }]
- }
- }
+ // For now, we don't have any extra data for the oauth providers, so we just pass an empty object.
+ const providerData = await sanitizeAndSerializeProviderData({})
+
+ const user = await createUser(
+ providerId,
+ providerData,
+ userFields,
+ )
- return createUser(userAndExternalAuthAssociation)
+ return user.id
+ }
}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/defaults.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/defaults.ts
deleted file mode 100644
index a6dede224d..0000000000
--- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/defaults.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-{{={= =}=}}
-import { generateAvailableDictionaryUsername } from '../../../core/auth.js'
-
-export async function getUserFieldsFn(_context, _args) {
- {=# isUsernameOnUserEntity =}
- const username = await generateAvailableDictionaryUsername()
- return { username }
- {=/ isUsernameOnUserEntity =}
- {=^ isUsernameOnUserEntity =}
- return {}
- {=/ isUsernameOnUserEntity =}
-}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts
index 130836ebb8..ac5a56dafe 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/oauth/init.ts
@@ -71,6 +71,6 @@ function ensureValidConfig(provider: ProviderConfig, config: OAuthConfig): void
export type OAuthImports = {
npmPackage: string;
userDefinedConfigFn?: UserDefinedConfigFn;
- getUserFieldsFn: GetUserFieldsFn;
oAuthConfig: OAuthConfig;
+ getUserFieldsFn?: GetUserFieldsFn;
};
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts b/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts
index 266c289247..390cd45923 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/oauth/types.ts
@@ -9,11 +9,11 @@ export type OAuthConfig = {
scope?: string[];
}
-export type CreateOAuthUser = Omit
+export type UserFieldsFromOAuthSignup = Prisma.{= userEntityName =}CreateInput
export type UserDefinedConfigFn = () => { [key: string]: any }
export type GetUserFieldsFn = (
context: typeof contextWithUserEntity,
args: { profile: { [key: string]: any } },
-) => Promise
+) => Promise
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/types.ts b/waspc/data/Generator/templates/server/src/auth/providers/types.ts
index 0889298c0a..e2ff6e09a7 100644
--- a/waspc/data/Generator/templates/server/src/auth/providers/types.ts
+++ b/waspc/data/Generator/templates/server/src/auth/providers/types.ts
@@ -1,10 +1,14 @@
+{{={= =}=}}
import type { Router, Request } from 'express'
-import type { User } from '../../entities'
+import type { Prisma } from '@prisma/client'
import type { Expand } from '../../universal/types'
+import type { ProviderName } from '../utils'
+
+type UserEntityCreateInput = Prisma.{= userEntityUpper =}CreateInput
export type ProviderConfig = {
// Unique provider identifier, used as part of URL paths
- id: string;
+ id: ProviderName;
displayName: string;
// Each provider config can have an init method which is ran on setup time
// e.g. for oAuth providers this is the time when the Passport strategy is registered.
@@ -20,20 +24,14 @@ export type InitData = {
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
-export function createDefineAdditionalSignupFieldsFn<
- // Wasp already includes these fields in the signup process
- ExistingFields extends keyof User,
- PossibleAdditionalFields = Expand<
- Partial>
+export type PossibleAdditionalSignupFields = Expand>
+
+export function defineAdditionalSignupFields(config: {
+ [key in keyof PossibleAdditionalSignupFields]: FieldGetter<
+ PossibleAdditionalSignupFields[key]
>
->() {
- return function defineFields(config: {
- [key in keyof PossibleAdditionalFields]: FieldGetter<
- PossibleAdditionalFields[key]
- >
- }) {
- return config
- }
+}) {
+ return config
}
type FieldGetter = (
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts b/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts
new file mode 100644
index 0000000000..9bb5841bf5
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/auth/providers/username/login.ts
@@ -0,0 +1,43 @@
+{{={= =}=}}
+import { verifyPassword, throwInvalidCredentialsError } from '../../../core/auth.js'
+import { handleRejection } from '../../../utils.js'
+
+import {
+ createProviderId,
+ findAuthIdentity,
+ findAuthWithUserBy,
+ createAuthToken,
+ deserializeAndSanitizeProviderData,
+} from '../../utils.js'
+import { ensureValidUsername, ensurePasswordIsPresent } from '../../validation.js'
+
+export default handleRejection(async (req, res) => {
+ const fields = req.body ?? {}
+ ensureValidArgs(fields)
+
+ const providerId = createProviderId('username', fields.username)
+ const authIdentity = await findAuthIdentity(providerId)
+ if (!authIdentity) {
+ throwInvalidCredentialsError()
+ }
+
+ try {
+ const providerData = deserializeAndSanitizeProviderData<'username'>(authIdentity.providerData)
+
+ await verifyPassword(providerData.hashedPassword, fields.password)
+ } catch(e) {
+ throwInvalidCredentialsError()
+ }
+
+ const auth = await findAuthWithUserBy({
+ id: authIdentity.authId
+ })
+ const token = await createAuthToken(auth.userId)
+
+ return res.json({ token })
+})
+
+function ensureValidArgs(args: unknown): void {
+ ensureValidUsername(args);
+ ensurePasswordIsPresent(args);
+}
diff --git a/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts b/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts
new file mode 100644
index 0000000000..478749b9b2
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/auth/providers/username/signup.ts
@@ -0,0 +1,46 @@
+{{={= =}=}}
+import { handleRejection } from '../../../utils.js'
+import {
+ createProviderId,
+ createUser,
+ rethrowPossibleAuthError,
+ sanitizeAndSerializeProviderData,
+} from '../../utils.js'
+import {
+ ensureValidUsername,
+ ensurePasswordIsPresent,
+ ensureValidPassword,
+} from '../../validation.js'
+import { validateAndGetAdditionalFields } from '../../utils.js'
+
+export default handleRejection(async (req, res) => {
+ const fields = req.body ?? {}
+ ensureValidArgs(fields)
+
+ const userFields = await validateAndGetAdditionalFields(fields)
+
+ const providerId = createProviderId('username', fields.username)
+ const providerData = await sanitizeAndSerializeProviderData<'username'>({
+ hashedPassword: fields.password,
+ })
+
+ try {
+ await createUser(
+ providerId,
+ providerData,
+ // Using any here because we want to avoid TypeScript errors and
+ // rely on Prisma to validate the data.
+ userFields as any
+ )
+ } catch (e: unknown) {
+ rethrowPossibleAuthError(e)
+ }
+
+ return res.json({ success: true })
+})
+
+function ensureValidArgs(args: unknown): void {
+ ensureValidUsername(args)
+ ensurePasswordIsPresent(args)
+ ensureValidPassword(args)
+}
diff --git a/waspc/data/Generator/templates/server/src/auth/user.ts b/waspc/data/Generator/templates/server/src/auth/user.ts
new file mode 100644
index 0000000000..a5d987fc4e
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/auth/user.ts
@@ -0,0 +1,27 @@
+// We decided not to deduplicate these helper functions in the server and the client.
+// We have them duplicated in this file and in data/Generator/templates/react-app/src/auth/user.ts
+// If you are changing the logic here, make sure to change it there as well.
+
+import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../_types/index'
+
+export function getEmail(user: User): string | null {
+ return findUserIdentity(user, "email")?.providerUserId ?? null;
+}
+
+export function getUsername(user: User): string | null {
+ return findUserIdentity(user, "username")?.providerUserId ?? null;
+}
+
+export function getFirstProviderUserId(user?: User): string | null {
+ if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
+ return null;
+ }
+
+ return user.auth.identities[0].providerUserId ?? null;
+}
+
+export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined {
+ return user.auth.identities.find(
+ (identity) => identity.providerName === providerName
+ );
+}
diff --git a/waspc/data/Generator/templates/server/src/auth/utils.ts b/waspc/data/Generator/templates/server/src/auth/utils.ts
index 5495937b56..0ff4c7d29e 100644
--- a/waspc/data/Generator/templates/server/src/auth/utils.ts
+++ b/waspc/data/Generator/templates/server/src/auth/utils.ts
@@ -1,11 +1,15 @@
{{={= =}=}}
-import { sign, verify } from '../core/auth.js'
+import { hashPassword, sign, verify } from '../core/auth.js'
import AuthError from '../core/AuthError.js'
import HttpError from '../core/HttpError.js'
import prisma from '../dbClient.js'
-import { isPrismaError, prismaErrorToHttpError, sleep } from '../utils.js'
-import { type {= userEntityUpper =} } from '../entities/index.js'
-import { type Prisma } from '@prisma/client';
+import { sleep } from '../utils.js'
+import {
+ type {= userEntityUpper =},
+ type {= authEntityUpper =},
+ type {= authIdentityEntityUpper =},
+} from '../entities/index.js'
+import { Prisma } from '@prisma/client';
import { throwValidationError } from './validation.js'
@@ -13,16 +17,43 @@ import { throwValidationError } from './validation.js'
{=& additionalSignupFields.importStatement =}
{=/ additionalSignupFields.isDefined =}
+import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js'
{=# additionalSignupFields.isDefined =}
const _waspAdditionalSignupFieldsConfig = {= additionalSignupFields.importIdentifier =}
{=/ additionalSignupFields.isDefined =}
{=^ additionalSignupFields.isDefined =}
-import { createDefineAdditionalSignupFieldsFn } from './providers/types.js'
-const _waspAdditionalSignupFieldsConfig = {} as ReturnType<
- ReturnType>
->
+const _waspAdditionalSignupFieldsConfig = {} as ReturnType
{=/ additionalSignupFields.isDefined =}
+export type EmailProviderData = {
+ hashedPassword: string;
+ isEmailVerified: boolean;
+ emailVerificationSentAt: string | null;
+ passwordResetSentAt: string | null;
+}
+
+export type UsernameProviderData = {
+ hashedPassword: string;
+}
+
+export type OAuthProviderData = {}
+
+/**
+ * This type is used for type-level programming e.g. to enumerate
+ * all possible provider data types.
+ *
+ * The keys of this type are the names of the providers and the values
+ * are the types of the provider data.
+ */
+export type PossibleProviderData = {
+ email: EmailProviderData;
+ username: UsernameProviderData;
+ google: OAuthProviderData;
+ github: OAuthProviderData;
+}
+
+export type ProviderName = keyof PossibleProviderData
+
export const contextWithUserEntity = {
entities: {
{= userEntityUpper =}: prisma.{= userEntityLower =}
@@ -34,31 +65,112 @@ export const authConfig = {
successRedirectPath: "{= successRedirectPath =}",
}
-export async function findUserBy(where: Prisma.{= userEntityUpper =}WhereUniqueInput): Promise<{= userEntityUpper =}> {
- return prisma.{= userEntityLower =}.findUnique({ where });
+/**
+ * ProviderId uniquely identifies an auth identity e.g.
+ * "email" provider with user id "test@test.com" or
+ * "google" provider with user id "1234567890".
+ *
+ * We use this type to avoid passing the providerName and providerUserId
+ * separately. Also, we can normalize the providerUserId to make sure it's
+ * consistent across different DB operations.
+ */
+export type ProviderId = {
+ providerName: ProviderName;
+ providerUserId: string;
}
-export async function createUser(data: Prisma.{= userEntityUpper =}CreateInput): Promise<{= userEntityUpper =}> {
- try {
- return await prisma.{= userEntityLower =}.create({ data })
- } catch (e) {
- rethrowPossiblePrismaError(e);
+export function createProviderId(providerName: ProviderName, providerUserId: string): ProviderId {
+ return {
+ providerName,
+ providerUserId: providerUserId.toLowerCase(),
}
}
-export async function deleteUser(user: {= userEntityUpper =}): Promise<{= userEntityUpper =}> {
- try {
- return await prisma.{= userEntityLower =}.delete({ where: { id: user.id } })
- } catch (e) {
- rethrowPossiblePrismaError(e);
+export async function findAuthIdentity(providerId: ProviderId): Promise<{= authIdentityEntityUpper =} | null> {
+ return prisma.{= authIdentityEntityLower =}.findUnique({
+ where: {
+ providerName_providerUserId: providerId,
+ }
+ });
+}
+
+/**
+ * Updates the provider data for the given auth identity.
+ *
+ * This function performs data sanitization and serialization.
+ * Sanitization is done by hashing the password, so this function
+ * expects the password received in the `providerDataUpdates`
+ * **not to be hashed**.
+ */
+export async function updateAuthIdentityProviderData(
+ providerId: ProviderId,
+ existingProviderData: PossibleProviderData[PN],
+ providerDataUpdates: Partial,
+): Promise<{= authIdentityEntityUpper =}> {
+ // We are doing the sanitization here only on updates to avoid
+ // hashing the password multiple times.
+ const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates);
+ const newProviderData = {
+ ...existingProviderData,
+ ...sanitizedProviderDataUpdates,
}
+ const serializedProviderData = await serializeProviderData(newProviderData);
+ return prisma.{= authIdentityEntityLower =}.update({
+ where: {
+ providerName_providerUserId: providerId,
+ },
+ data: { providerData: serializedProviderData },
+ });
}
-export async function createAuthToken(user: {= userEntityUpper =}): Promise {
- return sign(user.id);
+type FindAuthWithUserResult = {= authEntityUpper =} & {
+ {= userFieldOnAuthEntityName =}: {= userEntityUpper =}
}
-export async function verifyToken(token: string): Promise<{ id: any }> {
+export async function findAuthWithUserBy(
+ where: Prisma.{= authEntityUpper =}WhereInput
+): Promise {
+ return prisma.{= authEntityLower =}.findFirst({ where, include: { {= userFieldOnAuthEntityName =}: true }});
+}
+
+export async function createUser(
+ providerId: ProviderId,
+ serializedProviderData?: string,
+ userFields?: PossibleAdditionalSignupFields,
+): Promise<{= userEntityUpper =}> {
+ return prisma.{= userEntityLower =}.create({
+ data: {
+ // Using any here to prevent type errors when userFields are not
+ // defined. We want Prisma to throw an error in that case.
+ ...(userFields ?? {} as any),
+ {= authFieldOnUserEntityName =}: {
+ create: {
+ {= identitiesFieldOnAuthEntityName =}: {
+ create: {
+ providerName: providerId.providerName,
+ providerUserId: providerId.providerUserId,
+ providerData: serializedProviderData,
+ },
+ },
+ }
+ },
+ }
+ })
+}
+
+export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> {
+ return prisma.{= userEntityLower =}.deleteMany({ where: { auth: {
+ id: authId,
+ } } })
+}
+
+export async function createAuthToken(
+ userId: {= userEntityUpper =}['id']
+): Promise {
+ return sign(userId);
+}
+
+export async function verifyToken(token: string): Promise {
return verify(token);
}
@@ -69,24 +181,61 @@ export async function verifyToken(token: string): Promise<{ id: any }> {
// NOTE: Attacker measuring time to response can still determine
// if a user exists or not. We'll be able to avoid it when
// we implement e-mail sending via jobs.
-export async function doFakeWork() {
+export async function doFakeWork(): Promise {
const timeToWork = Math.floor(Math.random() * 1000) + 1000;
return sleep(timeToWork);
}
-export function rethrowPossiblePrismaError(e: unknown): void {
+export function rethrowPossibleAuthError(e: unknown): void {
if (e instanceof AuthError) {
throwValidationError(e.message);
- } else if (isPrismaError(e)) {
- throw prismaErrorToHttpError(e)
- } else {
- throw new HttpError(500)
}
+
+ // Prisma code P2002 is for unique constraint violations.
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
+ throw new HttpError(422, 'Save failed', {
+ message: `user with the same identity already exists`,
+ })
+ }
+
+ if (e instanceof Prisma.PrismaClientValidationError) {
+ // NOTE: Logging the error since this usually means that there are
+ // required fields missing in the request, we want the developer
+ // to know about it.
+ console.error(e)
+ throw new HttpError(422, 'Save failed', {
+ message: 'there was a database error'
+ })
+ }
+
+ // Prisma code P2021 is for missing table errors.
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2021') {
+ // NOTE: Logging the error since this usually means that the database
+ // migrations weren't run, we want the developer to know about it.
+ console.error(e)
+ console.info('🐝 This error can happen if you did\'t run the database migrations.')
+ throw new HttpError(500, 'Save failed', {
+ message: `there was a database error`,
+ })
+ }
+
+ // Prisma code P2003 is for foreign key constraint failure
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
+ console.error(e)
+ console.info(`🐝 This error can happen if you have some relation on your {= userEntityUpper =} entity
+ but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull".
+ Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`)
+ throw new HttpError(500, 'Save failed', {
+ message: `there was a database error`,
+ })
+ }
+
+ throw e
}
export async function validateAndGetAdditionalFields(data: {
[key: string]: unknown
-}) {
+}): Promise> {
const {
password: _password,
...sanitizedData
@@ -102,3 +251,49 @@ export async function validateAndGetAdditionalFields(data: {
}
return result;
}
+
+export function deserializeAndSanitizeProviderData(
+ providerData: string,
+ { shouldRemovePasswordField = false }: { shouldRemovePasswordField?: boolean } = {},
+): PossibleProviderData[PN] {
+ // NOTE: We are letting JSON.parse throw an error if the providerData is not valid JSON.
+ let data = JSON.parse(providerData) as PossibleProviderData[PN];
+
+ if (providerDataHasPasswordField(data) && shouldRemovePasswordField) {
+ delete data.hashedPassword;
+ }
+
+ return data;
+}
+
+export async function sanitizeAndSerializeProviderData(
+ providerData: PossibleProviderData[PN],
+): Promise {
+ return serializeProviderData(
+ await sanitizeProviderData(providerData)
+ );
+}
+
+function serializeProviderData(providerData: PossibleProviderData[PN]): string {
+ return JSON.stringify(providerData);
+}
+
+async function sanitizeProviderData(
+ providerData: PossibleProviderData[PN],
+): Promise {
+ const data = {
+ ...providerData,
+ };
+ if (providerDataHasPasswordField(data)) {
+ data.hashedPassword = await hashPassword(data.hashedPassword);
+ }
+
+ return data;
+}
+
+
+function providerDataHasPasswordField(
+ providerData: PossibleProviderData[keyof PossibleProviderData],
+): providerData is { hashedPassword: string } {
+ return 'hashedPassword' in providerData;
+}
diff --git a/waspc/data/Generator/templates/server/src/core/auth.js b/waspc/data/Generator/templates/server/src/core/auth.js
index 06f9b20dc8..33105b1cbb 100644
--- a/waspc/data/Generator/templates/server/src/core/auth.js
+++ b/waspc/data/Generator/templates/server/src/core/auth.js
@@ -8,13 +8,15 @@ import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
import HttpError from '../core/HttpError.js'
import config from '../config.js'
+import { deserializeAndSanitizeProviderData } from '../auth/utils.js'
const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)
const JWT_SECRET = config.auth.jwtSecret
-export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
+export const signData = (data, options) => jwtSign(data, JWT_SECRET, options)
+export const sign = (id, options) => signData({ id }, options)
export const verify = (token) => jwtVerify(token, JWT_SECRET)
const auth = handleRejection(async (req, res, next) => {
@@ -48,7 +50,17 @@ export async function getUserFromToken(token) {
}
}
- const user = await prisma.{= userEntityLower =}.findUnique({ where: { id: userIdFromToken } })
+ const user = await prisma.{= userEntityLower =}
+ .findUnique({
+ where: { id: userIdFromToken },
+ include: {
+ {= authFieldOnUserEntityName =}: {
+ include: {
+ {= identitiesFieldOnAuthEntityName =}: true
+ }
+ }
+ }
+ })
if (!user) {
throwInvalidCredentialsError()
}
@@ -57,9 +69,12 @@ export async function getUserFromToken(token) {
// password field from the object here, we must to do the same there).
// Ideally, these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
- const { password, ...userView } = user
-
- return userView
+ let sanitizedUser = { ...user }
+ sanitizedUser.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =} = sanitizedUser.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =}.map(identity => {
+ identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true })
+ return identity
+ });
+ return sanitizedUser
}
const SP = new SecurePassword()
diff --git a/waspc/data/Generator/templates/server/src/core/auth/prismaMiddleware.js b/waspc/data/Generator/templates/server/src/core/auth/prismaMiddleware.js
deleted file mode 100644
index 6670f2363c..0000000000
--- a/waspc/data/Generator/templates/server/src/core/auth/prismaMiddleware.js
+++ /dev/null
@@ -1,31 +0,0 @@
-{{={= =}=}}
-import { hashPassword } from '../auth.js'
-import { PASSWORD_FIELD } from '../../auth/validation.js'
-
-// Make sure password is always hashed before storing to the database.
-const registerPasswordHashing = (prismaClient) => {
- prismaClient.$use(async (params, next) => {
- if (params.model === '{= userEntityUpper =}') {
- if (['create', 'update', 'updateMany'].includes(params.action)) {
- if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) {
- params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD])
- }
- } else if (params.action === 'upsert') {
- if (params.args.create.hasOwnProperty(PASSWORD_FIELD)) {
- params.args.create[PASSWORD_FIELD] =
- await hashPassword(params.args.create[PASSWORD_FIELD])
- }
- if (params.args.update.hasOwnProperty(PASSWORD_FIELD)) {
- params.args.update[PASSWORD_FIELD] =
- await hashPassword(params.args.update[PASSWORD_FIELD])
- }
- }
- }
-
- return next(params)
- })
-}
-
-export const registerAuthMiddleware = (prismaClient) => {
- registerPasswordHashing(prismaClient)
-}
diff --git a/waspc/data/Generator/templates/server/src/crud/_operations.ts b/waspc/data/Generator/templates/server/src/crud/_operations.ts
index e69d53521b..7ffb5a1fd2 100644
--- a/waspc/data/Generator/templates/server/src/crud/_operations.ts
+++ b/waspc/data/Generator/templates/server/src/crud/_operations.ts
@@ -12,9 +12,7 @@ import type {
{=/ isAuthEnabled =}
_{= crud.entityUpper =},
} from "../_types";
-import type {
- Prisma,
-} from "@prisma/client";
+import type { Prisma } from "@prisma/client";
import { Payload } from "../_types/serialization.js";
import type {
{= crud.entityUpper =},
diff --git a/waspc/data/Generator/templates/server/src/dbClient.ts b/waspc/data/Generator/templates/server/src/dbClient.ts
index fead2ab166..e6319ff696 100644
--- a/waspc/data/Generator/templates/server/src/dbClient.ts
+++ b/waspc/data/Generator/templates/server/src/dbClient.ts
@@ -1,17 +1,10 @@
{{={= =}=}}
import Prisma from '@prisma/client'
-{=# isAuthEnabled =}
-import { registerAuthMiddleware } from './core/auth/prismaMiddleware.js'
-{=/ isAuthEnabled =}
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
- {=# isAuthEnabled =}
- registerAuthMiddleware(prismaClient)
- {=/ isAuthEnabled =}
-
return prismaClient
}
diff --git a/waspc/data/Generator/templates/server/src/entities/index.ts b/waspc/data/Generator/templates/server/src/entities/index.ts
index 576f05f289..591309c09d 100644
--- a/waspc/data/Generator/templates/server/src/entities/index.ts
+++ b/waspc/data/Generator/templates/server/src/entities/index.ts
@@ -9,6 +9,10 @@ export {
{=# entities =}
type {= name =},
{=/ entities =}
+ {=# isAuthEnabled =}
+ type {= authEntityName =},
+ type {= authIdentityEntityName =},
+ {=/ isAuthEnabled =}
} from "@prisma/client"
export type Entity =
diff --git a/waspc/data/Generator/templates/server/src/utils.js b/waspc/data/Generator/templates/server/src/utils.js
deleted file mode 100644
index d6c7ed639a..0000000000
--- a/waspc/data/Generator/templates/server/src/utils.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Prisma from '@prisma/client'
-import HttpError from './core/HttpError.js'
-
-import { readdir } from "fs";
-import { join, dirname } from "path";
-import { fileURLToPath } from 'url';
-
-/**
- * Decorator for async express middleware that handles promise rejections.
- * @param {Func} middleware - Express middleware function.
- * @returns {Func} Express middleware that is exactly the same as the given middleware but,
- * if given middleware returns promise, reject of that promise will be correctly handled,
- * meaning that error will be forwarded to next().
- */
-export const handleRejection = (middleware) => async (req, res, next) => {
- try {
- await middleware(req, res, next)
- } catch (error) {
- next(error)
- }
-}
-
-export const isPrismaError = (e) => {
- return e instanceof Prisma.PrismaClientKnownRequestError ||
- e instanceof Prisma.PrismaClientUnknownRequestError ||
- e instanceof Prisma.PrismaClientRustPanicError ||
- e instanceof Prisma.PrismaClientInitializationError ||
- e instanceof Prisma.PrismaClientValidationError
-}
-
-export const prismaErrorToHttpError = (e) => {
- if (e instanceof Prisma.PrismaClientKnownRequestError) {
- if (e.code === 'P2002') {
- return new HttpError(422, 'Save failed', {
- message: `user with the same ${e.meta.target.join(', ')} already exists`,
- target: e.meta.target
- })
- } else {
- // TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
- // and decide which are input errors (422) and which are not (500)
- // See: https://github.com/wasp-lang/wasp/issues/384
- return new HttpError(500)
- }
- } else if (e instanceof Prisma.PrismaClientValidationError) {
- return new HttpError(422, 'Save failed')
- } else {
- return new HttpError(500)
- }
-}
-
-export const sleep = ms => new Promise(r => setTimeout(r, ms))
-
-export function getDirFromFileUrl(fileUrl) {
- return fileURLToPath(dirname(fileUrl));
-}
-
-export async function importJsFilesFromDir(absoluteDir, relativePath, whitelist = null) {
- const pathToDir = join(absoluteDir, relativePath);
-
- return new Promise((resolve, reject) => {
- readdir(pathToDir, async (err, files) => {
- if (err) {
- return reject(err);
- }
- const importPromises = files
- .filter((file) => file.endsWith(".js") && isWhitelisted(file))
- .map((file) => import(`${pathToDir}/${file}`));
- resolve(Promise.all(importPromises));
- });
- });
-
- function isWhitelisted(file) {
- // No whitelist means all files are whitelisted
- if (!Array.isArray(whitelist)) {
- return true;
- }
- return whitelist.some((whitelistedFile) => file.endsWith(whitelistedFile));
- }
-}
diff --git a/waspc/data/Generator/templates/server/src/utils.ts b/waspc/data/Generator/templates/server/src/utils.ts
new file mode 100644
index 0000000000..3a38f87ede
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/utils.ts
@@ -0,0 +1,71 @@
+{{={= =}=}}
+import crypto from 'crypto'
+import { Request, Response, NextFunction } from 'express'
+
+import { readdir } from 'fs'
+import { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+{=# isAuthEnabled =}
+import { type SanitizedUser } from './_types/index.js'
+{=/ isAuthEnabled =}
+
+type RequestWithExtraFields = Request & {
+ {=# isAuthEnabled =}
+ user?: SanitizedUser
+ {=/ isAuthEnabled =}
+}
+
+/**
+ * Decorator for async express middleware that handles promise rejections.
+ * @param {Func} middleware - Express middleware function.
+ * @returns Express middleware that is exactly the same as the given middleware but,
+ * if given middleware returns promise, reject of that promise will be correctly handled,
+ * meaning that error will be forwarded to next().
+ */
+export const handleRejection = (
+ middleware: (
+ req: RequestWithExtraFields,
+ res: Response,
+ next: NextFunction
+ ) => any
+) =>
+async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
+ try {
+ await middleware(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms))
+
+export function getDirPathFromFileUrl(fileUrl: string): string {
+ return fileURLToPath(dirname(fileUrl))
+}
+
+export async function importJsFilesFromDir(
+ pathToDir: string,
+ whitelistedFileNames: string[] | null = null
+): Promise {
+ return new Promise((resolve, reject) => {
+ readdir(pathToDir, async (err, files) => {
+ if (err) {
+ return reject(err)
+ }
+ const importPromises = files
+ .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
+ .map((file) => import(`${pathToDir}/${file}`))
+ resolve(Promise.all(importPromises))
+ })
+ })
+
+ function isWhitelistedFileName(fileName: string) {
+ // No whitelist means all files are whitelisted
+ if (!Array.isArray(whitelistedFileNames)) {
+ return true
+ }
+
+ return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
+ }
+}
diff --git a/waspc/data/Generator/templates/server/src/webSocket/index.ts b/waspc/data/Generator/templates/server/src/webSocket/index.ts
index 1ccff7ffba..3393b500bc 100644
--- a/waspc/data/Generator/templates/server/src/webSocket/index.ts
+++ b/waspc/data/Generator/templates/server/src/webSocket/index.ts
@@ -4,6 +4,9 @@ import { Server } from 'socket.io'
import { EventsMap, DefaultEventsMap } from '@socket.io/component-emitter'
import prisma from '../dbClient.js'
+{=# isAuthEnabled =}
+import { type SanitizedUser } from '../_types/index.js'
+{=/ isAuthEnabled =}
{=& userWebSocketFn.importStatement =}
@@ -29,7 +32,9 @@ export type WebSocketDefinition<
) => Promise | void
export interface WaspSocketData {
- user?: any
+ {=# isAuthEnabled =}
+ user?: SanitizedUser
+ {=/ isAuthEnabled =}
}
export type ServerType = Parameters[0]
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest b/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest
index 96fee87b1c..412fac0d7d 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest
@@ -30,7 +30,7 @@ waspBuild/.wasp/build/server/src/server.ts
waspBuild/.wasp/build/server/src/types/index.ts
waspBuild/.wasp/build/server/src/universal/types.ts
waspBuild/.wasp/build/server/src/universal/url.ts
-waspBuild/.wasp/build/server/src/utils.js
+waspBuild/.wasp/build/server/src/utils.ts
waspBuild/.wasp/build/server/tsconfig.json
waspBuild/.wasp/build/web-app/.npmrc
waspBuild/.wasp/build/web-app/README.md
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums
index 83674f3df6..8ac76226b6 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums
@@ -130,7 +130,7 @@
"file",
"server/src/dbClient.ts"
],
- "20c67ca197da3de2d37528ceaff2e40af910be8177f346c6d5c2b2f983810c43"
+ "f26bfa4a60022d45195f3179548c4b09305f6a9cfe301b5edd1615677a59ae3f"
],
[
[
@@ -219,9 +219,9 @@
[
[
"file",
- "server/src/utils.js"
+ "server/src/utils.ts"
],
- "300e9bb586b163f2608acb27346b5c94ec3e58cdc25dace5381f3d0c6710a7ec"
+ "6a664e44a89efe8ed8cb7559a84bd2871422c38287b276fee2a7aed4680e1d1e"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/dbClient.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/dbClient.ts
index 11b87caa50..66e7801be3 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/dbClient.ts
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/dbClient.ts
@@ -4,7 +4,6 @@ import Prisma from '@prisma/client'
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
-
return prismaClient
}
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/utils.js b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/utils.js
deleted file mode 100644
index d6c7ed639a..0000000000
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/utils.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Prisma from '@prisma/client'
-import HttpError from './core/HttpError.js'
-
-import { readdir } from "fs";
-import { join, dirname } from "path";
-import { fileURLToPath } from 'url';
-
-/**
- * Decorator for async express middleware that handles promise rejections.
- * @param {Func} middleware - Express middleware function.
- * @returns {Func} Express middleware that is exactly the same as the given middleware but,
- * if given middleware returns promise, reject of that promise will be correctly handled,
- * meaning that error will be forwarded to next().
- */
-export const handleRejection = (middleware) => async (req, res, next) => {
- try {
- await middleware(req, res, next)
- } catch (error) {
- next(error)
- }
-}
-
-export const isPrismaError = (e) => {
- return e instanceof Prisma.PrismaClientKnownRequestError ||
- e instanceof Prisma.PrismaClientUnknownRequestError ||
- e instanceof Prisma.PrismaClientRustPanicError ||
- e instanceof Prisma.PrismaClientInitializationError ||
- e instanceof Prisma.PrismaClientValidationError
-}
-
-export const prismaErrorToHttpError = (e) => {
- if (e instanceof Prisma.PrismaClientKnownRequestError) {
- if (e.code === 'P2002') {
- return new HttpError(422, 'Save failed', {
- message: `user with the same ${e.meta.target.join(', ')} already exists`,
- target: e.meta.target
- })
- } else {
- // TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
- // and decide which are input errors (422) and which are not (500)
- // See: https://github.com/wasp-lang/wasp/issues/384
- return new HttpError(500)
- }
- } else if (e instanceof Prisma.PrismaClientValidationError) {
- return new HttpError(422, 'Save failed')
- } else {
- return new HttpError(500)
- }
-}
-
-export const sleep = ms => new Promise(r => setTimeout(r, ms))
-
-export function getDirFromFileUrl(fileUrl) {
- return fileURLToPath(dirname(fileUrl));
-}
-
-export async function importJsFilesFromDir(absoluteDir, relativePath, whitelist = null) {
- const pathToDir = join(absoluteDir, relativePath);
-
- return new Promise((resolve, reject) => {
- readdir(pathToDir, async (err, files) => {
- if (err) {
- return reject(err);
- }
- const importPromises = files
- .filter((file) => file.endsWith(".js") && isWhitelisted(file))
- .map((file) => import(`${pathToDir}/${file}`));
- resolve(Promise.all(importPromises));
- });
- });
-
- function isWhitelisted(file) {
- // No whitelist means all files are whitelisted
- if (!Array.isArray(whitelist)) {
- return true;
- }
- return whitelist.some((whitelistedFile) => file.endsWith(whitelistedFile));
- }
-}
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/utils.ts b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/utils.ts
new file mode 100644
index 0000000000..99492c5474
--- /dev/null
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/utils.ts
@@ -0,0 +1,64 @@
+import crypto from 'crypto'
+import { Request, Response, NextFunction } from 'express'
+
+import { readdir } from 'fs'
+import { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+
+type RequestWithExtraFields = Request & {
+}
+
+/**
+ * Decorator for async express middleware that handles promise rejections.
+ * @param {Func} middleware - Express middleware function.
+ * @returns Express middleware that is exactly the same as the given middleware but,
+ * if given middleware returns promise, reject of that promise will be correctly handled,
+ * meaning that error will be forwarded to next().
+ */
+export const handleRejection = (
+ middleware: (
+ req: RequestWithExtraFields,
+ res: Response,
+ next: NextFunction
+ ) => any
+) =>
+async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
+ try {
+ await middleware(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms))
+
+export function getDirPathFromFileUrl(fileUrl: string): string {
+ return fileURLToPath(dirname(fileUrl))
+}
+
+export async function importJsFilesFromDir(
+ pathToDir: string,
+ whitelistedFileNames: string[] | null = null
+): Promise {
+ return new Promise((resolve, reject) => {
+ readdir(pathToDir, async (err, files) => {
+ if (err) {
+ return reject(err)
+ }
+ const importPromises = files
+ .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
+ .map((file) => import(`${pathToDir}/${file}`))
+ resolve(Promise.all(importPromises))
+ })
+ })
+
+ function isWhitelistedFileName(fileName: string) {
+ // No whitelist means all files are whitelisted
+ if (!Array.isArray(whitelistedFileNames)) {
+ return true
+ }
+
+ return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
+ }
+}
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest b/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest
index d84378957c..0658a077b2 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/files.manifest
@@ -31,7 +31,7 @@ waspCompile/.wasp/out/server/src/server.ts
waspCompile/.wasp/out/server/src/types/index.ts
waspCompile/.wasp/out/server/src/universal/types.ts
waspCompile/.wasp/out/server/src/universal/url.ts
-waspCompile/.wasp/out/server/src/utils.js
+waspCompile/.wasp/out/server/src/utils.ts
waspCompile/.wasp/out/server/tsconfig.json
waspCompile/.wasp/out/web-app/.env
waspCompile/.wasp/out/web-app/.npmrc
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums
index bc63689d32..bb318f5532 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums
@@ -137,7 +137,7 @@
"file",
"server/src/dbClient.ts"
],
- "20c67ca197da3de2d37528ceaff2e40af910be8177f346c6d5c2b2f983810c43"
+ "f26bfa4a60022d45195f3179548c4b09305f6a9cfe301b5edd1615677a59ae3f"
],
[
[
@@ -226,9 +226,9 @@
[
[
"file",
- "server/src/utils.js"
+ "server/src/utils.ts"
],
- "300e9bb586b163f2608acb27346b5c94ec3e58cdc25dace5381f3d0c6710a7ec"
+ "6a664e44a89efe8ed8cb7559a84bd2871422c38287b276fee2a7aed4680e1d1e"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/dbClient.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/dbClient.ts
index 11b87caa50..66e7801be3 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/dbClient.ts
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/dbClient.ts
@@ -4,7 +4,6 @@ import Prisma from '@prisma/client'
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
-
return prismaClient
}
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/utils.js b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/utils.js
deleted file mode 100644
index d6c7ed639a..0000000000
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/utils.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Prisma from '@prisma/client'
-import HttpError from './core/HttpError.js'
-
-import { readdir } from "fs";
-import { join, dirname } from "path";
-import { fileURLToPath } from 'url';
-
-/**
- * Decorator for async express middleware that handles promise rejections.
- * @param {Func} middleware - Express middleware function.
- * @returns {Func} Express middleware that is exactly the same as the given middleware but,
- * if given middleware returns promise, reject of that promise will be correctly handled,
- * meaning that error will be forwarded to next().
- */
-export const handleRejection = (middleware) => async (req, res, next) => {
- try {
- await middleware(req, res, next)
- } catch (error) {
- next(error)
- }
-}
-
-export const isPrismaError = (e) => {
- return e instanceof Prisma.PrismaClientKnownRequestError ||
- e instanceof Prisma.PrismaClientUnknownRequestError ||
- e instanceof Prisma.PrismaClientRustPanicError ||
- e instanceof Prisma.PrismaClientInitializationError ||
- e instanceof Prisma.PrismaClientValidationError
-}
-
-export const prismaErrorToHttpError = (e) => {
- if (e instanceof Prisma.PrismaClientKnownRequestError) {
- if (e.code === 'P2002') {
- return new HttpError(422, 'Save failed', {
- message: `user with the same ${e.meta.target.join(', ')} already exists`,
- target: e.meta.target
- })
- } else {
- // TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
- // and decide which are input errors (422) and which are not (500)
- // See: https://github.com/wasp-lang/wasp/issues/384
- return new HttpError(500)
- }
- } else if (e instanceof Prisma.PrismaClientValidationError) {
- return new HttpError(422, 'Save failed')
- } else {
- return new HttpError(500)
- }
-}
-
-export const sleep = ms => new Promise(r => setTimeout(r, ms))
-
-export function getDirFromFileUrl(fileUrl) {
- return fileURLToPath(dirname(fileUrl));
-}
-
-export async function importJsFilesFromDir(absoluteDir, relativePath, whitelist = null) {
- const pathToDir = join(absoluteDir, relativePath);
-
- return new Promise((resolve, reject) => {
- readdir(pathToDir, async (err, files) => {
- if (err) {
- return reject(err);
- }
- const importPromises = files
- .filter((file) => file.endsWith(".js") && isWhitelisted(file))
- .map((file) => import(`${pathToDir}/${file}`));
- resolve(Promise.all(importPromises));
- });
- });
-
- function isWhitelisted(file) {
- // No whitelist means all files are whitelisted
- if (!Array.isArray(whitelist)) {
- return true;
- }
- return whitelist.some((whitelistedFile) => file.endsWith(whitelistedFile));
- }
-}
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/utils.ts b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/utils.ts
new file mode 100644
index 0000000000..99492c5474
--- /dev/null
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/utils.ts
@@ -0,0 +1,64 @@
+import crypto from 'crypto'
+import { Request, Response, NextFunction } from 'express'
+
+import { readdir } from 'fs'
+import { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+
+type RequestWithExtraFields = Request & {
+}
+
+/**
+ * Decorator for async express middleware that handles promise rejections.
+ * @param {Func} middleware - Express middleware function.
+ * @returns Express middleware that is exactly the same as the given middleware but,
+ * if given middleware returns promise, reject of that promise will be correctly handled,
+ * meaning that error will be forwarded to next().
+ */
+export const handleRejection = (
+ middleware: (
+ req: RequestWithExtraFields,
+ res: Response,
+ next: NextFunction
+ ) => any
+) =>
+async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
+ try {
+ await middleware(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms))
+
+export function getDirPathFromFileUrl(fileUrl: string): string {
+ return fileURLToPath(dirname(fileUrl))
+}
+
+export async function importJsFilesFromDir(
+ pathToDir: string,
+ whitelistedFileNames: string[] | null = null
+): Promise {
+ return new Promise((resolve, reject) => {
+ readdir(pathToDir, async (err, files) => {
+ if (err) {
+ return reject(err)
+ }
+ const importPromises = files
+ .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
+ .map((file) => import(`${pathToDir}/${file}`))
+ resolve(Promise.all(importPromises))
+ })
+ })
+
+ function isWhitelistedFileName(fileName: string) {
+ // No whitelist means all files are whitelisted
+ if (!Array.isArray(whitelistedFileNames)) {
+ return true
+ }
+
+ return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
+ }
+}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest b/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest
index a0b6cd1ea8..af31ebb1a2 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/files.manifest
@@ -24,17 +24,16 @@ waspComplexTest/.wasp/out/server/src/app.js
waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts
waspComplexTest/.wasp/out/server/src/auth/providers/index.ts
waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts
-waspComplexTest/.wasp/out/server/src/auth/providers/oauth/defaults.ts
waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts
waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts
waspComplexTest/.wasp/out/server/src/auth/providers/types.ts
+waspComplexTest/.wasp/out/server/src/auth/user.ts
waspComplexTest/.wasp/out/server/src/auth/utils.ts
waspComplexTest/.wasp/out/server/src/auth/validation.ts
waspComplexTest/.wasp/out/server/src/config.js
waspComplexTest/.wasp/out/server/src/core/AuthError.js
waspComplexTest/.wasp/out/server/src/core/HttpError.js
waspComplexTest/.wasp/out/server/src/core/auth.js
-waspComplexTest/.wasp/out/server/src/core/auth/prismaMiddleware.js
waspComplexTest/.wasp/out/server/src/crud/tasks.ts
waspComplexTest/.wasp/out/server/src/dbClient.ts
waspComplexTest/.wasp/out/server/src/dbSeed/types.ts
@@ -76,7 +75,7 @@ waspComplexTest/.wasp/out/server/src/server.ts
waspComplexTest/.wasp/out/server/src/types/index.ts
waspComplexTest/.wasp/out/server/src/universal/types.ts
waspComplexTest/.wasp/out/server/src/universal/url.ts
-waspComplexTest/.wasp/out/server/src/utils.js
+waspComplexTest/.wasp/out/server/src/utils.ts
waspComplexTest/.wasp/out/server/tsconfig.json
waspComplexTest/.wasp/out/web-app/.env
waspComplexTest/.wasp/out/web-app/.npmrc
@@ -110,6 +109,7 @@ waspComplexTest/.wasp/out/web-app/src/auth/pages/OAuthCodeExchange.jsx
waspComplexTest/.wasp/out/web-app/src/auth/pages/createAuthRequiredPage.jsx
waspComplexTest/.wasp/out/web-app/src/auth/types.ts
waspComplexTest/.wasp/out/web-app/src/auth/useAuth.ts
+waspComplexTest/.wasp/out/web-app/src/auth/user.ts
waspComplexTest/.wasp/out/web-app/src/config.js
waspComplexTest/.wasp/out/web-app/src/crud/tasks.ts
waspComplexTest/.wasp/out/web-app/src/entities/index.ts
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums
index f9c0e57cc9..2c7fdeee1b 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/.waspchecksums
@@ -18,7 +18,7 @@
"file",
"db/schema.prisma"
],
- "cbea4d60a2c1bef984008597eefc64540e95048d7a43cb718303f5628d386ae1"
+ "3e6bfc3dadfc9399a3fae3ea77cadb1aa3259efa0b8362ed77d6d5ee5013d393"
],
[
[
@@ -88,7 +88,7 @@
"file",
"server/src/_types/index.ts"
],
- "aa5f2c417b5732f732241362fb456b8d068a270d245d692e6530defbb035e778"
+ "92027caebe484c7d412f97be9bc3c39257d10f9f2bc2e447e52c6855d5d30ffc"
],
[
[
@@ -137,56 +137,56 @@
"file",
"server/src/auth/providers/config/google.ts"
],
- "ff060f3cb2437755867b9548dce153bd38b985dc3fc102073bd698d3d7fa23a7"
+ "62e519ae90c87e1032e53d089a8d6106331a278ecd1293767c8e8e9cb4848f6a"
],
[
[
"file",
"server/src/auth/providers/index.ts"
],
- "c56c888520fb6148883d86ede9e2226f3548d2ca929fb6b89145d69b490c775e"
+ "e1c33c91fd91a2de63619320c462817234c097e18d3b04a11c63d4216bcaca85"
],
[
[
"file",
"server/src/auth/providers/oauth/createRouter.ts"
],
- "63dbe409a2de70c55e3f4c01b5faa1da6d09ac0947ff90133c6c616c57ad75c7"
+ "da122a8a244ddbd9b84bba72a7da2f32ffa41c6093614c6c0d59e113244d2bbc"
],
[
[
"file",
- "server/src/auth/providers/oauth/defaults.ts"
+ "server/src/auth/providers/oauth/init.ts"
],
- "2bff7ab070b402aac4ea69a950c0ef6ba7e58c27d4727bdf6046961cb7c0dd48"
+ "cef00c764f6c6923c0138f114eaf0484ad30c4e9dde7f6b44a143061909a8ba1"
],
[
[
"file",
- "server/src/auth/providers/oauth/init.ts"
+ "server/src/auth/providers/oauth/types.ts"
],
- "ff022c6f9132db9c3833afd12018fecfcdd3243cbf9e5d1b69367864301fe085"
+ "00c951bd5dae77b7aedca90c0847f6e861e7f151e89b1906e794469981191b47"
],
[
[
"file",
- "server/src/auth/providers/oauth/types.ts"
+ "server/src/auth/providers/types.ts"
],
- "81c3ff4d945025fc2530b57db7ceecb5c14857e84d98e8c11f045282c25efe37"
+ "b647575a04eeb7824d95082a461d59763d034dc7d03a8fbcdd25143b6f8431b6"
],
[
[
"file",
- "server/src/auth/providers/types.ts"
+ "server/src/auth/user.ts"
],
- "323555d76755fe32b21084f063caf931faabcb5937c279cc706bbecad3361d43"
+ "5787f3cdab4739781090f2950ba432dca812483ec23c6319ac3f876118324d15"
],
[
[
"file",
"server/src/auth/utils.ts"
],
- "ab1f6a90dca62ba0b3b9e1931209c4db1ea4fd0c757fadd27e90b527bcd869e0"
+ "ba76300456ffdbd647923b27ff163df5e3efa016d7cd2a01af0b6a86dcd780a9"
],
[
[
@@ -221,28 +221,21 @@
"file",
"server/src/core/auth.js"
],
- "cb1941ba655c0300bbabda6b51096acdfcd28665ba2f07de676bacb26bd8cddc"
- ],
- [
- [
- "file",
- "server/src/core/auth/prismaMiddleware.js"
- ],
- "a9ccf84f089cf98a022fa800e5b3cc06d3a8b69f5785718dc068286836fd77fa"
+ "d708303af170e8159b93f0dda521b6f622c0f3add2d4f4f8f2fd88c0a4f7b79e"
],
[
[
"file",
"server/src/crud/tasks.ts"
],
- "5c7e55d9eecf8e54822b90662727fcab47c6171c0c49043b25f4e7073ecbe085"
+ "2c4e1f94939adf825df14624940019889394a0e56cdea2855686d67e0c08458a"
],
[
[
"file",
"server/src/dbClient.ts"
],
- "a749686af4e331a0b982e8c2acbfe00aff043340f34d54e80a34d417c6901e56"
+ "f26bfa4a60022d45195f3179548c4b09305f6a9cfe301b5edd1615677a59ae3f"
],
[
[
@@ -298,7 +291,7 @@
"file",
"server/src/entities/index.ts"
],
- "783fbc250d0628073328625ff299f120ac8bef45232c10b7d7b5897d42b788c1"
+ "cdd9cdbeebfdad8c54ddbf4978a3c4974faa66df05b87b42a5030fcc214394f8"
],
[
[
@@ -520,9 +513,9 @@
[
[
"file",
- "server/src/utils.js"
+ "server/src/utils.ts"
],
- "300e9bb586b163f2608acb27346b5c94ec3e58cdc25dace5381f3d0c6710a7ec"
+ "f8834df362946064f32ef6a145769f83d10da712ad3daa226243fc590f89618f"
],
[
[
@@ -753,7 +746,7 @@
"file",
"web-app/src/auth/types.ts"
],
- "0d37136807f6d196015d07b65ab56280ae5f56cac9be84992318886550ce4ad3"
+ "5ce8d0493c362093b0b2fc7b9df78a86688d3f40264ea8f29530f1d8fa67c4c6"
],
[
[
@@ -762,6 +755,13 @@
],
"f730cb58a5ebd12285b7568bec34f4e5615261580cff1727e64b5c871d784d62"
],
+ [
+ [
+ "file",
+ "web-app/src/auth/user.ts"
+ ],
+ "7113c286081f5597b822f5e576735d321cce38fcbd1a25db0d90e1163570068f"
+ ],
[
[
"file",
@@ -781,7 +781,7 @@
"file",
"web-app/src/entities/index.ts"
],
- "cfb9f8237f80aea777840621702d03803172b005b3aaee2fc0be6c9f1b1c8414"
+ "f3daa2f99b1ead27d95ef05fbd493c26b697a4c6d413432a1959d41e4cb205a2"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma
index 8b5c55f36b..69c01a4535 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma
@@ -14,6 +14,7 @@ model User {
username String @unique
password String
externalAuthAssociations SocialLogin[]
+ auth Auth?
}
model SocialLogin {
@@ -32,3 +33,19 @@ model Task {
isDone Boolean @default(false)
}
+model Auth {
+ id String @id @default(uuid())
+ userId Int? @unique
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
+ identities AuthIdentity[]
+
+}
+model AuthIdentity {
+ providerName String
+ providerUserId String
+ providerData String @default("{}")
+ authId String
+ auth Auth @relation(fields: [authId], references: [id], onDelete: Cascade)
+ @@id([providerName, providerUserId])
+
+}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum
index 70f188e833..b340294f3e 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/db/schema.prisma.wasp-generate-checksum
@@ -1 +1 @@
-cbea4d60a2c1bef984008597eefc64540e95048d7a43cb718303f5628d386ae1
\ No newline at end of file
+3e6bfc3dadfc9399a3fae3ea77cadb1aa3259efa0b8362ed77d6d5ee5013d393
\ No newline at end of file
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts
index 333f64f26b..23017df130 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/_types/index.ts
@@ -2,7 +2,16 @@ import { type Expand } from "../universal/types.js";
import { type Request, type Response } from 'express'
import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core'
import prisma from "../dbClient.js"
-import { type User } from "../entities"
+import {
+ type User,
+ type Auth,
+ type AuthIdentity,
+} from "../entities"
+import {
+ type EmailProviderData,
+ type UsernameProviderData,
+ type OAuthProviderData,
+} from '../auth/utils.js'
import { type _Entity } from "./taggedEntities"
import { type Payload } from "./serialization";
@@ -71,10 +80,21 @@ type Context = Expand<{
entities: Expand>
}>
-type ContextWithUser = Expand & { user?: SanitizedUser}>
+type ContextWithUser = Expand & { user?: SanitizedUser }>
// TODO: This type must match the logic in core/auth.js (if we remove the
// password field from the object there, we must do the same here). Ideally,
// these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
-export type SanitizedUser = Omit
+
+export type DeserializedAuthEntity = Expand & {
+ providerData: Omit | Omit | OAuthProviderData
+}>
+
+export type SanitizedUser = User & {
+ auth: Auth & {
+ identities: DeserializedAuthEntity[]
+ } | null
+}
+
+export type { ProviderName } from '../auth/utils.js'
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts
index a2e0363bd3..0525771050 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts
@@ -5,7 +5,7 @@ import { makeOAuthInit } from "../oauth/init.js";
import type { ProviderConfig } from "../types.js";
import type { OAuthConfig } from "../oauth/types.js";
-import { getUserFieldsFn as _waspGetUserFieldsFn } from '../oauth/defaults.js'
+const _waspGetUserFieldsFn = undefined
const _waspUserDefinedConfigFn = undefined
const _waspOAuthConfig: OAuthConfig = {
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/index.ts
index 877ddc8343..c96736abe3 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/index.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/index.ts
@@ -1,14 +1,15 @@
+import { join } from 'path'
import { Router } from "express";
-import { getDirFromFileUrl, importJsFilesFromDir } from "../../utils.js";
+import { getDirPathFromFileUrl, importJsFilesFromDir } from "../../utils.js";
import { ProviderConfig } from "./types";
-const allowedConfigs = [
+const whitelistedProviderConfigFileNames = [
"google.js",
];
-const providers = await importProviders(allowedConfigs);
+const providers = await importProviders(whitelistedProviderConfigFileNames);
const router = Router();
@@ -24,8 +25,9 @@ for (const provider of providers) {
export default router;
-async function importProviders(providerConfigs: string[]): Promise {
- const currentExecutionDir = getDirFromFileUrl(import.meta.url);
- const providers = await importJsFilesFromDir(currentExecutionDir, "./config", providerConfigs);
+async function importProviders(whitelistedProviderConfigFileNames: string[]): Promise {
+ const currentExecutionDir = getDirPathFromFileUrl(import.meta.url);
+ const pathToDirWithConfigs = join(currentExecutionDir, "./config");
+ const providers = await importJsFilesFromDir(pathToDirWithConfigs, whitelistedProviderConfigFileNames);
return providers.map((provider) => provider.default);
}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts
index 85a04a93c5..a64162b8dd 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts
@@ -1,21 +1,29 @@
import { Router } from "express"
import passport from "passport"
-import { v4 as uuidv4 } from 'uuid'
import prisma from '../../../dbClient.js'
import waspServerConfig from '../../../config.js'
-import { sign } from '../../../core/auth.js'
-import { authConfig, contextWithUserEntity, createUser } from "../../utils.js"
-
-import type { User } from '../../../entities';
+import {
+ type ProviderName,
+ type ProviderId,
+ createProviderId,
+ authConfig,
+ contextWithUserEntity,
+ createUser,
+ findAuthWithUserBy,
+ createAuthToken,
+ rethrowPossibleAuthError,
+ sanitizeAndSerializeProviderData,
+} from "../../utils.js"
+import { type User } from "../../../entities/index.js"
import type { ProviderConfig, RequestWithWasp } from "../types.js"
import type { GetUserFieldsFn } from "./types.js"
import { handleRejection } from "../../../utils.js"
// For oauth providers, we have an endpoint /login to get the auth URL,
// and the /callback endpoint which is used to get the actual access_token and the user info.
-export function createRouter(provider: ProviderConfig, initData: { passportStrategyName: string, getUserFieldsFn: GetUserFieldsFn }) {
+export function createRouter(provider: ProviderConfig, initData: { passportStrategyName: string, getUserFieldsFn?: GetUserFieldsFn }) {
const { passportStrategyName, getUserFieldsFn } = initData;
const router = Router();
@@ -41,46 +49,57 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat
throw new Error(`${provider.displayName} provider profile was missing required id property. This should not happen! Please contact Wasp.`);
}
- // Wrap call to getUserFieldsFn so we can invoke only if needed.
- const getUserFields = () => getUserFieldsFn(contextWithUserEntity, { profile: providerProfile });
- // TODO: In the future we could make this configurable, possibly associating an external account
- // with the currently logged in account, or by some DB lookup.
- const user = await findOrCreateUserByExternalAuthAssociation(provider.id, providerProfile.id, getUserFields);
+ const providerId = createProviderId(provider.id, providerProfile.id);
- const token = await sign(user.id);
- res.json({ token });
+ try {
+ const userId = await getUserIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn)
+ const token = await createAuthToken(userId)
+ res.json({ token })
+ } catch (e) {
+ rethrowPossibleAuthError(e)
+ }
})
)
return router;
}
-async function findOrCreateUserByExternalAuthAssociation(
- provider: string,
- providerId: string,
- getUserFields: () => ReturnType,
-): Promise {
- // Attempt to find a User by an external auth association.
- const externalAuthAssociation = await prisma.socialLogin.findFirst({
- where: { provider, providerId },
- include: { user: true }
+// We need a user id to create the auth token, so we either find an existing user
+// or create a new one if none exists for this provider.
+async function getUserIdFromProviderDetails(
+ providerId: ProviderId,
+ providerProfile: any,
+ getUserFieldsFn?: GetUserFieldsFn,
+): Promise {
+ const existingAuthIdentity = await prisma.authIdentity.findUnique({
+ where: {
+ providerName_providerUserId: providerId,
+ },
+ include: {
+ auth: {
+ include: {
+ user: true
+ }
+ }
+ }
})
- if (externalAuthAssociation) {
- return externalAuthAssociation.user
- }
+ if (existingAuthIdentity) {
+ return existingAuthIdentity.auth.user.id
+ } else {
+ const userFields = getUserFieldsFn
+ ? await getUserFieldsFn(contextWithUserEntity, { profile: providerProfile })
+ : {};
- // No external auth association linkage found. Create a new User using details from
- // `getUserFields()`. Additionally, associate the externalAuthAssociations with the new User.
- const userFields = await getUserFields()
- const userAndExternalAuthAssociation = {
- ...userFields,
- // TODO: Decouple social from usernameAndPassword auth.
- password: uuidv4(),
- externalAuthAssociations: {
- create: [{ provider, providerId }]
- }
- }
+ // For now, we don't have any extra data for the oauth providers, so we just pass an empty object.
+ const providerData = await sanitizeAndSerializeProviderData({})
+
+ const user = await createUser(
+ providerId,
+ providerData,
+ userFields,
+ )
- return createUser(userAndExternalAuthAssociation)
+ return user.id
+ }
}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/defaults.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/defaults.ts
deleted file mode 100644
index ad3df3f415..0000000000
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/defaults.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { generateAvailableDictionaryUsername } from '../../../core/auth.js'
-
-export async function getUserFieldsFn(_context, _args) {
- const username = await generateAvailableDictionaryUsername()
- return { username }
-}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts
index 130836ebb8..ac5a56dafe 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts
@@ -71,6 +71,6 @@ function ensureValidConfig(provider: ProviderConfig, config: OAuthConfig): void
export type OAuthImports = {
npmPackage: string;
userDefinedConfigFn?: UserDefinedConfigFn;
- getUserFieldsFn: GetUserFieldsFn;
oAuthConfig: OAuthConfig;
+ getUserFieldsFn?: GetUserFieldsFn;
};
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts
index 1589e88876..ca1e7a3f50 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts
@@ -8,11 +8,11 @@ export type OAuthConfig = {
scope?: string[];
}
-export type CreateOAuthUser = Omit
+export type UserFieldsFromOAuthSignup = Prisma.UserCreateInput
export type UserDefinedConfigFn = () => { [key: string]: any }
export type GetUserFieldsFn = (
context: typeof contextWithUserEntity,
args: { profile: { [key: string]: any } },
-) => Promise
+) => Promise
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts
index 0889298c0a..9defb94486 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/providers/types.ts
@@ -1,10 +1,13 @@
import type { Router, Request } from 'express'
-import type { User } from '../../entities'
+import type { Prisma } from '@prisma/client'
import type { Expand } from '../../universal/types'
+import type { ProviderName } from '../utils'
+
+type UserEntityCreateInput = Prisma.UserCreateInput
export type ProviderConfig = {
// Unique provider identifier, used as part of URL paths
- id: string;
+ id: ProviderName;
displayName: string;
// Each provider config can have an init method which is ran on setup time
// e.g. for oAuth providers this is the time when the Passport strategy is registered.
@@ -20,20 +23,14 @@ export type InitData = {
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
-export function createDefineAdditionalSignupFieldsFn<
- // Wasp already includes these fields in the signup process
- ExistingFields extends keyof User,
- PossibleAdditionalFields = Expand<
- Partial>
+export type PossibleAdditionalSignupFields = Expand>
+
+export function defineAdditionalSignupFields(config: {
+ [key in keyof PossibleAdditionalSignupFields]: FieldGetter<
+ PossibleAdditionalSignupFields[key]
>
->() {
- return function defineFields(config: {
- [key in keyof PossibleAdditionalFields]: FieldGetter<
- PossibleAdditionalFields[key]
- >
- }) {
- return config
- }
+}) {
+ return config
}
type FieldGetter = (
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/user.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/user.ts
new file mode 100644
index 0000000000..a5d987fc4e
--- /dev/null
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/user.ts
@@ -0,0 +1,27 @@
+// We decided not to deduplicate these helper functions in the server and the client.
+// We have them duplicated in this file and in data/Generator/templates/react-app/src/auth/user.ts
+// If you are changing the logic here, make sure to change it there as well.
+
+import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../_types/index'
+
+export function getEmail(user: User): string | null {
+ return findUserIdentity(user, "email")?.providerUserId ?? null;
+}
+
+export function getUsername(user: User): string | null {
+ return findUserIdentity(user, "username")?.providerUserId ?? null;
+}
+
+export function getFirstProviderUserId(user?: User): string | null {
+ if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
+ return null;
+ }
+
+ return user.auth.identities[0].providerUserId ?? null;
+}
+
+export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined {
+ return user.auth.identities.find(
+ (identity) => identity.providerName === providerName
+ );
+}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts
index b6d503bec0..ba1ad7074c 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/utils.ts
@@ -1,18 +1,49 @@
-import { sign, verify } from '../core/auth.js'
+import { hashPassword, sign, verify } from '../core/auth.js'
import AuthError from '../core/AuthError.js'
import HttpError from '../core/HttpError.js'
import prisma from '../dbClient.js'
-import { isPrismaError, prismaErrorToHttpError, sleep } from '../utils.js'
-import { type User } from '../entities/index.js'
-import { type Prisma } from '@prisma/client';
+import { sleep } from '../utils.js'
+import {
+ type User,
+ type Auth,
+ type AuthIdentity,
+} from '../entities/index.js'
+import { Prisma } from '@prisma/client';
import { throwValidationError } from './validation.js'
-import { createDefineAdditionalSignupFieldsFn } from './providers/types.js'
-const _waspAdditionalSignupFieldsConfig = {} as ReturnType<
- ReturnType>
->
+import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js'
+const _waspAdditionalSignupFieldsConfig = {} as ReturnType
+
+export type EmailProviderData = {
+ hashedPassword: string;
+ isEmailVerified: boolean;
+ emailVerificationSentAt: string | null;
+ passwordResetSentAt: string | null;
+}
+
+export type UsernameProviderData = {
+ hashedPassword: string;
+}
+
+export type OAuthProviderData = {}
+
+/**
+ * This type is used for type-level programming e.g. to enumerate
+ * all possible provider data types.
+ *
+ * The keys of this type are the names of the providers and the values
+ * are the types of the provider data.
+ */
+export type PossibleProviderData = {
+ email: EmailProviderData;
+ username: UsernameProviderData;
+ google: OAuthProviderData;
+ github: OAuthProviderData;
+}
+
+export type ProviderName = keyof PossibleProviderData
export const contextWithUserEntity = {
entities: {
@@ -25,31 +56,112 @@ export const authConfig = {
successRedirectPath: "/",
}
-export async function findUserBy(where: Prisma.UserWhereUniqueInput): Promise {
- return prisma.user.findUnique({ where });
+/**
+ * ProviderId uniquely identifies an auth identity e.g.
+ * "email" provider with user id "test@test.com" or
+ * "google" provider with user id "1234567890".
+ *
+ * We use this type to avoid passing the providerName and providerUserId
+ * separately. Also, we can normalize the providerUserId to make sure it's
+ * consistent across different DB operations.
+ */
+export type ProviderId = {
+ providerName: ProviderName;
+ providerUserId: string;
}
-export async function createUser(data: Prisma.UserCreateInput): Promise {
- try {
- return await prisma.user.create({ data })
- } catch (e) {
- rethrowPossiblePrismaError(e);
+export function createProviderId(providerName: ProviderName, providerUserId: string): ProviderId {
+ return {
+ providerName,
+ providerUserId: providerUserId.toLowerCase(),
}
}
-export async function deleteUser(user: User): Promise {
- try {
- return await prisma.user.delete({ where: { id: user.id } })
- } catch (e) {
- rethrowPossiblePrismaError(e);
+export async function findAuthIdentity(providerId: ProviderId): Promise {
+ return prisma.authIdentity.findUnique({
+ where: {
+ providerName_providerUserId: providerId,
+ }
+ });
+}
+
+/**
+ * Updates the provider data for the given auth identity.
+ *
+ * This function performs data sanitization and serialization.
+ * Sanitization is done by hashing the password, so this function
+ * expects the password received in the `providerDataUpdates`
+ * **not to be hashed**.
+ */
+export async function updateAuthIdentityProviderData(
+ providerId: ProviderId,
+ existingProviderData: PossibleProviderData[PN],
+ providerDataUpdates: Partial,
+): Promise {
+ // We are doing the sanitization here only on updates to avoid
+ // hashing the password multiple times.
+ const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates);
+ const newProviderData = {
+ ...existingProviderData,
+ ...sanitizedProviderDataUpdates,
}
+ const serializedProviderData = await serializeProviderData(newProviderData);
+ return prisma.authIdentity.update({
+ where: {
+ providerName_providerUserId: providerId,
+ },
+ data: { providerData: serializedProviderData },
+ });
}
-export async function createAuthToken(user: User): Promise {
- return sign(user.id);
+type FindAuthWithUserResult = Auth & {
+ user: User
}
-export async function verifyToken(token: string): Promise<{ id: any }> {
+export async function findAuthWithUserBy(
+ where: Prisma.AuthWhereInput
+): Promise {
+ return prisma.auth.findFirst({ where, include: { user: true }});
+}
+
+export async function createUser(
+ providerId: ProviderId,
+ serializedProviderData?: string,
+ userFields?: PossibleAdditionalSignupFields,
+): Promise {
+ return prisma.user.create({
+ data: {
+ // Using any here to prevent type errors when userFields are not
+ // defined. We want Prisma to throw an error in that case.
+ ...(userFields ?? {} as any),
+ auth: {
+ create: {
+ identities: {
+ create: {
+ providerName: providerId.providerName,
+ providerUserId: providerId.providerUserId,
+ providerData: serializedProviderData,
+ },
+ },
+ }
+ },
+ }
+ })
+}
+
+export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> {
+ return prisma.user.deleteMany({ where: { auth: {
+ id: authId,
+ } } })
+}
+
+export async function createAuthToken(
+ userId: User['id']
+): Promise {
+ return sign(userId);
+}
+
+export async function verifyToken(token: string): Promise {
return verify(token);
}
@@ -60,24 +172,61 @@ export async function verifyToken(token: string): Promise<{ id: any }> {
// NOTE: Attacker measuring time to response can still determine
// if a user exists or not. We'll be able to avoid it when
// we implement e-mail sending via jobs.
-export async function doFakeWork() {
+export async function doFakeWork(): Promise {
const timeToWork = Math.floor(Math.random() * 1000) + 1000;
return sleep(timeToWork);
}
-export function rethrowPossiblePrismaError(e: unknown): void {
+export function rethrowPossibleAuthError(e: unknown): void {
if (e instanceof AuthError) {
throwValidationError(e.message);
- } else if (isPrismaError(e)) {
- throw prismaErrorToHttpError(e)
- } else {
- throw new HttpError(500)
}
+
+ // Prisma code P2002 is for unique constraint violations.
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
+ throw new HttpError(422, 'Save failed', {
+ message: `user with the same identity already exists`,
+ })
+ }
+
+ if (e instanceof Prisma.PrismaClientValidationError) {
+ // NOTE: Logging the error since this usually means that there are
+ // required fields missing in the request, we want the developer
+ // to know about it.
+ console.error(e)
+ throw new HttpError(422, 'Save failed', {
+ message: 'there was a database error'
+ })
+ }
+
+ // Prisma code P2021 is for missing table errors.
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2021') {
+ // NOTE: Logging the error since this usually means that the database
+ // migrations weren't run, we want the developer to know about it.
+ console.error(e)
+ console.info('🐝 This error can happen if you did\'t run the database migrations.')
+ throw new HttpError(500, 'Save failed', {
+ message: `there was a database error`,
+ })
+ }
+
+ // Prisma code P2003 is for foreign key constraint failure
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
+ console.error(e)
+ console.info(`🐝 This error can happen if you have some relation on your User entity
+ but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull".
+ Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`)
+ throw new HttpError(500, 'Save failed', {
+ message: `there was a database error`,
+ })
+ }
+
+ throw e
}
export async function validateAndGetAdditionalFields(data: {
[key: string]: unknown
-}) {
+}): Promise> {
const {
password: _password,
...sanitizedData
@@ -93,3 +242,49 @@ export async function validateAndGetAdditionalFields(data: {
}
return result;
}
+
+export function deserializeAndSanitizeProviderData(
+ providerData: string,
+ { shouldRemovePasswordField = false }: { shouldRemovePasswordField?: boolean } = {},
+): PossibleProviderData[PN] {
+ // NOTE: We are letting JSON.parse throw an error if the providerData is not valid JSON.
+ let data = JSON.parse(providerData) as PossibleProviderData[PN];
+
+ if (providerDataHasPasswordField(data) && shouldRemovePasswordField) {
+ delete data.hashedPassword;
+ }
+
+ return data;
+}
+
+export async function sanitizeAndSerializeProviderData(
+ providerData: PossibleProviderData[PN],
+): Promise {
+ return serializeProviderData(
+ await sanitizeProviderData(providerData)
+ );
+}
+
+function serializeProviderData(providerData: PossibleProviderData[PN]): string {
+ return JSON.stringify(providerData);
+}
+
+async function sanitizeProviderData(
+ providerData: PossibleProviderData[PN],
+): Promise {
+ const data = {
+ ...providerData,
+ };
+ if (providerDataHasPasswordField(data)) {
+ data.hashedPassword = await hashPassword(data.hashedPassword);
+ }
+
+ return data;
+}
+
+
+function providerDataHasPasswordField(
+ providerData: PossibleProviderData[keyof PossibleProviderData],
+): providerData is { hashedPassword: string } {
+ return 'hashedPassword' in providerData;
+}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js
index 11c884307d..9c8c03ce91 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth.js
@@ -7,13 +7,15 @@ import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
import HttpError from '../core/HttpError.js'
import config from '../config.js'
+import { deserializeAndSanitizeProviderData } from '../auth/utils.js'
const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)
const JWT_SECRET = config.auth.jwtSecret
-export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
+export const signData = (data, options) => jwtSign(data, JWT_SECRET, options)
+export const sign = (id, options) => signData({ id }, options)
export const verify = (token) => jwtVerify(token, JWT_SECRET)
const auth = handleRejection(async (req, res, next) => {
@@ -47,7 +49,17 @@ export async function getUserFromToken(token) {
}
}
- const user = await prisma.user.findUnique({ where: { id: userIdFromToken } })
+ const user = await prisma.user
+ .findUnique({
+ where: { id: userIdFromToken },
+ include: {
+ auth: {
+ include: {
+ identities: true
+ }
+ }
+ }
+ })
if (!user) {
throwInvalidCredentialsError()
}
@@ -56,9 +68,12 @@ export async function getUserFromToken(token) {
// password field from the object here, we must to do the same there).
// Ideally, these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
- const { password, ...userView } = user
-
- return userView
+ let sanitizedUser = { ...user }
+ sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => {
+ identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true })
+ return identity
+ });
+ return sanitizedUser
}
const SP = new SecurePassword()
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth/prismaMiddleware.js b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth/prismaMiddleware.js
deleted file mode 100644
index 44eff27892..0000000000
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/core/auth/prismaMiddleware.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { hashPassword } from '../auth.js'
-import { PASSWORD_FIELD } from '../../auth/validation.js'
-
-// Make sure password is always hashed before storing to the database.
-const registerPasswordHashing = (prismaClient) => {
- prismaClient.$use(async (params, next) => {
- if (params.model === 'User') {
- if (['create', 'update', 'updateMany'].includes(params.action)) {
- if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) {
- params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD])
- }
- } else if (params.action === 'upsert') {
- if (params.args.create.hasOwnProperty(PASSWORD_FIELD)) {
- params.args.create[PASSWORD_FIELD] =
- await hashPassword(params.args.create[PASSWORD_FIELD])
- }
- if (params.args.update.hasOwnProperty(PASSWORD_FIELD)) {
- params.args.update[PASSWORD_FIELD] =
- await hashPassword(params.args.update[PASSWORD_FIELD])
- }
- }
- }
-
- return next(params)
- })
-}
-
-export const registerAuthMiddleware = (prismaClient) => {
- registerPasswordHashing(prismaClient)
-}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts
index 0306683e35..e2de0a4a3a 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/crud/tasks.ts
@@ -5,9 +5,7 @@ import type {
AuthenticatedQuery,
_Task,
} from "../_types";
-import type {
- Prisma,
-} from "@prisma/client";
+import type { Prisma } from "@prisma/client";
import { Payload } from "../_types/serialization.js";
import type {
Task,
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/dbClient.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/dbClient.ts
index c0720735ed..66e7801be3 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/dbClient.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/dbClient.ts
@@ -1,12 +1,9 @@
import Prisma from '@prisma/client'
-import { registerAuthMiddleware } from './core/auth/prismaMiddleware.js'
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
- registerAuthMiddleware(prismaClient)
-
return prismaClient
}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/entities/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/entities/index.ts
index c31c1b7724..d5eec2c3a2 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/entities/index.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/entities/index.ts
@@ -8,6 +8,8 @@ export {
type User,
type SocialLogin,
type Task,
+ type Auth,
+ type AuthIdentity,
} from "@prisma/client"
export type Entity =
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.js b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.js
deleted file mode 100644
index d6c7ed639a..0000000000
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Prisma from '@prisma/client'
-import HttpError from './core/HttpError.js'
-
-import { readdir } from "fs";
-import { join, dirname } from "path";
-import { fileURLToPath } from 'url';
-
-/**
- * Decorator for async express middleware that handles promise rejections.
- * @param {Func} middleware - Express middleware function.
- * @returns {Func} Express middleware that is exactly the same as the given middleware but,
- * if given middleware returns promise, reject of that promise will be correctly handled,
- * meaning that error will be forwarded to next().
- */
-export const handleRejection = (middleware) => async (req, res, next) => {
- try {
- await middleware(req, res, next)
- } catch (error) {
- next(error)
- }
-}
-
-export const isPrismaError = (e) => {
- return e instanceof Prisma.PrismaClientKnownRequestError ||
- e instanceof Prisma.PrismaClientUnknownRequestError ||
- e instanceof Prisma.PrismaClientRustPanicError ||
- e instanceof Prisma.PrismaClientInitializationError ||
- e instanceof Prisma.PrismaClientValidationError
-}
-
-export const prismaErrorToHttpError = (e) => {
- if (e instanceof Prisma.PrismaClientKnownRequestError) {
- if (e.code === 'P2002') {
- return new HttpError(422, 'Save failed', {
- message: `user with the same ${e.meta.target.join(', ')} already exists`,
- target: e.meta.target
- })
- } else {
- // TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
- // and decide which are input errors (422) and which are not (500)
- // See: https://github.com/wasp-lang/wasp/issues/384
- return new HttpError(500)
- }
- } else if (e instanceof Prisma.PrismaClientValidationError) {
- return new HttpError(422, 'Save failed')
- } else {
- return new HttpError(500)
- }
-}
-
-export const sleep = ms => new Promise(r => setTimeout(r, ms))
-
-export function getDirFromFileUrl(fileUrl) {
- return fileURLToPath(dirname(fileUrl));
-}
-
-export async function importJsFilesFromDir(absoluteDir, relativePath, whitelist = null) {
- const pathToDir = join(absoluteDir, relativePath);
-
- return new Promise((resolve, reject) => {
- readdir(pathToDir, async (err, files) => {
- if (err) {
- return reject(err);
- }
- const importPromises = files
- .filter((file) => file.endsWith(".js") && isWhitelisted(file))
- .map((file) => import(`${pathToDir}/${file}`));
- resolve(Promise.all(importPromises));
- });
- });
-
- function isWhitelisted(file) {
- // No whitelist means all files are whitelisted
- if (!Array.isArray(whitelist)) {
- return true;
- }
- return whitelist.some((whitelistedFile) => file.endsWith(whitelistedFile));
- }
-}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.ts
new file mode 100644
index 0000000000..a930149d08
--- /dev/null
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/utils.ts
@@ -0,0 +1,66 @@
+import crypto from 'crypto'
+import { Request, Response, NextFunction } from 'express'
+
+import { readdir } from 'fs'
+import { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+import { type SanitizedUser } from './_types/index.js'
+
+type RequestWithExtraFields = Request & {
+ user?: SanitizedUser
+}
+
+/**
+ * Decorator for async express middleware that handles promise rejections.
+ * @param {Func} middleware - Express middleware function.
+ * @returns Express middleware that is exactly the same as the given middleware but,
+ * if given middleware returns promise, reject of that promise will be correctly handled,
+ * meaning that error will be forwarded to next().
+ */
+export const handleRejection = (
+ middleware: (
+ req: RequestWithExtraFields,
+ res: Response,
+ next: NextFunction
+ ) => any
+) =>
+async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
+ try {
+ await middleware(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms))
+
+export function getDirPathFromFileUrl(fileUrl: string): string {
+ return fileURLToPath(dirname(fileUrl))
+}
+
+export async function importJsFilesFromDir(
+ pathToDir: string,
+ whitelistedFileNames: string[] | null = null
+): Promise {
+ return new Promise((resolve, reject) => {
+ readdir(pathToDir, async (err, files) => {
+ if (err) {
+ return reject(err)
+ }
+ const importPromises = files
+ .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
+ .map((file) => import(`${pathToDir}/${file}`))
+ resolve(Promise.all(importPromises))
+ })
+ })
+
+ function isWhitelistedFileName(fileName: string) {
+ // No whitelist means all files are whitelisted
+ if (!Array.isArray(whitelistedFileNames)) {
+ return true
+ }
+
+ return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
+ }
+}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts
index b9614390c4..4405410cc7 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/types.ts
@@ -1,2 +1,2 @@
// todo(filip): turn into a proper import/path
-export type { SanitizedUser as User } from '../../../server/src/_types/'
+export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../../../server/src/_types/'
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/user.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/user.ts
new file mode 100644
index 0000000000..5799c71ea7
--- /dev/null
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/auth/user.ts
@@ -0,0 +1,27 @@
+// We decided not to deduplicate these helper functions in the server and the client.
+// We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts
+// If you are changing the logic here, make sure to change it there as well.
+
+import type { User, ProviderName, DeserializedAuthEntity } from './types'
+
+export function getEmail(user: User): string | null {
+ return findUserIdentity(user, "email")?.providerUserId ?? null;
+}
+
+export function getUsername(user: User): string | null {
+ return findUserIdentity(user, "username")?.providerUserId ?? null;
+}
+
+export function getFirstProviderUserId(user?: User): string | null {
+ if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
+ return null;
+ }
+
+ return user.auth.identities[0].providerUserId ?? null;
+}
+
+export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined {
+ return user.auth.identities.find(
+ (identity) => identity.providerName === providerName
+ );
+}
diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/entities/index.ts b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/entities/index.ts
index 6435f6bd35..38ae6521e9 100644
--- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/entities/index.ts
+++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/web-app/src/entities/index.ts
@@ -8,6 +8,8 @@ export type {
User,
SocialLogin,
Task,
+ Auth,
+ AuthIdentity,
} from '@prisma/client'
export type Entity =
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest b/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest
index 1d8986f7d2..356ddfd991 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/files.manifest
@@ -38,7 +38,7 @@ waspJob/.wasp/out/server/src/server.ts
waspJob/.wasp/out/server/src/types/index.ts
waspJob/.wasp/out/server/src/universal/types.ts
waspJob/.wasp/out/server/src/universal/url.ts
-waspJob/.wasp/out/server/src/utils.js
+waspJob/.wasp/out/server/src/utils.ts
waspJob/.wasp/out/server/tsconfig.json
waspJob/.wasp/out/web-app/.env
waspJob/.wasp/out/web-app/.npmrc
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums
index 661e200e8a..66b385cb14 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums
@@ -137,7 +137,7 @@
"file",
"server/src/dbClient.ts"
],
- "20c67ca197da3de2d37528ceaff2e40af910be8177f346c6d5c2b2f983810c43"
+ "f26bfa4a60022d45195f3179548c4b09305f6a9cfe301b5edd1615677a59ae3f"
],
[
[
@@ -268,9 +268,9 @@
[
[
"file",
- "server/src/utils.js"
+ "server/src/utils.ts"
],
- "300e9bb586b163f2608acb27346b5c94ec3e58cdc25dace5381f3d0c6710a7ec"
+ "6a664e44a89efe8ed8cb7559a84bd2871422c38287b276fee2a7aed4680e1d1e"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/dbClient.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/dbClient.ts
index 11b87caa50..66e7801be3 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/dbClient.ts
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/dbClient.ts
@@ -4,7 +4,6 @@ import Prisma from '@prisma/client'
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
-
return prismaClient
}
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/utils.js b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/utils.js
deleted file mode 100644
index d6c7ed639a..0000000000
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/utils.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Prisma from '@prisma/client'
-import HttpError from './core/HttpError.js'
-
-import { readdir } from "fs";
-import { join, dirname } from "path";
-import { fileURLToPath } from 'url';
-
-/**
- * Decorator for async express middleware that handles promise rejections.
- * @param {Func} middleware - Express middleware function.
- * @returns {Func} Express middleware that is exactly the same as the given middleware but,
- * if given middleware returns promise, reject of that promise will be correctly handled,
- * meaning that error will be forwarded to next().
- */
-export const handleRejection = (middleware) => async (req, res, next) => {
- try {
- await middleware(req, res, next)
- } catch (error) {
- next(error)
- }
-}
-
-export const isPrismaError = (e) => {
- return e instanceof Prisma.PrismaClientKnownRequestError ||
- e instanceof Prisma.PrismaClientUnknownRequestError ||
- e instanceof Prisma.PrismaClientRustPanicError ||
- e instanceof Prisma.PrismaClientInitializationError ||
- e instanceof Prisma.PrismaClientValidationError
-}
-
-export const prismaErrorToHttpError = (e) => {
- if (e instanceof Prisma.PrismaClientKnownRequestError) {
- if (e.code === 'P2002') {
- return new HttpError(422, 'Save failed', {
- message: `user with the same ${e.meta.target.join(', ')} already exists`,
- target: e.meta.target
- })
- } else {
- // TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
- // and decide which are input errors (422) and which are not (500)
- // See: https://github.com/wasp-lang/wasp/issues/384
- return new HttpError(500)
- }
- } else if (e instanceof Prisma.PrismaClientValidationError) {
- return new HttpError(422, 'Save failed')
- } else {
- return new HttpError(500)
- }
-}
-
-export const sleep = ms => new Promise(r => setTimeout(r, ms))
-
-export function getDirFromFileUrl(fileUrl) {
- return fileURLToPath(dirname(fileUrl));
-}
-
-export async function importJsFilesFromDir(absoluteDir, relativePath, whitelist = null) {
- const pathToDir = join(absoluteDir, relativePath);
-
- return new Promise((resolve, reject) => {
- readdir(pathToDir, async (err, files) => {
- if (err) {
- return reject(err);
- }
- const importPromises = files
- .filter((file) => file.endsWith(".js") && isWhitelisted(file))
- .map((file) => import(`${pathToDir}/${file}`));
- resolve(Promise.all(importPromises));
- });
- });
-
- function isWhitelisted(file) {
- // No whitelist means all files are whitelisted
- if (!Array.isArray(whitelist)) {
- return true;
- }
- return whitelist.some((whitelistedFile) => file.endsWith(whitelistedFile));
- }
-}
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/utils.ts b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/utils.ts
new file mode 100644
index 0000000000..99492c5474
--- /dev/null
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/utils.ts
@@ -0,0 +1,64 @@
+import crypto from 'crypto'
+import { Request, Response, NextFunction } from 'express'
+
+import { readdir } from 'fs'
+import { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+
+type RequestWithExtraFields = Request & {
+}
+
+/**
+ * Decorator for async express middleware that handles promise rejections.
+ * @param {Func} middleware - Express middleware function.
+ * @returns Express middleware that is exactly the same as the given middleware but,
+ * if given middleware returns promise, reject of that promise will be correctly handled,
+ * meaning that error will be forwarded to next().
+ */
+export const handleRejection = (
+ middleware: (
+ req: RequestWithExtraFields,
+ res: Response,
+ next: NextFunction
+ ) => any
+) =>
+async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
+ try {
+ await middleware(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms))
+
+export function getDirPathFromFileUrl(fileUrl: string): string {
+ return fileURLToPath(dirname(fileUrl))
+}
+
+export async function importJsFilesFromDir(
+ pathToDir: string,
+ whitelistedFileNames: string[] | null = null
+): Promise {
+ return new Promise((resolve, reject) => {
+ readdir(pathToDir, async (err, files) => {
+ if (err) {
+ return reject(err)
+ }
+ const importPromises = files
+ .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
+ .map((file) => import(`${pathToDir}/${file}`))
+ resolve(Promise.all(importPromises))
+ })
+ })
+
+ function isWhitelistedFileName(fileName: string) {
+ // No whitelist means all files are whitelisted
+ if (!Array.isArray(whitelistedFileNames)) {
+ return true
+ }
+
+ return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
+ }
+}
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest b/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest
index f663697234..a8c4f8eeb5 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest
@@ -36,7 +36,7 @@ waspMigrate/.wasp/out/server/src/server.ts
waspMigrate/.wasp/out/server/src/types/index.ts
waspMigrate/.wasp/out/server/src/universal/types.ts
waspMigrate/.wasp/out/server/src/universal/url.ts
-waspMigrate/.wasp/out/server/src/utils.js
+waspMigrate/.wasp/out/server/src/utils.ts
waspMigrate/.wasp/out/server/tsconfig.json
waspMigrate/.wasp/out/web-app/.env
waspMigrate/.wasp/out/web-app/.npmrc
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums
index bffd68da92..a2bc2feb86 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums
@@ -137,7 +137,7 @@
"file",
"server/src/dbClient.ts"
],
- "20c67ca197da3de2d37528ceaff2e40af910be8177f346c6d5c2b2f983810c43"
+ "f26bfa4a60022d45195f3179548c4b09305f6a9cfe301b5edd1615677a59ae3f"
],
[
[
@@ -226,9 +226,9 @@
[
[
"file",
- "server/src/utils.js"
+ "server/src/utils.ts"
],
- "300e9bb586b163f2608acb27346b5c94ec3e58cdc25dace5381f3d0c6710a7ec"
+ "6a664e44a89efe8ed8cb7559a84bd2871422c38287b276fee2a7aed4680e1d1e"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/dbClient.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/dbClient.ts
index 11b87caa50..66e7801be3 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/dbClient.ts
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/dbClient.ts
@@ -4,7 +4,6 @@ import Prisma from '@prisma/client'
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
-
return prismaClient
}
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.js b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.js
deleted file mode 100644
index d6c7ed639a..0000000000
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Prisma from '@prisma/client'
-import HttpError from './core/HttpError.js'
-
-import { readdir } from "fs";
-import { join, dirname } from "path";
-import { fileURLToPath } from 'url';
-
-/**
- * Decorator for async express middleware that handles promise rejections.
- * @param {Func} middleware - Express middleware function.
- * @returns {Func} Express middleware that is exactly the same as the given middleware but,
- * if given middleware returns promise, reject of that promise will be correctly handled,
- * meaning that error will be forwarded to next().
- */
-export const handleRejection = (middleware) => async (req, res, next) => {
- try {
- await middleware(req, res, next)
- } catch (error) {
- next(error)
- }
-}
-
-export const isPrismaError = (e) => {
- return e instanceof Prisma.PrismaClientKnownRequestError ||
- e instanceof Prisma.PrismaClientUnknownRequestError ||
- e instanceof Prisma.PrismaClientRustPanicError ||
- e instanceof Prisma.PrismaClientInitializationError ||
- e instanceof Prisma.PrismaClientValidationError
-}
-
-export const prismaErrorToHttpError = (e) => {
- if (e instanceof Prisma.PrismaClientKnownRequestError) {
- if (e.code === 'P2002') {
- return new HttpError(422, 'Save failed', {
- message: `user with the same ${e.meta.target.join(', ')} already exists`,
- target: e.meta.target
- })
- } else {
- // TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
- // and decide which are input errors (422) and which are not (500)
- // See: https://github.com/wasp-lang/wasp/issues/384
- return new HttpError(500)
- }
- } else if (e instanceof Prisma.PrismaClientValidationError) {
- return new HttpError(422, 'Save failed')
- } else {
- return new HttpError(500)
- }
-}
-
-export const sleep = ms => new Promise(r => setTimeout(r, ms))
-
-export function getDirFromFileUrl(fileUrl) {
- return fileURLToPath(dirname(fileUrl));
-}
-
-export async function importJsFilesFromDir(absoluteDir, relativePath, whitelist = null) {
- const pathToDir = join(absoluteDir, relativePath);
-
- return new Promise((resolve, reject) => {
- readdir(pathToDir, async (err, files) => {
- if (err) {
- return reject(err);
- }
- const importPromises = files
- .filter((file) => file.endsWith(".js") && isWhitelisted(file))
- .map((file) => import(`${pathToDir}/${file}`));
- resolve(Promise.all(importPromises));
- });
- });
-
- function isWhitelisted(file) {
- // No whitelist means all files are whitelisted
- if (!Array.isArray(whitelist)) {
- return true;
- }
- return whitelist.some((whitelistedFile) => file.endsWith(whitelistedFile));
- }
-}
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.ts b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.ts
new file mode 100644
index 0000000000..99492c5474
--- /dev/null
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.ts
@@ -0,0 +1,64 @@
+import crypto from 'crypto'
+import { Request, Response, NextFunction } from 'express'
+
+import { readdir } from 'fs'
+import { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+
+type RequestWithExtraFields = Request & {
+}
+
+/**
+ * Decorator for async express middleware that handles promise rejections.
+ * @param {Func} middleware - Express middleware function.
+ * @returns Express middleware that is exactly the same as the given middleware but,
+ * if given middleware returns promise, reject of that promise will be correctly handled,
+ * meaning that error will be forwarded to next().
+ */
+export const handleRejection = (
+ middleware: (
+ req: RequestWithExtraFields,
+ res: Response,
+ next: NextFunction
+ ) => any
+) =>
+async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
+ try {
+ await middleware(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms))
+
+export function getDirPathFromFileUrl(fileUrl: string): string {
+ return fileURLToPath(dirname(fileUrl))
+}
+
+export async function importJsFilesFromDir(
+ pathToDir: string,
+ whitelistedFileNames: string[] | null = null
+): Promise {
+ return new Promise((resolve, reject) => {
+ readdir(pathToDir, async (err, files) => {
+ if (err) {
+ return reject(err)
+ }
+ const importPromises = files
+ .filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
+ .map((file) => import(`${pathToDir}/${file}`))
+ resolve(Promise.all(importPromises))
+ })
+ })
+
+ function isWhitelistedFileName(fileName: string) {
+ // No whitelist means all files are whitelisted
+ if (!Array.isArray(whitelistedFileNames)) {
+ return true
+ }
+
+ return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
+ }
+}
diff --git a/waspc/examples/crud-testing/main.wasp b/waspc/examples/crud-testing/main.wasp
index 06f0573feb..1e1807d030 100644
--- a/waspc/examples/crud-testing/main.wasp
+++ b/waspc/examples/crud-testing/main.wasp
@@ -1,6 +1,6 @@
app crudTesting {
wasp: {
- version: "^0.11.0"
+ version: "^0.12.0"
},
head: [
""
@@ -20,7 +20,10 @@ app crudTesting {
("zod", "^3.22.2")
],
db: {
- system: PostgreSQL
+ system: PostgreSQL,
+ seeds: [
+ import { migrateAuth } from "@server/seeds/migrateAuth.js"
+ ]
}
}
@@ -37,7 +40,7 @@ page LoginPage {
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
- component: import { SignupPage } from "@client/CustomSignupPage.tsx",
+ component: import { SignupPage } from "@client/SignupPage.tsx",
}
route DetailRoute { path: "/:id/:something?", to: DetailPage }
@@ -48,8 +51,6 @@ page DetailPage {
entity User {=psl
id Int @id @default(autoincrement())
- username String @unique
- password String
address String?
tasks Task[]
psl=}
@@ -91,5 +92,4 @@ job simplePrintJob {
action customSignup {
fn: import { signup } from "@server/auth.js",
- entities: [User]
}
\ No newline at end of file
diff --git a/waspc/examples/crud-testing/migrations/20231124155039_add_auth/migration.sql b/waspc/examples/crud-testing/migrations/20231124155039_add_auth/migration.sql
new file mode 100644
index 0000000000..280d1f5fc5
--- /dev/null
+++ b/waspc/examples/crud-testing/migrations/20231124155039_add_auth/migration.sql
@@ -0,0 +1,38 @@
+-- CreateTable
+CREATE TABLE "Auth" (
+ "id" TEXT NOT NULL,
+ "email" TEXT,
+ "username" TEXT,
+ "password" TEXT,
+ "isEmailVerified" BOOLEAN NOT NULL DEFAULT false,
+ "emailVerificationSentAt" TIMESTAMP(3),
+ "passwordResetSentAt" TIMESTAMP(3),
+ "userId" INTEGER,
+
+ CONSTRAINT "Auth_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SocialAuthProvider" (
+ "id" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerId" TEXT NOT NULL,
+ "authId" TEXT NOT NULL,
+
+ CONSTRAINT "SocialAuthProvider_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_email_key" ON "Auth"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_username_key" ON "Auth"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
+
+-- AddForeignKey
+ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "SocialAuthProvider" ADD CONSTRAINT "SocialAuthProvider_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/waspc/examples/crud-testing/migrations/20231212120120_inital/migration.sql b/waspc/examples/crud-testing/migrations/20231212120120_inital/migration.sql
new file mode 100644
index 0000000000..dcc286e347
--- /dev/null
+++ b/waspc/examples/crud-testing/migrations/20231212120120_inital/migration.sql
@@ -0,0 +1,24 @@
+/*
+ Warnings:
+
+ - You are about to drop the `SocialAuthProvider` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "SocialAuthProvider" DROP CONSTRAINT "SocialAuthProvider_authId_fkey";
+
+-- DropTable
+DROP TABLE "SocialAuthProvider";
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "providerName" TEXT NOT NULL,
+ "providerUserId" TEXT NOT NULL,
+ "providerData" TEXT NOT NULL DEFAULT '{}',
+ "authId" TEXT NOT NULL,
+
+ CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId")
+);
+
+-- AddForeignKey
+ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/waspc/examples/crud-testing/migrations/20231212132054_random/migration.sql b/waspc/examples/crud-testing/migrations/20231212132054_random/migration.sql
new file mode 100644
index 0000000000..90ca6265e3
--- /dev/null
+++ b/waspc/examples/crud-testing/migrations/20231212132054_random/migration.sql
@@ -0,0 +1,24 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `email` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `emailVerificationSentAt` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `isEmailVerified` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `password` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `passwordResetSentAt` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `username` on the `Auth` table. All the data in the column will be lost.
+
+*/
+-- DropIndex
+DROP INDEX "Auth_email_key";
+
+-- DropIndex
+DROP INDEX "Auth_username_key";
+
+-- AlterTable
+ALTER TABLE "Auth" DROP COLUMN "email",
+DROP COLUMN "emailVerificationSentAt",
+DROP COLUMN "isEmailVerified",
+DROP COLUMN "password",
+DROP COLUMN "passwordResetSentAt",
+DROP COLUMN "username";
diff --git a/waspc/examples/crud-testing/migrations/20231212132224_thrid/migration.sql b/waspc/examples/crud-testing/migrations/20231212132224_thrid/migration.sql
new file mode 100644
index 0000000000..5ad32a2c57
--- /dev/null
+++ b/waspc/examples/crud-testing/migrations/20231212132224_thrid/migration.sql
@@ -0,0 +1,13 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
+
+*/
+-- DropIndex
+DROP INDEX "User_username_key";
+
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "password",
+DROP COLUMN "username";
diff --git a/waspc/examples/crud-testing/src/client/MainPage.tsx b/waspc/examples/crud-testing/src/client/MainPage.tsx
index 96d604d314..6f53c22f02 100644
--- a/waspc/examples/crud-testing/src/client/MainPage.tsx
+++ b/waspc/examples/crud-testing/src/client/MainPage.tsx
@@ -1,64 +1,65 @@
-import "./Main.css";
+import './Main.css'
-import React, { useState } from "react";
-import { Link, routes } from "@wasp/router";
-import logout from "@wasp/auth/logout";
+import React, { useState } from 'react'
+import { Link, routes } from '@wasp/router'
+import logout from '@wasp/auth/logout'
+import { getUsername } from '@wasp/auth/user'
-import { tasks as tasksCrud } from "@wasp/crud/tasks";
-import { User } from "@wasp/entities";
+import { tasks as tasksCrud } from '@wasp/crud/tasks'
+import { User } from '@wasp/entities'
-const MainPage = ({ user }: { user: User }) => {
- const { data: tasks, isLoading } = tasksCrud.getAll.useQuery();
+const MainPage = () => {
+ const { data: tasks, isLoading } = tasksCrud.getAll.useQuery()
- type Task = NonNullable[number];
+ type Task = NonNullable[number]
- const createTask = tasksCrud.create.useAction();
- const deleteTask = tasksCrud.delete.useAction();
- const updateTask = tasksCrud.update.useAction();
+ const createTask = tasksCrud.create.useAction()
+ const deleteTask = tasksCrud.delete.useAction()
+ const updateTask = tasksCrud.update.useAction()
- const [newTaskTitle, setNewTaskTitle] = useState("");
- const [editTaskTitle, setEditTaskTitle] = useState("");
- const [error, setError] = useState("");
- const [isEditing, setIsEditing] = useState(null);
+ const [newTaskTitle, setNewTaskTitle] = useState('')
+ const [editTaskTitle, setEditTaskTitle] = useState('')
+ const [error, setError] = useState('')
+ const [isEditing, setIsEditing] = useState(null)
async function handleCreateTask(e: React.FormEvent) {
- setError("");
- e.preventDefault();
+ setError('')
+ e.preventDefault()
try {
await createTask({
title: newTaskTitle,
- });
+ })
} catch (err: unknown) {
- setError(`Error creating task: ${err as any}`);
+ setError(`Error creating task: ${err as any}`)
}
- setNewTaskTitle("");
+ setNewTaskTitle('')
}
async function handleUpdateTask(e: React.FormEvent) {
- setError("");
- e.preventDefault();
+ setError('')
+ e.preventDefault()
try {
- await updateTask({ id: isEditing!, title: editTaskTitle });
+ await updateTask({ id: isEditing!, title: editTaskTitle })
} catch (err: unknown) {
- setError("Error updating task.");
+ setError('Error updating task.')
}
- setIsEditing(null);
- setEditTaskTitle("");
+ setIsEditing(null)
+ setEditTaskTitle('')
}
function handleStartEditing(task: { id: number; title: string }) {
- setIsEditing(task.id);
- setEditTaskTitle(task.title);
+ setIsEditing(task.id)
+ setEditTaskTitle(task.title)
}
async function handleTaskDelete(task: { id: number }) {
try {
- if (!confirm("Are you sure you want to delete this task?")) {
- return;
+ if (!confirm('Are you sure you want to delete this task?')) {
+ return
}
- await deleteTask({ id: task.id });
+ await deleteTask({ id: task.id })
} catch (err: unknown) {
- setError("Error deleting task.");
+ setError('Error deleting task.')
}
}
@@ -92,12 +93,13 @@ const MainPage = ({ user }: { user: User }) => {
@@ -126,7 +128,7 @@ const MainPage = ({ user }: { user: User }) => {
- );
-};
+ )
+}
-export default MainPage;
+export default MainPage
diff --git a/waspc/examples/crud-testing/src/server/auth.ts b/waspc/examples/crud-testing/src/server/auth.ts
index 32dcd388e8..c0b111b628 100644
--- a/waspc/examples/crud-testing/src/server/auth.ts
+++ b/waspc/examples/crud-testing/src/server/auth.ts
@@ -5,6 +5,9 @@ import {
ensureValidPassword,
ensureValidUsername,
} from '@wasp/auth/validation.js'
+import prisma from '@wasp/dbClient.js'
+import { CustomSignup } from '@wasp/actions/types'
+import { sanitizeAndSerializeProviderData } from '@wasp/auth/utils.js'
export const fields = defineAdditionalSignupFields({
address: (data) => {
@@ -23,8 +26,6 @@ export const fields = defineAdditionalSignupFields({
},
})
-import { CustomSignup } from '@wasp/actions/types'
-
type CustomSignupInput = {
username: string
password: string
@@ -38,17 +39,28 @@ type CustomSignupOutput = {
export const signup: CustomSignup<
CustomSignupInput,
CustomSignupOutput
-> = async (args, { entities: { User } }) => {
+> = async (args) => {
ensureValidUsername(args)
ensurePasswordIsPresent(args)
ensureValidPassword(args)
try {
- await User.create({
+ await prisma.auth.create({
data: {
- username: args.username,
- password: args.password,
- address: args.address,
+ user: {
+ create: {
+ address: args.address,
+ },
+ },
+ identities: {
+ create: {
+ providerName: 'username',
+ providerUserId: args.username,
+ providerData: await sanitizeAndSerializeProviderData<'username'>({
+ hashedPassword: args.password,
+ }),
+ },
+ },
},
})
} catch (e: any) {
diff --git a/waspc/examples/crud-testing/src/server/seeds/migrateAuth.ts b/waspc/examples/crud-testing/src/server/seeds/migrateAuth.ts
new file mode 100644
index 0000000000..1739958489
--- /dev/null
+++ b/waspc/examples/crud-testing/src/server/seeds/migrateAuth.ts
@@ -0,0 +1,45 @@
+import { sanitizeAndSerializeProviderData } from '@wasp/auth/utils'
+import prisma from '@wasp/dbClient.js'
+
+export async function migrateAuth(db: typeof prisma) {
+ // 0. Update to the latest version of Wasp and run `wasp db migrate-dev`
+ // 1. Run this migration script
+ // 2. Then remove the username & password fields from User model
+ const users = await db.user.findMany()
+
+ for (let user of users) {
+ const username = (user as any).username
+ const authIdentity = await db.authIdentity.findUnique({
+ where: {
+ providerName_providerUserId: {
+ providerName: 'username',
+ providerUserId: username,
+ },
+ },
+ })
+
+ // If authIdentity already exists, skip this user
+ if (authIdentity) {
+ continue
+ }
+
+ db.auth.create({
+ data: {
+ user: {
+ connect: {
+ id: user.id,
+ },
+ },
+ identities: {
+ create: {
+ providerName: 'username',
+ providerUserId: username,
+ providerData: await sanitizeAndSerializeProviderData<'username'>({
+ hashedPassword: (user as any).password,
+ }),
+ },
+ },
+ },
+ })
+ }
+}
diff --git a/waspc/examples/crud-testing/src/server/tasks.ts b/waspc/examples/crud-testing/src/server/tasks.ts
index a79116b848..6c5c820a01 100644
--- a/waspc/examples/crud-testing/src/server/tasks.ts
+++ b/waspc/examples/crud-testing/src/server/tasks.ts
@@ -1,59 +1,47 @@
-import HttpError from "@wasp/core/HttpError.js";
-import type { GetQuery, GetAllQuery, CreateAction } from "@wasp/crud/tasks";
-import { Task, User } from "@wasp/entities";
-import { simplePrintJob } from "@wasp/jobs/simplePrintJob.js";
+import HttpError from '@wasp/core/HttpError.js'
+import type { GetQuery, GetAllQuery, CreateAction } from '@wasp/crud/tasks'
+import { Task } from '@wasp/entities'
export const getTask = (async (args, context) => {
return context.entities.Task.findUnique({
where: { id: args.id },
include: {
- user: { select: { username: true } },
+ user: {
+ // include: {
+ // auth: {
+ // select: { username: true },
+ // },
+ // },
+ },
},
- });
-}) satisfies GetQuery<
- { id: Task["id"] },
- | (Task & {
- user: Pick;
- })
- | null
->;
+ })
+}) satisfies GetQuery<{ id: Task['id'] }, {}>
export const getAllTasks = (async (args, context) => {
- const result = await simplePrintJob.submit({
- name: "moje ime",
- });
-
- await new Promise((resolve) => setTimeout(resolve, 3000));
-
- const details = await result.pgBoss.details();
-
- if (details && details.state === "completed") {
- console.log("Job started with data:", details.data);
- console.log("Job completed with output:", details.output.tasks);
- } else if (details) {
- console.log("Job state and output", details.state, details.output);
- }
-
return context.entities.Task.findMany({
- orderBy: { id: "desc" },
+ orderBy: { id: 'desc' },
select: {
id: true,
title: true,
user: {
- select: {
- username: true,
+ include: {
+ auth: {
+ include: {
+ identities: true,
+ },
+ },
},
},
},
- });
-}) satisfies GetAllQuery<{}, {}>;
+ })
+}) satisfies GetAllQuery<{}, {}>
export const createTask = (async (args, context) => {
if (!context.user) {
- throw new HttpError(401, "You must be logged in to create a task.");
+ throw new HttpError(401, 'You must be logged in to create a task.')
}
if (!args.title) {
- throw new HttpError(400, "Task title is required.");
+ throw new HttpError(400, 'Task title is required.')
}
return context.entities.Task.create({
data: {
@@ -64,5 +52,5 @@ export const createTask = (async (args, context) => {
},
},
},
- });
-}) satisfies CreateAction<{ title: Task["title"] }, Task>;
+ })
+}) satisfies CreateAction<{ title: Task['title'] }, Task>
diff --git a/waspc/examples/todoApp/migrations/20231124161113_initial/migration.sql b/waspc/examples/todoApp/migrations/20231124161113_initial/migration.sql
new file mode 100644
index 0000000000..280d1f5fc5
--- /dev/null
+++ b/waspc/examples/todoApp/migrations/20231124161113_initial/migration.sql
@@ -0,0 +1,38 @@
+-- CreateTable
+CREATE TABLE "Auth" (
+ "id" TEXT NOT NULL,
+ "email" TEXT,
+ "username" TEXT,
+ "password" TEXT,
+ "isEmailVerified" BOOLEAN NOT NULL DEFAULT false,
+ "emailVerificationSentAt" TIMESTAMP(3),
+ "passwordResetSentAt" TIMESTAMP(3),
+ "userId" INTEGER,
+
+ CONSTRAINT "Auth_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SocialAuthProvider" (
+ "id" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerId" TEXT NOT NULL,
+ "authId" TEXT NOT NULL,
+
+ CONSTRAINT "SocialAuthProvider_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_email_key" ON "Auth"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_username_key" ON "Auth"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
+
+-- AddForeignKey
+ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "SocialAuthProvider" ADD CONSTRAINT "SocialAuthProvider_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/waspc/examples/todoApp/migrations/20231124161208_remove_extras/migration.sql b/waspc/examples/todoApp/migrations/20231124161208_remove_extras/migration.sql
new file mode 100644
index 0000000000..8c10eaf5f5
--- /dev/null
+++ b/waspc/examples/todoApp/migrations/20231124161208_remove_extras/migration.sql
@@ -0,0 +1,26 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `email` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `emailVerificationSentAt` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `isEmailVerified` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `passwordResetSentAt` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the `SocialLogin` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "SocialLogin" DROP CONSTRAINT "SocialLogin_userId_fkey";
+
+-- DropIndex
+DROP INDEX "User_email_key";
+
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "email",
+DROP COLUMN "emailVerificationSentAt",
+DROP COLUMN "isEmailVerified",
+DROP COLUMN "password",
+DROP COLUMN "passwordResetSentAt";
+
+-- DropTable
+DROP TABLE "SocialLogin";
diff --git a/waspc/examples/todoApp/migrations/20231212135316_next/migration.sql b/waspc/examples/todoApp/migrations/20231212135316_next/migration.sql
new file mode 100644
index 0000000000..8d5980ab4a
--- /dev/null
+++ b/waspc/examples/todoApp/migrations/20231212135316_next/migration.sql
@@ -0,0 +1,44 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `email` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `emailVerificationSentAt` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `isEmailVerified` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `password` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `passwordResetSentAt` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the column `username` on the `Auth` table. All the data in the column will be lost.
+ - You are about to drop the `SocialAuthProvider` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "SocialAuthProvider" DROP CONSTRAINT "SocialAuthProvider_authId_fkey";
+
+-- DropIndex
+DROP INDEX "Auth_email_key";
+
+-- DropIndex
+DROP INDEX "Auth_username_key";
+
+-- AlterTable
+ALTER TABLE "Auth" DROP COLUMN "email",
+DROP COLUMN "emailVerificationSentAt",
+DROP COLUMN "isEmailVerified",
+DROP COLUMN "password",
+DROP COLUMN "passwordResetSentAt",
+DROP COLUMN "username";
+
+-- DropTable
+DROP TABLE "SocialAuthProvider";
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "providerName" TEXT NOT NULL,
+ "providerUserId" TEXT NOT NULL,
+ "providerData" TEXT NOT NULL DEFAULT '{}',
+ "authId" TEXT NOT NULL,
+
+ CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId")
+);
+
+-- AddForeignKey
+ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/waspc/examples/todoApp/src/client/App.tsx b/waspc/examples/todoApp/src/client/App.tsx
index 94c45f9541..acdbe144a7 100644
--- a/waspc/examples/todoApp/src/client/App.tsx
+++ b/waspc/examples/todoApp/src/client/App.tsx
@@ -7,6 +7,7 @@ import getDate from '@wasp/queries/getDate'
import { useSocket } from '@wasp/webSocket'
import './Main.css'
+import { getName } from './user'
export function App({ children }: any) {
const { data: user } = useAuth()
@@ -27,7 +28,7 @@ export function App({ children }: any) {
{user && (