Skip to content
Merged
Show file tree
Hide file tree
Changes from 103 commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
9ecfcad
feat: properly add invite table
tefkah Apr 2, 2025
172d3a9
feat: add sophisticated types/validation for invites
tefkah Apr 2, 2025
c8e3eeb
fix: add completed status
tefkah Apr 2, 2025
e29087d
feat: add invitebuilder
tefkah Apr 3, 2025
0a2bef9
chore: update pnpm-lock for some reason
tefkah Apr 3, 2025
d2dcbb1
fix: update inviteservice
tefkah Apr 3, 2025
694ae39
feat: add support for inviting users to multiple forms
tefkah Apr 7, 2025
559190a
fix: update invitebuilder to be simpler
tefkah Apr 7, 2025
6c960a8
fix: improve types once agian
tefkah Apr 7, 2025
21ade5e
fix: make the default community ids valid ids
tefkah Apr 8, 2025
13b872c
fix: improve action types
tefkah Apr 8, 2025
c0d3234
fix: improve action flow
tefkah Apr 8, 2025
fbdc4fb
fix: improve invitebuilder
tefkah Apr 8, 2025
6d93602
fix: fix db types
tefkah Apr 8, 2025
6bb8941
fix: change emails slightly
tefkah Apr 8, 2025
fa3183e
fix: improve error reporting
tefkah Apr 8, 2025
6e7e403
feat: add private form invite to croccroc
tefkah Apr 8, 2025
a09bb82
feat: add notice to user login when redirected
tefkah Apr 8, 2025
eb13c68
refactor: move notice component to components
tefkah Apr 8, 2025
6c15055
refactor: create place to put navigation logic
tefkah Apr 8, 2025
6d402b7
refactor: do actually split up magic link and invite flow, they aren'…
tefkah Apr 8, 2025
0d440e6
refactor: make the invite errors a bit simpler to check
tefkah Apr 8, 2025
0fa890b
feat: add tryCatch helper
tefkah Apr 8, 2025
51bbfc7
feat: add invite page
tefkah Apr 8, 2025
6d95e6f
chore: upgrade typescript
tefkah Apr 9, 2025
fb08de2
refactor: improve magic link construction a bit
tefkah Apr 9, 2025
a20e894
refactor: make notice params easier
tefkah Apr 9, 2025
a9cc0d3
refactor: improve invite types with full object
tefkah Apr 9, 2025
eec1dc0
feat: add notice to signup page
tefkah Apr 9, 2025
9e6ba83
fix: make sure type is still cool
tefkah Apr 9, 2025
242e8d6
chore: remove unnecessary types on password reset page
tefkah Apr 9, 2025
82d9f57
feat: add rejecting and accepting invites
tefkah Apr 9, 2025
a7c9336
feat: make signup logic slightly nicer
tefkah Apr 9, 2025
f65c72a
feat: unify signup forms, simply submitbutton
tefkah Apr 9, 2025
09a5b38
fx: make old signups more compatible with new form
tefkah Apr 9, 2025
ada70e6
fix: do not pass isX props to the actual button
tefkah Apr 9, 2025
5d67d67
docs: add some bad docs, clean up
tefkah Apr 9, 2025
f191e0b
fix: make signup and fix almost work perfectly
tefkah Apr 9, 2025
94b787e
fix: make it work!!
tefkah Apr 9, 2025
1bbb8fb
chore: remove logs
tefkah Apr 10, 2025
9a0becb
fix: improve signup email
tefkah Apr 10, 2025
ad02bad
fix: fix invite types
tefkah Apr 10, 2025
47d5450
chore: migrate
tefkah Apr 10, 2025
bc56733
fix: make createAndSend work
tefkah Apr 10, 2025
fe541e0
fix: make sure legacy invite email still works
tefkah Apr 10, 2025
2f2c8fc
fix: further refine createAndSend flow with default invite template
tefkah Apr 10, 2025
728770e
fix: fix renderwithpubcontext
tefkah Apr 10, 2025
ac5d8df
fix: remove assign test, not necessary
tefkah Apr 10, 2025
17471aa
fix: do not rely on all@pubpub.org to be member for e2e test
tefkah Apr 10, 2025
b020820
chore: merge main
tefkah Apr 10, 2025
dfb1051
refactor: move createInvite to inviteservice so we can use it in seed
tefkah Apr 10, 2025
1d9357a
feat: add ability to seed invites
tefkah Apr 10, 2025
7a704fb
test: add externalFormInviteTest
tefkah Apr 10, 2025
24a9219
test: add start of signup invite tests
tefkah Apr 10, 2025
87c3d24
test: add sooooo many tests
tefkah Apr 10, 2025
3711bbb
fix: make the tests work
tefkah Apr 10, 2025
6b2bfcc
fix: override user memberships
tefkah Apr 10, 2025
0ae30fd
fix: add data testid
tefkah Apr 10, 2025
d33b4be
fix: give up, enough tests
tefkah Apr 10, 2025
e4b3610
test: improve tests
tefkah Apr 10, 2025
5828995
chore: what did i do to pnpm-lock
tefkah Apr 10, 2025
67c8d2d
fix: fix some type errors
tefkah Apr 10, 2025
50f8132
Merge branch 'main' into tfk/invite-signup-rewrite
tefkah Apr 14, 2025
5b1442d
chore: rough merge
tefkah Apr 24, 2025
02bbdd1
refactor: simplify uselessness check
tefkah Apr 24, 2025
6dfa533
fix: fix type issues
tefkah Apr 24, 2025
94732fa
chore: rename base-signup form
tefkah Apr 24, 2025
2448c82
fix: fix test naming
tefkah Apr 24, 2025
a0f94c4
fix: fix last thing
tefkah Apr 24, 2025
318e96c
dev(seed): remove old jobs when running reset
tefkah Apr 24, 2025
622af01
fix: dont do silly stuff with the verify page
tefkah Apr 24, 2025
e352a1c
fix: fix onconflict generator
tefkah Apr 24, 2025
20ce3c5
fix: fix legacy signup redirect flow
tefkah Apr 24, 2025
8b1a05f
fix: trigger ci
tefkah Apr 24, 2025
358b23f
chore: remove logs
tefkah Apr 24, 2025
51254fa
chore: fix type issue
tefkah Apr 24, 2025
87fd7e8
chore: REALLY FIX THE TYPES NOW
tefkah Apr 24, 2025
f0b66e6
Merge branch 'main' into tfk/invite-signup-rewrite
tefkah Apr 24, 2025
d8da1f4
chore: import correct maybeWithTrx
tefkah Apr 24, 2025
7675ed4
chore: don't have weird main fn in try-catch
tefkah Apr 24, 2025
28d355c
chore: generate semi correct docs
tefkah Apr 24, 2025
defbc05
chore: cleanup test file
tefkah Apr 24, 2025
a04c178
docs: cleanup a bit
tefkah Apr 24, 2025
b849af4
chore: setup small test community st reviewing is easier
tefkah Apr 24, 2025
40f2953
fix: deterministically create invite urls
tefkah Apr 24, 2025
b4a7545
Merge branch 'main' into tfk/invite-signup-rewrite
tefkah Apr 24, 2025
a4f52a1
fix: give easy username
tefkah Apr 24, 2025
d5586e9
Merge branch 'tfk/invite-signup-rewrite' of https://github.com/pubpub…
tefkah Apr 24, 2025
ea8962c
Merge branch 'main' into tfk/invite-signup-rewrite
tefkah Apr 29, 2025
8c1de60
fix: remove unnecessary typescript update
tefkah Apr 29, 2025
b3f02f9
fix: re-add deprecation notice for default invites table
tefkah Apr 29, 2025
a569016
fix: separate out 'pubOrStage' to pub and stage
tefkah Apr 29, 2025
6b0c8c3
refactor: make migrations simpler
tefkah Apr 29, 2025
7c9ccab
fix: lint
tefkah Apr 29, 2025
0715176
fix: get rid of 'communityLevel' nomenclature
tefkah Apr 29, 2025
918bb0a
chore: remove logs
tefkah Apr 29, 2025
1540bc6
chore: update table-names
tefkah Apr 29, 2025
693dcfc
refactor(huge): first create user, then invite them
tefkah Apr 29, 2025
378c6b4
chore: lint errors
tefkah Apr 29, 2025
2014b25
fix: allow users to redirect to the correct place
tefkah Apr 29, 2025
34113fc
fix: update other place where invites are provisioned
tefkah Apr 29, 2025
b1ecd73
Merge branch 'main' into tfk/invite-signup-rewrite
tefkah Apr 29, 2025
f53f0b7
fix: consolidate member role comparison logic
tefkah Apr 30, 2025
0920840
docs: add a tiny bit of documentation for invites
tefkah Apr 30, 2025
b833860
fix: actually set invites to completed on successful accept, set to a…
tefkah Apr 30, 2025
b71dba1
Merge branch 'main' into tfk/invite-signup-rewrite
tefkah May 1, 2025
651cc35
fix: allow 'accepted' as in-between status, similar to pending
tefkah May 1, 2025
0305360
docs: add table of statuses
tefkah May 1, 2025
345e9d7
test: re-enable and make work rejection flow test
tefkah May 1, 2025
56b359b
fix+test: make clearer distinction between invalid and valid invites,…
tefkah May 1, 2025
a7d8bdf
Remove multiple memberships checks since they're enforced by database…
kalilsn May 6, 2025
620325e
Merge branch 'main' into tfk/invite-signup-rewrite
kalilsn May 6, 2025
81c3562
Merge branch 'main' into tfk/invite-signup-rewrite
kalilsn May 6, 2025
82f330e
Fix skipped tests
kalilsn May 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/.prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
app/c/\[communitySlug\]/developers/docs/stoplight.styles.css
app/c/\[communitySlug\]/developers/docs/stoplight.styles.css
vitest-bench.local.json
1 change: 1 addition & 0 deletions core/actions/_lib/runActionInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ const _runActionInstance = async (
communityId: pub.communityId as CommunitiesId,
lastModifiedBy,
actionRunId: args.actionRunId,
userId: isActionUserInitiated ? args.userId : undefined,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bc i want to track who invited a user through an email action, if any

});

