Skip to content

Commit

Permalink
Merge pull request #5239 from systeminit/wendy/eng-2900-add-ability-t…
Browse files Browse the repository at this point in the history
…o-set-an-expiration-date-on-a-token

form to create API token now requires an expiration time string
  • Loading branch information
wendybujalski authored Jan 11, 2025
2 parents 8ff5530 + af55f4a commit 10cd77e
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 54 deletions.
45 changes: 37 additions & 8 deletions app/auth-portal/src/pages/WorkspaceAuthTokensPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,39 @@

<Stack>
<ErrorMessage :asyncState="createAuthToken" />
<div
<form
v-if="workspace.role === 'OWNER'"
class="flex flex-row flex-wrap items-end gap-md"
class="flex flex-row flex-wrap items-center justify-center gap-md"
>
<VormInput
v-model="createAuthTokenName"
inlineLabel
label="Token Name*"
@keydown.enter.prevent="createAuthToken.execute()"
label="Token Name"
required
placeholder="A name for your token."
@keydown.enter.prevent="onFormSubmit"
/>
<VormInput
v-model="createAuthTokenExpiration"
inlineLabel
label="Expiration"
required
placeholder="48h, 1d, 1m, 1y, etc."
type="time-string"
:maxLength="99"
@keydown.enter.prevent="onFormSubmit"
/>
<VButton
:disabled="!(createAuthTokenName?.length > 0)"
:disabled="validationState.isError"
:loading="createAuthToken.isLoading.value"
loadingText="Creating ..."
tone="action"
variant="solid"
@click="createAuthToken.execute()"
@click="onFormSubmit"
>
Generate API Token
</VButton>
</div>
</form>
<ErrorMessage :asyncState="authTokens" />
<div v-if="authTokens.state.value" class="relative">
<Stack>
Expand Down Expand Up @@ -131,6 +143,7 @@ import {
ErrorMessage,
Modal,
IconButton,
useValidatedInputGroup,
} from "@si/vue-lib/design-system";
import { useAsyncState } from "@vueuse/core";
import { apiData } from "@si/vue-lib/pinia";
Expand Down Expand Up @@ -177,14 +190,20 @@ const createAuthToken = useAsyncState(
if (_.isEmpty(createAuthTokenName.value)) return;
const { authToken, token } = await apiData(
api.CREATE_AUTH_TOKEN(props.workspaceId, createAuthTokenName.value),
api.CREATE_AUTH_TOKEN(
props.workspaceId,
createAuthTokenName.value,
createAuthTokenExpiration.value,
),
);
if (authTokens.state.value) {
authTokens.state.value[authToken.id] = authToken;
}
tokenCopied.value = false;
createAuthTokenName.value = "";
createAuthTokenExpiration.value = "";
validationMethods.resetAll();
tokenDisplayModalRef.value?.open();
return token;
Expand All @@ -195,6 +214,8 @@ const createAuthToken = useAsyncState(
/** Name of token to create */
const createAuthTokenName = ref("");
/** Expiration time of token to create */
const createAuthTokenExpiration = ref("");
/** Token modal */
const tokenDisplayModalRef = ref<InstanceType<typeof Modal> | null>(null);
Expand All @@ -206,4 +227,12 @@ async function copyToken() {
}
tokenCopied.value = true;
}
const { validationState, validationMethods } = useValidatedInputGroup();
const onFormSubmit = async () => {
if (validationMethods.hasError()) return;
await createAuthToken.execute();
};
</script>
8 changes: 6 additions & 2 deletions app/auth-portal/src/store/authTokens.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ export const useAuthTokensApi = defineStore("authTokens", {
});
},

