Cleanup and refactor AuthStore (#6086)

This commit is contained in:
Tom Moor
2023-10-28 12:43:50 -04:00
committed by GitHub
parent 3cd90f3e74
commit 1e847dc1cf
11 changed files with 136 additions and 184 deletions

View File

@@ -42,7 +42,7 @@ function Icon({ className }: { className?: string }) {
} }
export default function LanguagePrompt() { export default function LanguagePrompt() {
const { auth, ui } = useStores(); const { ui } = useStores();
const { t } = useTranslation(); const { t } = useTranslation();
const user = useCurrentUser(); const user = useCurrentUser();
const language = detectLanguage(); const language = detectLanguage();
@@ -75,9 +75,7 @@ export default function LanguagePrompt() {
<Link <Link
onClick={async () => { onClick={async () => {
ui.setLanguagePromptDismissed(); ui.setLanguagePromptDismissed();
await auth.updateUser({ await user.save({ language });
language,
});
}} }}
> >
{t("Change Language")} {t("Change Language")}

View File

@@ -93,7 +93,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
); );
provider.on("authenticationFailed", () => { provider.on("authenticationFailed", () => {
void auth.fetch().catch(() => { void auth.fetchAuth().catch(() => {
history.replace(homePath()); history.replace(homePath());
}); });
}); });

View File

@@ -28,7 +28,7 @@ import ImageInput from "./components/ImageInput";
import SettingRow from "./components/SettingRow"; import SettingRow from "./components/SettingRow";
function Details() { function Details() {
const { auth, dialogs, ui } = useStores(); const { dialogs, ui } = useStores();
const { t } = useTranslation(); const { t } = useTranslation();
const team = useCurrentTeam(); const team = useCurrentTeam();
const theme = useTheme(); const theme = useTheme();
@@ -65,7 +65,7 @@ function Details() {
} }
try { try {
await auth.updateTeam({ await team.save({
name, name,
subdomain, subdomain,
defaultCollectionId, defaultCollectionId,
@@ -80,16 +80,7 @@ function Details() {
toast.error(err.message); toast.error(err.message);
} }
}, },
[ [team, name, subdomain, defaultCollectionId, publicBranding, customTheme, t]
auth,
name,
subdomain,
defaultCollectionId,
team.preferences,
publicBranding,
customTheme,
t,
]
); );
const handleNameChange = React.useCallback( const handleNameChange = React.useCallback(
@@ -107,9 +98,7 @@ function Details() {
); );
const handleAvatarUpload = async (avatarUrl: string) => { const handleAvatarUpload = async (avatarUrl: string) => {
await auth.updateTeam({ await team.save({ avatarUrl });
avatarUrl,
});
toast.success(t("Logo updated")); toast.success(t("Logo updated"));
}; };
@@ -288,8 +277,8 @@ function Details() {
/> />
</SettingRow> </SettingRow>
<Button type="submit" disabled={auth.isSaving || !isValid}> <Button type="submit" disabled={team.isSaving || !isValid}>
{auth.isSaving ? `${t("Saving")}` : t("Save")} {team.isSaving ? `${t("Saving")}` : t("Save")}
</Button> </Button>
{can.delete && ( {can.delete && (

View File

@@ -9,23 +9,20 @@ import Scene from "~/components/Scene";
import Switch from "~/components/Switch"; import Switch from "~/components/Switch";
import Text from "~/components/Text"; import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import SettingRow from "./components/SettingRow"; import SettingRow from "./components/SettingRow";
function Features() { function Features() {
const { auth } = useStores();
const team = useCurrentTeam(); const team = useCurrentTeam();
const { t } = useTranslation(); const { t } = useTranslation();
const handlePreferenceChange = const handlePreferenceChange =
(inverted = false) => (inverted = false) =>
async (ev: React.ChangeEvent<HTMLInputElement>) => { async (ev: React.ChangeEvent<HTMLInputElement>) => {
const preferences = { team.setPreference(
...team.preferences, ev.target.name as TeamPreference,
[ev.target.name]: inverted ? !ev.target.checked : ev.target.checked, inverted ? !ev.target.checked : ev.target.checked
}; );
await team.save();
await auth.updateTeam({ preferences });
toast.success(t("Settings saved")); toast.success(t("Settings saved"));
}; };

View File

@@ -19,24 +19,23 @@ import SettingRow from "./components/SettingRow";
function Preferences() { function Preferences() {
const { t } = useTranslation(); const { t } = useTranslation();
const { dialogs, auth } = useStores(); const { dialogs } = useStores();
const user = useCurrentUser(); const user = useCurrentUser();
const team = useCurrentTeam(); const team = useCurrentTeam();
const handlePreferenceChange = const handlePreferenceChange =
(inverted = false) => (inverted = false) =>
async (ev: React.ChangeEvent<HTMLInputElement>) => { async (ev: React.ChangeEvent<HTMLInputElement>) => {
const preferences = { user.setPreference(
...user.preferences, ev.target.name as UserPreference,
[ev.target.name]: inverted ? !ev.target.checked : ev.target.checked, inverted ? !ev.target.checked : ev.target.checked
}; );
await user.save();
await auth.updateUser({ preferences });
toast.success(t("Preferences saved")); toast.success(t("Preferences saved"));
}; };
const handleLanguageChange = async (language: string) => { const handleLanguageChange = async (language: string) => {
await auth.updateUser({ language }); await user.save({ language });
toast.success(t("Preferences saved")); toast.success(t("Preferences saved"));
}; };

View File

@@ -9,12 +9,10 @@ import Input from "~/components/Input";
import Scene from "~/components/Scene"; import Scene from "~/components/Scene";
import Text from "~/components/Text"; import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser"; import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import ImageInput from "./components/ImageInput"; import ImageInput from "./components/ImageInput";
import SettingRow from "./components/SettingRow"; import SettingRow from "./components/SettingRow";
const Profile = () => { const Profile = () => {
const { auth } = useStores();
const user = useCurrentUser(); const user = useCurrentUser();
const form = React.useRef<HTMLFormElement>(null); const form = React.useRef<HTMLFormElement>(null);
const [name, setName] = React.useState<string>(user.name || ""); const [name, setName] = React.useState<string>(user.name || "");
@@ -24,9 +22,7 @@ const Profile = () => {
ev.preventDefault(); ev.preventDefault();
try { try {
await auth.updateUser({ await user.save({ name });
name,
});
toast.success(t("Profile saved")); toast.success(t("Profile saved"));
} catch (err) { } catch (err) {
toast.error(err.message); toast.error(err.message);
@@ -38,9 +34,7 @@ const Profile = () => {
}; };
const handleAvatarUpload = async (avatarUrl: string) => { const handleAvatarUpload = async (avatarUrl: string) => {
await auth.updateUser({ await user.save({ avatarUrl });
avatarUrl,
});
toast.success(t("Profile picture updated")); toast.success(t("Profile picture updated"));
}; };
@@ -49,7 +43,7 @@ const Profile = () => {
}; };
const isValid = form.current?.checkValidity(); const isValid = form.current?.checkValidity();
const { isSaving } = auth; const { isSaving } = user;
return ( return (
<Scene title={t("Profile")} icon={<ProfileIcon />}> <Scene title={t("Profile")} icon={<ProfileIcon />}>

View File

@@ -24,7 +24,7 @@ import DomainManagement from "./components/DomainManagement";
import SettingRow from "./components/SettingRow"; import SettingRow from "./components/SettingRow";
function Security() { function Security() {
const { auth, authenticationProviders, dialogs } = useStores(); const { authenticationProviders, dialogs } = useStores();
const team = useCurrentTeam(); const team = useCurrentTeam();
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
@@ -61,13 +61,13 @@ function Security() {
async (newData) => { async (newData) => {
try { try {
setData(newData); setData(newData);
await auth.updateTeam(newData); await team.save(newData);
showSuccessMessage(); showSuccessMessage();
} catch (err) { } catch (err) {
toast.error(err.message); toast.error(err.message);
} }
}, },
[auth, showSuccessMessage] [team, showSuccessMessage]
); );
const handleChange = React.useCallback( const handleChange = React.useCallback(

View File

@@ -11,7 +11,6 @@ import Input from "~/components/Input";
import NudeButton from "~/components/NudeButton"; import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip"; import Tooltip from "~/components/Tooltip";
import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import SettingRow from "./SettingRow"; import SettingRow from "./SettingRow";
type Props = { type Props = {
@@ -19,7 +18,6 @@ type Props = {
}; };
function DomainManagement({ onSuccess }: Props) { function DomainManagement({ onSuccess }: Props) {
const { auth } = useStores();
const team = useCurrentTeam(); const team = useCurrentTeam();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -35,16 +33,14 @@ function DomainManagement({ onSuccess }: Props) {
const handleSaveDomains = React.useCallback(async () => { const handleSaveDomains = React.useCallback(async () => {
try { try {
await auth.updateTeam({ await team.save({ allowedDomains });
allowedDomains,
});
onSuccess(); onSuccess();
setExistingDomainsTouched(false); setExistingDomainsTouched(false);
updateLastKnownDomainCount(allowedDomains.length); updateLastKnownDomainCount(allowedDomains.length);
} catch (err) { } catch (err) {
toast.error(err.message); toast.error(err.message);
} }
}, [auth, allowedDomains, onSuccess]); }, [team, allowedDomains, onSuccess]);
const handleRemoveDomain = async (index: number) => { const handleRemoveDomain = async (index: number) => {
const newDomains = allowedDomains.filter((_, i) => index !== i); const newDomains = allowedDomains.filter((_, i) => index !== i);
@@ -132,7 +128,7 @@ function DomainManagement({ onSuccess }: Props) {
<Button <Button
type="button" type="button"
onClick={handleSaveDomains} onClick={handleSaveDomains}
disabled={auth.isSaving} disabled={team.isSaving}
> >
<Trans>Save changes</Trans> <Trans>Save changes</Trans>
</Button> </Button>

View File

@@ -2,7 +2,7 @@ import * as Sentry from "@sentry/react";
import invariant from "invariant"; import invariant from "invariant";
import { observable, action, computed, autorun, runInAction } from "mobx"; import { observable, action, computed, autorun, runInAction } from "mobx";
import { getCookie, setCookie, removeCookie } from "tiny-cookie"; import { getCookie, setCookie, removeCookie } from "tiny-cookie";
import { CustomTheme, TeamPreferences, UserPreferences } from "@shared/types"; import { CustomTheme } from "@shared/types";
import Storage from "@shared/utils/Storage"; import Storage from "@shared/utils/Storage";
import { getCookieDomain, parseDomain } from "@shared/utils/domains"; import { getCookieDomain, parseDomain } from "@shared/utils/domains";
import RootStore from "~/stores/RootStore"; import RootStore from "~/stores/RootStore";
@@ -10,17 +10,19 @@ import Policy from "~/models/Policy";
import Team from "~/models/Team"; import Team from "~/models/Team";
import User from "~/models/User"; import User from "~/models/User";
import env from "~/env"; import env from "~/env";
import { PartialWithId } from "~/types";
import { client } from "~/utils/ApiClient"; import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop"; import Desktop from "~/utils/Desktop";
import Logger from "~/utils/Logger"; import Logger from "~/utils/Logger";
import isCloudHosted from "~/utils/isCloudHosted"; import isCloudHosted from "~/utils/isCloudHosted";
import Store from "./base/Store";
const AUTH_STORE = "AUTH_STORE"; const AUTH_STORE = "AUTH_STORE";
const NO_REDIRECT_PATHS = ["/", "/create", "/home", "/logout"]; const NO_REDIRECT_PATHS = ["/", "/create", "/home", "/logout"];
type PersistedData = { type PersistedData = {
user?: User; user?: PartialWithId<User>;
team?: Team; team?: PartialWithId<Team>;
collaborationToken?: string; collaborationToken?: string;
availableTeams?: { availableTeams?: {
id: string; id: string;
@@ -46,14 +48,14 @@ export type Config = {
providers: Provider[]; providers: Provider[];
}; };
export default class AuthStore { export default class AuthStore extends Store<Team> {
/* The user that is currently signed in. */ /* The ID of the user that is currently signed in. */
@observable @observable
user?: User | null; currentUserId?: string | null;
/* The team that the current user is signed into. */ /* The ID of the team that is currently signed in. */
@observable @observable
team?: Team | null; currentTeamId?: string | null;
/* A short-lived token to be used to authenticate with the collaboration server. */ /* A short-lived token to be used to authenticate with the collaboration server. */
@observable @observable
@@ -69,21 +71,10 @@ export default class AuthStore {
isSignedIn: boolean; isSignedIn: boolean;
}[]; }[];
/* A list of cancan policies for the current user. */
@observable
policies: Policy[] = [];
/* The authentication provider the user signed in with. */ /* The authentication provider the user signed in with. */
@observable @observable
lastSignedIn?: string | null; lastSignedIn?: string | null;
/* Whether the user is currently saving their profile or team settings. */
@observable
isSaving = false;
@observable
isFetching = true;
/* Whether the user is currently suspended. */ /* Whether the user is currently suspended. */
@observable @observable
isSuspended = false; isSuspended = false;
@@ -99,12 +90,14 @@ export default class AuthStore {
rootStore: RootStore; rootStore: RootStore;
constructor(rootStore: RootStore) { constructor(rootStore: RootStore) {
super(rootStore, Team);
this.rootStore = rootStore; this.rootStore = rootStore;
// attempt to load the previous state of this store from localstorage // attempt to load the previous state of this store from localstorage
const data: PersistedData = Storage.get(AUTH_STORE) || {}; const data: PersistedData = Storage.get(AUTH_STORE) || {};
this.rehydrate(data); this.rehydrate(data);
void this.fetch(); void this.fetchAuth();
// persists this entire store to localstorage whenever any keys are changed // persists this entire store to localstorage whenever any keys are changed
autorun(() => { autorun(() => {
@@ -138,21 +131,44 @@ export default class AuthStore {
@action @action
rehydrate(data: PersistedData) { rehydrate(data: PersistedData) {
this.user = data.user ? new User(data.user, this as any) : undefined; if (data.policies) {
this.team = data.team ? new Team(data.team, this as any) : undefined; this.addPolicies(data.policies);
}
if (data.team) {
this.add(data.team);
}
if (data.user) {
this.rootStore.users.add(data.user);
}
this.currentUserId = data.user?.id;
this.collaborationToken = data.collaborationToken; this.collaborationToken = data.collaborationToken;
this.lastSignedIn = getCookie("lastSignedIn"); this.lastSignedIn = getCookie("lastSignedIn");
this.addPolicies(data.policies);
} }
addPolicies(policies?: Policy[]) { /** The current user */
if (policies) { @computed
// cache policies in this store so that they are persisted between sessions get user() {
this.policies = policies; return this.currentUserId
policies.forEach((policy) => this.rootStore.policies.add(policy)); ? this.rootStore.users.get(this.currentUserId)
} : undefined;
} }
/** The current team */
@computed
get team() {
return this.orderedData.at(0);
}
/** The current team's policies */
@computed
get policies() {
return this.currentTeamId
? [this.rootStore.policies.get(this.currentTeamId)]
: [];
}
/** Whether the user is signed in */
@computed @computed
get authenticated(): boolean { get authenticated(): boolean {
return !!this.user && !!this.team; return !!this.user && !!this.team;
@@ -177,7 +193,7 @@ export default class AuthStore {
}; };
@action @action
fetch = async () => { fetchAuth = async () => {
this.isFetching = true; this.isFetching = true;
try { try {
@@ -185,21 +201,23 @@ export default class AuthStore {
credentials: "same-origin", credentials: "same-origin",
}); });
invariant(res?.data, "Auth not available"); invariant(res?.data, "Auth not available");
runInAction("AuthStore#fetch", () => {
runInAction("AuthStore#refresh", () => {
const { data } = res;
this.addPolicies(res.policies); this.addPolicies(res.policies);
const { user, team } = res.data; this.add(data.team);
this.user = new User(user, this as any); this.rootStore.users.add(data.user);
this.team = new Team(team, this as any); this.currentUserId = data.user.id;
this.currentTeamId = data.team.id;
this.availableTeams = res.data.availableTeams; this.availableTeams = res.data.availableTeams;
this.collaborationToken = res.data.collaborationToken; this.collaborationToken = res.data.collaborationToken;
if (env.SENTRY_DSN) { if (env.SENTRY_DSN) {
Sentry.configureScope(function (scope) { Sentry.configureScope(function (scope) {
scope.setUser({ scope.setUser({ id: this.currentUserId });
id: user.id, scope.setExtra("team", this.team.name);
}); scope.setExtra("teamId", this.team.id);
scope.setExtra("team", team.name);
scope.setExtra("teamId", team.id);
}); });
} }
@@ -207,16 +225,16 @@ export default class AuthStore {
// Occurs when the (sub)domain is changed in admin and the user hits an old url // Occurs when the (sub)domain is changed in admin and the user hits an old url
const { hostname, pathname } = window.location; const { hostname, pathname } = window.location;
if (this.team.domain) { if (data.team.domain) {
if (this.team.domain !== hostname) { if (data.team.domain !== hostname) {
window.location.href = `${team.url}${pathname}`; window.location.href = `${data.team.url}${pathname}`;
return; return;
} }
} else if ( } else if (
isCloudHosted && isCloudHosted &&
parseDomain(hostname).teamSubdomain !== (team.subdomain ?? "") parseDomain(hostname).teamSubdomain !== (data.team.subdomain ?? "")
) { ) {
window.location.href = `${team.url}${pathname}`; window.location.href = `${data.team.url}${pathname}`;
return; return;
} }
@@ -250,79 +268,28 @@ export default class AuthStore {
deleteUser = async (data: { code: string }) => { deleteUser = async (data: { code: string }) => {
await client.post(`/users.delete`, data); await client.post(`/users.delete`, data);
runInAction("AuthStore#deleteUser", () => { runInAction("AuthStore#deleteUser", () => {
this.user = null; this.currentUserId = null;
this.team = null; this.currentTeamId = null;
this.collaborationToken = null; this.collaborationToken = null;
this.availableTeams = this.availableTeams?.filter( this.availableTeams = this.availableTeams?.filter(
(team) => team.id !== this.team?.id (team) => team.id !== this.team?.id
); );
this.policies = [];
}); });
}; };
@action @action
deleteTeam = async (data: { code: string }) => { deleteTeam = async (data: { code: string }) => {
await client.post(`/teams.delete`, data); await client.post(`/teams.delete`, data);
runInAction("AuthStore#deleteTeam", () => { runInAction("AuthStore#deleteTeam", () => {
this.user = null; this.currentUserId = null;
this.currentTeamId = null;
this.availableTeams = this.availableTeams?.filter( this.availableTeams = this.availableTeams?.filter(
(team) => team.id !== this.team?.id (team) => team.id !== this.team?.id
); );
this.policies = [];
}); });
}; };
@action
updateUser = async (params: {
name?: string;
avatarUrl?: string | null;
language?: string;
preferences?: UserPreferences;
}) => {
this.isSaving = true;
const previousData = this.user?.toAPI();
try {
this.user?.updateData(params);
const res = await client.post(`/users.update`, params);
invariant(res?.data, "User response not available");
this.user?.updateData(res.data);
this.addPolicies(res.policies);
} catch (err) {
this.user?.updateData(previousData);
throw err;
} finally {
this.isSaving = false;
}
};
@action
updateTeam = async (params: {
name?: string;
avatarUrl?: string | null | undefined;
sharing?: boolean;
defaultCollectionId?: string | null;
subdomain?: string | null | undefined;
allowedDomains?: string[] | null | undefined;
preferences?: TeamPreferences;
}) => {
this.isSaving = true;
const previousData = this.team?.toAPI();
try {
this.team?.updateData(params);
const res = await client.post(`/team.update`, params);
invariant(res?.data, "Team response not available");
this.team?.updateData(res.data);
this.addPolicies(res.policies);
} catch (err) {
this.team?.updateData(previousData);
throw err;
} finally {
this.isSaving = false;
}
};
@action @action
createTeam = async (params: { name: string }) => { createTeam = async (params: { name: string }) => {
this.isSaving = true; this.isSaving = true;
@@ -378,10 +345,9 @@ export default class AuthStore {
} }
// clear all credentials from cache (and local storage via autorun) // clear all credentials from cache (and local storage via autorun)
this.user = null; this.currentUserId = null;
this.team = null; this.currentTeamId = null;
this.collaborationToken = null; this.collaborationToken = null;
this.policies = [];
// Tell the host application we logged out, if any allows window cleanup. // Tell the host application we logged out, if any allows window cleanup.
void Desktop.bridge?.onLogout?.(); void Desktop.bridge?.onLogout?.();

View File

@@ -56,11 +56,8 @@ export default class RootStore {
webhookSubscriptions: WebhookSubscriptionsStore; webhookSubscriptions: WebhookSubscriptionsStore;
constructor() { constructor() {
// PoliciesStore must be initialized before AuthStore
this.policies = new PoliciesStore(this);
this.apiKeys = new ApiKeysStore(this); this.apiKeys = new ApiKeysStore(this);
this.authenticationProviders = new AuthenticationProvidersStore(this); this.authenticationProviders = new AuthenticationProvidersStore(this);
this.auth = new AuthStore(this);
this.collections = new CollectionsStore(this); this.collections = new CollectionsStore(this);
this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this); this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this);
this.comments = new CommentsStore(this); this.comments = new CommentsStore(this);
@@ -73,6 +70,7 @@ export default class RootStore {
this.memberships = new MembershipsStore(this); this.memberships = new MembershipsStore(this);
this.notifications = new NotificationsStore(this); this.notifications = new NotificationsStore(this);
this.pins = new PinsStore(this); this.pins = new PinsStore(this);
this.policies = new PoliciesStore(this);
this.presence = new DocumentPresenceStore(); this.presence = new DocumentPresenceStore();
this.revisions = new RevisionsStore(this); this.revisions = new RevisionsStore(this);
this.searches = new SearchesStore(this); this.searches = new SearchesStore(this);
@@ -84,6 +82,9 @@ export default class RootStore {
this.views = new ViewsStore(this); this.views = new ViewsStore(this);
this.fileOperations = new FileOperationsStore(this); this.fileOperations = new FileOperationsStore(this);
this.webhookSubscriptions = new WebhookSubscriptionsStore(this); this.webhookSubscriptions = new WebhookSubscriptionsStore(this);
// AuthStore must be initialized last as it makes use of the other stores.
this.auth = new AuthStore(this);
} }
logout() { logout() {

View File

@@ -21,34 +21,46 @@ import * as T from "./schema";
const router = new Router(); const router = new Router();
const emailEnabled = !!(env.SMTP_HOST || env.ENVIRONMENT === "development"); const emailEnabled = !!(env.SMTP_HOST || env.ENVIRONMENT === "development");
const handleTeamUpdate = async (ctx: APIContext<T.TeamsUpdateSchemaReq>) => {
const { transaction } = ctx.state;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId, {
include: [{ model: TeamDomain, separate: true }],
lock: transaction.LOCK.UPDATE,
transaction,
});
authorize(user, "update", team);
const updatedTeam = await teamUpdater({
params: ctx.input.body,
user,
team,
ip: ctx.request.ip,
transaction,
});
ctx.body = {
data: presentTeam(updatedTeam),
policies: presentPolicies(user, [updatedTeam]),
};
};
router.post( router.post(
"team.update", "team.update",
rateLimiter(RateLimiterStrategy.TenPerHour), rateLimiter(RateLimiterStrategy.TenPerHour),
auth(), auth(),
validate(T.TeamsUpdateSchema), validate(T.TeamsUpdateSchema),
transaction(), transaction(),
async (ctx: APIContext<T.TeamsUpdateSchemaReq>) => { handleTeamUpdate
const { transaction } = ctx.state; );
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId, {
include: [{ model: TeamDomain }],
transaction,
});
authorize(user, "update", team);
const updatedTeam = await teamUpdater({ router.post(
params: ctx.input.body, "teams.update",
user, rateLimiter(RateLimiterStrategy.TenPerHour),
team, auth(),
ip: ctx.request.ip, validate(T.TeamsUpdateSchema),
transaction, transaction(),
}); handleTeamUpdate
ctx.body = {
data: presentTeam(updatedTeam),
policies: presentPolicies(user, [updatedTeam]),
};
}
); );
router.post( router.post(