if (isClientExceptionOptions(result)) {
Expand Down
172 changes: 92 additions & 80 deletions core/actions/email/run.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use server";

import { jsonObjectFrom } from "kysely/helpers/postgres";

import type { CommunityMembershipsId } from "db/public";
import { logger } from "logger";
import { assert, expect } from "utils";
Expand All @@ -11,95 +9,109 @@ import type { RenderWithPubContext } from "~/lib/server/render/pub/renderWithPub
import { db } from "~/kysely/database";
import { getCommunitySlug } from "~/lib/server/cache/getCommunitySlug";
import * as Email from "~/lib/server/email";
import { maybeWithTrx } from "~/lib/server/maybeWithTrx";
import { coalesceMemberships, selectCommunityMemberships } from "~/lib/server/member";
import { renderMarkdownWithPub } from "~/lib/server/render/pub/renderMarkdownWithPub";
import { isClientException } from "~/lib/serverActions";
import { defineRun } from "../types";

export const run = defineRun<typeof action>(async ({ pub, config, args, communityId }) => {
try {
const communitySlug = await getCommunitySlug();
const recipientEmail = args?.recipientEmail ?? config.recipientEmail;
const recipientMemberId = (args?.recipientMember ?? config.recipientMember) as
| CommunityMembershipsId
| undefined;
export const run = defineRun<typeof action>(
async ({ pub, config, args, communityId, actionRunId, userId }) => {
try {
const result = await maybeWithTrx(db, async (trx) => {
const communitySlug = await getCommunitySlug();
const recipientEmail = args?.recipientEmail ?? config.recipientEmail;
const recipientMemberId = (args?.recipientMember ?? config.recipientMember) as
| CommunityMembershipsId
| undefined;

assert(
recipientEmail !== undefined || recipientMemberId !== undefined,
"No email recipient was specified"
);
assert(
recipientEmail !== undefined || recipientMemberId !== undefined,
"No recipient was specified for email"
);

let recipient: RenderWithPubContext["recipient"] | undefined;
let recipient: RenderWithPubContext["recipient"] | undefined;

if (recipientMemberId !== undefined) {
recipient = await db
.selectFrom("community_memberships")
.select((eb) => [
"community_memberships.id",
jsonObjectFrom(
eb
.selectFrom("users")
.whereRef("users.id", "=", "community_memberships.userId")
.selectAll("users")
)
.$notNull()
.as("user"),
])
.where("id", "=", recipientMemberId)
.executeTakeFirstOrThrow(
() => new Error(`Could not find member with ID ${recipientMemberId}`)
);
}
if (recipientMemberId !== undefined) {
const memberships = await selectCommunityMemberships({
id: recipientMemberId,
}).execute();
if (!memberships.length) {
throw new Error(`Could not find member with ID ${recipientMemberId}`);
}

const renderMarkdownWithPubContext = {
communityId,
communitySlug,
recipient,
pub,
} as RenderWithPubContext;
const membership = coalesceMemberships(memberships);

const html = await renderMarkdownWithPub(
args?.body ?? config.body,
renderMarkdownWithPubContext
);
const subject = await renderMarkdownWithPub(
args?.subject ?? config.subject,
renderMarkdownWithPubContext,
true
);
recipient = {
id: membership.id,
user: membership.user,
};
} else if (recipientEmail !== undefined) {
recipient = {
email: recipientEmail,
};
} else {
throw new Error("No recipient was specified");
}

const result = await Email.generic({
to: expect(recipient?.user.email ?? recipientEmail),
subject,
html,
}).send();
const renderMarkdownWithPubContext = {
communityId,
communitySlug,
recipient,
pub,
inviter: {
userId,
actionRunId,
},
trx,
} as RenderWithPubContext;

if (isClientException(result)) {
logger.error({
msg: "An error occurred while sending an email",
error: result.error,
pub,
config,
args,
renderMarkdownWithPubContext,
});
} else {
logger.info({
msg: "Successfully sent email",
pub,
config,
args,
renderMarkdownWithPubContext,
});
}
const html = await renderMarkdownWithPub(
args?.body ?? config.body,
renderMarkdownWithPubContext
);
const subject = await renderMarkdownWithPub(
args?.subject ?? config.subject,
renderMarkdownWithPubContext,
true
);

const result = await Email.generic({
to: expect(recipient.email ?? recipient.user.email),
subject,
html,
}).send();

return result;
} catch (error) {
logger.error({ msg: "Failed to send email", error });
if (isClientException(result)) {
logger.error({
msg: "An error occurred while sending an email",
error: result.error,
pub,
config,
args,
renderMarkdownWithPubContext,
});
} else {
logger.info({
msg: "Successfully sent email",
pub,
config,
args,
renderMarkdownWithPubContext,
});
}

return {
title: "Failed to Send Email",
error: error.message,
cause: error,
};
return result;
});
return result;
} catch (error) {
logger.error({ msg: "Failed to send email", error });

return {
title: "Failed to Send Email",
error: error.message,
cause: error,
};
}
}
});
);
5 changes: 5 additions & 0 deletions core/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ActionRunsId,
CommunitiesId,
StagesId,
UsersId,
} from "db/public";
import type { LastModifiedBy } from "db/types";
import type { Dependency, FieldConfig, FieldConfigItem } from "ui/auto-form";
Expand Down Expand Up @@ -47,6 +48,10 @@ export type RunProps<T extends Action> =
*/
lastModifiedBy: LastModifiedBy;
actionRunId: ActionRunsId;
/**
* The user ID of the user who initiated the action, if any
*/
userId?: UsersId;
}
: never;