async CREATE_AUTH_TOKEN(workspaceId: WorkspaceId, name?: string) {
async CREATE_AUTH_TOKEN(
workspaceId: WorkspaceId,
name?: string,
expiration?: string,
) {
return new ApiRequest<{ authToken: AuthToken; token: string }>({
method: "post",
url: ["workspaces", { workspaceId }, "authTokens"],
params: { name },
params: { name, expiration },
keyRequestStatusBy: workspaceId,
});
},
Expand Down
19 changes: 17 additions & 2 deletions bin/auth-api/src/routes/auth_token.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { authorizeWorkspaceRoute } from "./workspace.routes";
import { ApiError } from "../lib/api-error";
import { router } from ".";

// get all authTokens for the given workspace
router.get("/workspaces/:workspaceId/authTokens", async (ctx) => {
const { workspaceId } = await authorizeWorkspaceRoute(ctx, undefined);

Expand All @@ -28,27 +29,37 @@ router.get("/workspaces/:workspaceId/authTokens", async (ctx) => {
ctx.body = { authTokens };
});

// create a new authToken for the given workspace
router.post("/workspaces/:workspaceId/authTokens", async (ctx) => {
const {
userId,
workspaceId,
} = await authorizeWorkspaceRoute(ctx, RoleType.OWNER);

// TODO - this should also get an expiration instead of just defaulting to 1d!
// Get params from body
const { name } = validate(
const { name, expiration } = validate(
ctx.request.body,
z.object({
name: z.optional(z.string()),
expiration: z.optional(z.string()),
}),
);

let expiresIn;
if (expiration && expiration.trim().toLocaleLowerCase() !== 'never' && expiration.trim().toLocaleLowerCase() !== "0") {
expiresIn = expiration;
} else {
expiresIn = "30d";
}

// Create the token
const token = createSdfAuthToken({
userId,
workspaceId,
role: "automation",
}, {
expiresIn: "1d",
expiresIn,
jwtid: ulid(),
});

Expand All @@ -58,11 +69,13 @@ router.post("/workspaces/:workspaceId/authTokens", async (ctx) => {
ctx.body = { authToken, token };
});

// get the given authToken for the given workspace
router.get("/workspaces/:workspaceId/authTokens/:authTokenId", async (ctx) => {
const { authToken } = await authorizeAuthTokenRoute(ctx, undefined);
ctx.body = { authToken };
});

// rename the given authToken for the given workspace
router.put("/workspaces/:workspaceId/authTokens/:authTokenId", async (ctx) => {
const { authToken } = await authorizeAuthTokenRoute(ctx, RoleType.OWNER);

Expand All @@ -79,6 +92,8 @@ router.put("/workspaces/:workspaceId/authTokens/:authTokenId", async (ctx) => {
ctx.body = { authToken };
});

// delete the given authToken for the given workspace
// TODO - This does not currently fully revoke the token, just removes it from prisma!
router.delete("/workspaces/:workspaceId/authTokens/:authTokenId", async (ctx) => {
const { authTokenId } = await authorizeAuthTokenRoute(ctx, RoleType.OWNER);

Expand Down
1 change: 1 addition & 0 deletions lib/vue-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"local-storage-fallback": "^4.1.3",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"ms-typescript": "^2.0.0",
"pinia": "^2.2.4",
"posthog-js": "^1.57.2",
"tailwindcss-capsize": "^3.0.3",
Expand Down
3 changes: 3 additions & 0 deletions lib/vue-lib/src/design-system/forms/VormInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ type InputTypes =
| "tel"
| "text"
| "textarea"
| "time-string"
| "url";

// object of the shape { option1Value: 'Label 1', }
Expand Down Expand Up @@ -483,6 +484,8 @@ const validationRules = computed(() => {
rules.push({ fn: validators.url, message: "Invalid URL" });
if (props.type === "email")
rules.push({ fn: validators.email, message: "Invalid email" });
if (props.type === "time-string")
rules.push({ fn: validators.timeString, message: "Invalid time string" });

if (props.regex) {
rules.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
reactive,
} from "vue";
import * as _ from "lodash-es";
import { toMs } from "ms-typescript";

type ValidatedInputGroupRegistrar = {
register(component: ComponentInternalInstance, isGroup: boolean): void;
Expand Down Expand Up @@ -252,6 +253,12 @@ export const validators = {
req(typeof value === "string" ? value.trim() : value),
url: (value: any) => !req(value) || URL_REGEX.test(value),
email: (value: any) => !req(value) || EMAIL_REGEX.test(value),
timeString: (value: any) =>
value &&
value.length > 0 &&
Number.isNaN(Number(value)) &&
!Number.isNaN(toMs(value)) &&
toMs(value) !== 0,
equals: (mustEqual: any) => (value: any) => value === mustEqual,
regex: (regexRaw: RegExp | string) => {
const regex = _.isString(regexRaw) ? new RegExp(regexRaw) : regexRaw;
Expand Down
Loading

0 comments on commit 10cd77e

Please sign in to comment.