Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7372be0
add all evaluation fields
gabestein Oct 10, 2023
ea7316c
add checkbox and fix seed fiel
gabestein Oct 17, 2023
1045b09
style and use boolean checkbox
gabestein Oct 17, 2023
745f3a8
html and remake confidence schema
gabestein Oct 18, 2023
14d0437
make confidence into def'
gabestein Oct 18, 2023
fe78d4c
deref local stuff
gabestein Oct 19, 2023
ae465c7
support custom rendering
gabestein Oct 19, 2023
6ffee07
sliders but not working minmax
gabestein Oct 19, 2023
62e50c3
boom working slider
gabestein Oct 19, 2023
9ed77d1
add renderer files and title
gabestein Oct 19, 2023
8cc3f52
styling
gabestein Oct 19, 2023
587309c
exclude shit
gabestein Oct 19, 2023
de504c1
add default value to checkboxes
gabestein Oct 19, 2023
bfab398
style tweaks
gabestein Oct 19, 2023
7e32ef4
use component and simplify ternary
gabestein Oct 26, 2023
8f709c8
Disable changessets action
kalilsn Oct 5, 2023
ec95a65
Allow users with db records to claim supabase accounts
kalilsn Oct 11, 2023
ba4aa6e
Allow users to be created in supabase
kalilsn Oct 20, 2023
e25af39
Add script to invite users from csv
kalilsn Oct 20, 2023
784df8d
Create memberships for users
kalilsn Oct 20, 2023
4303788
rebase, memo, order fields
gabestein Nov 2, 2023
2421331
generatedSchema dependency
gabestein Nov 2, 2023
0643386
Avoid accidentally running seed script
kalilsn Oct 30, 2023
ee55f94
Use signup instead of create + reset
kalilsn Oct 30, 2023
8696294
Disable claiming accounts
kalilsn Nov 2, 2023
e3e33b3
Make sure we're setting supabase_id rather than id on user creation
kalilsn Nov 2, 2023
c7c3273
Display reset error to user
kalilsn Nov 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/changesets.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: changesets
on:
push:
branches:
- main
# on:
# push:
# branches:
# - main
env:
CI: true
PNPM_CACHE_FOLDER: .pnpm-store
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ root
└── ...
```

- `core` holds the primary app that is hosted on `www.pubpub.org`.
- `core` holds the primary app that is hosted on `v7.pubpub.org`.
- `integrations` holds the integrations developed by the PubPub team. 3rd party integrations may be developed and hosted elsewhere.
- `packages` holds libraries and npm packages that are used by `core`, `integrations`, and 3rd party integration developers.

Expand Down
2 changes: 1 addition & 1 deletion core/app/(user)/forgot/ForgotForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function ForgotForm() {
setIsLoading(true);
setFailure(false);
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: "https://www.pubpub.org/reset",
redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset`,
});
if (error) {
console.error(error);
Expand Down
12 changes: 6 additions & 6 deletions core/app/(user)/reset/ResetForm.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
"use client";
import React, { useState, FormEvent } from "react";
import { Button } from "ui";
import { supabase } from "lib/supabase";
import { formatSupabaseError, supabase } from "lib/supabase";
import { useRouter } from "next/navigation";

export default function ResetForm() {
const router = useRouter();
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [failure, setFailure] = useState(false);
const [error, setError] = useState("");

const handleSubmit = async (evt: FormEvent<EventTarget>) => {
setIsLoading(true);
setFailure(false);
setError("");
evt.preventDefault();
const { data, error } = await supabase.auth.updateUser({
password,
});
if (error) {
setIsLoading(false);
setFailure(true);
setError(formatSupabaseError(error));
} else if (data) {
setIsLoading(false);
setSuccess(true);
Expand All @@ -46,8 +46,8 @@ export default function ResetForm() {
<Button variant="outline" type="submit" disabled={!password}>
Set new password
</Button>
{failure && (
<div className={"text-red-700 my-4"}>Error reseting password</div>
{error && (
<div className={"text-red-700 my-4"}>Error resetting password: {error}</div>
)}
</form>
</div>
Expand Down
2 changes: 1 addition & 1 deletion core/app/(user)/settings/SettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function SettingsForm({
const resetPassword = async () => {
setResetIsLoading(true);
const { error } = await supabase.auth.resetPasswordForEmail(initEmail, {
redirectTo: "https://www.pubpub.org/reset",
redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset`,
});
if (error) {
console.error(error);
Expand Down
2 changes: 1 addition & 1 deletion core/app/api/supabase-webhooks/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function POST(req: NextRequest) {
try {
await prisma.user.update({
where: {
id: body.record.id
supabaseId: body.record.id
},
data: {
email: newEmail
Expand Down
73 changes: 49 additions & 24 deletions core/app/api/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { NextRequest, NextResponse } from "next/server";
import prisma from "prisma/db";
import { getServerSupabase } from "lib/supabaseServer";
import { generateHash, getSlugSuffix, slugifyString } from "lib/string";
import { getLoginId } from "lib/auth/loginId";
import { getSupabaseId } from "lib/auth/loginId";
import { BadRequestError, ForbiddenError, UnauthorizedError, handleErrors } from "~/lib/server";
import { captureException } from "@sentry/nextjs";

export type UserPostBody = {
firstName: string;
Expand All @@ -23,11 +24,22 @@ export async function POST(req: NextRequest) {
const submittedData: UserPostBody = await req.json();
const { firstName, lastName, email, password } = submittedData;
const supabase = getServerSupabase();

const existingUser = await prisma.user.findUnique({
where: {
email,
},
});

if (existingUser?.supabaseId) {
throw new ForbiddenError("User already exists");
}

const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: "https://www.pubpub.org/confirm",
emailRedirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/login`,
},
});
/* Supabase returns:
Expand All @@ -48,46 +60,59 @@ export async function POST(req: NextRequest) {
session: null
}
*/

console.log("Supabase user", data);
if (error || !data.user) {
console.error("Supabase createUser error:");
console.error(error);
console.error("Supabase createUser error: ", error);
captureException(error);
return NextResponse.json({ message: "Supabase createUser error" }, { status: 500 });
}
await prisma.user.create({
data: {
id: data.user.id,
slug: `${slugifyString(firstName)}${
lastName ? `-${slugifyString(lastName)}` : ""
}-${generateHash(4, "0123456789")}`,
firstName,
lastName: lastName || undefined,
email,
},
});

return NextResponse.json({}, { status: 201 });
if (existingUser) {
// TODO: create community membership here, update name, slug etc.
// await prisma.user.update({
// where: {
// email,
// },
// data: {
// supabaseId: data.user.id,
// },
// });
return NextResponse.json({ message: "Existing account claimed" }, { status: 200 });
} else {
const newUser = await prisma.user.create({
data: {
supabaseId: data.user.id,
slug: `${slugifyString(firstName)}${
lastName ? `-${slugifyString(lastName)}` : ""
}-${generateHash(4, "0123456789")}`,
firstName,
lastName: lastName || undefined,
email,
},
});
return NextResponse.json({message: "New user created"}, { status: 201 });
}
});
}

export async function PUT(req: NextRequest) {
return await handleErrors(async () => {
const loginId = await getLoginId(req);
if (!loginId) {
const supabaseId = await getSupabaseId(req);
if (!supabaseId) {
throw new UnauthorizedError();
}
const submittedData: UserPutBody = await req.json();
const { firstName, lastName } = submittedData;
const currentData = await prisma.user.findUnique({
where: { id: loginId },
where: { supabaseId },
});
if (!currentData) {
throw new BadRequestError("Unable to find user");
}
const slugSuffix = getSlugSuffix(currentData.slug);
await prisma.user.update({
where: {
id: loginId,
},
where: { supabaseId },
data: {
slug: `${slugifyString(firstName)}-${slugSuffix}`,
firstName,
Expand All @@ -101,8 +126,8 @@ export async function PUT(req: NextRequest) {
// Used to determine if an email is available when a user attempts to change theirs
export async function GET(req: NextRequest) {
return await handleErrors(async () => {
const loginId = await getLoginId(req);
if (!loginId) {
const supabaseId = await getSupabaseId(req);
if (!supabaseId) {
throw new UnauthorizedError();
}

Expand Down
51 changes: 46 additions & 5 deletions core/lib/auth/loginData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,60 @@ import { cache } from "react";
import { cookies } from "next/headers";
import prisma from "~/prisma/db";
import { REFRESH_NAME, TOKEN_NAME } from "~/lib/auth/cookies";
import { getIdFromJWT } from "~/lib/auth/loginId";
import { getUserInfoFromJWT } from "~/lib/auth/loginId";
import { generateHash, slugifyString } from "../string";

/* This is only called from Server Component functions */
/* When in the API, use getLoginId from loginId.ts */
export const getLoginData = cache(async () => {
const nextCookies = cookies();
const sessionJWTCookie = nextCookies.get(TOKEN_NAME) || { value: "" };
const sessionRefreshCookie = nextCookies.get(REFRESH_NAME) || { value: "" };
const loginId = await getIdFromJWT(sessionJWTCookie.value, sessionRefreshCookie.value);
if (!loginId) {
const supabaseUser = await getUserInfoFromJWT(
sessionJWTCookie.value,
sessionRefreshCookie.value
);
if (!supabaseUser?.id) {
return undefined;
}
return prisma.user.findUnique({
where: { id: loginId },
let user = await prisma.user.findUnique({
where: { supabaseId: supabaseUser.id },
});

if (!user) {
// They successfully logged in via supabase, but no corresponding record was found in the
// app database

if (!supabaseUser.email) {
throw new Error(
`Unable to create corresponding local record for supabase user ${supabaseUser.id}`
);
}

// TODO: Instead of this, we should force invited users to visit the settings screen and set
// a name before progressing
const firstName = supabaseUser.user_metadata.firstName ?? "";
const lastName = supabaseUser.user_metadata.lastName ?? null;
const communityId = supabaseUser.user_metadata.communityId;
const canAdmin = supabaseUser.user_metadata.canAdmin ?? false;

user = await prisma.user.create({
data: {
email: supabaseUser.email,
supabaseId: supabaseUser.id,
firstName,
lastName,
slug: `${slugifyString(firstName)}${
lastName ? `-${slugifyString(lastName)}` : ""
}-${generateHash(4, "0123456789")}`,
memberships: {
create: {
communityId,
canAdmin,
}
}
},
});
}
return user;
});
76 changes: 50 additions & 26 deletions core/lib/auth/loginId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,71 @@ import jwt from "jsonwebtoken";

import { getRefreshCookie, getTokenCookie } from "~/lib/auth/cookies";
import { getServerSupabase } from "~/lib/supabaseServer";
import type { UserAppMetadata, UserMetadata } from "@supabase/supabase-js";

const JWT_SECRET: string = process.env.JWT_SECRET || "";
const DATABASE_URL: string = process.env.DATABASE_URL || "";

/* This is only called from API calls */
/* When rendering server components, use getLoginData from loginData.ts */
export async function getLoginId(req: NextRequest): Promise<string> {
export async function getSupabaseId(req: NextRequest): Promise<string> {
const sessionJWT = getTokenCookie(req);
if (!sessionJWT) {
return ""
}
const refreshToken = getRefreshCookie(req);
return await getIdFromJWT(sessionJWT, refreshToken);
return await getSupabaseIdFromJWT(sessionJWT, refreshToken);
}

export async function getIdFromJWT(sessionJWT?: string, refreshToken?: string): Promise<string> {
export async function getSupabaseIdFromJWT(sessionJWT?: string, refreshToken?: string) {
if (!sessionJWT) {
return "";
}
try {
const { sub: userId } = await jwt.verify(sessionJWT, JWT_SECRET);
if (typeof userId !== "string") {
throw new Error("userId is not a string");
}
return userId;
} catch (jwtError) {
console.error("In getLoginSession", jwtError);
/* We may get a jwtError if it has expired. In which case, */
/* we try to use the refreshToken to sign back in before */
/* waiting for the client to that after initial page load. */
const supabase = getServerSupabase();
const { data, error } = await supabase.auth.refreshSession({
refresh_token: refreshToken || "",
});
if (error) {
console.error("Error refreshing session:", error.message)
return "";

const user = await getUserInfoFromJWT(sessionJWT, refreshToken)

if (!user?.id) {
return ""
}

return user.id;
}

type jwtUser = {
id: string,
email?: string,
aud: string,
app_metadata: UserAppMetadata,
user_metadata: UserMetadata,
role?: string,
}

export async function getUserInfoFromJWT(sessionJWT: string, refreshToken?: string): Promise<jwtUser | null> {
try {
const decoded = await jwt.verify(sessionJWT, JWT_SECRET);
if (typeof decoded === 'string' || !decoded.sub) {
throw new Error('Invalid jwt payload')
}
// TODO: actually validate the JWT payload!
// Rename `sub` to `id` for a consistent return type with the User that the
// refreshSession method below returns
return { id: decoded.sub, ...decoded} as jwtUser
}
if (!data?.user?.id) {
return "";
catch (jwtError) {
console.error("Error verifying jwt", jwtError);
/* We may get a jwtError if it has expired. In which case, */
/* we try to use the refreshToken to sign back in before */
/* waiting for the client to that after initial page load. */
const supabase = getServerSupabase();
const { data, error } = await supabase.auth.refreshSession({
refresh_token: refreshToken || "",
});
if (error) {
console.error("Error refreshing session:", error.message)
return null;
}
if (!data.user?.id) {
return null;
}
return data.user;
}
return data.user.id;
}
}
4 changes: 3 additions & 1 deletion core/lib/supabase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { AuthError, createClient, SupabaseClient } from "@supabase/supabase-js";

export let supabase: SupabaseClient;

Expand All @@ -13,3 +13,5 @@ export const createBrowserSupabase = () => {
},
});
};

export const formatSupabaseError = (error: AuthError) => `${error.name} ${error.status}: ${error.message}`
Loading