Still refactoring...

This commit is contained in:
Eric van der Vlist 2023-02-24 18:00:40 +01:00
parent a0bed34b43
commit f4ba7afb69
15 changed files with 373 additions and 277 deletions

View File

@ -1,13 +1,13 @@
import { createForm } from '@felte/solid';
import reporter from '@felte/reporter-tippy';
import { Button, Dialog, TextField } from '@kobalte/core';
import { Component, createSignal } from 'solid-js';
import { Component, createSignal, JSXElement, Show } from 'solid-js';
import 'tippy.js/dist/tippy.css';
interface CredentialsType {
username: string;
password: string;
hostname?: string;
database: string;
}
const [adminCredentials, setAdminCredentials] = createSignal<CredentialsType>();
@ -155,3 +155,13 @@ const Credentials: Component<Props> = (props) => {
};
export default Credentials;
export const CheckCredentials: Component<{ children: JSXElement }> = (
props
) => {
return (
<Show when={!!adminCredentials()} fallback={<Credentials />}>
{props.children}
</Show>
);
};

View File

@ -1 +1 @@
export { default, adminCredentials } from './Credentials';
export { default, adminCredentials, CheckCredentials } from './Credentials';

View File

@ -1,88 +1,30 @@
import { createForm } from '@felte/solid';
import { Component, createSignal, Match, Switch } from 'solid-js';
import { TextField, Button, Dialog } from '@kobalte/core';
import './style.css';
import { createServerAction$ } from 'solid-start/server';
import PouchDb from 'pouchdb';
import { Component, createEffect, Match, Switch } from 'solid-js';
import { TextField, Button } from '@kobalte/core';
import reporter from '@felte/reporter-tippy';
import { v4 as uuid } from 'uuid';
import { put } from '~/lib/db';
import './style.css';
import { useNavigate } from 'solid-start';
import { toHex } from '~/lib/to-hex';
import { cloneDeep } from 'lodash';
import { adminCredentials } from '../credentials';
import { put } from '~/lib/put';
import { del } from '~/lib/del';
import { isFunction } from 'lodash';
interface Props {
values?: any;
values?: () => any;
}
const User: Component<Props> = (props) => {
const navigate = useNavigate();
const [openDialog, setOpenDialog] = createSignal(false);
const [openUsernamePasswordDialog, setOpenUserNamePasswordDialog] =
createSignal(false);
const getValues = () => {
if (!isFunction(props?.values)) {
return null;
}
return props.values();
};
const [status, setStatus] = createSignal<string>();
const [saving, save] = createServerAction$(async (args: any) => {
const { props, values } = args;
console.log({
caller: 'User / save',
props,
values,
args,
id: props.values?._id,
});
const db = new PouchDb('.db');
const id = props.values?._id ?? `user/${uuid()}`;
await put({
db,
doc: {
_id: id,
date: new Date().toISOString(),
type: 'user',
...values,
},
});
return id;
});
const [dbChecking, dbCheck] = createServerAction$(async (args: any) => {
console.log({
caller: 'User / dbCheck',
args,
});
let baseDbInfoAuth;
try {
const baseDb = new PouchDb(cloneDeep(args));
baseDbInfoAuth = await baseDb.info();
} catch (error) {
baseDbInfoAuth = { error };
}
let baseDbInfoAnon;
try {
const baseDb = new PouchDb(args.name);
baseDbInfoAnon = await baseDb.info();
} catch (error) {
baseDbInfoAnon = { error };
}
const name = `${args.name}/userdb-${toHex(args.auth.username)}`;
let userDbInfo;
try {
const userDb = new PouchDb({ ...args, name });
userDbInfo = await userDb.info();
} catch (error) {
userDbInfo = { error };
}
let userDbInfoAnon;
try {
const userDb = new PouchDb(name);
userDbInfoAnon = await userDb.info();
} catch (error) {
userDbInfoAnon = { error };
}
return { baseDbInfoAuth, baseDbInfoAnon, userDbInfo, userDbInfoAnon };
});
const isNew = () => !getValues();
const submitHandler = async (values: any, context: any) => {
console.log({
@ -91,9 +33,10 @@ const User: Component<Props> = (props) => {
values,
context,
});
const id = await save({ values, props });
if (!props.values) {
navigate(`/user/${encodeURIComponent(id)}`);
const id = getValues()?._id ?? `user:${uuid()}`;
await put(id, values, isNew());
if (isNew()) {
navigate(`/user/${id}`);
}
};
@ -104,42 +47,79 @@ const User: Component<Props> = (props) => {
const deleteHandler = async () => {
console.log({
caller: 'User / deleteHandler',
id: getValues()?._id,
props,
});
await save({ props, values: { _deleted: true } });
await del(getValues()?._id);
navigate(`/user/`);
};
const { form, data } = createForm({
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;
}
if (isNew()) {
const authString = `${username}:${password}`;
const headers = new Headers();
headers.set('Authorization', 'Basic ' + btoa(authString));
const response = await fetch(
`${database}/_users/org.couchdb.user:${values.username}`,
{
method: 'HEAD',
mode: 'cors',
headers,
}
);
if (response.status === 200) {
errors.username = 'The user is already existing';
}
}
return errors;
};
const { form, data, setData, setInitialValues, reset } = createForm({
onSubmit: submitHandler,
initialValues: props?.values,
extend: reporter(),
validate: validationHandler,
initialValues: getValues(),
});
const dbCheckHandler = async () => {
const result = await dbCheck({
name: data('database'),
auth: { username: data('username'), password: data('password') },
});
console.log({
caller: 'User / dbCheckHandler',
props,
data: data(),
result,
});
if (result.baseDbInfoAnon.error) {
setStatus('baseError');
} else if (!result.userDbInfo.error) {
setStatus('OK');
} else if (
result.userDbInfoAnon.error.reason ===
'You are not authorized to access this db.'
) {
setStatus('passwordError');
createEffect(() => {
console.log({ caller: 'user / createEffect', values: getValues() });
if (isNew()) {
const credentials = adminCredentials();
if (!credentials) {
return;
}
const { database } = credentials;
setData('database', database);
} else {
setStatus('notFound');
setInitialValues(getValues());
reset();
console.log({
caller: 'user / createEffect',
values: getValues(),
data: data(),
});
}
setOpenDialog(true);
};
});
const createUserHandler = async () => {
console.log({
@ -147,117 +127,70 @@ const User: Component<Props> = (props) => {
props,
data: data(),
});
setOpenUserNamePasswordDialog(true);
// setOpenUserNamePasswordDialog(true);
};
console.log({
caller: 'User ',
props,
values: getValues(),
});
return (
<>
<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'
/>
<TextField.ErrorMessage>
Please provide a valid URL
</TextField.ErrorMessage>
</TextField.Root>
<TextField.Root>
<TextField.Label>User name</TextField.Label>
<TextField.Input
type='text'
name='username'
required={true}
placeholder='user name'
/>
</TextField.Root>
<TextField.Root>
<TextField.Label>Password</TextField.Label>
<TextField.Input
type='text'
name='password'
required={true}
placeholder='Password'
autocomplete='off'
/>
<TextField.ErrorMessage>
Please provide a valid password
</TextField.ErrorMessage>
</TextField.Root>
<Switch>
<Match when={!!props.values}>
<Button.Root type='submit'>Save</Button.Root>
<Button.Root onclick={deleteHandler}>Delete</Button.Root>
</Match>
<Match when={!props.values}>
<Button.Root type='submit'>Create</Button.Root>
</Match>
</Switch>
<Button.Root onclick={dbCheckHandler}>DB check</Button.Root>
<Button.Root onclick={cancelHandler}>Cancel</Button.Root>
</form>
<Dialog.Root isOpen={openDialog()} onOpenChange={setOpenDialog}>
<Dialog.Portal>
<Dialog.Overlay class='dialog__overlay' />
<div class='dialog__positioner'>
<Dialog.Content class='dialog__content'>
<div class='dialog__header'>
<Dialog.Title class='dialog__title'>
Database check
</Dialog.Title>
<Dialog.CloseButton class='dialog__close-button'>
X
</Dialog.CloseButton>
</div>
<Dialog.Description class='dialog__description'>
<Switch>
<Match when={status() === 'baseError'}>
The database address seems to be wrong !!
</Match>
<Match when={status() === 'OK'}>
The user is already existing.
</Match>
<Match when={status() === 'notFound'}>
<div>The user must be created.</div>
<Button.Root onclick={createUserHandler}>
Create user
</Button.Root>
</Match>
<Match when={status() === 'passwordError'}>
<div>
The user seems to exist but the password is wrong.
</div>
<Button.Root onclick={createUserHandler}>
Update password
</Button.Root>
</Match>
</Switch>
</Dialog.Description>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
</>
<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>
<Switch>
<Match when={!isNew()}>
<Button.Root type='submit'>Save</Button.Root>
<Button.Root onclick={deleteHandler}>Delete</Button.Root>
</Match>
<Match when={isNew()}>
<Button.Root type='submit'>Create</Button.Root>
</Match>
</Switch>
<Button.Root onclick={cancelHandler}>Cancel</Button.Root>
</form>
);
};

View File

@ -1,34 +1,20 @@
import { Button } from '@kobalte/core';
import { A } from '@solidjs/router';
import PouchDb from 'pouchdb';
import { Component, createEffect, For } from 'solid-js';
import { createServerAction$ } from 'solid-start/server';
import { useNavigate } from 'solid-start';
interface Props {}
interface Props {
users: any;
}
const Users: Component<Props> = (props) => {
const { users } = props;
const navigate = useNavigate();
const [users, getUsers] = createServerAction$(
async (values: any) => {
const db = new PouchDb('.db');
const results = await db.allDocs({
include_docs: true,
startkey: 'user/',
endkey: 'user/\ufff0',
});
console.log({ caller: 'Users / serverAction', results });
return results.rows;
}
);
getUsers();
createEffect(() => {
console.log({
caller: 'Users',
users: users.result,
users: users(),
});
});
@ -40,7 +26,7 @@ const Users: Component<Props> = (props) => {
<>
<Button.Root onclick={newHandler}>New</Button.Root>
<ul>
<For each={users.result}>
<For each={users()}>
{(user: any) => {
console.log({
caller: 'Users / loop',
@ -48,9 +34,7 @@ const Users: Component<Props> = (props) => {
});
return (
<li>
<A href={encodeURIComponent(user.id)}>
{user.doc.mail}
</A>
<A href={user.id}>{user.doc.mail}</A>
</li>
);
}}

View File

@ -0,0 +1,27 @@
import { adminCredentials } from '~/components/credentials';
import { headersWithAuth } from './headers-with-auth';
export const adminDbExists = async () => {
const credentials = adminCredentials();
if (!credentials) {
return null;
}
const { database } = credentials;
const headers = headersWithAuth();
if (!headers) {
return null;
}
const response = await fetch(`${database}/_dyomedea_users`, {
method: 'HEAD',
mode: 'cors',
headers,
});
if (response.status !== 200) {
console.error({ caller: 'dbExists', response });
}
return response.status === 200;
};

View File

@ -0,0 +1,29 @@
import { adminCredentials } from '~/components/credentials';
import { headersWithAuth } from './headers-with-auth';
export const createAdminDb = async () => {
const credentials = adminCredentials();
if (!credentials) {
return null;
}
const { database } = credentials;
const headers = headersWithAuth();
if (!headers) {
return null;
}
const response = await fetch(`${database}/dyomedea_users`, {
method: 'PUT',
mode: 'cors',
headers,
});
console.log({ caller: 'dbExists', response });
if (![200, 201].includes(response.status)) {
console.error({ caller: 'dbExists', response });
}
return true;
};

View File

@ -1,11 +0,0 @@
export const put = async (params: { db: any; doc: any }) => {
const { db, doc } = params;
try {
const previous = await db.get(doc._id);
doc._rev = previous._rev;
} catch (error) {
console.error({ caller: 'put', doc, error });
}
console.log({ caller: 'put', doc });
db.put(doc);
};

6
src/lib/del.ts Normal file
View File

@ -0,0 +1,6 @@
import { put } from './put';
export const del = async (id: string) => {
const content = { _deleted: true };
return await put(id, content, false);
};

10
src/lib/get-url.ts Normal file
View File

@ -0,0 +1,10 @@
import { adminCredentials } from '~/components/credentials';
export const getUrl = (id: string) => {
const credentials = adminCredentials();
if (!credentials) {
return '';
}
const { database } = credentials;
return `${database}/dyomedea_users/${id}`;
};

23
src/lib/get.ts Normal file
View File

@ -0,0 +1,23 @@
import { adminCredentials } from '~/components/credentials';
import { headersWithAuth } from './headers-with-auth';
export const get = async (id: string) => {
const credentials = adminCredentials();
if (!credentials) {
return null;
}
const { database } = credentials;
const headers = headersWithAuth();
if (!headers) {
return null;
}
const response = await fetch(`${database}/dyomedea_users/${id}`, {
method: 'GET',
mode: 'cors',
headers,
});
return await response.json();
};

View File

@ -0,0 +1,13 @@
import { adminCredentials } from '~/components/credentials';
export const headersWithAuth = () => {
const credentials = adminCredentials();
if (!credentials) {
return null;
}
const { username, password } = credentials;
const authString = `${username}:${password}`;
const headers = new Headers();
headers.set('Authorization', 'Basic ' + btoa(authString));
return headers;
};

32
src/lib/put.ts Normal file
View File

@ -0,0 +1,32 @@
import { adminCredentials } from '~/components/credentials';
import { get } from './get';
import { getUrl } from './get-url';
import { headersWithAuth } from './headers-with-auth';
export const put = async (id: string, content: any, isNew: boolean = false) => {
const credentials = adminCredentials();
if (!credentials) {
return null;
}
const { database } = credentials;
if (!isNew) {
const previous = await get(id);
content._rev = previous._rev;
}
const headers = headersWithAuth();
if (!headers) {
return null;
}
headers.set('Content-type', 'application/json; charset=UTF-8');
const response = await fetch(getUrl(id), {
method: 'PUT',
mode: 'cors',
headers,
body: JSON.stringify(content),
});
return await response.json();
};

View File

@ -1,36 +1,30 @@
import { useParams } from 'solid-start';
import { createServerAction$ } from 'solid-start/server';
import PouchDb from 'pouchdb';
import User from '~/components/user';
import { createEffect, Show } from 'solid-js';
import { createEffect, createSignal } from 'solid-js';
import { adminCredentials, CheckCredentials } from '~/components/credentials';
import { get } from '~/lib/get';
export default () => {
const params = useParams();
const [user, getUser] = createServerAction$(
async (id: string) => {
const db = new PouchDb('.db');
const result = await db.get(id);
console.log({ caller: 'Users / serverAction', result });
return result;
const { id } = useParams();
const [user, setUser] = createSignal();
createEffect(async () => {
if (!user()) {
setUser(await get(id));
console.log({
caller: '/routes/user/[id].ts / createEffect',
user: user(),
adminCredentials: adminCredentials(),
});
}
);
getUser(decodeURIComponent(params.id));
createEffect(() => {
console.log({
caller: 'Users/[id]',
params,
user: user.result,
});
});
return (
<main>
<h1>User</h1>
<Show when={!user.pending} fallback={<>Loading...</>}>
<User values={user.result} />
</Show>
<CheckCredentials>
<User values={user} />
</CheckCredentials>
</main>
);
};

View File

@ -1,21 +1,64 @@
import { createEffect, Show } from 'solid-js';
import Credentials, { adminCredentials } from '~/components/credentials';
import { createEffect } from 'solid-js';
import { createRouteData, refetchRouteData, useRouteData } from 'solid-start';
import { adminCredentials, CheckCredentials } from '~/components/credentials';
import Users from '~/components/users';
import { adminDbExists } from '~/lib/admin-db-exists';
import { createAdminDb } from '~/lib/create-admin-db';
import { headersWithAuth } from '~/lib/headers-with-auth';
export function routeData() {
return createRouteData(async () => {
if (!(await adminDbExists())) {
await createAdminDb();
}
const credentials = adminCredentials();
if (!credentials) {
return null;
}
const { database } = credentials;
const headers = headersWithAuth();
if (!headers) {
return null;
}
headers.set('Content-type', 'application/json; charset=UTF-8');
const response = await fetch(`${database}/dyomedea_users/_all_docs`, {
method: 'POST',
mode: 'cors',
headers,
body: JSON.stringify({
include_docs: true,
startkey: 'user:',
endkey: 'user:\ufff0',
}),
});
const results = await response.json();
console.log({ caller: 'user route / routeData', results });
return results.rows;
});
}
createEffect(() => {
console.log({
caller: 'user/index / createEffect',
adminCredentials: adminCredentials(),
});
const credentials = adminCredentials();
if (!!credentials) {
console.log({
caller: 'routes/user/index.tsx / createEffect',
adminCredentials: credentials,
});
refetchRouteData();
}
});
export default () => {
const users = useRouteData<typeof routeData>();
// const users = routeData();
return (
<main>
<h1>Users</h1>
<Show when={!!adminCredentials()} fallback={<Credentials />}>
<Users />
</Show>
<CheckCredentials>
<Users users={users} />
</CheckCredentials>
</main>
);
};

View File

@ -1,10 +1,13 @@
import User from "~/components/user";
import { CheckCredentials } from '~/components/credentials';
import User from '~/components/user';
export default () => {
return (
<main>
<h1>New user</h1>
<User />
<CheckCredentials>
<User />
</CheckCredentials>
</main>
);
};