Skip to content

Added entityName property #762

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 58 additions & 45 deletions docs/public/stack-auth-cli-template.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,67 @@
import webbrowser
import urllib.parse


def prompt_cli_login(
*,
base_url: str = "https://api.stack-auth.com",
app_url: str,
project_id: str,
publishable_client_key: str,
*,
base_url: str = "https://api.stack-auth.com",
app_url: str,
project_id: str,
publishable_client_key: str,
):
if not app_url:
raise Exception("app_url is required and must be set to the URL of the app you're authenticating with")
if not project_id:
raise Exception("project_id is required")
if not publishable_client_key:
raise Exception("publishable_client_key is required")
if not app_url:
raise Exception(
"app_url is required and must be set to the URL of the app you're authenticating with"
)
if not project_id:
raise Exception("project_id is required")
if not publishable_client_key:
raise Exception("publishable_client_key is required")

def post(endpoint, json):
return requests.request(
'POST',
f'{base_url}{endpoint}',
headers={
'Content-Type': 'application/json',
'x-stack-project-id': project_id,
'x-stack-access-type': 'client',
'x-stack-publishable-client-key': publishable_client_key,
},
json=json,
)
def post(endpoint, json):
return requests.request(
"POST",
f"{base_url}{endpoint}",
headers={
"Content-Type": "application/json",
"x-stack-project-id": project_id,
"x-stack-access-type": "client",
"x-stack-publishable-client-key": publishable_client_key,
},
json=json,
)

# Step 1: Initiate the CLI auth process
init = post('/api/v1/auth/cli', {
'expires_in_millis': 10 * 60 * 1000,
})
if init.status_code != 200:
raise Exception(f"Failed to initiate CLI auth: {init.status_code} {init.text}")
polling_code = init.json()['polling_code']
login_code = init.json()['login_code']
# Step 1: Initiate the CLI auth process
init = post(
"/api/v1/auth/cli",
{
"expires_in_millis": 10 * 60 * 1000,
},
)
if init.status_code != 200:
raise Exception(f"Failed to initiate CLI auth: {init.status_code} {init.text}")
polling_code = init.json()["polling_code"]
login_code = init.json()["login_code"]

# Step 2: Open the browser for the user to authenticate
url = f'{app_url}/handler/cli-auth-confirm?login_code={urllib.parse.quote(login_code)}'
print(f"Opening browser to authenticate. If it doesn't open automatically, please visit:\n{url}")
webbrowser.open(url)
# Step 2: Open the browser for the user to authenticate
url = f"{app_url}/handler/cli-auth-confirm?login_code={urllib.parse.quote(login_code)}"
print(
f"Opening browser to authenticate. If it doesn't open automatically, please visit:\n{url}"
)
webbrowser.open(url)

# Step 3: Retrieve the token
while True:
status = post('/api/v1/auth/cli/poll', {
'polling_code': polling_code,
})
if status.status_code != 200 and status.status_code != 201:
raise Exception(f"Failed to get CLI auth status: {status.status_code} {status.text}")
if status.json()['status'] == 'success':
return status.json()['refresh_token']
time.sleep(2)
# Step 3: Retrieve the token
while True:
status = post(
"/api/v1/auth/cli/poll",
{
"polling_code": polling_code,
},
)
if status.status_code != 200 and status.status_code != 201:
raise Exception(
f"Failed to get CLI auth status: {status.status_code} {status.text}"
)
if status.json()["status"] == "success":
return status.json()["refresh_token"]
time.sleep(2)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"eslint-plugin-react": "^7.37.2",
"jsdom": "^24.1.3",
"only-allow": "^1.2.1",
"pluralize": "^8.0.0",
"rimraf": "^5.0.10",
"tsup": "^8.3.5",
"turbo": "^2.2.3",
Expand Down
8 changes: 6 additions & 2 deletions packages/template/src/components-page/account-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ProfilePage } from "./account-settings/profile-page/profile-page";
import { SettingsPage } from './account-settings/settings/settings-page';
import { TeamCreationPage } from './account-settings/teams/team-creation-page';
import { TeamPage } from './account-settings/teams/team-page';
import { pluralize } from "pluralize";

