Add file store

This commit is contained in:
andrea 2023-06-28 17:21:59 +02:00
parent da2b13597a
commit 371379e981
64 changed files with 327 additions and 8343 deletions

View file

@ -1,49 +0,0 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"name": {
"type": "object",
"properties": {
"first": {
"title": "First Name",
"type": "string"
},
"last": {
"title": "Last Name",
"type": "string"
}
}
}
},
"required": [
"email"
],
"additionalProperties": false
}
}
}

View file

@ -1,91 +0,0 @@
version: v0.8.0-alpha.3
dsn: memory
serve:
public:
base_url: http://localhost:4433/
cors:
enabled: true
allowed_origins:
- http://localhost:3000
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
allowed_headers:
- Authorization
- Cookie
- Content-Type
exposed_headers:
- Content-Type
- Set-Cookie
admin:
base_url: http://kratos:4434/
selfservice:
default_browser_return_url: http://localhost:3000/
allowed_return_urls:
- http://localhost:3000
methods:
password:
enabled: true
flows:
error:
ui_url: http://localhost:3000/auth/login
settings:
ui_url: http://localhost:3000/auth/settings
privileged_session_max_age: 15m
recovery:
enabled: true
ui_url: http://localhost:3000/auth/recovery
verification:
enabled: true
ui_url: http://localhost:3000/auth/verification
after:
default_browser_return_url: http://localhost:3000/
logout:
after:
default_browser_return_url: http://localhost:3000/auth/login
login:
ui_url: http://localhost:3000/auth/login
registration:
ui_url: http://localhost:3000/auth/registration
after:
password:
hooks:
-
hook: session
log:
level: info
format: text
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
hashers:
algorithm: bcrypt
bcrypt:
cost: 8
identity:
default_schema_id: preset://email
schemas:
- id: preset://email
url: file:///etc/config/kratos/identity.schema.json
courier:
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true

View file

@ -1,42 +0,0 @@
version: '3.7'
services:
kratos:
volumes:
- type: volume
source: kratos-sqlite
target: /var/lib/sqlite
read_only: false
- type: bind
source: ./contrib/quickstart/kratos/cloud
target: /etc/config/kratos
kratos-migrate:
volumes:
- type: volume
source: kratos-sqlite
target: /var/lib/sqlite
read_only: false
- type: bind
source: ./contrib/quickstart/kratos/cloud
target: /etc/config/kratos
# kratos-selfservice-ui-node:
# ports:
# - "4438:4438"
# environment:
# - PORT=4438
# - KRATOS_BROWSER_URL=http://localhost:4455/
# kratos-caddy:
# image: caddy:2.4.5-alpine
# ports:
# - "4455:4455"
# volumes:
# - type: bind
# source: ./contrib/quickstart/kratos/cloud/Caddyfile
# target: /etc/caddy/Caddyfile
# command: caddy run -watch -config /etc/caddy/Caddyfile
# restart: on-failure
# networks:
# - intranet

View file

@ -1,49 +0,0 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"name": {
"type": "object",
"properties": {
"first": {
"title": "First Name",
"type": "string"
},
"last": {
"title": "Last Name",
"type": "string"
}
}
}
},
"required": [
"email"
],
"additionalProperties": false
}
}
}

View file

@ -1,84 +0,0 @@
version: v0.7.1-alpha.1
dsn: memory
serve:
public:
base_url: http://127.0.0.1:4433/
cors:
enabled: true
admin:
base_url: http://kratos:4434/
selfservice:
default_browser_return_url: http://127.0.0.1:4455/
allowed_return_urls:
- http://127.0.0.1:4455
methods:
password:
enabled: true
flows:
error:
ui_url: http://127.0.0.1:4455/error
settings:
ui_url: http://127.0.0.1:4455/settings
privileged_session_max_age: 15m
recovery:
enabled: true
ui_url: http://127.0.0.1:4455/recovery
verification:
enabled: true
ui_url: http://127.0.0.1:4455/verification
after:
default_browser_return_url: http://127.0.0.1:4455/
logout:
after:
default_browser_return_url: http://127.0.0.1:4455/login
login:
ui_url: http://127.0.0.1:4455/login
lifespan: 10m
registration:
lifespan: 10m
ui_url: http://127.0.0.1:4455/registration
after:
password:
hooks:
-
hook: session
log:
level: debug
format: text
leak_sensitive_values: true
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
cipher:
- 32-LONG-SECRET-NOT-SECURE-AT-ALL
ciphers:
algorithm: xchacha20-poly1305
hashers:
algorithm: bcrypt
bcrypt:
cost: 8
identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
courier:
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true

View file

@ -1,40 +0,0 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"website": {
"type": "object"
}
},
"required": [
"website",
"email"
],
"additionalProperties": false
}
}
}

View file

@ -1,17 +0,0 @@
local claims = {
email_verified: false
} + std.extVar('claims');
{
identity: {
traits: {
// Allowing unverified email addresses enables account
// enumeration attacks, especially if the value is used for
// e.g. verification or as a password login identifier.
//
// Therefore we only return the email if it (a) exists and (b) is marked verified
// by GitHub.
[if "email" in claims && claims.email_verified then "email" else null]: claims.email,
},
},
}

View file

@ -1,60 +0,0 @@
-
id: "ory:kratos:public"
upstream:
preserve_host: true
url: "http://kratos:4433"
strip_path: /.ory/kratos/public
match:
url: "http://127.0.0.1:4455/.ory/kratos/public/<**>"
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
authenticators:
-
handler: noop
authorizer:
handler: allow
mutators:
- handler: noop
-
id: "ory:kratos-selfservice-ui-node:anonymous"
upstream:
preserve_host: true
url: "http://kratos-selfservice-ui-node:4435"
match:
url: "http://127.0.0.1:4455/<{registration,welcome,recovery,verification,login,error,**.css,**.js,**.png,}>"
methods:
- GET
authenticators:
-
handler: anonymous
authorizer:
handler: allow
mutators:
-
handler: noop
-
id: "ory:kratos-selfservice-ui-node:protected"
upstream:
preserve_host: true
url: "http://kratos-selfservice-ui-node:4435"
match:
url: "http://127.0.0.1:4455/<{debug,dashboard,settings}>"
methods:
- GET
authenticators:
-
handler: cookie_session
authorizer:
handler: allow
mutators:
- handler: id_token
errors:
- handler: redirect
config:
to: http://127.0.0.1:4455/login

