Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/collaborators/project-invite-tokens.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Manage tokens that can be used to add new users who7know the token to a project.89TODO:10- we don't allow adjusting the usage_limit, so hide that for now.11- the default expire time is "2 weeks" and user can't edit that yet, except to set expire to now.1213*/1415// Load the code that checks for the PROJECT_INVITE_QUERY_PARAM16// when user gets signed in, and handles it.1718import { Button, Card, DatePicker, Form, Modal, Popconfirm, Table } from "antd";19import dayjs from "dayjs";20import { join } from "path";2122import { alert_message } from "@cocalc/frontend/alerts";23import {24React,25useIsMountedRef,26useState,27} from "@cocalc/frontend/app-framework";28import {29CopyToClipBoard,30Gap,31Icon,32Loading,33TimeAgo,34} from "@cocalc/frontend/components";35import { appBasePath } from "@cocalc/frontend/customize/app-base-path";36import { CancelText } from "@cocalc/frontend/i18n/components";37import { webapp_client } from "@cocalc/frontend/webapp-client";38import { ProjectInviteToken } from "@cocalc/util/db-schema/project-invite-tokens";39import { secure_random_token, server_weeks_ago } from "@cocalc/util/misc";40import { PROJECT_INVITE_QUERY_PARAM } from "./handle-project-invite";4142const { useForm } = Form;4344const TOKEN_LENGTH = 16;45const MAX_TOKENS = 200;46const COLUMNS = [47{ title: "Invite Link", dataIndex: "token", key: "token", width: 300 },48{ title: "Created", dataIndex: "created", key: "created", width: 150 },49{ title: "Expires", dataIndex: "expires", key: "expires", width: 150 },50{ title: "Redemption Count", dataIndex: "counter", key: "counter" },51/* { title: "Limit", dataIndex: "usage_limit", key: "usage_limit" },*/52];5354interface Props {55project_id: string;56}5758export const ProjectInviteTokens: React.FC<Props> = React.memo(59({ project_id }) => {60// blah61const [expanded, set_expanded] = useState<boolean>(false);62const [tokens, set_tokens] = useState<undefined | ProjectInviteToken[]>(63undefined,64);65const is_mounted_ref = useIsMountedRef();66const [fetching, set_fetching] = useState<boolean>(false);67const [addModalVisible, setAddModalVisible] = useState<boolean>(false);68const [form] = useForm();6970async function fetch_tokens() {71try {72set_fetching(true);73const { query } = await webapp_client.async_query({74query: {75project_invite_tokens: [76{77project_id,78token: null,79created: null,80expires: null,81usage_limit: null,82counter: null,83},84],85},86});87if (!is_mounted_ref.current) return;88set_tokens(query.project_invite_tokens);89} catch (err) {90alert_message({91type: "error",92message: `Error getting project invite tokens: ${err}`,93});94} finally {95if (is_mounted_ref.current) {96set_fetching(false);97}98}99}100101const heading = (102<div>103<a104onClick={() => {105if (!expanded) {106fetch_tokens();107}108set_expanded(!expanded);109}}110style={{ cursor: "pointer", fontSize: "12pt" }}111>112{" "}113<Icon114style={{ width: "20px" }}115name={expanded ? "caret-down" : "caret-right"}116/>{" "}117Invite collaborators by sending them an invite URL...118</a>119</div>120);121if (!expanded) {122return heading;123}124125async function add_token(expires) {126if (tokens != null && tokens.length > MAX_TOKENS) {127// TODO: just in case of some weird abuse... and until we implement128// deletion of tokens. Maybe the backend will just purge129// anything that has expired after a while.130alert_message({131type: "error",132message:133"You have hit the hard limit on the number of invite tokens for a single project. Please contact support.",134});135return;136}137const token = secure_random_token(TOKEN_LENGTH);138139try {140await webapp_client.async_query({141query: {142project_invite_tokens: {143token,144project_id,145created: webapp_client.server_time(),146expires: expires,147},148},149});150} catch (err) {151alert_message({152type: "error",153message: `Error creating project invite token: ${err}`,154});155}156if (!is_mounted_ref.current) return;157fetch_tokens();158}159160async function add_token_two_week() {161let expires = server_weeks_ago(-2);162add_token(expires);163}164165function render_create_token() {166return (167<Popconfirm168title={169"Create a link that people can use to get added as a collaborator to this project."170}171onConfirm={add_token_two_week}172okText={"Yes, create token"}173cancelText={<CancelText />}174>175<Button disabled={fetching}>176<Icon name="plus-circle" />177<Gap /> Create two weeks token178</Button>179</Popconfirm>180);181}182const handleAdd = () => {183setAddModalVisible(true);184};185186const handleModalOK = () => {187// const name = form.getFieldValue("name");188const expire = form.getFieldValue("expire");189add_token(expire);190setAddModalVisible(false);191form.resetFields();192};193194const handleModalCancel = () => {195setAddModalVisible(false);196form.resetFields();197};198199function render_create_custom_token() {200return (201<Button onClick={handleAdd}>202<Icon name="plus-circle" /> Create custom token203</Button>204);205}206207function render_refresh() {208return (209<Button onClick={fetch_tokens} disabled={fetching}>210<Icon name="refresh" spin={fetching} />211<Gap /> Refresh212</Button>213);214}215216async function expire_token(token) {217// set token to be expired218try {219await webapp_client.async_query({220query: {221project_invite_tokens: {222token,223project_id,224expires: webapp_client.server_time(),225},226},227});228} catch (err) {229alert_message({230type: "error",231message: `Error expiring project invite token: ${err}`,232});233}234if (!is_mounted_ref.current) return;235fetch_tokens();236}237238function render_expire_button(token, expires) {239if (expires && expires <= webapp_client.server_time()) {240return "(REVOKED)";241}242return (243<Popconfirm244title={"Revoke this token?"}245description={246<div style={{ maxWidth: "400px" }}>247This will make it so this token cannot be used anymore. Anybody248who has already redeemed the token is not removed from this249project.250</div>251}252onConfirm={() => expire_token(token)}253okText={"Yes, revoke this token"}254cancelText={"Cancel"}255>256<Button size="small">Revoke...</Button>257</Popconfirm>258);259}260261function render_tokens() {262if (tokens == null) return <Loading />;263const dataSource: any[] = [];264for (const data of tokens) {265const { token, counter, usage_limit, created, expires } = data;266dataSource.push({267key: token,268token:269expires && expires <= webapp_client.server_time() ? (270<span style={{ textDecoration: "line-through" }}>{token}</span>271) : (272<CopyToClipBoard273inputWidth="250px"274value={`${document.location.origin}${join(275appBasePath,276"app",277)}?${PROJECT_INVITE_QUERY_PARAM}=${token}`}278/>279),280counter,281usage_limit: usage_limit ?? "∞",282created: created ? <TimeAgo date={created} /> : undefined,283expires: expires ? (284<span>285<TimeAgo date={expires} /> <Gap />286{render_expire_button(token, expires)}287</span>288) : undefined,289data,290});291}292return (293<Table294dataSource={dataSource}295columns={COLUMNS}296pagination={{ pageSize: 4 }}297scroll={{ y: 240 }}298/>299);300}301302return (303<Card style={{ width: "100%", overflowX: "auto" }}>304{heading}305<br />306<br />307{render_create_token()}308<Gap />309{render_create_custom_token()}310<Gap />311{render_refresh()}312<br />313<br />314{render_tokens()}315<br />316<br />317<Modal318open={addModalVisible}319title="Create a New Inviting Token"320okText="Create token"321cancelText={<CancelText />}322onCancel={handleModalCancel}323onOk={handleModalOK}324>325<Form form={form} layout="vertical">326<Form.Item327name="expire"328label="Expire"329rules={[330{331required: false,332message:333"Optional date when token will be automatically expired",334},335]}336>337<DatePicker338changeOnBlur339showTime340disabledDate={(current) => {341// disable all dates before today342return current && current < dayjs();343}}344/>345</Form.Item>346</Form>347</Modal>348</Card>349);350},351);352353354