const Icon = ({ name }: { name: keyof typeof icons }) => {
const LucideIcon = icons[name];
Expand All @@ -24,6 +25,7 @@ const Icon = ({ name }: { name: keyof typeof icons }) => {

export function AccountSettings(props: {
fullPage?: boolean,
entityName?: string,
extraItems?: ({
title: string,
content: React.ReactNode,
Expand Down Expand Up @@ -66,6 +68,8 @@ export function AccountSettings(props: {
const userFromHook = useUser({ or: props.mockUser ? 'return-null' : 'redirect' });
const stackApp = useStackApp();
const projectFromHook = stackApp.useProject();
const entityName = props.entityName ?? "Team";
const entityPlural = pluralize(entityName);

// Use mock data if provided, otherwise use real data
const user = props.mockUser ? {
Expand Down Expand Up @@ -151,7 +155,7 @@ export function AccountSettings(props: {
content: item.content,
} as const)) || []),
...(teams.length > 0 || project.config.clientTeamCreationEnabled) ? [{
title: t('Teams'),
title: t('{entityPlural}', { entityPlural: entityPlural }),
type: 'divider',
}] as const : [],
...teams.map(team => ({
Expand All @@ -166,7 +170,7 @@ export function AccountSettings(props: {
</Suspense>,
} as const)),
...project.config.clientTeamCreationEnabled ? [{
title: t('Create a team'),
title: t('Create a {entity}', { entity: entityName.toLowerCase() }),
icon: <Icon name="CirclePlus"/>,
type: 'item',
id: 'team-creation',
Expand Down
12 changes: 8 additions & 4 deletions packages/template/src/components-page/team-creation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import { FormWarningText } from "../components/elements/form-warning";
import { MaybeFullPage } from "../components/elements/maybe-full-page";
import { useTranslation } from "../lib/translations";

export function TeamCreation(props: { fullPage?: boolean }) {
export function TeamCreation(props: {
fullPage?: boolean,
entityName?: string,
}) {
const { t } = useTranslation();
const entityName = props.entityName ?? "Team";

const schema = yupObject({
displayName: yupString().defined().nonEmpty(t('Please enter a team name')),
displayName: yupString().defined().nonEmpty(t('Please enter {entity} name', { entity: entityName.toLowerCase() }))
});

const { register, handleSubmit, formState: { errors } } = useForm({
Expand All @@ -29,7 +33,7 @@ export function TeamCreation(props: { fullPage?: boolean }) {
const navigate = app.useNavigate();

if (!project.config.clientTeamCreationEnabled) {
return <MessageCard title={t('Team creation is not enabled')} />;
return <MessageCard title={t('{entity} creation is not enabled', { entity: entityName })} />;
}

const onSubmit = async (data: yup.InferType<typeof schema>) => {
Expand All @@ -48,7 +52,7 @@ export function TeamCreation(props: { fullPage?: boolean }) {
<div className='stack-scope flex flex-col items-stretch' style={{ maxWidth: '380px', flexBasis: '380px', padding: props.fullPage ? '1rem' : 0 }}>
<div className="text-center mb-6">
<Typography type='h2'>
{t('Create a Team')}
{t('Create {entity}', {entity: entityName})}
</Typography>
</div>
<form
Expand Down
38 changes: 25 additions & 13 deletions packages/template/src/components-page/team-invitation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ const cachedGetInvitationDetails = cacheFunction(async (stackApp: StackClientApp
return await stackApp.getTeamInvitationDetails(code);
});

function TeamInvitationInner(props: { fullPage?: boolean, searchParams: Record<string, string> }) {
function TeamInvitationInner(props: {
fullPage?: boolean,
searchParams: Record<string, string>,
entityName?: string,
}) {
const { t } = useTranslation();
const stackApp = useStackApp();
const [success, setSuccess] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const details = React.use(cachedGetInvitationDetails(stackApp, props.searchParams.code || ''));
const entityName = props.entityName ?? "Team"

if (errorMessage || details.status === 'error') {
return (
Expand All @@ -33,7 +38,7 @@ function TeamInvitationInner(props: { fullPage?: boolean, searchParams: Record<s
if (success) {
return (
<MessageCard
title={t('Team invitation')}
title={t('{entity} invitation', {entity: entityName})}
fullPage={props.fullPage}
primaryButtonText="Go home"
primaryAction={() => stackApp.redirectToHome()}
Expand All @@ -46,7 +51,7 @@ function TeamInvitationInner(props: { fullPage?: boolean, searchParams: Record<s

return (
<MessageCard
title={t('Team invitation')}
title={t('{entity} invitation', {entity: entityName})}
fullPage={props.fullPage}
primaryButtonText={t('Join')}
primaryAction={() => runAsynchronouslyWithAlert(async () => {
Expand All @@ -65,26 +70,33 @@ function TeamInvitationInner(props: { fullPage?: boolean, searchParams: Record<s
);
}

export function TeamInvitation({ fullPage=false, searchParams }: { fullPage?: boolean, searchParams: Record<string, string> }) {
export function TeamInvitation(props: {
fullPage?: boolean,
searchParams: Record<string, string>
entityName?: string,
}) {
const { t } = useTranslation();
const user = useUser();
const stackApp = useStackApp();
const fullPage = props.fullPage ?? false;
const searchParams = props.searchParams;
const entityName = props.entityName ?? "Team";

const invalidJsx = (
<MessageCard title={t('Invalid Team Invitation Link')} fullPage={fullPage}>
<Typography>{t('Please double check if you have the correct team invitation link.')}</Typography>
<MessageCard title={t('Invalid {entity} Invitation Link', {entity: entityName})} fullPage={fullPage}>
<Typography>{t('Please double check if you have the correct {entity} invitation link.', {entity: entityName.toLowerCase()})}</Typography>
</MessageCard>
);

const expiredJsx = (
<MessageCard title={t('Expired Team Invitation Link')} fullPage={fullPage}>
<Typography>{t('Your team invitation link has expired. Please request a new team invitation link ')}</Typography>
<MessageCard title={t('Expired {entity} Invitation Link', {entity: entityName})} fullPage={fullPage}>
<Typography>{t('Your {entity} invitation link has expired. Please request a new {entity} invitation link ', {entity: entityName.toLowerCase()})}</Typography>
</MessageCard>
);

const usedJsx = (
<MessageCard title={t('Used Team Invitation Link')} fullPage={fullPage}>
<Typography>{t('This team invitation link has already been used.')}</Typography>
<MessageCard title={t('Used {entity} Invitation Link', {entity: entityName})} fullPage={fullPage}>
<Typography>{t('This {entity} invitation link has already been used.', {entity: entityName.toLowerCase()})}</Typography>
</MessageCard>
);

Expand All @@ -96,14 +108,14 @@ export function TeamInvitation({ fullPage=false, searchParams }: { fullPage?: bo
if (!user) {
return (
<MessageCard
title={t('Team invitation')}
title={t('{entity} invitation', {entity: entityName})}
fullPage={fullPage}
primaryButtonText={t('Sign in')}
primaryAction={() => stackApp.redirectToSignIn()}
secondaryButtonText={t('Cancel')}
secondaryAction={() => stackApp.redirectToHome()}
>
<Typography>{t('Sign in or create an account to join the team.')}</Typography>
<Typography>{t('Sign in or create an account to join the {entity}.', {entity: entityName.toLowerCase()})}</Typography>
</MessageCard>
);
}
Expand All @@ -123,5 +135,5 @@ export function TeamInvitation({ fullPage=false, searchParams }: { fullPage?: bo
}
}

return <TeamInvitationInner fullPage={fullPage} searchParams={searchParams} />;
return <TeamInvitationInner fullPage={fullPage} searchParams={searchParams} entityName={entityName}/>;
};
19 changes: 12 additions & 7 deletions packages/template/src/components/selected-team-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ import { Suspense, useEffect, useMemo } from "react";
import { Team, useStackApp, useUser } from "..";
import { useTranslation } from "../lib/translations";
import { TeamIcon } from "./team-icon";
import { pluralize } from 'pluralize';

type MockTeam = {
id: string,
displayName: string,
profileImageUrl?: string | null,
entityName?: string,
};

type SelectedTeamSwitcherProps<AllowNull extends boolean = false> = {
urlMap?: (team: AllowNull extends true ? Team | null : Team) => string,
selectedTeam?: Team,
noUpdateSelectedTeam?: boolean,
entityName?: string,
allowNull?: AllowNull,
nullLabel?: string,
onChange?: (team: AllowNull extends true ? Team | null : Team) => void,
Expand Down Expand Up @@ -78,6 +81,8 @@ function Inner<AllowNull extends boolean>(props: SelectedTeamSwitcherProps<Allow
const selectedTeam = user?.selectedTeam || props.selectedTeam;
const rawTeams = user?.useTeams();
const teams = useMemo(() => rawTeams?.sort((a, b) => b.id === selectedTeam?.id ? 1 : -1), [rawTeams, selectedTeam]);
const entityName = props.entityName ?? "Team";
const pluralEntity = pluralize(entityName)
Comment on lines +84 to +85
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider caching pluralized value in useMemo to prevent unnecessary recalculations on re-renders


useEffect(() => {
if (!props.noUpdateSelectedTeam && props.selectedTeam && !props.mockUser) {
Expand All @@ -94,7 +99,7 @@ function Inner<AllowNull extends boolean>(props: SelectedTeamSwitcherProps<Allow
if (value !== 'null-sentinel') {
team = teams?.find(team => team.id === value) || null;
if (!team) {
throw new StackAssertionError('Team not found, this should not happen');
throw new StackAssertionError('{entity} not found, this should not happen', {entity: entityName});
Copy link
Contributor

Choose a reason for hiding this comment

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

The error message in the StackAssertionError contains spaces and is not machine‐readable. Consider using a unique, identifier-like string (e.g. "selected-team-not-found") as the first argument.

Suggested change
throw new StackAssertionError('{entity} not found, this should not happen', {entity: entityName});
throw new StackAssertionError('selected-team-not-found', {entity: entityName});

This comment was generated because it violated a code review rule: mrule_dFi2eJA7OgSpYeiv.

}
} else {
team = null;
Expand All @@ -118,14 +123,14 @@ function Inner<AllowNull extends boolean>(props: SelectedTeamSwitcherProps<Allow
}}
>
<SelectTrigger className="stack-scope max-w-64">
<SelectValue placeholder="Select team"/>
<SelectValue placeholder={`Select ${entityName.toLowerCase()}`}/>
</SelectTrigger>
<SelectContent className="stack-scope">
{user?.selectedTeam ? <SelectGroup>
<SelectLabel>
<div className="flex items-center justify-between">
<span>
{t('Current team')}
{t('Current {entity}', {entity: entityName.toLowerCase()})}
</span>
<Button
variant='ghost'
Expand Down Expand Up @@ -154,14 +159,14 @@ function Inner<AllowNull extends boolean>(props: SelectedTeamSwitcherProps<Allow
<SelectItem value="null-sentinel">
<div className="flex items-center gap-2">
<TeamIcon team='personal' />
<Typography className="max-w-40 truncate">{props.nullLabel || t('No team')}</Typography>
<Typography className="max-w-40 truncate">{props.nullLabel || t('No {entity}', {entity: entityName.toLowerCase()})}</Typography>
</div>
</SelectItem>
</SelectGroup>}

{teams?.length ?
<SelectGroup>
<SelectLabel>{t('Other teams')}</SelectLabel>
<SelectLabel>{t('Other {pluralEntity}', {pluralEntity: pluralEntity})}</SelectLabel>
{teams.filter(team => team.id !== user?.selectedTeam?.id)
.map(team => (
<SelectItem value={team.id} key={team.id}>
Expand All @@ -175,7 +180,7 @@ function Inner<AllowNull extends boolean>(props: SelectedTeamSwitcherProps<Allow

{!teams?.length && !props.allowNull ?
<SelectGroup>
<SelectLabel>{t('No teams yet')}</SelectLabel>
<SelectLabel>{t('No {pluralEntity} yet', {pluralEntity: pluralEntity})}</SelectLabel>
</SelectGroup> : null}

{project.config.clientTeamCreationEnabled && <>
Expand All @@ -191,7 +196,7 @@ function Inner<AllowNull extends boolean>(props: SelectedTeamSwitcherProps<Allow
className="w-full"
variant='ghost'
>
<PlusCircle className="mr-2 h-4 w-4"/> {t('Create a team')}
<PlusCircle className="mr-2 h-4 w-4"/> {t('Create {entity}', {entity: entityName.toLowerCase()})}
</Button>
</div>
</>}
Expand Down
Loading