View file

@ -1,18 +0,0 @@
{
"keys": [
{
"use": "sig",
"kty": "RSA",
"kid": "a2aa9739-d753-4a0d-87ee-61f101050277",
"alg": "RS256",
"n": "zpjSl0ySsdk_YC4ZJYYV-cSznWkzndTo0lyvkYmeBkW60YHuHzXaviHqonY_DjFBdnZC0Vs_QTWmBlZvPzTp4Oni-eOetP-Ce3-B8jkGWpKFOjTLw7uwR3b3jm_mFNiz1dV_utWiweqx62Se0SyYaAXrgStU8-3P2Us7_kz5NnBVL1E7aEP40aB7nytLvPhXau-YhFmUfgykAcov0QrnNY0DH0eTcwL19UysvlKx6Uiu6mnbaFE1qx8X2m2xuLpErfiqj6wLCdCYMWdRTHiVsQMtTzSwuPuXfH7J06GTo3I1cEWN8Mb-RJxlosJA_q7hEd43yYisCO-8szX0lgCasw",
"e": "AQAB",
"d": "x3dfY_rna1UQTmFToBoMn6Edte47irhkra4VSNPwwaeTTvI-oN2TO51td7vo91_xD1nw-0c5FFGi4V2UfRcudBv9LD1rHt_O8EPUh7QtAUeT3_XXgjx1Xxpqu5goMZpkTyGZ-B6JzOY3L8lvWQ_Qeia1EXpvxC-oTOjJnKZeuwIPlcoNKMRU-mIYOnkRFfnUvrDm7N9UZEp3PfI3vhE9AquP1PEvz5KTUYkubsfmupqqR6FmMUm6ulGT7guhBw9A3vxIYbYGKvXLdBvn68mENrEYxXrwmu6ITMh_y208M5rC-hgEHIAIvMu1aVW6jNgyQTunsGST3UyrSbwjI0K9UQ",
"p": "77fDvnfHRFEgyi7mh0c6fAdtMEMJ05W8NwTG_D-cSwfWipfTwJJrroWoRwEgdAg5AWGq-MNUzrubTVXoJdC2T4g1o-VRZkKKYoMvav3CvOIMzCBxBs9I_GAKr5NCSk7maksMqiCTMhmkoZ5RPuMYMY_YzxKNAbjBd9qFLfaVAqs",
"q": "3KEmPA2XQkf7dvtpY1Xkp1IfMV_UBdmYk7J6dB5BYqzviQWdEFvWaSATJ_7qV1dw0JDZynOgipp8gvoL-RepfjtArhPz41wB3J2xmBYrBr1sJ-x5eqAvMkQk2bd5KTor44e79TRIkmkFYAIdUQ5JdVXPA13S8WUZfb_bAbwaCBk",
"dp": "5uyy32AJkNFKchqeLsE6INMSp0RdSftbtfCfM86fZFQno5lA_qjOnO_avJPkTILDT4ZjqoKYxxJJOEXCffNCPPltGvbE5GrDXsUbP8k2-LgWNeoml7XFjIGEqcCFQoohQ1IK4DTDN6cmRh76C0e_Pbdh15D6TydJEIlsdGuu_kM",
"dq": "aegFNYCEojFxeTzX6vIZL2RRSt8oJKK-Be__reu0EUzYMtr5-RdMhev6phFMph54LfXKRc9ZOg9MQ4cJ5klAeDKzKpyzTukkj6U20b2aa8LTvxpZec6YuTVSxxu2Ul71IGRQijTNvVIiXWLGddk409Ub6Q7JqkyQfvdwhpWnnUk",
"qi": "P68-EwgcRy9ce_PZ75c909cU7dzCiaGcTX1psJiXmQAFBcG0msWfsyHGbllOZG27pKde78ORGJDYDNk1FqTwsogZyCP87EiBmOoqXWnMvKYfJ1DOx7x42LMAGwMD3bgQj9jgRACxFJG4n3NI6uFlFruyl_CLQzwW_rQFHshLK7Q"
}
]
}

View file

@ -1,88 +0,0 @@
log:
level: debug
format: json
serve:
proxy:
cors:
enabled: true
allowed_origins:
- "*"
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
allowed_headers:
- Authorization
- Content-Type
exposed_headers:
- Content-Type
allow_credentials: true
debug: true
errors:
fallback:
- json
handlers:
redirect:
enabled: true
config:
to: http://127.0.0.1:4455/login
when:
-
error:
- unauthorized
- forbidden
request:
header:
accept:
- text/html
json:
enabled: true
config:
verbose: true
access_rules:
matching_strategy: glob
repositories:
- file:///etc/config/oathkeeper/access-rules.yml
authenticators:
anonymous:
enabled: true
config:
subject: guest
cookie_session:
enabled: true
config:
check_session_url: http://kratos:4433/sessions/whoami
preserve_path: true
extra_from: "@this"
subject_from: "identity.id"
only:
- ory_kratos_session
noop:
enabled: true
authorizers:
allow:
enabled: true
mutators:
noop:
enabled: true
id_token:
enabled: true
config:
issuer_url: http://127.0.0.1:4455/
jwks_url: file:///etc/config/oathkeeper/id_token.jwks.json
claims: |
{
"session": {{ .Extra | toJson }}
}

View file

@ -1,61 +0,0 @@
version: '3.7'
services:
kratos-migrate:
image: oryd/kratos:v0.10.1
environment:
- DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc
volumes:
- type: volume
source: kratos-sqlite
target: /var/lib/sqlite
read_only: false
- type: bind
source: ./contrib/quickstart/kratos/email-password
target: /etc/config/kratos
command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
restart: on-failure
networks:
- intranet
# kratos-selfservice-ui-node:
# image: oryd/kratos-selfservice-ui-node:v0.10.1
# environment:
# - KRATOS_PUBLIC_URL=http://kratos:4433/
# - KRATOS_BROWSER_URL=http://127.0.0.1:4433/
# networks:
# - intranet
# restart: on-failure
kratos:
depends_on:
- kratos-migrate
image: oryd/kratos:v0.10.1
ports:
- '4433:4433' # public
- '4434:4434' # admin
restart: unless-stopped
environment:
- DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true
- LOG_LEVEL=trace
command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier
volumes:
- type: volume
source: kratos-sqlite
target: /var/lib/sqlite
read_only: false
- type: bind
source: ./contrib/quickstart/kratos/email-password
target: /etc/config/kratos
networks:
- intranet
# mailslurper:
# image: oryd/mailslurper:latest-smtps
# ports:
# - '4436:4436'
# - '4437:4437'
# networks:
# - intranet
networks:
intranet:
volumes:
kratos-sqlite:

