mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
parent
7901d87a12
commit
72eda48c55
@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const user = useUser({ or: 'redirect' });
|
||||
const connection = user.useConnection('spotify', { or: 'redirect' });
|
||||
const connection = user.useConnectedAccount('spotify', { or: 'redirect' });
|
||||
const token = connection.useAccessToken();
|
||||
const [playList, setPlayList] = useState<any>();
|
||||
|
||||
@ -25,7 +25,7 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={async () => console.log(await (await user.getConnection('spotify', { or: 'redirect', scopes: ['playlist-read-private'] })).getAccessToken())}>
|
||||
<Button onClick={async () => console.log(await (await user.getConnectedAccount('spotify', { or: 'redirect', scopes: ['playlist-read-private'] })).getAccessToken())}>
|
||||
Get Spotify Playlist
|
||||
</Button>
|
||||
<div>
|
||||
@ -34,4 +34,4 @@ export default function Page() {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { stackServerApp } from "src/stack";
|
||||
|
||||
export const createTeam = async (data) => {
|
||||
const user = await stackServerApp.getServerUser();
|
||||
const user = await stackServerApp.getUser();
|
||||
if (!user) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
@ -80,11 +80,9 @@ if (error) {
|
||||
}
|
||||
```
|
||||
|
||||
## `CurrentServerUser` Object
|
||||
## `CurrentServerUser`
|
||||
|
||||
You can call `stackServerApp.getServerUser()` to get the `CurrentServerUser` object. `CurrentServerUser` has all the attributes and methods of `CurrentUser` and some additional ones shown below.
|
||||
|
||||
Note the `CurrentServerUser` should only be used on the server side because it contains server only metadata.
|
||||
If you call `getUser()` on a `StackServerApp`, you will get the `CurrentServerUser` object. It contains all the fields and methods of `CurrentUser`, but also has some additional data.
|
||||
|
||||
### Attributes
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { UserTable } from "@/components/data-table/user-table";
|
||||
|
||||
export default function PageClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const allUsers = stackAdminApp.useServerUsers();
|
||||
const allUsers = stackAdminApp.useUsers();
|
||||
|
||||
return (
|
||||
<PageLayout title="Users">
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { smartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { deindent, typedCapitalize } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
import * as yup from "yup";
|
||||
|
||||
export const POST = smartRouteHandler({
|
||||
request: yup.object({
|
||||
auth: yup.object({
|
||||
type: yup.mixed(),
|
||||
user: yup.mixed(),
|
||||
project: yup.mixed(),
|
||||
}).nullable(),
|
||||
method: yup.string().oneOf(["POST"]).required(),
|
||||
body: yup.mixed(),
|
||||
}),
|
||||
response: yup.object({
|
||||
statusCode: yup.number().oneOf([200]).required(),
|
||||
bodyType: yup.string().oneOf(["text"]).required(),
|
||||
body: yup.string().required(),
|
||||
}),
|
||||
handler: async (req) => {
|
||||
captureError("check-feature-support", new StackAssertionError(`${req.auth?.user?.primaryEmail || "User"} tried to check support of unsupported feature: ${JSON.stringify(req.body, null, 2)}`, { req }));
|
||||
return {
|
||||
statusCode: 200,
|
||||
bodyType: "text",
|
||||
body: deindent`
|
||||
${req.body?.featureName ?? "This feature"} is not yet supported. Please reach out to Stack support for more information.
|
||||
`,
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -1,3 +1,4 @@
|
||||
import { serverUserInclude } from "@/lib/users";
|
||||
import { createPrismaCrudHandlers } from "@/route-handlers/prisma-handler";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { usersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
|
||||
@ -22,9 +23,7 @@ export const usersCrudHandlers = createPrismaCrudHandlers(usersCrud, "projectUse
|
||||
},
|
||||
};
|
||||
},
|
||||
include: async () => ({
|
||||
projectUserOAuthAccounts: true,
|
||||
}),
|
||||
include: async () => serverUserInclude,
|
||||
createNotFoundError: () => new KnownErrors.UserNotFound(),
|
||||
crudToPrisma: async (crud, { auth }) => {
|
||||
const projectId = auth.project.id;
|
||||
|
||||
@ -166,7 +166,7 @@ export function TeamMemberTable(props: { members: ServerTeamMember[], team: Serv
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const promises = props.members.map(async member => {
|
||||
const user = await member.getUser();
|
||||
const user = member.user;
|
||||
const permissions = await user.listPermissions(props.team, { direct: true });
|
||||
return {
|
||||
user,
|
||||
|
||||
@ -207,8 +207,8 @@ export function PermissionListField<F extends FieldValues>(props: {
|
||||
/>
|
||||
</FormControl>
|
||||
<FieldLabel>
|
||||
{permission.id}
|
||||
{inheritedFrom && <span className="text-gray-500"> {inheritedFrom}</span>}
|
||||
{permission.id}
|
||||
{inheritedFrom && <span className="text-gray-500 ml-1">{inheritedFrom}</span>}
|
||||
</FieldLabel>
|
||||
<FormMessage />
|
||||
</label>
|
||||
|
||||
@ -93,6 +93,10 @@ export type ProjectDB = Prisma.ProjectGetPayload<{ include: FullProjectInclude }
|
||||
};
|
||||
|
||||
export async function whyNotProjectAdmin(projectId: string, adminAccessToken: string): Promise<"unparsable-access-token" | "access-token-expired" | "wrong-project-id" | "not-admin" | null> {
|
||||
if (!adminAccessToken) {
|
||||
return "unparsable-access-token";
|
||||
}
|
||||
|
||||
let decoded;
|
||||
try {
|
||||
decoded = await decodeAccessToken(adminAccessToken);
|
||||
@ -100,7 +104,7 @@ export async function whyNotProjectAdmin(projectId: string, adminAccessToken: st
|
||||
if (error instanceof KnownErrors.AccessTokenExpired) {
|
||||
return "access-token-expired";
|
||||
}
|
||||
console.warn("Failed to decode a user-provided access token. This may not be an error (for example, it could happen if the client changed Stack app hosts), but could indicate one.", error);
|
||||
console.warn("Failed to decode a user-provided admin access token. This may not be an error (for example, it could happen if the client changed Stack app hosts), but could indicate one.", error);
|
||||
return "unparsable-access-token";
|
||||
}
|
||||
const { userId, projectId: accessTokenProjectId } = decoded;
|
||||
|
||||
@ -3,14 +3,19 @@ import { TeamJson } from "@stackframe/stack-shared/dist/interface/clientInterfac
|
||||
import { ServerTeamCustomizableJson, ServerTeamJson, ServerTeamMemberJson } from "@stackframe/stack-shared/dist/interface/serverInterface";
|
||||
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { getServerUserFromDbType } from "./users";
|
||||
import { serverUserInclude } from "./users";
|
||||
|
||||
export const fullTeamMemberInclude = {
|
||||
// TODO technically we can split this; listUserTeams only needs `team`, and listServerTeams only needs `projectUser`; listTeams needs neither
|
||||
// note: this is a function to prevent circular dependencies between the teams and users file
|
||||
export const createFullTeamMemberInclude = () => ({
|
||||
team: true,
|
||||
} as const satisfies Prisma.TeamMemberInclude;
|
||||
projectUser: {
|
||||
include: serverUserInclude,
|
||||
},
|
||||
} as const satisfies Prisma.TeamMemberInclude);
|
||||
|
||||
export type ServerTeamMemberDB = Prisma.TeamMemberGetPayload<{ include: {
|
||||
projectUser: true,
|
||||
}, }>;
|
||||
export type ServerTeamMemberDB = Prisma.TeamMemberGetPayload<{ include: ReturnType<typeof createFullTeamMemberInclude> }>;
|
||||
|
||||
export async function listUserTeams(projectId: string, userId: string): Promise<TeamJson[]> {
|
||||
const members = await prismaClient.teamMember.findMany({
|
||||
@ -18,7 +23,7 @@ export async function listUserTeams(projectId: string, userId: string): Promise<
|
||||
projectId,
|
||||
projectUserId: userId,
|
||||
},
|
||||
include: fullTeamMemberInclude,
|
||||
include: createFullTeamMemberInclude(),
|
||||
});
|
||||
|
||||
return members.map((member) => ({
|
||||
@ -56,9 +61,7 @@ export async function listServerTeamMembers(projectId: string, teamId: string):
|
||||
projectId,
|
||||
teamId,
|
||||
},
|
||||
include: {
|
||||
projectUser: true
|
||||
},
|
||||
include: createFullTeamMemberInclude(),
|
||||
});
|
||||
|
||||
return members.map((member) => getServerTeamMemberFromDbType(member));
|
||||
@ -133,9 +136,26 @@ export async function removeUserFromTeam(projectId: string, teamId: string, user
|
||||
});
|
||||
}
|
||||
|
||||
export function getClientTeamFromServerTeam(team: ServerTeamJson): TeamJson {
|
||||
return {
|
||||
id: team.id,
|
||||
displayName: team.displayName,
|
||||
createdAtMillis: team.createdAtMillis,
|
||||
};
|
||||
}
|
||||
|
||||
export function getServerTeamFromDbType(team: Prisma.TeamGetPayload<{}>): ServerTeamJson {
|
||||
return {
|
||||
id: team.teamId,
|
||||
displayName: team.displayName,
|
||||
createdAtMillis: team.createdAt.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getServerTeamMemberFromDbType(member: ServerTeamMemberDB): ServerTeamMemberJson {
|
||||
return {
|
||||
userId: member.projectUserId,
|
||||
user: getServerUserFromDbType(member.projectUser),
|
||||
teamId: member.teamId,
|
||||
displayName: member.projectUser.displayName,
|
||||
};
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import { UserJson, ServerUserJson, KnownError, KnownErrors } from "@stackframe/stack-shared";
|
||||
import { UserJson, ServerUserJson, KnownErrors } from "@stackframe/stack-shared";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prismaClient } from "@/prisma-client";
|
||||
import { getProject } from "@/lib/projects";
|
||||
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
|
||||
import { UserUpdateJson } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
||||
import { ServerUserUpdateJson } from "@stackframe/stack-shared/dist/interface/serverInterface";
|
||||
import { addUserToTeam, createServerTeam } from "./teams";
|
||||
import { addUserToTeam, createServerTeam, getClientTeamFromServerTeam, getServerTeamFromDbType } from "./teams";
|
||||
|
||||
export type ServerUserDB = Prisma.ProjectUserGetPayload<{ include: {
|
||||
export const serverUserInclude = {
|
||||
projectUserOAuthAccounts: true,
|
||||
}, }>;
|
||||
selectedTeam: true,
|
||||
} as const satisfies Prisma.ProjectUserInclude;
|
||||
|
||||
export type ServerUserDB = Prisma.ProjectUserGetPayload<{ include: typeof serverUserInclude }>;
|
||||
|
||||
export async function getClientUser(projectId: string, userId: string): Promise<UserJson | null> {
|
||||
return await updateClientUser(projectId, userId, {});
|
||||
@ -24,9 +27,7 @@ export async function listServerUsers(projectId: string): Promise<ServerUserJson
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
include: {
|
||||
projectUserOAuthAccounts: true,
|
||||
},
|
||||
include: serverUserInclude,
|
||||
});
|
||||
|
||||
return users.map((u) => getServerUserFromDbType(u));
|
||||
@ -68,9 +69,7 @@ export async function updateServerUser(
|
||||
projectUserId: userId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
projectUserOAuthAccounts: true,
|
||||
},
|
||||
include: serverUserInclude,
|
||||
data: filterUndefined({
|
||||
displayName: update.displayName,
|
||||
primaryEmail: update.primaryEmail,
|
||||
@ -125,6 +124,7 @@ function getClientUserFromServerUser(serverUser: ServerUserJson): UserJson {
|
||||
hasPassword: serverUser.hasPassword,
|
||||
oauthProviders: serverUser.oauthProviders,
|
||||
selectedTeamId: serverUser.selectedTeamId,
|
||||
selectedTeam: serverUser.selectedTeam && getClientTeamFromServerTeam(serverUser.selectedTeam),
|
||||
};
|
||||
}
|
||||
|
||||
@ -146,6 +146,7 @@ export function getServerUserFromDbType(
|
||||
authWithEmail: user.authWithEmail,
|
||||
oauthProviders: user.projectUserOAuthAccounts.map((a) => a.oauthProviderConfigId),
|
||||
selectedTeamId: user.selectedTeamId,
|
||||
selectedTeam: user.selectedTeam && getServerTeamFromDbType(user.selectedTeam),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ export type UserJson = UserCustomizableJson & {
|
||||
authWithEmail: boolean,
|
||||
oauthProviders: string[],
|
||||
selectedTeamId: string | null,
|
||||
selectedTeam: TeamJson | null,
|
||||
};
|
||||
|
||||
export type UserUpdateJson = Partial<UserCustomizableJson>;
|
||||
@ -435,6 +436,18 @@ export class StackClientInterface {
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
public async checkFeatureSupport(options: { featureName?: string } & ReadonlyJson): Promise<never> {
|
||||
const res = await this.sendClientRequest("/check-feature-support", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(options),
|
||||
}, null);
|
||||
|
||||
throw new StackAssertionError(await res.text());
|
||||
}
|
||||
|
||||
async sendForgotPasswordEmail(
|
||||
email: string,
|
||||
redirectUrl: string,
|
||||
|
||||
@ -29,7 +29,9 @@ export type ServerOrglikeJson = OrglikeJson & {};
|
||||
export type ServerTeamCustomizableJson = ServerOrglikeCustomizableJson;
|
||||
export type ServerTeamJson = ServerOrglikeJson;
|
||||
|
||||
export type ServerTeamMemberJson = TeamMemberJson
|
||||
export type ServerTeamMemberJson = TeamMemberJson & {
|
||||
user: ServerUserJson,
|
||||
};
|
||||
|
||||
export type ServerPermissionDefinitionCustomizableJson = {
|
||||
readonly id: string,
|
||||
|
||||
@ -46,12 +46,12 @@ export function suspendIfSsr(caller?: string) {
|
||||
2. The component is rendered in the root (outermost) layout.tsx or template.tsx file. Next.js does not wrap those files in a Suspense boundary, even if there is a loading.tsx file in the same folder. To fix it, wrap your layout inside a route group like this:
|
||||
|
||||
- app
|
||||
- layout.tsx // contains <html> and <body>, alongside providers and other components that don't need ${caller ?? "this code path"}
|
||||
- loading.tsx // required for suspense
|
||||
- (main)
|
||||
- layout.tsx // contains the main layout of your app, like a sidebar or a header, and can use ${caller ?? "this code path"}
|
||||
- route.tsx // your actual main page
|
||||
- the rest of your app
|
||||
- - layout.tsx // contains <html> and <body>, alongside providers and other components that don't need ${caller ?? "this code path"}
|
||||
- - loading.tsx // required for suspense
|
||||
- - (main)
|
||||
- - - layout.tsx // contains the main layout of your app, like a sidebar or a header, and can use ${caller ?? "this code path"}
|
||||
- - - route.tsx // your actual main page
|
||||
- - - the rest of your app
|
||||
|
||||
For more information on this approach, see Next's documentation on route groups: https://nextjs.org/docs/app/building-your-application/routing/route-groups
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ export default async function StackHandler<HasTokenStore extends boolean>({
|
||||
}
|
||||
|
||||
async function redirectIfHasUser() {
|
||||
const user = await app.getServerUser();
|
||||
const user = await app.getUser();
|
||||
if (user) {
|
||||
redirect(app.urls.afterSignIn);
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export default function MagicLinkSignIn() {
|
||||
<FormWarningText text={errors.email?.message?.toString()} />
|
||||
|
||||
<Button disabled={sent} style={{ marginTop: '1.5rem' }} type="submit">
|
||||
{sent ? 'Email sent' : 'Send magic link'}
|
||||
{sent ? 'Email sent!' : 'Send magic link'}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -30,7 +30,7 @@ function TeamIcon(props: { displayName: string }) {
|
||||
export default function TeamSwitcher(props: TeamSwitcherProps) {
|
||||
const user = useUser();
|
||||
const router = useRouter();
|
||||
const selectedTeam = user?.useSelectedTeam();
|
||||
const selectedTeam = user?.selectedTeam;
|
||||
const rawTeams = user?.useTeams();
|
||||
const teams = useMemo(() => rawTeams?.sort((a, b) => b.id === selectedTeam?.id ? 1 : -1), [rawTeams, selectedTeam]);
|
||||
|
||||
|
||||
@ -2,16 +2,15 @@ import { CurrentUser, GetUserOptions as AppGetUserOptions, StackClientApp, Curre
|
||||
import { StackContext } from "../providers/stack-provider-client";
|
||||
import { useContext } from "react";
|
||||
|
||||
type GetUserOptions = AppGetUserOptions<true> & {
|
||||
projectIdMustMatch?: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current user object. Equivalent to `useStackApp().useUser()`.
|
||||
*
|
||||
* @returns the current user
|
||||
*/
|
||||
|
||||
type GetUserOptions = AppGetUserOptions & {
|
||||
projectIdMustMatch?: string,
|
||||
}
|
||||
|
||||
export function useUser(options: GetUserOptions & { or: 'redirect' | 'throw', projectIdMustMatch: "internal" }): CurrentInternalUser;
|
||||
export function useUser(options: GetUserOptions & { or: 'redirect' | 'throw' }): CurrentUser;
|
||||
export function useUser(options: GetUserOptions & { projectIdMustMatch: "internal" }): CurrentInternalUser | null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,6 @@ export default function StackProvider({
|
||||
}
|
||||
|
||||
function UserFetcher(props: { app: StackClientApp<true> }) {
|
||||
const userPromise = props.app.getUser().then((user) => user?.toJson() ?? null);
|
||||
const userPromise = props.app.getUser().then((user) => user?.toClientJson() ?? null);
|
||||
return <UserSetter userJsonPromise={userPromise} />;
|
||||
}
|
||||
|
||||
132
packages/stack/tsup.config.bundled_91n8y5hl35c.mjs
Normal file
132
packages/stack/tsup.config.bundled_91n8y5hl35c.mjs
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user