feat: role system

This commit is contained in:
2026-03-18 20:11:07 +03:00
parent 73070fe233
commit c3127b8d47
47 changed files with 2425 additions and 768 deletions

View File

@@ -0,0 +1,25 @@
import { languageInstance } from "@shared";
import { User, UserCity } from "../UserStore";
export const getMeApi = async (): Promise<User> => {
const response = await languageInstance("ru").get("/auth/me");
return response.data as User;
};
export const getMeCitiesApi = async (): Promise<{
ru: UserCity[];
en: UserCity[];
zh: UserCity[];
}> => {
const [ru, en, zh] = await Promise.all([
languageInstance("ru").get("/auth/me"),
languageInstance("en").get("/auth/me"),
languageInstance("zh").get("/auth/me"),
]);
return {
ru: ((ru.data as User).cities ?? []),
en: ((en.data as User).cities ?? []),
zh: ((zh.data as User).cities ?? []),
};
};

View File

@@ -1,15 +1,13 @@
import { API_URL, decodeJWT } from "@shared";
import { API_URL, decodeJWT, mobxFetch } from "@shared";
import { canRead as checkCanRead, canWrite as checkCanWrite } from "../../lib/permissions";
import { makeAutoObservable, runInAction } from "mobx";
import axios, { AxiosError } from "axios";
import { User, UserCity } from "../UserStore";
import { getMeApi, getMeCitiesApi } from "./api";
type LoginResponse = {
token: string;
user: {
id: number;
name: string;
email: string;
is_admin: boolean;
};
user: Pick<User, "id" | "name" | "email" | "is_admin" | "cities">;
};
class AuthStore {
@@ -48,7 +46,7 @@ class AuthStore {
{
email,
password,
}
},
);
const data = response.data;
@@ -89,6 +87,78 @@ class AuthStore {
get user() {
return this.payload?.user;
}
get isAdmin(): boolean {
return (
this.me?.is_admin === true ||
(this.me?.roles ?? []).includes("admin")
);
}
me: User | null = null;
meLoading = false;
meError: string | null = null;
meCities: { ru: UserCity[]; en: UserCity[]; zh: UserCity[] } = {
ru: [],
en: [],
zh: [],
};
getMeAction = mobxFetch<void, User, AuthStore>({
store: this,
value: "me",
loading: "meLoading",
error: "meError",
fn: getMeApi,
onSuccess: () => {
this.fetchMeCities();
},
});
fetchMeCities = async () => {
const cities = await getMeCitiesApi();
runInAction(() => {
this.meCities = cities;
});
};
canWrite = (resource: string): boolean => {
const roles = this.me?.roles ?? [];
if (roles.includes("admin")) {
return true;
}
if (resource === "map") {
return roles.some((role) =>
["routes_rw", "stations_rw", "sights_rw"].includes(role),
);
}
return checkCanWrite(roles, resource);
};
hasRole = (role: string): boolean => {
const roles = this.me?.roles ?? [];
return roles.includes("admin") || roles.includes(role);
};
canRead = (resource: string): boolean => {
if (resource === "map") {
return this.canWrite("map");
}
return checkCanRead(this.me?.roles, resource);
};
canAccess = (permission: string): boolean => {
// If permission looks like a concrete role (e.g. snapshot_create/snapshot_rw),
// check it as-is; otherwise treat it as a resource name.
if (permission.includes("_")) {
return this.hasRole(permission);
}
return this.canRead(permission);
};
}
export const authStore = new AuthStore();