Merge remote-tracking branch 'origin' into token-auth

This commit is contained in:
Tuan Dang
2024-07-09 11:52:14 +07:00
22 changed files with 390 additions and 28 deletions
@@ -83,6 +83,82 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/user-management/users",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
searchTerm: z.string().default(""),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(20)
}),
response: {
200: z.object({
users: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
}).array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const users = await server.services.superAdmin.getUsers({
...req.query
});
return {
users
};
}
});
server.route({
method: "DELETE",
url: "/user-management/users/:userId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
userId: z.string()
}),
response: {
200: z.object({
users: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
})
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const users = await server.services.superAdmin.deleteUser(req.params.userId);
return {
users
};
}
});
server.route({ server.route({
method: "POST", method: "POST",
url: "/signup", url: "/signup",
+1 -1
View File
@@ -297,7 +297,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const user = await server.services.user.deleteMe(req.permission.id); const user = await server.services.user.deleteUser(req.permission.id);
return { user }; return { user };
} }
}); });
@@ -12,7 +12,7 @@ import { AuthMethod } from "../auth/auth-type";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminSignUpDTO } from "./super-admin-types"; import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
type TSuperAdminServiceFactoryDep = { type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory; serverCfgDAL: TSuperAdminDALFactory;
@@ -209,9 +209,25 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization }; return { token, user: userInfo, organization };
}; };
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({
limit,
offset,
searchTerm,
sortBy: "username"
});
};
const deleteUser = async (userId: string) => {
const user = await userDAL.deleteById(userId);
return user;
};
return { return {
initServerCfg, initServerCfg,
updateServerCfg, updateServerCfg,
adminSignUp adminSignUp,
getUsers,
deleteUser
}; };
}; };
@@ -16,6 +16,12 @@ export type TAdminSignUpDTO = {
userAgent: string; userAgent: string;
}; };
export type TAdminGetUsersDTO = {
offset: number;
limit: number;
searchTerm: string;
};
export enum LoginMethod { export enum LoginMethod {
EMAIL = "email", EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
+38 -3
View File
@@ -7,10 +7,11 @@ import {
TUserActionsUpdate, TUserActionsUpdate,
TUserEncryptionKeys, TUserEncryptionKeys,
TUserEncryptionKeysInsert, TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate TUserEncryptionKeysUpdate,
TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TUserDALFactory = ReturnType<typeof userDALFactory>; export type TUserDALFactory = ReturnType<typeof userDALFactory>;
@@ -18,6 +19,39 @@ export const userDALFactory = (db: TDbClient) => {
const userOrm = ormify(db, TableName.Users); const userOrm = ormify(db, TableName.Users);
const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx); const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx);
const getUsersByFilter = async ({
limit,
offset,
searchTerm,
sortBy
}: {
limit: number;
offset: number;
searchTerm: string;
sortBy?: keyof TUsers;
}) => {
try {
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
if (searchTerm) {
query = query.where((qb) => {
void qb
.whereILike("email", `%${searchTerm}%`)
.orWhereILike("firstName", `%${searchTerm}%`)
.orWhereILike("lastName", `%${searchTerm}%`)
.orWhereLike("username", `%${searchTerm}%`);
});
}
if (sortBy) {
query = query.orderBy(sortBy);
}
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users));
} catch (error) {
throw new DatabaseError({ error, name: "Get users by filter" });
}
};
// USER ENCRYPTION FUNCTIONS // USER ENCRYPTION FUNCTIONS
// ------------------------- // -------------------------
const findUserEncKeyByUsername = async ({ username }: { username: string }) => { const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
@@ -159,6 +193,7 @@ export const userDALFactory = (db: TDbClient) => {
upsertUserEncryptionKey, upsertUserEncryptionKey,
createUserEncryption, createUserEncryption,
findOneUserAction, findOneUserAction,
createUserAction createUserAction,
getUsersByFilter
}; };
}; };
+2 -2
View File
@@ -201,7 +201,7 @@ export const userServiceFactory = ({
return user; return user;
}; };
const deleteMe = async (userId: string) => { const deleteUser = async (userId: string) => {
const user = await userDAL.deleteById(userId); const user = await userDAL.deleteById(userId);
return user; return user;
}; };
@@ -301,7 +301,7 @@ export const userServiceFactory = ({
toggleUserMfa, toggleUserMfa,
updateUserName, updateUserName,
updateAuthMethods, updateAuthMethods,
deleteMe, deleteUser,
getMe, getMe,
createUserAction, createUserAction,
getUserAction, getUserAction,
@@ -49,8 +49,8 @@ Server-related logic is handled in `/src/server`. To connect the service layer t
## Writing API Routes ## Writing API Routes
1. To create a route component, run `npm generate:component`. 1. To create a route component, run `npm run generate:component`.
2. Select option 3, type the router name in dash-case, and provide the version number. This will generate a router file in `src/server/routes/v<version-number>/<router component name>` 2. Select option 3, type the router name in dash-case, and provide the version number. This will generate a router file in `src/server/routes/v<version-number>/<router component name>`
1. Implement your logic to connect with the service layer as needed. 1. Implement your logic to connect with the service layer as needed.
2. Import the router component in the version folder's index.ts. For instance, if it's in v1, import it in `v1/index.ts`. 2. Import the router component in the version folder's index.ts. For instance, if it's in v1, import it in `v1/index.ts`.
3. Finally, register it under the appropriate prefix for access. 3. Finally, register it under the appropriate prefix for access.
@@ -5,7 +5,7 @@ description: "Learn how to share time & view-count bound secrets securely with a
--- ---
Developers frequently need to share secrets with team members, contractors, or other third parties, which can be risky due to potential leaks or misuse. Developers frequently need to share secrets with team members, contractors, or other third parties, which can be risky due to potential leaks or misuse.
Infisical offers a secure solution for sharing secrets over the internet in a time and view count bound manner. Infisical offers a secure solution for sharing secrets over the internet in a time and view count bound manner. It is possible to share secrets without signing up via [share.infisical.com](https://share.infisical.com) or via Infisical Dashboard (which has more advanced funcitonality).
With its zero-knowledge architecture, secrets shared via Infisical remain unreadable even to Infisical itself. With its zero-knowledge architecture, secrets shared via Infisical remain unreadable even to Infisical itself.
+1 -1
View File
@@ -81,7 +81,7 @@ const AlertDescription = forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div ref={ref} className={twMerge("text-sm [&_p]:leading-relaxed", className)} {...props} /> <div ref={ref} className={twMerge("text-sm", className)} {...props} />
)); ));
AlertDescription.displayName = "AlertDescription"; AlertDescription.displayName = "AlertDescription";
+2 -2
View File
@@ -1,2 +1,2 @@
export { useCreateAdminUser, useUpdateServerConfig } from "./mutation"; export { useAdminDeleteUser, useCreateAdminUser, useUpdateServerConfig } from "./mutation";
export { useGetServerConfig } from "./queries"; export { useAdminGetUsers, useGetServerConfig } from "./queries";
+17 -1
View File
@@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request";
import { organizationKeys } from "../organization/queries"; import { organizationKeys } from "../organization/queries";
import { User } from "../users/types"; import { User } from "../users/types";
import { adminQueryKeys } from "./queries"; import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import { TCreateAdminUserDTO, TServerConfig } from "./types"; import { TCreateAdminUserDTO, TServerConfig } from "./types";
export const useCreateAdminUser = () => { export const useCreateAdminUser = () => {
@@ -43,3 +43,19 @@ export const useUpdateServerConfig = () => {
} }
}); });
}; };
export const useAdminDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userId: string) => {
await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}`);
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getUsers]
});
}
});
};
+30 -3
View File
@@ -1,11 +1,17 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TServerConfig } from "./types"; import { User } from "../types";
import { AdminGetUsersFilters, TServerConfig } from "./types";
export const adminStandaloneKeys = {
getUsers: "get-users"
};
export const adminQueryKeys = { export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const
}; };
const fetchServerConfig = async () => { const fetchServerConfig = async () => {
@@ -32,3 +38,24 @@ export const useGetServerConfig = ({
...options, ...options,
enabled: options?.enabled ?? true enabled: options?.enabled ?? true
}); });
export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
return useInfiniteQuery({
queryKey: adminQueryKeys.getUsers(filters),
queryFn: async ({ pageParam }) => {
const { data } = await apiRequest.get<{ users: User[] }>(
"/api/v1/admin/user-management/users",
{
params: {
...filters,
offset: pageParam
}
}
);
return data.users;
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined
});
};
+5
View File
@@ -37,3 +37,8 @@ export type TCreateAdminUserDTO = {
verifier: string; verifier: string;
salt: string; salt: string;
}; };
export type AdminGetUsersFilters = {
limit: number;
searchTerm: string;
};
+1 -1
View File
@@ -9,8 +9,8 @@ export {
useAddUserToOrg, useAddUserToOrg,
useCreateAPIKey, useCreateAPIKey,
useDeleteAPIKey, useDeleteAPIKey,
useDeleteMe,
useDeleteOrgMembership, useDeleteOrgMembership,
useDeleteUser,
useGetMyAPIKeys, useGetMyAPIKeys,
useGetMyAPIKeysV2, useGetMyAPIKeysV2,
useGetMyIp, useGetMyIp,
+2 -1
View File
@@ -28,6 +28,7 @@ export const userKeys = {
myAPIKeys: ["api-keys"] as const, myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const, myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const, mySessions: ["sessions"] as const,
listUsers: ["user-list"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
}; };
@@ -40,7 +41,7 @@ export const fetchUserDetails = async () => {
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails); export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
export const useDeleteUser = () => { export const useDeleteMe = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@@ -15,6 +15,8 @@ import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { import {
Alert,
AlertDescription,
Button, Button,
Card, Card,
CardTitle, CardTitle,
@@ -40,7 +42,7 @@ export default function FlyioCreateIntegrationPage() {
const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById( const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById(
(integrationAuthId as string) ?? "" (integrationAuthId as string) ?? ""
); );
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = const { data: integrationAuthApps = [], isLoading: isIntegrationAuthAppsLoading } =
useGetIntegrationAuthApps({ useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "" integrationAuthId: (integrationAuthId as string) ?? ""
}); });
@@ -130,6 +132,13 @@ export default function FlyioCreateIntegrationPage() {
</Link> </Link>
</div> </div>
</CardTitle> </CardTitle>
<div className="px-6 pb-4">
<Alert hideTitle variant="warning">
<AlertDescription>
All current secrets linked to the related Fly.io project will be deleted before Infisical secrets are pushed to your Fly.io project.
</AlertDescription>
</Alert>
</div>
<FormControl label="Project Environment" className="px-6"> <FormControl label="Project Environment" className="px-6">
<Select <Select
value={selectedSourceEnvironment} value={selectedSourceEnvironment}
@@ -2,17 +2,17 @@ import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { useDeleteUser } from "@app/hooks/api"; import { useDeleteMe } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteAccountSection = () => { export const DeleteAccountSection = () => {
const router = useRouter(); const router = useRouter();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteAccount" "deleteAccount"
] as const); ] as const);
const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteUser(); const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteMe();
const handleDeleteAccountSubmit = async () => { const handleDeleteAccountSubmit = async () => {
try { try {
@@ -26,11 +26,13 @@ import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
import { AuthPanel } from "./AuthPanel"; import { AuthPanel } from "./AuthPanel";
import { RateLimitPanel } from "./RateLimitPanel"; import { RateLimitPanel } from "./RateLimitPanel";
import { UserPanel } from "./UserPanel";
enum TabSections { enum TabSections {
Settings = "settings", Settings = "settings",
Auth = "auth", Auth = "auth",
RateLimit = "rate-limit" RateLimit = "rate-limit",
Users = "users"
} }
enum SignUpModes { enum SignUpModes {
@@ -135,6 +137,7 @@ export const AdminDashboardPage = () => {
<Tab value={TabSections.Settings}>General</Tab> <Tab value={TabSections.Settings}>General</Tab>
<Tab value={TabSections.Auth}>Authentication</Tab> <Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.RateLimit}>Rate Limit</Tab> <Tab value={TabSections.RateLimit}>Rate Limit</Tab>
<Tab value={TabSections.Users}>Users</Tab>
</div> </div>
</TabList> </TabList>
<TabPanel value={TabSections.Settings}> <TabPanel value={TabSections.Settings}>
@@ -320,6 +323,9 @@ export const AdminDashboardPage = () => {
<TabPanel value={TabSections.RateLimit}> <TabPanel value={TabSections.RateLimit}>
<RateLimitPanel /> <RateLimitPanel />
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Users}>
<UserPanel />
</TabPanel>
</Tabs> </Tabs>
</div> </div>
)} )}
@@ -0,0 +1,161 @@
import { useState } from "react";
import { faMagnifyingGlass, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Input,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
import { useAdminDeleteUser, useAdminGetUsers } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const UserPanelTable = ({
handlePopUpOpen
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeUser"]>,
data: {
username: string;
id: string;
}
) => void;
}) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const { user } = useUser();
const userId = user?.id || "";
const debounedSearchTerm = useDebounce(searchUserFilter, 500);
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
limit: 20,
searchTerm: debounedSearchTerm
});
const isEmpty = !isLoading && !data?.pages?.[0].length;
return (
<>
<Input
value={searchUserFilter}
onChange={(e) => setSearchUserFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search users..."
/>
<div className="mt-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-5/12">Name</Th>
<Th className="w-5/12">Username</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="users" />}
{!isLoading &&
data?.pages?.map((users) =>
users.map(({ username, email, firstName, lastName, id }) => {
const name = firstName || lastName ? `${firstName} ${lastName}` : "-";
return (
<Tr key={`user-${id}`} className="w-full">
<Td className="w-5/12">{name}</Td>
<Td className="w-5/12">{email}</Td>
<Td>
{userId !== id && (
<div className="flex justify-end">
<IconButton
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
isDisabled={userId === id}
onClick={() => handlePopUpOpen("removeUser", { username, id })}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
)}
</Td>
</Tr>
);
})
)}
</TBody>
</Table>
{!isLoading && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="star"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of list"}
</Button>
)}
</div>
</>
);
};
export const UserPanel = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeUser"
] as const);
const { mutateAsync: deleteUser } = useAdminDeleteUser();
const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string };
try {
await deleteUser(id);
createNotification({
type: "success",
text: "Successfully deleted user"
});
} catch (err) {
createNotification({
type: "error",
text: "Error deleting user"
});
}
handlePopUpClose("removeUser");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
</div>
<UserPanelTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}
deleteKey="remove"
title={`Are you sure you want to delete User with username ${
(popUp?.removeUser?.data as { id: string; username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
</div>
);
};
+2 -2
View File
@@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.6.2 version: v0.6.3
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "v0.6.2" appVersion: "v0.6.3"
@@ -82,4 +82,6 @@ spec:
securityContext: securityContext:
runAsNonRoot: true runAsNonRoot: true
serviceAccountName: {{ include "secrets-operator.fullname" . }}-controller-manager serviceAccountName: {{ include "secrets-operator.fullname" . }}-controller-manager
terminationGracePeriodSeconds: 10 terminationGracePeriodSeconds: 10
nodeSelector: {{ toYaml .Values.controllerManager.nodeSelector | nindent 8 }}
tolerations: {{ toYaml .Values.controllerManager.tolerations | nindent 8 }}
+2
View File
@@ -43,6 +43,8 @@ controllerManager:
replicas: 1 replicas: 1
serviceAccount: serviceAccount:
annotations: {} annotations: {}
nodeSelector: {}
tolerations: []
kubernetesClusterDomain: cluster.local kubernetesClusterDomain: cluster.local
metricsService: metricsService:
ports: ports: