diff --git a/apps/e2e/tests/js/app.test.ts b/apps/e2e/tests/js/app.test.ts new file mode 100644 index 000000000..60e16af42 --- /dev/null +++ b/apps/e2e/tests/js/app.test.ts @@ -0,0 +1,62 @@ +import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids"; +import { it } from "../helpers"; +import { createApp, scaffoldProject } from "./js-helpers"; + +it("should scaffold the project", async ({ expect }) => { + const { project } = await scaffoldProject(); + expect(project.displayName).toBe("New Project"); +}); + +it("should sign up with credential", async ({ expect }) => { + const { clientApp } = await createApp(); + const result1 = await clientApp.signUpWithCredential({ + email: "test@test.com", + password: "password", + verificationCallbackUrl: "http://localhost:3000", + }); + + expect(result1).toMatchInlineSnapshot(` + { + "data": undefined, + "status": "ok", + } + `); + + const result2 = await clientApp.signInWithCredential({ + email: "test@test.com", + password: "password", + }); + + expect(result2).toMatchInlineSnapshot(` + { + "data": undefined, + "status": "ok", + } + `); +}); + +it("should create user on the server", async ({ expect }) => { + const { serverApp } = await createApp(); + const user = await serverApp.createUser({ + primaryEmail: "test@test.com", + password: "password", + primaryEmailAuthEnabled: true, + }); + + expect(isUuid(user.id)).toBe(true); + + const user2 = await serverApp.getUser(user.id); + expect(user2?.id).toBe(user.id); + + const result = await serverApp.signInWithCredential({ + email: "test@test.com", + password: "password", + }); + + expect(result).toMatchInlineSnapshot(` + { + "data": undefined, + "status": "ok", + } + `); +}); diff --git a/apps/e2e/tests/js/general.test.ts b/apps/e2e/tests/js/general.test.ts deleted file mode 100644 index d7e58eefc..000000000 --- a/apps/e2e/tests/js/general.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { it } from "../helpers"; -import { scaffoldProject } from "./js-helpers"; - -it("should scaffold the project", async ({ expect }) => { - const { project } = await scaffoldProject(); - expect(project.displayName).toBe("New Project"); -}); diff --git a/apps/e2e/tests/js/js-helpers.ts b/apps/e2e/tests/js/js-helpers.ts index b8256ae2a..9dc362639 100644 --- a/apps/e2e/tests/js/js-helpers.ts +++ b/apps/e2e/tests/js/js-helpers.ts @@ -1,5 +1,4 @@ -import { AdminProjectUpdateOptions, StackAdminApp } from '@stackframe/js'; -import { wait } from '@stackframe/stack-shared/dist/utils/promises'; +import { AdminProjectUpdateOptions, StackAdminApp, StackClientApp, StackServerApp } from '@stackframe/js'; import { STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_ADMIN_KEY, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_SERVER_KEY } from '../helpers'; export async function scaffoldProject(body?: AdminProjectUpdateOptions) { @@ -19,17 +18,56 @@ export async function scaffoldProject(body?: AdminProjectUpdateOptions) { password: "password", verificationCallbackUrl: "https://stack-js-test.example.com/verify", }); - const user = await internalApp.getUser({ + const adminUser = await internalApp.getUser({ or: 'throw', }); - const project = await user.createProject({ + const project = await adminUser.createProject({ displayName: body?.displayName || 'New Project', ...body, }); return { project, - user, + adminUser, + }; +} + +export async function createApp(body?: AdminProjectUpdateOptions) { + const { project, adminUser } = await scaffoldProject(body); + const adminApp = new StackAdminApp({ + projectId: project.id, + baseUrl: STACK_BACKEND_BASE_URL, + projectOwnerSession: adminUser._internalSession, + tokenStore: "memory", + }); + + const apiKey = await adminApp.createApiKey({ + description: 'test', + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), + hasPublishableClientKey: true, + hasSecretServerKey: true, + hasSuperSecretAdminKey: false, + }); + + const serverApp = new StackServerApp({ + baseUrl: STACK_BACKEND_BASE_URL, + projectId: project.id, + publishableClientKey: apiKey.publishableClientKey, + secretServerKey: apiKey.secretServerKey, + tokenStore: "memory", + }); + + const clientApp = new StackClientApp({ + baseUrl: STACK_BACKEND_BASE_URL, + projectId: project.id, + publishableClientKey: apiKey.publishableClientKey, + tokenStore: "memory", + }); + + return { + serverApp, + clientApp, + adminApp, }; } diff --git a/packages/template/src/lib/auth.ts b/packages/template/src/lib/auth.ts index 3fa6697e1..aa224c5f8 100644 --- a/packages/template/src/lib/auth.ts +++ b/packages/template/src/lib/auth.ts @@ -19,8 +19,8 @@ export async function signInWithOAuth( const { codeChallenge, state } = await saveVerifierAndState(); const location = await iface.getOAuthUrl({ provider: options.provider, - redirectUrl: constructRedirectUrl(options.redirectUrl), - errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl), + redirectUrl: constructRedirectUrl(options.redirectUrl, "redirectUrl"), + errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl, "errorRedirectUrl"), codeChallenge, state, type: "authenticate", @@ -43,9 +43,9 @@ export async function addNewOAuthProviderOrScope( const { codeChallenge, state } = await saveVerifierAndState(); const location = await iface.getOAuthUrl({ provider: options.provider, - redirectUrl: constructRedirectUrl(options.redirectUrl), - errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl), - afterCallbackRedirectUrl: constructRedirectUrl(window.location.href), + redirectUrl: constructRedirectUrl(options.redirectUrl, "redirectUrl"), + errorRedirectUrl: constructRedirectUrl(options.errorRedirectUrl, "errorRedirectUrl"), + afterCallbackRedirectUrl: constructRedirectUrl(window.location.href, "afterCallbackRedirectUrl"), codeChallenge, state, type: "link", @@ -129,7 +129,7 @@ export async function callOAuthCallback( try { return Result.ok(await iface.callOAuthCallback({ oauthParams: consumed.originalUrl.searchParams, - redirectUri: constructRedirectUrl(redirectUrl), + redirectUri: constructRedirectUrl(redirectUrl, "redirectUri"), codeVerifier: consumed.codeVerifier, state: consumed.state, })); diff --git a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts index e8be1ad34..2e73bc2c4 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts @@ -628,14 +628,11 @@ export class _StackClientAppImplIncomplete> { - if (!options?.callbackUrl && !await this._getCurrentUrl()) { - throw new Error("Cannot send forgot password email without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `sendForgotPasswordEmail({ email, callbackUrl: ... })`"); - } - return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset)); + return await this._interface.sendForgotPasswordEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.passwordReset, "callbackUrl")); } async sendMagicLinkEmail(email: string, options?: { callbackUrl?: string }): Promise> { - if (!options?.callbackUrl && !await this._getCurrentUrl()) { - throw new Error("Cannot send magic link email without a callback URL from the server or without a redirect method. Make sure you pass the `callbackUrl` option: `sendMagicLinkEmail({ email, callbackUrl: ... })`"); - } - return await this._interface.sendMagicLinkEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback)); + return await this._interface.sendMagicLinkEmail(email, options?.callbackUrl ?? constructRedirectUrl(this.urls.magicLinkCallback, "callbackUrl")); } async resetPassword(options: { password: string, code: string }): Promise> { @@ -1398,7 +1394,7 @@ export class _StackClientAppImplIncomplete> { this._ensurePersistentTokenStore(); const session = await this._getSession(); - const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification); + const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification, "verificationCallbackUrl"); const result = await this._interface.signUpWithCredential( options.email, options.password, diff --git a/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts index 2f02acf9b..42e035014 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts @@ -1,9 +1,9 @@ import { KnownErrors, StackServerInterface } from "@stackframe/stack-shared"; import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/crud/contact-channels"; +import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions"; import { TeamInvitationCrud } from "@stackframe/stack-shared/dist/interface/crud/team-invitation"; import { TeamMemberProfilesCrud } from "@stackframe/stack-shared/dist/interface/crud/team-member-profiles"; import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions"; -import { ProjectPermissionDefinitionsCrud, ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions"; import { TeamsCrud } from "@stackframe/stack-shared/dist/interface/crud/teams"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { InternalSession } from "@stackframe/stack-shared/dist/sessions"; @@ -17,7 +17,7 @@ import { constructRedirectUrl } from "../../../../utils/url"; import { GetUserOptions, HandlerUrls, OAuthScopesOnSignIn, TokenStoreInit } from "../../common"; import { OAuthConnection } from "../../connected-accounts"; import { ServerContactChannel, ServerContactChannelCreateOptions, ServerContactChannelUpdateOptions, serverContactChannelCreateOptionsToCrud, serverContactChannelUpdateOptionsToCrud } from "../../contact-channels"; -import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminProjectPermissionDefinition } from "../../permissions"; +import { AdminProjectPermissionDefinition, AdminTeamPermission, AdminTeamPermissionDefinition } from "../../permissions"; import { EditableTeamMemberProfile, ServerListUsersOptions, ServerTeam, ServerTeamCreateOptions, ServerTeamUpdateOptions, ServerTeamUser, Team, TeamInvitation, serverTeamCreateOptionsToCrud, serverTeamUpdateOptionsToCrud } from "../../teams"; import { ProjectCurrentServerUser, ServerUser, ServerUserCreateOptions, ServerUserUpdateOptions, serverUserCreateOptionsToCrud, serverUserUpdateOptionsToCrud } from "../../users"; import { StackServerAppConstructorOptions } from "../interfaces/server-app"; @@ -151,11 +151,7 @@ export class _StackServerAppImplIncomplete