Expand Down
18 changes: 0 additions & 18 deletions core/app/(user)/login/Notice.tsx

This file was deleted.

11 changes: 9 additions & 2 deletions core/app/(user)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { redirect } from "next/navigation";

import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants";
import { getLoginData } from "~/lib/authentication/loginData";
import { Notice } from "../../components/Notice";
import LoginForm from "./LoginForm";

export default async function Login({
searchParams,
}: {
searchParams: {
searchParams: Promise<{
error?: string;
};
notice?: string;
body?: string;
}>;
}) {
const { user } = await getLoginData();

Expand All @@ -27,9 +30,13 @@ export default async function Login({
redirect("/settings");
}

const { notice, error, body } = await searchParams;

return (
<div className="mx-auto max-w-sm">
<LoginForm />
{notice && <Notice type="notice" title={notice} body={body} />}
{error && <Notice type="error" title={error} body={body} />}
Comment on lines +38 to +39
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also a new addition: the login screen now can display notices. by default, if redirected to here bc of getPageLoginData, itll display an error saying "you need to be signed in to access that page". i added this bc i need the user to sign in to their account if its an invite for an existing account, and i think its nice to show why you need to do something on the login page, rather than just being shown a blank one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

big improvement, i think we'll use this a lot!

{/* <div className="text-gray-600 text-center mt-6">
Don't have an account?{" "}
<Link
Expand Down
73 changes: 53 additions & 20 deletions core/app/(user)/magic-link/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { logger } from "logger";
import { db } from "~/kysely/database";
import { lucia } from "~/lib/authentication/lucia";
import { createRedirectUrl } from "~/lib/redirect";
import { redirectToLogin } from "~/lib/server/navigation/redirects";
import { InvalidTokenError, TokenFailureReason, validateToken } from "~/lib/server/token";

const redirectToURL = (
Expand Down Expand Up @@ -53,15 +54,7 @@ const handleInvalidToken = ({
return redirectToURL(`/invalid-token?redirectTo=${encodeURIComponent(redirectTo)}`);
};

export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams;
const token = searchParams.get("token");
const redirectTo = searchParams.get("redirectTo");

if (!token || !redirectTo) {
return NextResponse.redirect(new URL("/login", req.url));
}

const handleTokenFlow = async (token: string, redirectTo: string, req: NextRequest) => {
const validatedTokenPromise = validateToken(token);

const currentSessionCookie = (await cookies()).get(lucia.sessionCookieName)?.value;
Expand All @@ -77,21 +70,22 @@ export async function GET(req: NextRequest) {

if (tokenSettled.status === "rejected") {
logger.debug({ msg: "Token validation failed", reason: tokenSettled.reason });
if (!(tokenSettled.reason instanceof InvalidTokenError)) {
logger.error({
msg: `Token validation unexpectedly failed with reason: ${tokenSettled.reason}`,
reason: tokenSettled.reason,
});

throw tokenSettled.reason;
if (tokenSettled.reason instanceof InvalidTokenError) {
return handleInvalidToken({
redirectTo,
tokenType: tokenSettled.reason.tokenType,
reason: tokenSettled.reason.reason,
token,
});
}

return handleInvalidToken({
redirectTo,
tokenType: tokenSettled.reason.tokenType,
reason: tokenSettled.reason.reason,
token,
logger.error({
msg: `Token validation unexpectedly failed with reason: ${tokenSettled.reason}`,
reason: tokenSettled.reason,
});

throw tokenSettled.reason;
}

const currentSession =
Expand Down Expand Up @@ -127,4 +121,43 @@ export async function GET(req: NextRequest) {
});

return redirectToURL(redirectTo, req);
};

export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams;
const token = searchParams.get("token");
const redirectTo = searchParams.get("redirectTo");

if (!redirectTo) {
logger.error({
msg: "Magic link did not contain a redirectTo",
url: req.nextUrl,
cookies: req.cookies.getAll(),
});
return redirectToLogin({
loginNotice: {
type: "error",
title: "Your magic link is invalid",
},
});
}

if (token) {
return handleTokenFlow(token, redirectTo, req);
}

logger.error({
msg: "Magic link did not contain a token",
url: req.nextUrl,
cookies: req.cookies.getAll(),
});

return redirectToLogin({
loginNotice: {
type: "error",
title: "Your magic link is invalid",
// maybe to expressive to users
body: "You magic link did not contain a magic link token.",
},
});
}
Loading
Loading