2
frontend/.gitignore vendored
View file

@ -1,2 +0,0 @@
node_modules
.svelte-kit

View file

@ -1,70 +0,0 @@
# kratos-selfservice-svelte-node
Self-service [Svelte](https://svelte.dev/) node for
[Ory Kratos](https://github.com/ory/kratos). It has no style or decoration.
Apply your custom style according to your application.
## Install
```bash
git clone https://github.com/emrahcom/kratos-selfservice-svelte-node.git
cd kratos-selfservice-svelte-node
npm install
```
## Config
Change `src/lib/config.ts` according to your environment.
```javascript
export const KRATOS = "https://___KRATOS_FQDN___";
export const APP = "https://___APP_FQDN___";
```
## Run (dev)
```bash
npm run dev -- --host --port 3000
```
## Run (prod)
```bash
npm run build
node build/index.js
```
## Pages
- Landing page
`/`
- Secure dashboard
`/dashboard`
- Registration
`/registration`
- Login
`/login`
- Settings
`/settings`
- Recovery
`/recovery`
- Verification
`/verification`
- Logout
`/logout`

File diff suppressed because it is too large Load diff

View file

@ -1,37 +0,0 @@
{
"name": "kratos-svelte-ui",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/adapter-node": "^1.0.0-next.94",
"@sveltejs/kit": "next",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.44.0",
"svelte-check": "^2.7.1",
"svelte-preprocess": "^4.10.6",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^3.1.0"
},
"type": "module",
"dependencies": {
"@codemirror/lang-javascript": "^6.1.0",
"codemirror": "^6.0.1"
}
}

View file

@ -1,9 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface PageError {}
// interface Platform {}
}

View file

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"
/>
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View file

@ -1,139 +0,0 @@
<script context="module">
import { EditorView, minimalSetup, basicSetup } from "codemirror";
import { StateEffect } from "@codemirror/state";
export { minimalSetup, basicSetup };
</script>
<script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
let dom: any;
let _mounted = false;
onMount(() => {
_mounted = true;
return () => {
_mounted = false;
};
});
export let view: any = null;
/* `doc` is deliberately made non-reactive for not storing a reduntant string
besides the editor. Also, setting doc to undefined will not trigger an
update, so that you can clear it after setting one. */
export let doc: string;
/* Set this if you would like to listen to all transactions via `update` event. */
export let verbose = false;
/* Cached doc string so that we don't extract strings in bulk over and over. */
let _docCached: string = "";
/* Overwrite the bulk of the text with the one specified. */
function _setText(text: string) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: text },
});
}
const subscribers = new Set();
/* And here comes the reactivity, implemented as a r/w store. */
export const docStore = {
ready: () => view !== null,
subscribe(cb) {
subscribers.add(cb);
if (!this.ready()) {
cb(null);
} else {
if (_docCached == null) {
_docCached = view.state.doc.toString();
}
cb(_docCached);
}
return () => void subscribers.delete(cb);
},
set(newValue: string) {
if (!_mounted) {
throw new Error(
"Cannot set docStore when the component is not mounted."
);
}
const inited = _initEditorView(newValue);
if (!inited) _setText(newValue);
},
};
export let extensions = minimalSetup;
function _reconfigureExtensions() {
if (view === null) return;
view.dispatch({
effects: StateEffect.reconfigure.of(extensions),
});
}
$: extensions, _reconfigureExtensions();
function _editorTxHandler(tr: any) {
this.update([tr]);
if (verbose) {
dispatch("update", tr);
}
if (tr.docChanged) {
_docCached = "";
if (subscribers.size) {
dispatchDocStore((_docCached = tr.newDoc.toString()));
}
dispatch("change", { view: this, tr });
}
}
function dispatchDocStore(s) {
for (const cb of subscribers) {
cb(s);
}
}
// the view will be inited with the either doc (as long as that it is not `undefined`)
// or the value in docStore once set
function _initEditorView(initialDoc: string) {
if (view !== null) {
return false;
}
view = new EditorView({
doc: initialDoc,
extensions,
parent: dom,
dispatch: _editorTxHandler,
});
return true;
}
$: if (_mounted && doc !== undefined) {
_initEditorView(doc);
dispatchDocStore(doc);
}
onDestroy(() => {
if (view !== null) {
view.destroy();
}
});
</script>
<div class="codemirror" bind:this="{dom}"></div>
<style>
.codemirror {
display: contents;
}
</style>

View file

@ -1,29 +0,0 @@
<script lang="ts">
import Messages from "$lib/components/kratos/messages.svelte";
import type { Node } from "$lib/kratos/types";
export let node: Node;
const attr = node.attributes;
let labelText: string;
labelText = attr.name;
if (node.meta && node.meta.label) labelText = node.meta.label.text;
</script>
<!-- -------------------------------------------------------------------------->
<fieldset>
<label>
<span>{labelText}</span>
<input
type="email"
name={attr.name}
value={attr.value ?? ""}
placeholder={labelText}
disabled={attr.disabled}
required={attr.required}
/>
</label>
</fieldset>
<Messages messages={node.messages} />

View file

@ -1,10 +0,0 @@
<script lang="ts">
import type { Node } from "$lib/kratos/types";
export let node: Node;
const attr = node.attributes;
</script>
<!-- -------------------------------------------------------------------------->
<input type="hidden" name={attr.name} value={attr.value ?? ""} />

View file

