Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand invite key generation #634

Merged
merged 14 commits into from
Feb 25, 2024
4 changes: 2 additions & 2 deletions frontend/src/graphql/mutations/GenerateInviteCode.gql
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mutation GenerateInviteCode {
generateInviteCode
mutation GenerateInviteCodes($input: GenerateInviteCodeInput) {
generateInviteCodes(input: $input)
}
10 changes: 5 additions & 5 deletions frontend/src/graphql/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
ResetPasswordMutationVariables,
RegenerateApiKeyMutation,
RegenerateApiKeyMutationVariables,
GenerateInviteCodeMutation,
GenerateInviteCodesMutation,
GrantInviteMutation,
GrantInviteMutationVariables,
RescindInviteCodeMutation,
Expand Down Expand Up @@ -112,7 +112,7 @@ import CancelEditGQL from "./CancelEdit.gql";
import ChangePasswordGQL from "./ChangePassword.gql";
import ResetPasswordGQL from "./ResetPassword.gql";
import RegenerateAPIKeyGQL from "./RegenerateAPIKey.gql";
import GenerateInviteCodeGQL from "./GenerateInviteCode.gql";
import GenerateInviteCodesGQL from "./GenerateInviteCode.gql";
import GrantInviteGQL from "./GrantInvite.gql";
import RescindInviteCodeGQL from "./RescindInviteCode.gql";
import RevokeInviteGQL from "./RevokeInvite.gql";
Expand Down Expand Up @@ -286,9 +286,9 @@ export const useRegenerateAPIKey = (
>
) => useMutation(RegenerateAPIKeyGQL, options);

export const useGenerateInviteCode = (
options?: MutationHookOptions<GenerateInviteCodeMutation>
) => useMutation(GenerateInviteCodeGQL, options);
export const useGenerateInviteCodes = (
options?: MutationHookOptions<GenerateInviteCodesMutation>
) => useMutation(GenerateInviteCodesGQL, options);

export const useGrantInvite = (
options?: MutationHookOptions<
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/graphql/queries/User.gql
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ query User($name: String!) {
name
}
invite_tokens
active_invite_codes
invite_codes {
id
uses
expires
}
vote_count {
accept
reject
Expand Down
84 changes: 72 additions & 12 deletions frontend/src/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,12 @@ export enum GenderFilterEnum {
UNKNOWN = "UNKNOWN",
}

export type GenerateInviteCodeInput = {
keys?: InputMaybe<Scalars["Int"]>;
ttl?: InputMaybe<Scalars["Int"]>;
uses?: InputMaybe<Scalars["Int"]>;
};

export type GrantInviteInput = {
amount: Scalars["Int"];
user_id: Scalars["ID"];
Expand Down Expand Up @@ -404,6 +410,13 @@ export type IntCriterionInput = {
value: Scalars["Int"];
};

export type InviteKey = {
__typename: "InviteKey";
expires?: Maybe<Scalars["Time"]>;
id: Scalars["ID"];
uses?: Maybe<Scalars["Int"]>;
};

export type Measurements = {
__typename: "Measurements";
band_size?: Maybe<Scalars["Int"]>;
Expand Down Expand Up @@ -440,8 +453,10 @@ export type Mutation = {
favoritePerformer: Scalars["Boolean"];
/** Favorite or unfavorite a studio */
favoriteStudio: Scalars["Boolean"];
/** Generates an invite code using an invite token */
/** @deprecated Use generateInviteCodes */
generateInviteCode?: Maybe<Scalars["ID"]>;
/** Generates an invite code using an invite token */
generateInviteCodes: Array<Scalars["ID"]>;
/** Adds invite tokens for a user */
grantInvite: Scalars["Int"];
imageCreate?: Maybe<Image>;
Expand Down Expand Up @@ -538,6 +553,10 @@ export type MutationFavoriteStudioArgs = {
id: Scalars["ID"];
};

export type MutationGenerateInviteCodesArgs = {
input?: InputMaybe<GenerateInviteCodeInput>;
};

export type MutationGrantInviteArgs = {
input: GrantInviteInput;
};
Expand Down Expand Up @@ -1736,6 +1755,7 @@ export type UrlInput = {

export type User = {
__typename: "User";
/** @deprecated Use invite_codes instead */
active_invite_codes?: Maybe<Array<Scalars["String"]>>;
/** Calls to the API from this user over a configurable time period */
api_calls: Scalars["Int"];
Expand All @@ -1746,6 +1766,7 @@ export type User = {
/** Should not be visible to other users */
email?: Maybe<Scalars["String"]>;
id: Scalars["ID"];
invite_codes?: Maybe<Array<InviteKey>>;
invite_tokens?: Maybe<Scalars["Int"]>;
invited_by?: Maybe<User>;
name: Scalars["String"];
Expand Down Expand Up @@ -4378,13 +4399,13 @@ export type FavoriteStudioMutation = {
favoriteStudio: boolean;
};

export type GenerateInviteCodeMutationVariables = Exact<{
[key: string]: never;
export type GenerateInviteCodesMutationVariables = Exact<{
input?: InputMaybe<GenerateInviteCodeInput>;
}>;

export type GenerateInviteCodeMutation = {
export type GenerateInviteCodesMutation = {
__typename: "Mutation";
generateInviteCode?: string | null;
generateInviteCodes: Array<string>;
};

export type GrantInviteMutationVariables = Exact<{
Expand Down Expand Up @@ -18496,8 +18517,13 @@ export type UserQuery = {
api_key?: string | null;
api_calls: number;
invite_tokens?: number | null;
active_invite_codes?: Array<string> | null;
invited_by?: { __typename: "User"; id: string; name: string } | null;
invite_codes?: Array<{
__typename: "InviteKey";
id: string;
uses?: number | null;
expires?: string | null;
}> | null;
vote_count: {
__typename: "UserVoteCount";
accept: number;
Expand Down Expand Up @@ -23737,27 +23763,50 @@ export const FavoriteStudioDocument = {
FavoriteStudioMutation,
FavoriteStudioMutationVariables
>;
export const GenerateInviteCodeDocument = {
export const GenerateInviteCodesDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "GenerateInviteCode" },
name: { kind: "Name", value: "GenerateInviteCodes" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: {
kind: "Variable",
name: { kind: "Name", value: "input" },
},
type: {
kind: "NamedType",
name: { kind: "Name", value: "GenerateInviteCodeInput" },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "generateInviteCode" },
name: { kind: "Name", value: "generateInviteCodes" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: {
kind: "Variable",
name: { kind: "Name", value: "input" },
},
},
],
},
],
},
},
],
} as unknown as DocumentNode<
GenerateInviteCodeMutation,
GenerateInviteCodeMutationVariables
GenerateInviteCodesMutation,
GenerateInviteCodesMutationVariables
>;
export const GrantInviteDocument = {
kind: "Document",
Expand Down Expand Up @@ -49024,7 +49073,18 @@ export const UserDocument = {
},
{
kind: "Field",
name: { kind: "Name", value: "active_invite_codes" },
name: { kind: "Name", value: "invite_codes" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "uses" } },
{
kind: "Field",
name: { kind: "Name", value: "expires" },
},
],
},
},
{
kind: "Field",
Expand Down
106 changes: 106 additions & 0 deletions frontend/src/pages/users/GenerateInviteKeyModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { FC, useState, useMemo } from "react";
import { Modal, Button, Form } from "react-bootstrap";
import { GenerateInviteCodeInput } from "src/graphql";
import { formatDateTime } from "src/utils";

interface ModalProps {
callback: (input?: GenerateInviteCodeInput) => void;
}

const ms = 1000;
const minutesInSeconds = 60;
const hoursInSeconds = 60 * minutesInSeconds;
const daysInSeconds = 24 * hoursInSeconds;
const yearsInSeconds = 365 * daysInSeconds;

export const GenerateInviteKeyModal: FC<ModalProps> = ({ callback }) => {
const [keyAmount, setKeyAmount] = useState(1);
const [keyUses, setKeyUses] = useState(1);
const [keyExpireAmount, setKeyExpireAmount] = useState(30);
const [keyExpireUnit, setKeyExpireUnit] = useState(daysInSeconds);

const handleCancel = () => callback();
const handleAccept = () =>
callback({
keys: keyAmount,
uses: keyUses,
ttl: keyExpireAmount * keyExpireUnit,
});

const expireTime = useMemo(() => {
const ret = new Date();
ret.setTime(ret.getTime() + keyExpireAmount * keyExpireUnit * ms);
return ret;
}, [keyExpireAmount, keyExpireUnit]);

return (
<Modal show onHide={handleCancel}>
<Modal.Header closeButton>
<b>Generate Invite Keys</b>
</Modal.Header>
<Modal.Body>
<Form>
<Form.Group controlId="key-amount">
<Form.Label>Amount of Keys</Form.Label>
<Form.Control
value={keyAmount}
onChange={(e) => setKeyAmount(parseInt(e.currentTarget.value))}
type="number"
min={1}
max={100}
placeholder="Enter number of keys"
/>
</Form.Group>
<Form.Group controlId="key-uses" className="mt-4">
<Form.Label>Uses per key</Form.Label>
<Form.Control
value={keyUses}
onChange={(e) => setKeyUses(parseInt(e.currentTarget.value))}
type="number"
min={0}
max={100}
placeholder="Uses per key"
/>
<Form.Text className="text-muted">
Enter 0 for unlimited uses.
</Form.Text>
</Form.Group>
<Form.Group controlId="key-expiration" className="mt-4">
<Form.Label>Expire time</Form.Label>
<Form.Control
type="number"
min={1}
value={keyExpireAmount}
onChange={(e) =>
setKeyExpireAmount(parseInt(e.currentTarget.value))
}
/>
<Form.Select
value={keyExpireUnit}
onChange={(e) => {
setKeyExpireUnit(parseInt(e.currentTarget.value));
}}
className="mt-2"
>
<option value={minutesInSeconds}>Minutes</option>
<option value={hoursInSeconds}>Hours</option>
<option value={daysInSeconds}>Days</option>
<option value={yearsInSeconds}>Years</option>
</Form.Select>
<Form.Text className="text-muted">
Expires at {formatDateTime(expireTime)}
</Form.Text>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleAccept}>
Generate
</Button>
<Button variant="secondary" onClick={handleCancel}>
Cancel
</Button>
</Modal.Footer>
</Modal>
);
};
Loading
Loading