mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +00:00
Merge pull request #3489 from Infisical/feat/add-user-get-token-and-revamp-session-management
feat: add user get token CLI and revamp session management
This commit is contained in:
@@ -252,6 +252,31 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/me/sessions/:sessionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sessionId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.authToken.revokeMySessionById(req.permission.id, req.params.sessionId);
|
||||||
|
return {
|
||||||
|
message: "Successfully revoked session"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/me",
|
url: "/me",
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ export const tokenDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter);
|
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession)
|
||||||
|
.where(filter)
|
||||||
|
.orderBy("lastUsed", "desc");
|
||||||
|
|
||||||
return sessions;
|
return sessions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ name: "Find all token session", error });
|
throw new DatabaseError({ name: "Find all token session", error });
|
||||||
|
|||||||
@@ -151,6 +151,9 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
|
|
||||||
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||||
|
|
||||||
|
const revokeMySessionById = async (userId: string, sessionId: string) =>
|
||||||
|
tokenDAL.deleteTokenSession({ userId, id: sessionId });
|
||||||
|
|
||||||
const validateRefreshToken = async (refreshToken?: string) => {
|
const validateRefreshToken = async (refreshToken?: string) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (!refreshToken)
|
if (!refreshToken)
|
||||||
@@ -223,6 +226,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
clearTokenSessionById,
|
clearTokenSessionById,
|
||||||
getTokenSessionByUser,
|
getTokenSessionByUser,
|
||||||
revokeAllMySessions,
|
revokeAllMySessions,
|
||||||
|
revokeMySessionById,
|
||||||
validateRefreshToken,
|
validateRefreshToken,
|
||||||
fnValidateJwtIdentity,
|
fnValidateJwtIdentity,
|
||||||
getUserTokenSessionById
|
getUserTokenSessionById
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/config"
|
"github.com/Infisical/infisical-merge/packages/config"
|
||||||
"github.com/Infisical/infisical-merge/packages/models"
|
"github.com/Infisical/infisical-merge/packages/models"
|
||||||
@@ -85,6 +88,57 @@ var switchCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userGetCmd = &cobra.Command{
|
||||||
|
Use: "get",
|
||||||
|
Short: "Used to get properties of an Infisical profile",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical user get",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var userGetTokenCmd = &cobra.Command{
|
||||||
|
Use: "token",
|
||||||
|
Short: "Used to get the access token of an Infisical user",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical user get token",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
util.RequireLogin()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "[infisical user get token]: Unable to get logged in user token")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JTWToken, ".")
|
||||||
|
if len(tokenParts) != 3 {
|
||||||
|
util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(tokenParts[1])
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "[infisical user get token]: Unable to decode token payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenPayload struct {
|
||||||
|
TokenVersionId string `json:"tokenVersionId"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(payload, &tokenPayload); err != nil {
|
||||||
|
util.HandleError(err, "[infisical user get token]: Unable to parse token payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
|
||||||
|
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var updateCmd = &cobra.Command{
|
var updateCmd = &cobra.Command{
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Used to update properties of an Infisical profile",
|
Short: "Used to update properties of an Infisical profile",
|
||||||
@@ -185,6 +239,8 @@ var domainCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
updateCmd.AddCommand(domainCmd)
|
updateCmd.AddCommand(domainCmd)
|
||||||
userCmd.AddCommand(updateCmd)
|
userCmd.AddCommand(updateCmd)
|
||||||
|
userGetCmd.AddCommand(userGetTokenCmd)
|
||||||
|
userCmd.AddCommand(userGetCmd)
|
||||||
userCmd.AddCommand(switchCmd)
|
userCmd.AddCommand(switchCmd)
|
||||||
rootCmd.AddCommand(userCmd)
|
rootCmd.AddCommand(userCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,22 +8,46 @@ infisical user
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
This command allows you to manage the current logged in users on the CLI
|
This command allows you to manage the current logged in users on the CLI
|
||||||
|
|
||||||
### Sub-commands
|
### Sub-commands
|
||||||
|
|
||||||
<Accordion title="infisical user switch" defaultOpen="true">
|
<Accordion title="infisical user switch" defaultOpen="true">
|
||||||
Use this command to switch between profiles that are currently logged into the CLI
|
Use this command to switch between profiles that are currently logged into the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
infisical user switch
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
|
||||||
infisical user switch
|
|
||||||
```
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="infisical user update domain">
|
<Accordion title="infisical user update domain">
|
||||||
With this command, you can modify the backend API that is utilized for all requests associated with a specific profile.
|
With this command, you can modify the backend API that is utilized for all requests associated with a specific profile.
|
||||||
For instance, you have the option to point the profile to use either the Infisical Cloud or your own self-hosted Infisical instance.
|
For instance, you have the option to point the profile to use either the Infisical Cloud or your own self-hosted Infisical instance.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
infisical user update domain
|
infisical user update domain
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical user get token">
|
||||||
|
Use this command to get your current Infisical access token and session information. This command requires you to be logged in.
|
||||||
|
|
||||||
|
The command will display:
|
||||||
|
|
||||||
|
- Your session ID
|
||||||
|
- Your full JWT access token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
infisical user get token
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Session ID: abc123-xyz-456
|
||||||
|
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
useAddUserToWsE2EE,
|
useAddUserToWsE2EE,
|
||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
|
useRevokeMySessionById,
|
||||||
useSendEmailVerificationCode,
|
useSendEmailVerificationCode,
|
||||||
useVerifyEmailVerificationCode
|
useVerifyEmailVerificationCode
|
||||||
} from "./mutation";
|
} from "./mutation";
|
||||||
|
|||||||
@@ -171,3 +171,16 @@ export const useResendOrgMemberInvitation = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useRevokeMySessionById = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (sessionId: string) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v2/users/me/sessions/${sessionId}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries({ queryKey: userKeys.mySessions });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,3 +11,65 @@ export const formatReservedPaths = (secretPath: string) => {
|
|||||||
export const camelCaseToSpaces = (input: string) => {
|
export const camelCaseToSpaces = (input: string) => {
|
||||||
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
|
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatSessionUserAgent = (userAgent: string) => {
|
||||||
|
const result = {
|
||||||
|
os: "Unknown",
|
||||||
|
browser: "Unknown",
|
||||||
|
device: "Desktop"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Operating System detection
|
||||||
|
if (userAgent.includes("Windows")) {
|
||||||
|
result.os = "Windows";
|
||||||
|
} else if (
|
||||||
|
userAgent.includes("Mac OS") ||
|
||||||
|
userAgent.includes("Macintosh") ||
|
||||||
|
userAgent.includes("macOS")
|
||||||
|
) {
|
||||||
|
result.os = "macOS";
|
||||||
|
} else if (userAgent.includes("Linux") && !userAgent.includes("Android")) {
|
||||||
|
result.os = "Linux";
|
||||||
|
} else if (userAgent.includes("Android")) {
|
||||||
|
result.os = "Android";
|
||||||
|
result.device = "Mobile";
|
||||||
|
} else if (
|
||||||
|
userAgent.includes("iOS") ||
|
||||||
|
userAgent.includes("iPhone") ||
|
||||||
|
userAgent.includes("iPad")
|
||||||
|
) {
|
||||||
|
result.os = "iOS";
|
||||||
|
result.device = userAgent.includes("iPad") ? "Tablet" : "Mobile";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser detection
|
||||||
|
if (userAgent.includes("Firefox/")) {
|
||||||
|
result.browser = "Firefox";
|
||||||
|
} else if (userAgent.includes("Edge/") || userAgent.includes("Edg/")) {
|
||||||
|
result.browser = "Edge";
|
||||||
|
} else if (userAgent.includes("Brave/") || userAgent.includes("Brave ")) {
|
||||||
|
result.browser = "Brave";
|
||||||
|
} else if (
|
||||||
|
userAgent.includes("Chrome/") &&
|
||||||
|
!userAgent.includes("Chromium/") &&
|
||||||
|
!userAgent.includes("Edg/")
|
||||||
|
) {
|
||||||
|
result.browser = "Chrome";
|
||||||
|
} else if (
|
||||||
|
userAgent.includes("Safari/") &&
|
||||||
|
!userAgent.includes("Chrome/") &&
|
||||||
|
!userAgent.includes("Chromium/")
|
||||||
|
) {
|
||||||
|
result.browser = "Safari";
|
||||||
|
} else if (userAgent.includes("Opera/") || userAgent.includes("OPR/")) {
|
||||||
|
result.browser = "Opera";
|
||||||
|
} else if (userAgent.includes("Trident/") || userAgent.includes("MSIE")) {
|
||||||
|
result.browser = "Internet Explorer";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAgent.toLowerCase() === "cli") {
|
||||||
|
result.browser = "CLI";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|||||||
+116
-42
@@ -1,6 +1,8 @@
|
|||||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
@@ -9,59 +11,131 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useGetMySessions } from "@app/hooks/api";
|
import { Button } from "@app/components/v2/Button";
|
||||||
|
import { useGetMySessions, useRevokeMySessionById } from "@app/hooks/api";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
import { timeAgo } from "@app/lib/fn/date";
|
||||||
|
import { formatSessionUserAgent } from "@app/lib/fn/string";
|
||||||
|
|
||||||
|
const formatLocalDateTime = (date: Date): string => {
|
||||||
|
return date.toLocaleString(undefined, {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const SessionsTable = () => {
|
export const SessionsTable = () => {
|
||||||
const { data, isPending } = useGetMySessions();
|
const { data, isPending } = useGetMySessions();
|
||||||
|
const { mutateAsync: revokeMySessionById } = useRevokeMySessionById();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"deleteSession"
|
||||||
|
] as const);
|
||||||
|
|
||||||
const formatDate = (dateToFormat: string) => {
|
const handleSignOut = async (sessionId: string) => {
|
||||||
const date = new Date(dateToFormat);
|
try {
|
||||||
const year = date.getFullYear();
|
await revokeMySessionById(sessionId);
|
||||||
const month = date.getMonth() + 1;
|
createNotification({
|
||||||
const day = date.getDate();
|
text: "Session revoked successfully",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to revoke session",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const formattedDate = `${day}/${month}/${year}`;
|
handlePopUpClose("deleteSession");
|
||||||
|
|
||||||
return formattedDate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<>
|
||||||
<Table>
|
<DeleteActionModal
|
||||||
<THead>
|
isOpen={popUp.deleteSession.isOpen}
|
||||||
<Tr>
|
title="Are you sure want to sign out of this session?"
|
||||||
<Th>Created</Th>
|
onChange={(isOpen) => handlePopUpToggle("deleteSession", isOpen)}
|
||||||
<Th>Last active</Th>
|
deleteKey="confirm"
|
||||||
<Th>IP address</Th>
|
onDeleteApproved={() =>
|
||||||
<Th>Device</Th>
|
handleSignOut((popUp?.deleteSession?.data as { sessionId: string })?.sessionId)
|
||||||
</Tr>
|
}
|
||||||
</THead>
|
/>
|
||||||
<TBody>
|
<TableContainer className="mt-4">
|
||||||
{isPending && <TableSkeleton columns={4} innerKey="sesssions" />}
|
<Table>
|
||||||
{!isPending &&
|
<THead>
|
||||||
data &&
|
|
||||||
data.length > 0 &&
|
|
||||||
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
|
|
||||||
return (
|
|
||||||
<Tr className="h-10" key={`session-${id}`}>
|
|
||||||
<Td>{formatDate(createdAt)}</Td>
|
|
||||||
<Td>{formatDate(lastUsed)}</Td>
|
|
||||||
<Td>{ip}</Td>
|
|
||||||
<Td>{userAgent}</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!isPending && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={4}>
|
<Th>IP & Session ID</Th>
|
||||||
<EmptyState title="No sessions on file" icon={faServer} />
|
<Th>OS & Browser</Th>
|
||||||
</Td>
|
<Th>Last accessed</Th>
|
||||||
|
<Th>Manage</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
</THead>
|
||||||
</TBody>
|
<TBody>
|
||||||
</Table>
|
{isPending && <TableSkeleton columns={4} innerKey="sessions" />}
|
||||||
</TableContainer>
|
{!isPending &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
|
||||||
|
const { os, browser } = formatSessionUserAgent(userAgent);
|
||||||
|
const lastUsedDate = new Date(lastUsed);
|
||||||
|
const createdAtDate = new Date(createdAt);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr className="h-20" key={`session-${id}`}>
|
||||||
|
<Td>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{ip}</span>
|
||||||
|
<span className="text-sm text-gray-500">ID: {id}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{os}</span>
|
||||||
|
<span className="text-sm text-gray-500">{browser}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Tooltip content={formatLocalDateTime(lastUsedDate)}>
|
||||||
|
<span className="font-medium">{timeAgo(lastUsedDate, new Date())}</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={formatLocalDateTime(createdAtDate)}>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
Created {timeAgo(createdAtDate, new Date())}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen("deleteSession", { sessionId: id })}
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isPending && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4}>
|
||||||
|
<EmptyState title="No sessions on file" icon={faServer} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user