@ -1,74 +0,0 @@
<script lang="ts">
export let isHidden: boolean;
const toggleVisibility = () => {
isHidden = !isHidden;
};
</script>
<!-- -------------------------------------------------------------------------->
<svg class="password-visibility-toggle" on:click={toggleVisibility}>
{#if isHidden}
<path
d="M8 2.36365
C 4.36364 2.36365 1.25818 4.62547 0 7.81819
C 1.25818 11.0109 4.36364 13.2727 8 13.2727
C 11.6364 13.2727 14.7418 11.0109 16 7.81819
C 14.7418 4.62547 11.6364 2.36365 8 2.36365
Z
M8 11.4546
C 5.99273 11.4546 4.36364 9.82547 4.36364 7.81819
C 4.36364 5.81092 5.99273 4.18183 8 4.18183
C 10.0073 4.18183 11.6364 5.81092 11.6364 7.81819
C 11.6364 9.82547 10.0073 11.4546 8 11.4546
Z
M8 5.63637
C 6.79273 5.63637 5.81818 6.61092 5.81818 7.81819
C 5.81818 9.02547 6.79273 10 8 10
C 9.20727 10 10.1818 9.02547 10.1818 7.81819
C 10.1818 6.61092 9.20727 5.63637 8 5.63637
Z"
/>
{:else}
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.8222 1.85355
C 15.0175 1.65829 15.0175 1.34171 14.8222 1.14645
C 14.627 0.951184 14.3104 0.951184 14.1151 1.14645
L 12.005 3.25653
C 10.8901 2.482 9.56509 1.92505 8.06 1.92505
C 3 1.92505 0 7.92505 0 7.92505
C 0 7.92505 1.16157 10.2482 3.25823 12.0033
L 1.19366 14.0679
C 0.998396 14.2632 0.998396 14.5798 1.19366 14.775
C 1.38892 14.9703 1.7055 14.9703 1.90076 14.775
L 14.8222 1.85355
Z
M 4.85879 10.4028
L 6.29159 8.96998
C 6.10643 8.66645 6 8.3089 6 7.92505
C 6 6.81505 6.89 5.92505 8 5.92505
C 8.38385 5.92505 8.7414 6.03148 9.04493 6.21664
L 10.4777 4.78384
C 9.79783 4.24654 8.93821 3.92505 8 3.92505
C 5.8 3.92505 4 5.72505 4 7.92505
C 4 8.86326 4.32149 9.72288 4.85879 10.4028
Z
M 11.8644 6.88906
L 13.8567 4.8968
C 15.2406 6.40616 16 7.92505 16 7.92505
C 16 7.92505 13 13.925 8.06 13.925
C 7.09599 13.925 6.20675 13.7073 5.39878 13.3547
L 6.96401 11.7895
C 7.29473 11.8779 7.64207 11.925 8 11.925
C 10.22 11.925 12 10.145 12 7.92505
C 12 7.56712 11.9529 7.21978 11.8644 6.88906
Z
M 9.33847 9.41501
L 9.48996 9.26352
C 9.44222 9.31669 9.39164 9.36726 9.33847 9.41501
Z"
/>
{/if}
</svg>

View file

@ -1,26 +0,0 @@
<script lang="ts">
import Messages from "$lib/components/kratos/messages.svelte";
import PasswordToggle from "$lib/components/kratos/fieldset-password-toggle.svelte";
import type { Node } from "$lib/kratos/types";
export let node: Node;
const attr = node.attributes;
let isHidden = true;
let labelText: string;
labelText = attr.name;
if (node.meta && node.meta.label) labelText = node.meta.label.text;
</script>
<!-- -------------------------------------------------------------------------->
<input
type={isHidden ? "password" : "text"}
name={attr.name}
value={attr.value ?? ""}
placeholder={labelText}
disabled={attr.disabled}
required={attr.required}
/>
<Messages messages={node.messages} />

View file

@ -1,16 +0,0 @@
<script lang="ts">
import type { Node } from "$lib/kratos/types";
export let node: Node;
const attr = node.attributes;
let labelText: string;
labelText = "Submit";
if (node.meta && node.meta.label) labelText = node.meta.label.text;
</script>
<!-- -------------------------------------------------------------------------->
<button type="submit" name={attr.name} value={attr.value}>
{labelText}
</button>

View file

@ -1,24 +0,0 @@
<script lang="ts">
import Messages from "$lib/components/kratos/messages.svelte";
import type { Node } from "$lib/kratos/types";
export let node: Node;
const attr = node.attributes;
let labelText: string;
labelText = attr.name;
if (node.meta && node.meta.label) labelText = node.meta.label.text;
</script>
<!-- -------------------------------------------------------------------------->
<input
type="text"
name={attr.name}
value={attr.value ?? ""}
placeholder={labelText}
disabled={attr.disabled}
required={attr.required}
/>
<Messages messages={node.messages} />

View file

@ -1,27 +0,0 @@
<script lang="ts">
import Hidden from "$lib/components/kratos/fieldset-hidden.svelte";
import Password from "$lib/components/kratos/fieldset-password.svelte";
import Text from "$lib/components/kratos/fieldset-text.svelte";
import Email from "$lib/components/kratos/fieldset-email.svelte";
import Submit from "$lib/components/kratos/fieldset-submit.svelte";
import type { Node } from "$lib/kratos/types";
export let nodes: Node[];
</script>
<!-- -------------------------------------------------------------------------->
{#each nodes as node}
{#if node.attributes.type === "hidden"}
<Hidden {node} />
{:else if node.attributes.type === "password"}
<Password {node} />
{:else if node.attributes.type === "text"}
<Text {node} />
{:else if node.attributes.type === "email"}
<Email {node} />
{:else if node.attributes.type === "submit"}
<Submit {node} />
{:else}
unknow type
{/if}
{/each}

View file

@ -1,16 +0,0 @@
<script lang="ts">
import Fieldsets from "$lib/components/kratos/fieldsets.svelte";
import type { KratosForm } from "$lib/kratos/types";
export let dm: KratosForm;
export let groups: string[];
const nodes = dm.ui.nodes.filter(
(n) => n.type === "input" && groups.includes(n.group),
);
</script>
<!-- -------------------------------------------------------------------------->
<form action={dm.ui.action} method={dm.ui.method}>
<Fieldsets {nodes} />
</form>

View file

@ -1,14 +0,0 @@
<script lang="ts">
import type { Message } from "$lib/kratos/types";
export let messages: Message[];
</script>
<!-- -------------------------------------------------------------------------->
{#if messages}
{#each messages as msg}
{msg.id} -
{msg.type} -
{msg.text}<br />
{/each}
{/if}

View file

@ -1,2 +0,0 @@
export const KRATOS = "http://localhost:4433";
export const APP = "http://localhost:3000";

View file

@ -1,11 +0,0 @@
export async function get(url: string) {
const res = await fetch(url, {
credentials: "include",
headers: {
"Accept": "application/json",
},
mode: "cors",
});
return res;
}

View file

@ -1,82 +0,0 @@
import { browser } from "$app/environment";
import { KRATOS } from "$lib/config";
import { get } from "$lib/http";
import type {
KratosError,
KratosForm,
KratosIdentity,
KratosLogout,
} from "$lib/kratos/types";
// -----------------------------------------------------------------------------
export function getFlowId(urlSearch: string): string {
const qs = new URLSearchParams(urlSearch);
const flowId = qs.get("flow");
if (flowId) return flowId;
return "";
}
// -----------------------------------------------------------------------------
export async function getIdentity(): Promise<KratosIdentity> {
if (!browser) throw new Error("no browser environment");
const url = `${KRATOS}/sessions/whoami`;
const res = await get(url);
if (res.status !== 200) {
throw new Error("no identity");
}
const dm = await res.json();
return dm.identity;
}
// -----------------------------------------------------------------------------
export async function getDataModels(
flow: string,
flowId: string
): Promise<KratosForm | KratosError> {
if (!flowId) throw new Error("no flowId");
// if (!browser) throw new Error("no browser environment");
const url = `${KRATOS}/self-service/${flow}/flows?id=${flowId}`;
const res = await get(url);
const dm = await res.json();
if (dm.error) {
dm.instanceOf = "KratosError";
if (dm.error.details && dm.error.details.redirect_to) {
window.location.href = dm.error.details.redirect_to;
}
} else if (dm.ui) {
dm.instanceOf = "KratosForm";
} else {
throw new Error("unexpected Kratos object");
}
return dm;
}
// -----------------------------------------------------------------------------
export async function getLogoutDataModels(): Promise<
KratosLogout | KratosError
> {
if (!browser) throw new Error("no browser environment");
const url = `${KRATOS}/self-service/logout/browser`;
const res = await get(url);
const dm = await res.json();
if (dm.error) {
dm.instanceOf = "KratosError";
} else if (dm.logout_url) {
dm.instanceOf = "KratosLogout";
} else {
throw new Error("unexpected Kratos object");
}
return dm;
}

View file

@ -1,109 +0,0 @@
// -----------------------------------------------------------------------------
export interface Attributes {
name: string;
type: string;
value?: string;
disabled: boolean;
required?: boolean;
}
// -----------------------------------------------------------------------------
export interface Label {
id: number;
type: string;
text: string;
context?: unknown;
}
// -----------------------------------------------------------------------------
export interface Message {
id: number;
type: string;
text: string;
context?: unknown;
}
// -----------------------------------------------------------------------------
export interface Meta {
label?: Label;
}
// -----------------------------------------------------------------------------
export interface Node {
type: string;
group: string;
attributes: Attributes;
messages: Message[];
meta: Meta;
}
// -----------------------------------------------------------------------------
export interface UI {
action: string;
method: string;
messages?: Message[];
nodes: Node[];
"updated_at": string;
}
// -----------------------------------------------------------------------------
export interface KratosForm {
instanceOf: "KratosForm";
id: string;
type: string;
forced?: boolean;
ui: UI;
"created_at"?: string;
"expires_at": string;
"issued_at": string;
"updated_at"?: string;
"request_url": string;
}
// -----------------------------------------------------------------------------
export interface KratosError {
instanceOf: "KratosError";
error: {
code: number;
message: string;
status: string;
reason?: string;
details?: {
docs: string;
hint: string;
"redirect_to": string;
"reject_reason": string;
};
};
}
// -----------------------------------------------------------------------------
export interface KratosLogout {
instanceOf: "KratosLogout";
"logout_url": string;
}
// -----------------------------------------------------------------------------
export interface KratosIdentity {
id: string;
traits: {
email: string;
};
state: string;
"created_at": string;
"updated_at": string;
}
// -----------------------------------------------------------------------------
export interface KratosLoad {
status?: number;
redirect?: string;
props?: {
[key: string]:
| string
| KratosError
| KratosForm
| KratosIdentity
| KratosLogout;
};
}

View file

@ -1,4 +0,0 @@
import { writable } from "svelte/store";
import type { KratosIdentity } from "$lib/kratos/types";
export default writable({} as KratosIdentity);

View file

@ -1,18 +0,0 @@
<script lang="ts">
import { browser } from "$app/environment";
import { get } from "svelte/store";
import identity from "$lib/stores/kratos/identity";
const _identity = get(identity);
if (browser) {
if (_identity.id) {
console.log("signed in");
} else {
console.log("not signed in");
}
}
</script>
<!-- -------------------------------------------------------------------------->
<slot />

View file

@ -1,16 +0,0 @@
import { getIdentity } from "$lib/kratos";
import identity from "$lib/stores/kratos/identity";
// ---------------------------Q--------------------------------------------------
export const csr = true;
export const prerender = false;
export async function load() {
await getIdentity()
.then((_identity) => {
identity.set(_identity);
})
.catch(() => {
//no identity
});
}

View file

@ -1,17 +0,0 @@
<script lang="ts">
import { APP } from "$lib/config";
import { get } from "svelte/store";
import identity from "$lib/stores/kratos/identity";
const _identity = get(identity);
</script>
<section id="welcome">
<h1>Probo</h1>
{#if !_identity.id}
<p><a href="{APP}/auth/login">Login</a></p>
{:else}
<p><a href="{APP}/dashboard">Dashboard</a></p>
{/if}
</section>

View file

@ -1,39 +0,0 @@
<script lang="ts">
import { APP, KRATOS } from "$lib/config";
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { getFlowId, getDataModels } from "$lib/kratos";
import Form from "$lib/components/kratos/form.svelte";
import Messages from "$lib/components/kratos/messages.svelte";
// const flowId = getFlowId($page.url.search); -->
// if (browser && !flowId) -->
// window.location.href = `${KRATOS}/self-service/login/browser`; -->
// const pr = getDataModels("login", flowId); -->
export let data: PageData;
$: pr = data.pr;
</script>
<!-- -------------------------------------------------------------------------->
<div id="login">
{#await data.pr then dm} {#if dm.instanceOf === "KratosForm"}
<div id="login">
<h2>Sign in</h2>
{#if dm.ui.messages}
<Messages messages="{dm.ui.messages}" />
{/if} <Form {dm} groups={["default", "password"]} />
<hr />
<div>
<p><a href="{APP}/auth/recovery">Forget password?</a></p>
<p><a href="{APP}/auth/registration">Don't have an account?</a></p>
</div>
</div>
{:else}
<p>Something went wrong</p>
{/if} {/await}
</div>

View file

@ -1,16 +0,0 @@
import { getFlowId, getDataModels } from "$lib/kratos";
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { KRATOS } from "$lib/config";
/** @type {import('./$types').PageLoad} */
export async function load({ url }) {
const flowId = getFlowId(url.search);
if (browser && !flowId)
window.location.href = `${KRATOS}/self-service/login/browser`;
const pr = getDataModels("login", flowId);
return { pr: pr };
}

View file

@ -1,15 +0,0 @@
import { browser } from "$app/environment";
import { getLogoutDataModels } from "$lib/kratos";
// -----------------------------------------------------------------------------
export async function load() {
if (!browser) return {};
const dm = await getLogoutDataModels();
if (dm.instanceOf === "KratosLogout") {
window.location.replace(dm.logout_url);
} else {
window.location.replace("/");
}
}

View file

@ -1,33 +0,0 @@
<script lang="ts">
import { KRATOS } from "$lib/config";
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { getFlowId, getDataModels } from "$lib/kratos";
import Form from "$lib/components/kratos/form.svelte";
import Messages from "$lib/components/kratos/messages.svelte";
const flowId = getFlowId($page.url.search);
if (browser && !flowId)
window.location.href = `${KRATOS}/self-service/recovery/browser`;
const pr = getDataModels("recovery", flowId);
</script>
<!-- -------------------------------------------------------------------------->
<section id="recovery">
{#await pr then dm}
{#if dm.instanceOf === "KratosForm"}
<div class="container" id="recovery">
<h2 class="subheading">recovery</h2>
{#if dm.ui.messages}
<Messages messages={dm.ui.messages} />
{:else}
<Form {dm} groups={["default", "link"]} />
{/if}
</div>
{:else}
<p>Something went wrong</p>
{/if}
{/await}
</section>

View file

@ -1,39 +0,0 @@
<script lang="ts">
import { APP, KRATOS } from "$lib/config";
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { getFlowId, getDataModels } from "$lib/kratos";
import Form from "$lib/components/kratos/form.svelte";
import Messages from "$lib/components/kratos/messages.svelte";
const flowId = getFlowId($page.url.search);
if (browser && !flowId)
window.location.href = `${KRATOS}/self-service/registration/browser`;
const pr = getDataModels("registration", flowId);
</script>
<!-- -------------------------------------------------------------------------->
<section id="registration">
{#await pr then dm}
{#if dm.instanceOf === "KratosForm"}
<div class="container" id="registration">
<h2 class="subheading">Registration</h2>
{#if dm.ui.messages}
<Messages messages={dm.ui.messages} />
{/if}
<Form {dm} groups={["default", "password"]} />
<hr class="divider" />
<section class="alternative-actions">
<p><a href="{APP}/login">Already have an account?</a></p>
</section>
</div>
{:else}
<p>Something went wrong</p>
{/if}
{/await}
</section>

View file

@ -1,35 +0,0 @@
<script lang="ts">
import { KRATOS } from "$lib/config";
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { getFlowId, getDataModels } from "$lib/kratos";
import Form from "$lib/components/kratos/form.svelte";
import Messages from "$lib/components/kratos/messages.svelte";
const flowId = getFlowId($page.url.search);
if (browser && !flowId)
window.location.href = `${KRATOS}/self-service/settings/browser`;
const pr = getDataModels("settings", flowId);
</script>
<!-- -------------------------------------------------------------------------->
<section id="settings">
{#await pr then dm}
{#if dm.instanceOf === "KratosForm"}
<div class="container" id="settings">
<h2 class="subheading">Settings</h2>
{#if dm.ui.messages}
<Messages messages={dm.ui.messages} />
{:else}
<Form {dm} groups={["default", "profile"]} />
<hr class="divider" />
<Form {dm} groups={["default", "password"]} />
{/if}
</div>
{:else}
<p>Something went wrong</p>
{/if}
{/await}
</section>

View file

@ -1,42 +0,0 @@
<script lang="ts">
import { KRATOS } from "$lib/config";
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { get } from "svelte/store";
import { getFlowId, getDataModels } from "$lib/kratos";
import identity from "$lib/stores/kratos/identity";
import Form from "$lib/components/kratos/form.svelte";
import Messages from "$lib/components/kratos/messages.svelte";
const _identity = get(identity);
const flowId = getFlowId($page.url.search);
if (browser) {
if (!_identity) {
window.location.href = `${KRATOS}/self-service/login/browser`;
} else if (!flowId) {
window.location.href = `${KRATOS}/self-service/verification/browser`;
}
}
const pr = getDataModels("verification", flowId);
</script>
<!-- -------------------------------------------------------------------------->
<section id="verification">
{#await pr then dm}
{#if dm.instanceOf === "KratosForm"}
<div class="container" id="verification">
<h2 class="subheading">verification</h2>
{#if dm.ui.messages}
<Messages messages={dm.ui.messages} />
{:else}
<Form {dm} groups={["default", "link"]} />
{/if}
</div>
{:else}
<p>Something went wrong</p>
{/if}
{/await}
</section>

View file

@ -1,16 +0,0 @@
<script lang="ts">
import { browser } from "$app/environment";
import { KRATOS } from "$lib/config";
import { get } from "svelte/store";
import identity from "$lib/stores/kratos/identity";
const _identity = get(identity);
if (browser && !_identity.id) {
const loginUrl = `${KRATOS}/self-service/login/browser`;
window.location.replace(loginUrl);
}
</script>
<!-- -------------------------------------------------------------------------->
<slot />

View file

@ -1,29 +0,0 @@
import { get } from "svelte/store";
import identity from "$lib/stores/kratos/identity";
type Question = {
text: string;
};
type Answer = {
text: string;
};
type Quiz = {
uid: string;
question: Question;
answers: Answer[];
};
/** @type {import('./$types').PageLoad} */
export async function load() {
const _identity = get(identity);
if (_identity) {
const res = await fetch("http://localhost:8080/quizzes");
const response = await res.json();
if (response.status === "success") {
return { quizzes: (await response.content) as Quiz[] };
}
}
}

View file

@ -1,42 +0,0 @@
<script lang="ts">
import { APP } from "$lib/config";
// import { get } from "svelte/store";
// import identity from "$lib/stores/kratos/identity";
import CodeMirror, { basicSetup } from "$lib/components/codemirror/codemirror.svelte";
import type { PageData } from './$types';
export let data: PageData;
$: quizzes = data.quizzes;
// const _identity = get(identity);
let email = "";
let store: any;
// if (_identity && _identity.traits) email = _identity.traits.email;
function changeHandler({ detail: {tr: any} }) {
console.log('change', tr.changes.toJSON())
}
</script>
<section id="dashboard">
<h2>Probo Dashboard</h2>
<p>Hello {email}</p>
{#each quizzes as quiz}
<ul>
<li>{quiz.Question.Text}</li>
</ul>
{/each}
<CodeMirror doc={'Testo della domanda\n\n* Risposta 1\n* Risposta 2\n* Risposta 3'}
bind:docStore={store}
extensions={basicSetup}
on:change={changeHandler}>
</CodeMirror>
<p><a href="{APP}/auth/settings">Settings</a></p>
<p><a href="{APP}/auth/logout">Logout</a></p>
</section>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,16 +0,0 @@
//import adapter from '@sveltejs/adapter-auto';
import adapter from "@sveltejs/adapter-node";
import preprocess from "svelte-preprocess";
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter: adapter(),
},
};
export default config;

View file

@ -1,26 +0,0 @@
#!/bin/bash
# Set Session Name
SESSION="kratos-svelte-login"
SESSIONEXISTS=$(tmux list-sessions | grep $SESSION)
# Only create tmux session if it doesn't already exist
if [ "$SESSIONEXISTS" = "" ]
then
# Start New Session with our name
tmux new-session -d -s $SESSION
# Name first Pane and start zsh
tmux rename-window -t 0 'src'
# Create and setup pane for running the backend
# tmux send-keys -t 'Main' 'bash' C-m 'clear' C-m 'cd backend && go build -o backend . && ./backend' C-m
tmux send-keys -t 'src' 'cd src && npm run dev -- --host --port 3000' C-m
# Create an horizontal pane for terminal commands
tmux split-window -vf -l 1
tmux send-keys 'emacs src/routes/+page.svelte &' Enter
fi
# Attach Session, on the Main window
tmux attach-session -t $SESSION:0

View file

@ -1,18 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the
// relevant includes/excludes from the referenced tsconfig.json
// TypeScript does not merge them in
}

View file

@ -1,13 +0,0 @@
import { sveltekit } from "@sveltejs/kit/vite";
import type { UserConfig } from "vite";
const config: UserConfig = {
plugins: [sveltekit()],
server: {
hmr: {
clientPort: 3000,
},
},
};
export default config;

View file

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"git.andreafazzi.eu/andrea/probo/hasher/sha256" "git.andreafazzi.eu/andrea/probo/hasher/sha256"
"git.andreafazzi.eu/andrea/probo/logger"
"git.andreafazzi.eu/andrea/probo/store/memory" "git.andreafazzi.eu/andrea/probo/store/memory"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -12,7 +13,7 @@ import (
const port = "8080" const port = "8080"
func main() { func main() {
// logger.SetLevel(logger.DebugLevel) logger.SetLevel(logger.DebugLevel)
server := NewProboCollectorServer( server := NewProboCollectorServer(
memory.NewMemoryProboCollectorStore( memory.NewMemoryProboCollectorStore(

21
misc/logseq/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Andrea Fazzi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -2,6 +2,7 @@ package models
type Quiz struct { type Quiz struct {
ID string ID string
Hash string
Question *Question Question *Question
Answers []*Answer Answers []*Answer
Correct *Answer Correct *Answer

BIN
probo Executable file

Binary file not shown.

View file

@ -10,7 +10,6 @@ import (
"git.andreafazzi.eu/andrea/probo/models" "git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store" "git.andreafazzi.eu/andrea/probo/store"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"log"
) )
type ProboCollectorServer struct { type ProboCollectorServer struct {
@ -101,7 +100,6 @@ func (ps *ProboCollectorServer) updateQuizHandler(w http.ResponseWriter, r *http
} }
func (ps *ProboCollectorServer) readAllQuiz(w http.ResponseWriter, r *http.Request) ([]*models.Quiz, error) { func (ps *ProboCollectorServer) readAllQuiz(w http.ResponseWriter, r *http.Request) ([]*models.Quiz, error) {
log.Println(r)
quizzes, err := ps.store.ReadAllQuizzes() quizzes, err := ps.store.ReadAllQuizzes()
if err != nil { if err != nil {
return nil, err return nil, err

7
store/file/.md Normal file
View file

@ -0,0 +1,7 @@
Newly created question text.
* Answer 1
* Answer 1
* Answer 2
* Answer 3
* Answer 4

162
store/file/file.go Normal file
View file

@ -0,0 +1,162 @@
package file
import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"strings"
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store/memory"
)
type FileProboCollectorStore struct {
Dir string
memoryStore *memory.MemoryProboCollectorStore
}
func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error) {
s := new(FileProboCollectorStore)
files, err := ioutil.ReadDir(dirname)
if err != nil {
return nil, err
}
markdownFiles := make([]fs.FileInfo, 0)
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.HasSuffix(filename, ".md") {
markdownFiles = append(markdownFiles, file)
}
}
if len(markdownFiles) == 0 {
return nil, fmt.Errorf("The directory is empty.")
}
s.memoryStore = memory.NewMemoryProboCollectorStore(
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
)
for _, file := range markdownFiles {
filename := file.Name()
fullPath := filepath.Join(dirname, filename)
content, err := os.ReadFile(fullPath)
if err != nil {
return nil, err
}
quiz, err := QuizFromMarkdown(string(content))
if err != nil {
return nil, err
}
s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{
Quiz: quiz,
})
}
s.Dir = dirname
return s, nil
}
func (s *FileProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
return s.memoryStore.ReadAllQuizzes()
}
func (s *FileProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) {
quiz, err := s.memoryStore.CreateQuiz(r)
if err != nil {
return nil, err
}
err = s.writeMarkdownFile(quiz)
if err != nil {
return nil, err
}
return quiz, nil
}
func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
if quiz.Question == nil {
return "", errors.New("Quiz should contain a question but it wasn't provided.")
}
if len(quiz.Answers) == 0 {
return "", errors.New("Quiz should contain at least 2 answers but none was provided.")
}
if quiz.Correct == nil {
return "", errors.New("Quiz should contain a correct answer but non was provided.")
}
correctAnswer := "* " + quiz.Correct.Text
var otherAnswers string
for _, answer := range quiz.Answers {
otherAnswers += "* " + answer.Text + "\n"
}
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers
return markdown, nil
}
func QuizFromMarkdown(markdown string) (*client.Quiz, error) {
lines := strings.Split(markdown, "\n")
questionText := ""
answers := []*client.Answer{}
for _, line := range lines {
if strings.HasPrefix(line, "*") {
answerText := strings.TrimPrefix(line, "* ")
correct := len(answers) == 0
answer := &client.Answer{Text: answerText, Correct: correct}
answers = append(answers, answer)
} else {
if questionText != "" {
questionText += "\n"
}
questionText += line
}
}
questionText = strings.TrimRight(questionText, "\n")
if questionText == "" {
return nil, fmt.Errorf("Question text should not be empty.")
}
if len(answers) < 2 {
return nil, fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers))
}
question := &client.Question{Text: questionText}
quiz := &client.Quiz{Question: question, Answers: answers}
return quiz, nil
}
func (s *FileProboCollectorStore) writeMarkdownFile(quiz *models.Quiz) error {
markdown, err := MarkdownFromQuiz(quiz)
if err != nil {
return err
}
filename := filepath.Join(s.Dir, quiz.Hash+".md")
err = ioutil.WriteFile(filename, []byte(markdown), 0644)
if err != nil {
return err
}
return nil
}

114
store/file/file_test.go Normal file
View file

@ -0,0 +1,114 @@
package file
import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/models"
"github.com/remogatto/prettytest"
)
type testSuite struct {
prettytest.Suite
}
func TestRunner(t *testing.T) {
prettytest.Run(
t,
new(testSuite),
)
}
func (t *testSuite) TestQuizFromMarkdown() {
markdown := `Question text (1).
Question text (2).
Question text (3).
* Answer 1
* Answer 2
* Answer 3
* Answer 4`
expectedQuiz := &client.Quiz{
Question: &client.Question{Text: "Question text (1).\n\nQuestion text (2).\n\nQuestion text (3)."},
Answers: []*client.Answer{
{Text: "Answer 1", Correct: true},
{Text: "Answer 2", Correct: false},
{Text: "Answer 3", Correct: false},
{Text: "Answer 4", Correct: false},
},
}
quiz, err := QuizFromMarkdown(markdown)
t.Nil(err, fmt.Sprintf("Quiz should be parsed without errors: %v", err))
if !t.Failed() {
t.True(reflect.DeepEqual(quiz, expectedQuiz), fmt.Sprintf("Expected %+v, got %+v", expectedQuiz, quiz))
}
}
func (t *testSuite) TestReadAllQuizzes() {
store, err := NewFileProboCollectorStore("./test/quizzes")
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
if !t.Failed() {
result, err := store.ReadAllQuizzes()
t.True(err == nil, fmt.Sprintf("Quizzes should be returned without errors: %v", err))
if !t.Failed() {
t.Equal(
2,
len(result),
fmt.Sprintf("The store contains 3 files but only 2 should be parsed (duplicated quiz). Total of parsed quizzes are instead %v", len(result)),
)
t.Equal("Question text 1.", result[0].Question.Text)
}
}
}
func (t *testSuite) TestCreateQuiz() {
dirname := "./test/quizzes"
store, err := NewFileProboCollectorStore(dirname)
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
_, err = store.CreateQuiz(
&client.CreateUpdateQuizRequest{
Quiz: &client.Quiz{
Question: &client.Question{Text: "Newly created question text."},
Answers: []*client.Answer{
{Text: "Answer 1", Correct: true},
{Text: "Answer 2", Correct: false},
{Text: "Answer 3", Correct: false},
{Text: "Answer 4", Correct: false},
},
},
})
t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err))
newFilename := filepath.Join(
dirname,
"94ed4e9cdf8e0a75a2c5ce925cb791ebc5977ce1801e12059f58ce4d66c0c7f6.md",
)
exists, err := os.Stat(newFilename)
t.Nil(err, "Stat should not return an error")
if !t.Failed() {
t.True(exists != nil, "The new quiz file was not created.")
err := os.Remove(newFilename)
t.Nil(err, "Stat should not return an error")
}
}
func testsAreEqual(got, want []*models.Quiz) bool {
return reflect.DeepEqual(got, want)
}

View file

@ -0,0 +1,5 @@
Question text 1.
* Answer 1
* Answer 2
* Answer 3

View file

@ -0,0 +1,7 @@
Question text 2.
* Answer 1
* Answer 2
* Answer 3
* Answer 4

View file

@ -0,0 +1,7 @@
Question text 2.
* Answer 1
* Answer 2
* Answer 3
* Answer 4

View file

@ -90,6 +90,7 @@ func (s *MemoryProboCollectorStore) createQuizFromHash(id string, hash string, q
defer s.lock.Unlock() defer s.lock.Unlock()
quiz.ID = id quiz.ID = id
quiz.Hash = hash
s.quizzesHashes[hash] = quiz s.quizzesHashes[hash] = quiz
s.quizzes[id] = quiz s.quizzes[id] = quiz