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 { createForm } from '@felte/solid';
import reporter from '@felte/reporter-tippy'; import reporter from '@felte/reporter-tippy';
import { Button, Dialog, TextField } from '@kobalte/core'; 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'; import 'tippy.js/dist/tippy.css';
interface CredentialsType { interface CredentialsType {
username: string; username: string;
password: string; password: string;
hostname?: string; database: string;
} }
const [adminCredentials, setAdminCredentials] = createSignal<CredentialsType>(); const [adminCredentials, setAdminCredentials] = createSignal<CredentialsType>();
@ -155,3 +155,13 @@ const Credentials: Component<Props> = (props) => {
}; };
export default Credentials; 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 { createForm } from '@felte/solid';
import { Component, createSignal, Match, Switch } from 'solid-js'; import { Component, createEffect, Match, Switch } from 'solid-js';
import { TextField, Button, Dialog } from '@kobalte/core'; import { TextField, Button } from '@kobalte/core';
import reporter from '@felte/reporter-tippy';
import './style.css';
import { createServerAction$ } from 'solid-start/server';
import PouchDb from 'pouchdb';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { put } from '~/lib/db'; import './style.css';
import { useNavigate } from 'solid-start'; import { useNavigate } from 'solid-start';
import { toHex } from '~/lib/to-hex'; import { adminCredentials } from '../credentials';
import { cloneDeep } from 'lodash'; import { put } from '~/lib/put';
import { del } from '~/lib/del';
import { isFunction } from 'lodash';
interface Props { interface Props {
values?: any; values?: () => any;
} }
const User: Component<Props> = (props) => { const User: Component<Props> = (props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [openDialog, setOpenDialog] = createSignal(false); const getValues = () => {
const [openUsernamePasswordDialog, setOpenUserNamePasswordDialog] = if (!isFunction(props?.values)) {
createSignal(false); return null;
}
return props.values();
};
const [status, setStatus] = createSignal<string>(); const isNew = () => !getValues();
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 submitHandler = async (values: any, context: any) => { const submitHandler = async (values: any, context: any) => {
console.log({ console.log({
@ -91,9 +33,10 @@ const User: Component<Props> = (props) => {
values, values,
context, context,
}); });
const id = await save({ values, props }); const id = getValues()?._id ?? `user:${uuid()}`;
if (!props.values) { await put(id, values, isNew());
navigate(`/user/${encodeURIComponent(id)}`); if (isNew()) {
navigate(`/user/${id}`);
} }
}; };
@ -104,42 +47,79 @@ const User: Component<Props> = (props) => {
const deleteHandler = async () => { const deleteHandler = async () => {
console.log({ console.log({
caller: 'User / deleteHandler', caller: 'User / deleteHandler',
id: getValues()?._id,
props, props,
}); });
await save({ props, values: { _deleted: true } }); await del(getValues()?._id);
navigate(`/user/`); 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, onSubmit: submitHandler,
initialValues: props?.values, extend: reporter(),
validate: validationHandler,
initialValues: getValues(),
}); });
const dbCheckHandler = async () => { createEffect(() => {
const result = await dbCheck({ console.log({ caller: 'user / createEffect', values: getValues() });
name: data('database'), if (isNew()) {
auth: { username: data('username'), password: data('password') }, const credentials = adminCredentials();
}); if (!credentials) {
console.log({ return;
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');
} else {
setStatus('notFound');
} }
setOpenDialog(true); const { database } = credentials;
}; setData('database', database);
} else {
setInitialValues(getValues());
reset();
console.log({
caller: 'user / createEffect',
values: getValues(),
data: data(),
});
}
});
const createUserHandler = async () => { const createUserHandler = async () => {
console.log({ console.log({
@ -147,16 +127,16 @@ const User: Component<Props> = (props) => {
props, props,
data: data(), data: data(),
}); });
setOpenUserNamePasswordDialog(true); // setOpenUserNamePasswordDialog(true);
}; };
console.log({ console.log({
caller: 'User ', caller: 'User ',
props, props,
values: getValues(),
}); });
return ( return (
<>
<form use:form> <form use:form>
<TextField.Root> <TextField.Root>
<TextField.Label>Mail address</TextField.Label> <TextField.Label>Mail address</TextField.Label>
@ -177,10 +157,8 @@ const User: Component<Props> = (props) => {
name='database' name='database'
required={true} required={true}
placeholder='Database URL' placeholder='Database URL'
readOnly={true}
/> />
<TextField.ErrorMessage>
Please provide a valid URL
</TextField.ErrorMessage>
</TextField.Root> </TextField.Root>
<TextField.Root> <TextField.Root>
<TextField.Label>User name</TextField.Label> <TextField.Label>User name</TextField.Label>
@ -189,6 +167,7 @@ const User: Component<Props> = (props) => {
name='username' name='username'
required={true} required={true}
placeholder='user name' placeholder='user name'
readOnly={!isNew()}
/> />
</TextField.Root> </TextField.Root>
<TextField.Root> <TextField.Root>
@ -200,64 +179,18 @@ const User: Component<Props> = (props) => {
placeholder='Password' placeholder='Password'
autocomplete='off' autocomplete='off'
/> />
<TextField.ErrorMessage>
Please provide a valid password
</TextField.ErrorMessage>
</TextField.Root> </TextField.Root>
<Switch> <Switch>
<Match when={!!props.values}> <Match when={!isNew()}>
<Button.Root type='submit'>Save</Button.Root> <Button.Root type='submit'>Save</Button.Root>
<Button.Root onclick={deleteHandler}>Delete</Button.Root> <Button.Root onclick={deleteHandler}>Delete</Button.Root>
</Match> </Match>
<Match when={!props.values}> <Match when={isNew()}>
<Button.Root type='submit'>Create</Button.Root> <Button.Root type='submit'>Create</Button.Root>
</Match> </Match>
</Switch> </Switch>
<Button.Root onclick={dbCheckHandler}>DB check</Button.Root>
<Button.Root onclick={cancelHandler}>Cancel</Button.Root> <Button.Root onclick={cancelHandler}>Cancel</Button.Root>
</form> </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>
</>
); );
}; };

View File

@ -1,34 +1,20 @@
import { Button } from '@kobalte/core'; import { Button } from '@kobalte/core';
import { A } from '@solidjs/router'; import { A } from '@solidjs/router';
import PouchDb from 'pouchdb';
import { Component, createEffect, For } from 'solid-js'; import { Component, createEffect, For } from 'solid-js';
import { createServerAction$ } from 'solid-start/server';
import { useNavigate } from 'solid-start'; import { useNavigate } from 'solid-start';
interface Props {} interface Props {
users: any;
}
const Users: Component<Props> = (props) => { const Users: Component<Props> = (props) => {
const { users } = props;
const navigate = useNavigate(); 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(() => { createEffect(() => {
console.log({ console.log({
caller: 'Users', caller: 'Users',
users: users.result, users: users(),
}); });
}); });
@ -40,7 +26,7 @@ const Users: Component<Props> = (props) => {
<> <>
<Button.Root onclick={newHandler}>New</Button.Root> <Button.Root onclick={newHandler}>New</Button.Root>
<ul> <ul>
<For each={users.result}> <For each={users()}>
{(user: any) => { {(user: any) => {
console.log({ console.log({
caller: 'Users / loop', caller: 'Users / loop',
@ -48,9 +34,7 @@ const Users: Component<Props> = (props) => {
}); });
return ( return (
<li> <li>
<A href={encodeURIComponent(user.id)}> <A href={user.id}>{user.doc.mail}</A>
{user.doc.mail}
</A>
</li> </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 { useParams } from 'solid-start';
import { createServerAction$ } from 'solid-start/server';
import PouchDb from 'pouchdb';
import User from '~/components/user'; 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 () => { export default () => {
const params = useParams(); const { id } = useParams();
const [user, getUser] = createServerAction$( const [user, setUser] = createSignal();
async (id: string) => {
const db = new PouchDb('.db');
const result = await db.get(id);
console.log({ caller: 'Users / serverAction', result });
return result;
}
);
getUser(decodeURIComponent(params.id)); createEffect(async () => {
if (!user()) {
createEffect(() => { setUser(await get(id));
console.log({ console.log({
caller: 'Users/[id]', caller: '/routes/user/[id].ts / createEffect',
params, user: user(),
user: user.result, adminCredentials: adminCredentials(),
}); });
}
}); });
return ( return (
<main> <main>
<h1>User</h1> <h1>User</h1>
<Show when={!user.pending} fallback={<>Loading...</>}> <CheckCredentials>
<User values={user.result} /> <User values={user} />
</Show> </CheckCredentials>
</main> </main>
); );
}; };

View File

@ -1,21 +1,64 @@
import { createEffect, Show } from 'solid-js'; import { createEffect } from 'solid-js';
import Credentials, { adminCredentials } from '~/components/credentials'; import { createRouteData, refetchRouteData, useRouteData } from 'solid-start';
import { adminCredentials, CheckCredentials } from '~/components/credentials';
import Users from '~/components/users'; 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(() => { createEffect(() => {
const credentials = adminCredentials();
if (!!credentials) {
console.log({ console.log({
caller: 'user/index / createEffect', caller: 'routes/user/index.tsx / createEffect',
adminCredentials: adminCredentials(), adminCredentials: credentials,
}); });
refetchRouteData();
}
}); });
export default () => { export default () => {
const users = useRouteData<typeof routeData>();
// const users = routeData();
return ( return (
<main> <main>
<h1>Users</h1> <h1>Users</h1>
<Show when={!!adminCredentials()} fallback={<Credentials />}> <CheckCredentials>
<Users /> <Users users={users} />
</Show> </CheckCredentials>
</main> </main>
); );
}; };

View File

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