@@ -15,9 +15,12 @@ import {
1515 Input ,
1616 InputNumber ,
1717 Popconfirm ,
18+ Radio ,
19+ Space ,
1820 Switch ,
1921 Table ,
2022} from "antd" ;
23+ import type { RadioChangeEvent } from "antd" ;
2124import dayjs from "dayjs" ;
2225import { List } from "immutable" ;
2326import { pick , sortBy } from "lodash" ;
@@ -36,6 +39,7 @@ import {
3639 Saving ,
3740 TimeAgo ,
3841} from "@cocalc/frontend/components" ;
42+ import Copyable from "@cocalc/frontend/components/copy-to-clipboard" ;
3943import { query } from "@cocalc/frontend/frame-editors/generic/client" ;
4044import { CancelText } from "@cocalc/frontend/i18n/components" ;
4145import { RegistrationTokenSetFields } from "@cocalc/util/db-schema/types" ;
@@ -52,6 +56,43 @@ interface Token {
5256 limit ?: number ;
5357 counter ?: number ; // readonly
5458 expires ?: dayjs . Dayjs ; // DB uses Date objects, watch out!
59+ ephemeral ?: number ;
60+ customize ?: {
61+ disableCollaborators ?: boolean ;
62+ disableAI ?: boolean ;
63+ } ;
64+ }
65+
66+ const HOUR_MS = 60 * 60 * 1000 ;
67+ const EPHEMERAL_PRESETS = [
68+ { key : "6h" , label : "6 hours" , value : 6 * HOUR_MS } ,
69+ { key : "1d" , label : "1 day" , value : 24 * HOUR_MS } ,
70+ { key : "1w" , label : "1 week" , value : 7 * 24 * HOUR_MS } ,
71+ ] as const ;
72+ const CUSTOM_PRESET_KEY = "custom" ;
73+
74+ function msToHours ( value ?: number ) : number | undefined {
75+ if ( value == null ) return undefined ;
76+ return value / HOUR_MS ;
77+ }
78+
79+ function findPresetKey ( value ?: number ) : string | undefined {
80+ if ( value == null ) return undefined ;
81+ return EPHEMERAL_PRESETS . find ( ( preset ) => preset . value === value ) ?. key ;
82+ }
83+
84+ function formatEphemeralHours ( value ?: number ) : string {
85+ const hours = msToHours ( value ) ;
86+ return hours == null ? "" : `${ round1 ( hours ) } h` ;
87+ }
88+
89+ function ephemeralSignupUrl ( token ?: string ) : string {
90+ if ( ! token ) return "" ;
91+ if ( typeof window === "undefined" ) {
92+ return `/ephemeral?token=${ token } ` ;
93+ }
94+ const { protocol, host } = window . location ;
95+ return `${ protocol } //${ host } /ephemeral?token=${ token } ` ;
5596}
5697
5798function use_registration_tokens ( ) {
@@ -84,6 +125,8 @@ function use_registration_tokens() {
84125 expires : null ,
85126 limit : null ,
86127 disabled : null ,
128+ ephemeral : null ,
129+ customize : null ,
87130 } ,
88131 } ,
89132 } ) ;
@@ -140,11 +183,19 @@ function use_registration_tokens() {
140183 "expires" ,
141184 "limit" ,
142185 "descr" ,
186+ "ephemeral" ,
187+ "customize" ,
143188 ] as RegistrationTokenSetFields [ ] ) ;
144189 // set optional field to undefined (to get rid of it)
145- [ "descr" , "limit" , "expires" ] . forEach (
190+ [ "descr" , "limit" , "expires" , "ephemeral" ] . forEach (
146191 ( k : RegistrationTokenSetFields ) => ( val [ k ] = val [ k ] ?? undefined ) ,
147192 ) ;
193+ if ( val . customize != null ) {
194+ const { disableCollaborators, disableAI } = val . customize ;
195+ if ( ! disableCollaborators && ! disableAI ) {
196+ val . customize = undefined ;
197+ }
198+ }
148199 try {
149200 set_saving ( true ) ;
150201 await query ( {
@@ -278,7 +329,7 @@ export function RegistrationToken() {
278329
279330 const onFinish = ( values ) => save ( values ) ;
280331 const onRandom = ( ) => form . setFieldsValue ( { token : new_random_token ( ) } ) ;
281- const limit_min = editing != null ? editing . counter ?? 0 : 0 ;
332+ const limit_min = editing != null ? ( editing . counter ?? 0 ) : 0 ;
282333
283334 return (
284335 < Form
@@ -304,6 +355,98 @@ export function RegistrationToken() {
304355 < Form . Item name = "limit" label = "Limit" rules = { [ { required : false } ] } >
305356 < InputNumber min = { limit_min } step = { 1 } />
306357 </ Form . Item >
358+ < Form . Item name = "ephemeral" hidden >
359+ < InputNumber />
360+ </ Form . Item >
361+ < Form . Item label = "Ephemeral lifetime" >
362+ < Form . Item
363+ noStyle
364+ shouldUpdate = { ( prev , curr ) => prev . ephemeral !== curr . ephemeral }
365+ >
366+ { ( formInstance ) => {
367+ const ephemeral = formInstance . getFieldValue ( "ephemeral" ) ;
368+ const presetKey = findPresetKey ( ephemeral ) ;
369+ const selection =
370+ presetKey ??
371+ ( ephemeral != null ? CUSTOM_PRESET_KEY : undefined ) ;
372+ const customHours = msToHours ( ephemeral ) ;
373+
374+ const handleRadioChange = ( {
375+ target : { value } ,
376+ } : RadioChangeEvent ) => {
377+ if ( value === CUSTOM_PRESET_KEY ) {
378+ if ( ephemeral == null ) {
379+ formInstance . setFieldsValue ( { ephemeral : HOUR_MS } ) ;
380+ }
381+ return ;
382+ }
383+ const preset = EPHEMERAL_PRESETS . find (
384+ ( option ) => option . key === value ,
385+ ) ;
386+ formInstance . setFieldsValue ( {
387+ ephemeral : preset ?. value ,
388+ } ) ;
389+ } ;
390+
391+ const handleCustomHoursChange = (
392+ hours : number | string | null ,
393+ ) => {
394+ const numeric =
395+ typeof hours === "string" ? parseFloat ( hours ) : hours ;
396+ if ( typeof numeric === "number" && ! isNaN ( numeric ) ) {
397+ formInstance . setFieldsValue ( {
398+ ephemeral : numeric >= 0 ? numeric * HOUR_MS : undefined ,
399+ } ) ;
400+ } else {
401+ formInstance . setFieldsValue ( { ephemeral : undefined } ) ;
402+ }
403+ } ;
404+
405+ return (
406+ < >
407+ < Radio . Group value = { selection } onChange = { handleRadioChange } >
408+ { EPHEMERAL_PRESETS . map ( ( { key, label } ) => (
409+ < Radio key = { key } value = { key } >
410+ { label }
411+ </ Radio >
412+ ) ) }
413+ < Radio value = { CUSTOM_PRESET_KEY } > Custom</ Radio >
414+ </ Radio . Group >
415+ { selection === CUSTOM_PRESET_KEY && (
416+ < div style = { { marginTop : "10px" } } >
417+ < InputNumber
418+ min = { 0 }
419+ step = { 1 }
420+ value = { customHours ?? undefined }
421+ onChange = { handleCustomHoursChange }
422+ placeholder = "Enter hours"
423+ /> { " " }
424+ hours
425+ </ div >
426+ ) }
427+ </ >
428+ ) ;
429+ } }
430+ </ Form . Item >
431+ </ Form . Item >
432+ < Form . Item label = "Restrictions" >
433+ < Space direction = "vertical" >
434+ < Form . Item
435+ name = { [ "customize" , "disableCollaborators" ] }
436+ valuePropName = "checked"
437+ noStyle
438+ >
439+ < Checkbox > Disable configuring collaborators</ Checkbox >
440+ </ Form . Item >
441+ < Form . Item
442+ name = { [ "customize" , "disableAI" ] }
443+ valuePropName = "checked"
444+ noStyle
445+ >
446+ < Checkbox > Disable artificial intelligence</ Checkbox >
447+ </ Form . Item >
448+ </ Space >
449+ </ Form . Item >
307450 < Form . Item name = "active" label = "Active" valuePropName = "checked" >
308451 < Switch />
309452 </ Form . Item >
@@ -400,6 +543,22 @@ export function RegistrationToken() {
400543 defaultSortOrder = { "ascend" }
401544 sorter = { ( a , b ) => a . token . localeCompare ( b . token ) }
402545 />
546+ < Table . Column < Token >
547+ title = "Ephemeral link"
548+ width = { 240 }
549+ render = { ( _ , token ) => {
550+ if ( ! token ?. ephemeral ) return null ;
551+ const url = ephemeralSignupUrl ( token . token ) ;
552+ if ( ! url ) return null ;
553+ return (
554+ < Copyable
555+ value = { url }
556+ inputWidth = "14em"
557+ outerStyle = { { width : "100%" } }
558+ />
559+ ) ;
560+ } }
561+ />
403562 < Table . Column < Token > title = "Description" dataIndex = "descr" />
404563 < Table . Column < Token >
405564 title = "Uses"
@@ -411,6 +570,21 @@ export function RegistrationToken() {
411570 dataIndex = "limit"
412571 render = { ( text ) => ( text != null ? text : "∞" ) }
413572 />
573+ < Table . Column < Token >
574+ title = "Ephemeral (hours)"
575+ dataIndex = "ephemeral"
576+ render = { ( value ) => formatEphemeralHours ( value ) }
577+ />
578+ < Table . Column < Token >
579+ title = "Restrict collaborators"
580+ render = { ( _ , token ) =>
581+ token . customize ?. disableCollaborators ? "Yes" : ""
582+ }
583+ />
584+ < Table . Column < Token >
585+ title = "Disable AI"
586+ render = { ( _ , token ) => ( token . customize ?. disableAI ? "Yes" : "" ) }
587+ />
414588 < Table . Column < Token >
415589 title = "% Used"
416590 dataIndex = "used"
0 commit comments