backend/src/components/user/User.tsx

446 lines
12 KiB
TypeScript

import { createForm } from '@felte/solid';
import {
Component,
createSignal,
Index,
Match,
onMount,
Show,
Switch,
} from 'solid-js';
import { TextField, Button, Progress } from '@kobalte/core';
import reporter from '@felte/reporter-tippy';
import './style.css';
import { A, useNavigate } from 'solid-start';
import { adminCredentials } from '../credentials';
import { put } from '~/lib/put';
import { del } from '~/lib/del';
import { isFunction } from 'lodash';
import { userId } from '~/lib/user-id';
import { userExists } from '~/lib/user-exists';
import { update } from '~/lib/update';
import { replicationDocument } from '~/lib/replication-document';
import { get } from '~/lib/get';
import { v4 as uuid } from 'uuid';
interface Props {
values?: () => any;
}
const User: Component<Props> = (props) => {
const navigate = useNavigate();
const [progress, setProgress] = createSignal(-1);
const defaultValues = () => ({});
const values = props.values ?? defaultValues;
const [previousSubscriptions, setPreviousSubscriptions] = createSignal(
values().subscriptions ?? []
);
const credentials = adminCredentials();
const { database } = credentials || { database: null };
const isNew = () => !isFunction(props?.values);
const getValues = () => {
if (isNew()) {
return { database, subscriptions: [], token: uuid() };
}
return { token: uuid(), subscriptions: [], ...values() };
};
const invitationLink = (type = 'https') => {
// TODO: make these URLs configurable
if (type === 'https') {
const configUrl = `https://admin.dyomedea.app/api/conf/${
getValues().token
}`;
return `https://web.dyomedea.app#${btoa(configUrl)}`;
} else {
const configUrl = `https://admin.dyomedea.app/api/conf/${
getValues().token
}`;
return `geo:?invitation=${btoa(configUrl)}`;
}
};
const submitHandler = async (values: any, context: any) => {
console.log({
caller: 'User / submitHandler',
props,
values,
context,
});
if (!database) {
return;
}
const { username } = values;
setProgress(0);
const id = getValues()?._id ?? userId(username, values.database);
await put(id, values, isNew());
const couchUserId = `org.couchdb.user:${username}`;
const userDoc = {
_id: couchUserId,
name: username,
password: values.password,
type: 'user',
roles: [],
};
await put(couchUserId, userDoc, isNew(), '_users');
setProgress(1);
const updatedSubscriptions = values.subscriptions ?? [];
const isIn = (username: string, subs: any[]) => {
for (let i = 0; i < subs.length; i++) {
let sub = subs[i];
if (username === sub.username) {
return true;
}
}
return false;
};
console.log({
caller: 'User / submitHandler',
previousSubscriptions: previousSubscriptions(),
updatedSubscriptions,
});
const addSubscriptionFactory =
(subscriptionUserName: string) => (userDocument: any) => {
const subscriptions = userDocument.subscriptions || [];
subscriptions.push({ username, direction: '' });
userDocument.subscriptions = subscriptions;
return { username: subscriptionUserName, ...userDocument };
};
const removeSubscriptionFactory =
(subscriptionUserName: string) => (userDocument: any) => {
const subscriptions = userDocument.subscriptions || [];
userDocument.subscriptions = subscriptions.filter(
(subscription: any) => subscription.username !== username
);
return { username: subscriptionUserName, ...userDocument };
};
const defaultUserDocument = {
database,
};
const currentUserId = userId(data('username'), data('database'));
const credentials = adminCredentials();
const adminUser = credentials?.username || '';
for (let i = 0; i < updatedSubscriptions.length; i++) {
let subscription = updatedSubscriptions[i];
if (!isIn(subscription.username, previousSubscriptions())) {
await update(
userId(subscription.username, database),
addSubscriptionFactory(subscription.username),
defaultUserDocument
);
const otherUserId = userId(subscription.username, data('database'));
const otherUserPassword = (await get(otherUserId)).password;
let repDocument = replicationDocument(
currentUserId,
data('password'),
otherUserId,
otherUserPassword,
adminUser
);
await put(repDocument._id, repDocument, false, '_replicator');
repDocument = replicationDocument(
otherUserId,
otherUserPassword,
currentUserId,
data('password'),
adminUser
);
await put(repDocument._id, repDocument, false, '_replicator');
console.log({
caller: 'User / submitHandler / new subscription',
username: subscription.username,
});
}
}
setProgress(2);
for (let i = 0; i < previousSubscriptions().length; i++) {
let subscription = previousSubscriptions()[i];
if (!isIn(subscription.username, updatedSubscriptions)) {
await update(
userId(subscription.username, database),
removeSubscriptionFactory(subscription.username),
defaultUserDocument
);
const otherUserId = userId(subscription.username, data('database'));
await del(`${currentUserId}=>${otherUserId}`, '_replicator');
await del(`${otherUserId}=>${currentUserId}`, '_replicator');
console.log({
caller: 'User / submitHandler / deleted subscription',
username: subscription.username,
});
}
}
setProgress(3);
setInitialValues(values);
setPreviousSubscriptions(values.subscriptions);
setIsDirty(false);
setProgress(-1);
navigate(`/user/${id}`);
};
const cancelHandler = () => {
navigate(`/user/`);
};
const deleteHandler = async () => {
console.log({
caller: 'User / deleteHandler',
id: getValues()?._id,
props,
});
await del(getValues()?._id);
navigate(`/user/`);
};
const validationHandler = async (values: any) => {
let errors: any = {};
const credentials = adminCredentials();
if (!credentials) {
return errors;
}
const { database, username, password } = credentials;
try {
const response = await fetch(database, { mode: 'cors' });
const dbStatus = await response.json();
if (!dbStatus.couchdb) {
errors.database = 'The URL is not a couchdb instance';
}
} catch (error) {
errors.database = "Can't access the database";
}
if (!!errors.database) {
return errors;
}
const subscriptions = values.subscriptions ?? [];
for (let i = 0; i < subscriptions.length; i++) {
let subscription = subscriptions[i];
if (
!!subscription.username &&
!(await userExists(subscription.username))
) {
if (!errors.subscriptions) {
errors.subscriptions = [];
}
errors.subscriptions[i] = { username: "User doesn't exist" };
}
}
if (isNew()) {
if (await userExists(values.username)) {
errors.username = 'The user is already existing';
}
}
console.log({ caller: 'Users / validationHandler', values, errors });
return errors;
};
const {
form,
data,
setData,
setIsDirty,
setInitialValues,
reset,
addField,
unsetField,
isDirty,
isValid,
} = createForm({
onSubmit: submitHandler,
extend: reporter(),
validate: validationHandler,
initialValues: getValues(),
});
const createUserHandler = async () => {
console.log({
caller: 'User / createUserHandler',
props,
data: data(),
});
// setOpenUserNamePasswordDialog(true);
};
const subscriptions = () => data('subscriptions');
function removeSubscription(index: number) {
return () => {
unsetField(`subscriptions.${index}`);
setIsDirty(true);
};
}
function addSubscription(index?: number) {
return () => {
addField(`subscriptions`, { username: '', direction: '' }, index);
setIsDirty(true);
};
}
console.log({
caller: 'User ',
props,
values: getValues(),
data: data(),
});
return (
<>
<Show when={progress() >= 0}>
<Progress.Root
value={progress()}
minValue={0}
maxValue={3}
getValueLabel={({ value, max }) =>
`${value} of ${max} tasks completed`
}
class='progress'
>
<div class='progress__label-container'>
<Progress.Label class='progress__label'>
Processing...
</Progress.Label>
<Progress.ValueLabel class='progress__value-label' />
</div>
<Progress.Track class='progress__track'>
<Progress.Fill class='progress__fill' />
</Progress.Track>
</Progress.Root>
</Show>
<Show when={progress() < 0}>
<Show when={!isNew()}>
<h2>{userId(data('username'), data('database'))}</h2>
</Show>
<h3>
Invitations :
<A href={invitationLink()} target='_blank'>
https
</A>,
<a href={invitationLink('geo')} target='_blank'>
geo
</a>
</h3>
<form use:form>
<TextField.Root>
<TextField.Label>Mail address</TextField.Label>
<TextField.Input
type='mail'
name='mail'
required={true}
placeholder='Email address'
/>
<TextField.ErrorMessage>
Please provide a valid URL
</TextField.ErrorMessage>
</TextField.Root>
<TextField.Root>
<TextField.Label>Database</TextField.Label>
<TextField.Input
type='url'
name='database'
required={true}
placeholder='Database URL'
readOnly={true}
/>
</TextField.Root>
<TextField.Root>
<TextField.Label>User name</TextField.Label>
<TextField.Input
type='text'
name='username'
required={true}
placeholder='user name'
readOnly={!isNew()}
/>
</TextField.Root>
<TextField.Root>
<TextField.Label>Password</TextField.Label>
<TextField.Input
type='text'
name='password'
required={true}
placeholder='Password'
autocomplete='off'
/>
</TextField.Root>
<table>
<thead>
<tr>
<th>
<button type='button' onClick={addSubscription()}>
+
</button>
</th>
<th>User name</th>
</tr>
</thead>
<tbody>
<Index each={subscriptions()}>
{(_, index) => (
<tr>
<td>
<button type='button' onClick={removeSubscription(index)}>
-
</button>
</td>
<td>
<TextField.Root>
<TextField.Input
type='text'
name={`subscriptions.${index}.username`}
required={true}
placeholder='user name'
/>
</TextField.Root>
</td>
</tr>
)}
</Index>
</tbody>
</table>
<Switch>
<Match when={!isNew()}>
<Button.Root type='submit' disabled={!isDirty() || !isValid()}>
Save
</Button.Root>
<Button.Root onclick={deleteHandler}>Delete</Button.Root>
</Match>
<Match when={isNew()}>
<Button.Root type='submit' isDisabled={!isValid()}>
Create
</Button.Root>
</Match>
</Switch>
<Button.Root onclick={cancelHandler}>Back</Button.Root>
</form>
</Show>
</>
);
};
export default User;