├── .HTF
└── spec.history
├── .gitignore
├── LICENSE
├── LICENSE.md
├── Makefile
├── README.md
├── Setup.hs
├── app
└── main.hs
├── bin
├── GetWords.hs
├── InsertWords.hs
├── add_word.sh
├── db-create-database.sh
├── db-drop-database.sh
├── db-recreate-database.sh
├── db_dump.sh
├── edit_word.sh
├── insert_test_data.sh
├── question.sh
├── remove_word.sh
├── setup_database.sh
└── tests.sh
├── config.yml.example
├── db_restore_data.sh
├── deploy
├── 0001-words.sql
├── 0002-users.sql
├── 0003-tokens.sql
├── 0004-session.sql
├── 0005-scoring.sql
└── 0006-update-user.sql
├── deploysetup
└── deploysetup.sh
├── drop-ddl.sql
├── front
├── .babelrc
├── FlashCardEdit.js
├── FullscreenNavigation.jsx
├── Makefile
├── TopNavigation.jsx
├── main.js
├── package-lock.json
├── package.json
├── utils.js
└── webpack.config.js
├── izidict.cabal
├── package.yaml
├── queries.sql
├── questionary.py
├── revert
├── 0001-words.sql
├── 0002-users.sql
├── 0003-tokens.sql
├── 0004-session.sql
├── 0005-scoring.sql
└── 0006-update-user.sql
├── sqitch.conf
├── sqitch.plan
├── sql.load
├── sql
├── addFlashCard.sql
├── checkPassword.sql
├── decreaseBucketFlashCard.sql
├── deleteFlashCard.sql
├── getFlashCard.sql
├── getFlashCards.sql
├── getNewRandomSession.sql
├── getNewRandomSessionWithoutPasswordCheck.sql
├── getQuizzFlashCard.sql
├── increaseBucketFlashCard.sql
├── insertDefaultFlashCards.sql
├── insertUser.sql
├── insert_word.sql
├── registerUser.sql
├── searchFlashCards.sql
├── test_data.sql
├── updateFlashCard.sql
├── updatePassword.sql
├── verifyFlashCardRectoAnswer.sql
└── verifyFlashCardVersoAnswer.sql
├── src
├── API
│ └── Facebook.hs
├── Auth.hs
├── Cache.hs
├── Css.hs
├── Data.hs
├── Data
│ ├── APIFacebook.hs
│ ├── Exception.hs
│ ├── FlashCard.hs
│ ├── GrantUser.hs
│ ├── Message.hs
│ ├── NewUser.hs
│ ├── Session.hs
│ ├── Settings.hs
│ ├── User.hs
│ └── Word.hs
├── Database.hs
├── Database
│ ├── Queries.hs
│ ├── SqlRow.hs
│ └── Types.hs
├── Form.hs
├── HandlerM.hs
├── Mustache.hs
├── Queries.sql
├── Server.hs
├── SharedEnv.hs
├── Template.hs
├── Utils.hs
└── templates
│ ├── account.html
│ ├── dashboard.html
│ ├── flashcard.add.html
│ ├── flashcard.edit.html
│ ├── footer.html
│ ├── head.html
│ ├── header-logo.html
│ ├── home.html
│ ├── login.html
│ ├── nav.fullscreen.html
│ ├── nav.login.html
│ ├── quizz.answer.html
│ ├── quizz.finish.html
│ ├── quizz.html
│ ├── register.html
│ ├── robots.txt
│ └── sitemap.xml
├── stack.yaml
├── static
├── css
│ └── bootstrap.css
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.svg
│ ├── glyphicons-halflings-regular.ttf
│ └── glyphicons-halflings-regular.woff
└── tmp
│ ├── autogen-FQn2C_-6.js
│ ├── autogen-PE46a7-4.css
│ ├── autogen-dAT6QVJx.js
│ └── autogen-f9mZBX8C.js
├── templates
├── default-layout-wrapper.hamlet
├── default-layout.hamlet
├── default-layout.lucius
├── homepage.hamlet
├── homepage.julius
└── homepage.lucius
├── test
├── Spec.hs
├── index.html
└── tests.sh
├── verify
├── 0001-words.sql
├── 0002-users.sql
├── 0003-tokens.sql
├── 0004-session.sql
├── 0005-scoring.sql
└── 0006-update-user.sql
└── www
├── favico.png
├── favicon.ico
├── fonts
├── Roboto-Black.ttf
├── Roboto-BlackItalic.ttf
├── Roboto-Bold.ttf
├── Roboto-BoldItalic.ttf
├── Roboto-Italic.ttf
├── Roboto-Light.ttf
├── Roboto-LightItalic.ttf
├── Roboto-Medium.ttf
├── Roboto-MediumItalic.ttf
├── Roboto-Regular.ttf
├── Roboto-Thin.ttf
├── Roboto-ThinItalic.ttf
├── RobotoSlab-Bold.ttf
├── RobotoSlab-Light.ttf
├── RobotoSlab-Regular.ttf
├── RobotoSlab-Thin.ttf
└── SansForgetica-Regular.otf
├── home.html
├── images
├── dictionnary.logo.png
├── icons.png
├── icons.xcf
├── idea.1.jpg
├── memory.1.jpg
├── memory.2.jpg
└── memory.3.jpg
├── index.html
├── js
└── izidict.js
├── login.html
├── normalize.css
├── profile.html
└── register.html
/.HTF/spec.history:
--------------------------------------------------------------------------------
1 | {"version":0,"runs":[{"startTime":"2018-08-23T15:52:05.234146905Z","tests":[{"testId":"Main:nonEmpty","result":"fail","timedOut":false,"timeMs":0},{"testId":"Main:empty","result":"pass","timedOut":false,"timeMs":0},{"testId":"Main:reverse","result":"fail","timedOut":false,"timeMs":0},{"testId":"Main:reverseReplay","result":"fail","timedOut":false,"timeMs":0}]},{"startTime":"2018-08-23T15:48:56.338142376Z","tests":[{"testId":"Main:nonEmpty","result":"fail","timedOut":false,"timeMs":0},{"testId":"Main:empty","result":"pass","timedOut":false,"timeMs":0},{"testId":"Main:reverse","result":"fail","timedOut":false,"timeMs":0}]}]}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .stack-work/
2 | .tags
3 | front/node_modules/*
4 | config.yml
5 | cubes/*
6 | backup.sql
7 | debian/*
8 | *.env
9 | words.csv
10 | words.sql
11 | db_dump.sql
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright aRkadeFR (c) 2018
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the following
13 | disclaimer in the documentation and/or other materials provided
14 | with the distribution.
15 |
16 | * Neither the name of aRkadeFR nor the names of other
17 | contributors may be used to endorse or promote products derived
18 | from this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Web Server for Words
2 | Copyright (C) 2018 aRkadeFR
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with this program. If not, see .
16 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | POSIXCUBE_BIN ?= ~/Projects/posixcube/posixcube.sh
2 |
3 | .PHONY: ghcid
4 | ghcid:
5 | # http://www.parsonsmatt.org/2018/05/19/ghcid_for_the_win.html
6 | ghcid --command "cabal new-repl backend-exe" --test "main"
7 |
8 | .PHONY: test
9 | test:
10 | pg_virtualenv -s $(MAKE) fulltest
11 |
12 | .PHONY: fulltest
13 | fulltest:
14 | ./bin/setup_database.sh
15 | stack test
16 |
17 | .PHONY: clean
18 | clean:
19 | cabal new-clean
20 |
21 | .PHONY: deb
22 | deb: build clean
23 | dpkg-buildpackage -us -uc
24 |
25 | .PHONY: build
26 | build: clean
27 | cabal new-build
28 |
29 | .PHONY: serve
30 | serve:
31 | stack exec backend-exe
32 |
33 | .PHONY: deploybaseconfiguration
34 | deploybaseconfiguration:
35 | ${POSIXCUBE_BIN} -u root -h izidict.com -c ./cubes/base_configuration
36 |
37 | .PHONY: deploydehydrated
38 | deploydehydrated:
39 | ${POSIXCUBE_BIN} -u root -h izidict.com -e ./production.env -c ./cubes/dehydrated
40 |
41 | .PHONY: deployprimarydb
42 | deployprimarydb:
43 | ${POSIXCUBE_BIN} -u flog -h izidict.com -c ./cubes/postgresql
44 |
45 | .PHONY: deploynginx
46 | deploynginx:
47 | ${POSIXCUBE_BIN} -u flog -h izidict.com -e ./production.env -c ./cubes/nginx
48 |
49 | BACKEND_BIN=$(stack exec -- which backend-exe)
50 |
51 | .PHONY: deploy
52 | deploy:
53 | scp ../izidict_0.1_all.deb root@izidict.com:~/
54 | ssh root@izidict.com -- dpkg -i izidict_0.1_amd64.deb do
20 |
21 | _ <- waitAny [asyncThreadApp]
22 |
23 | return ()
24 |
--------------------------------------------------------------------------------
/bin/GetWords.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE QuasiQuotes #-}
2 |
3 | import System.IO
4 | import Database.HDBC.PostgreSQL (withPostgreSQL)
5 | import Database.HDBC
6 | import Database.YeshQL
7 | import Options.Applicative
8 | import Data.Semigroup ((<>))
9 |
10 |
11 | [yesh|
12 | -- name:getWords :: (Int, String, String, String, String, Int)
13 | SELECT
14 | *
15 | FROM
16 | words
17 | ORDER BY
18 | random()
19 | LIMIT 5
20 | |]
21 |
22 | printWords :: IO ()
23 | printWords = withPostgreSQL "service = words" $ \conn -> do
24 | putStrLn "It works! I have a connection to get the words"
25 | maybeWord <- getWords conn
26 | commit conn
27 | disconnect conn
28 | -- apply a the printStrLn to each row of maybeWord that are encapsulated into maybes
29 | putStrLn $ "these are my 5 words: " ++ show maybeWord
30 | return ()
31 |
32 | main :: IO ()
33 | main = printWords
34 |
--------------------------------------------------------------------------------
/bin/InsertWords.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE QuasiQuotes #-}
2 |
3 | import System.IO
4 | import Database.HDBC.PostgreSQL (withPostgreSQL)
5 | import Database.HDBC
6 | import Database.YeshQL
7 | import Options.Applicative
8 | import Data.Semigroup ((<>))
9 |
10 | data WordAdded = WordAdded
11 | { language :: String
12 | , word :: String
13 | , definition :: String }
14 |
15 | wordAdded :: Parser WordAdded
16 | wordAdded = WordAdded
17 | <$> strOption
18 | ( long "language"
19 | <> short 'l'
20 | <> help "Language of this word" )
21 | <*> strOption
22 | ( long "word"
23 | <> short 'w'
24 | <> help "Word to be added" )
25 | <*> strOption
26 | ( long "definition"
27 | <> short 'd'
28 | <> help "Definition of the word" )
29 |
30 | [yesh|
31 | -- name:insertWord
32 | -- :wordLanguage :: String
33 | -- :wordWord :: String
34 | -- :wordDefinition :: String
35 | INSERT INTO
36 | words (language, word, definition)
37 | VALUES
38 | (:wordLanguage, :wordWord, :wordDefinition)
39 | |]
40 |
41 | addWord :: WordAdded -> IO ()
42 | addWord (WordAdded language word definition) = withPostgreSQL "service = words" $ \conn -> do
43 | putStrLn "It works! I have a connection add a word"
44 | wordId <- insertWord language word definition conn
45 | commit conn
46 | disconnect conn
47 | putStrLn "and we're good!"
48 | return ()
49 | addWord _ = return ()
50 |
51 | main :: IO ()
52 | main = addWord =<< execParser opts
53 | where
54 | opts = info (wordAdded <**> helper)
55 | ( fullDesc
56 | <> progDesc "Add a new word into the database"
57 | <> header "insert-words - add a new word into your custom database" )
58 |
--------------------------------------------------------------------------------
/bin/add_word.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ./bin/add_word.sh
4 |
5 | echo "Add a word"
6 |
--------------------------------------------------------------------------------
/bin/db-create-database.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "CREATE DATABASE words WITH OWNER = 'arkadefr';" | psql postgres
4 |
--------------------------------------------------------------------------------
/bin/db-drop-database.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo 'DROP DATABASE words;' | psql postgres
4 |
--------------------------------------------------------------------------------
/bin/db-recreate-database.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export PGSERVICE="words"
4 |
5 | ./drop-database.sh
6 |
7 | ./create-database.sh
8 |
9 | psql -f create-schema-ddl.sql
10 |
--------------------------------------------------------------------------------
/bin/db_dump.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | . ./env
4 |
5 | pg_dump --data-only --format plain -f db_dump.sql words
6 |
--------------------------------------------------------------------------------
/bin/edit_word.sh:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/bin/edit_word.sh
--------------------------------------------------------------------------------
/bin/insert_test_data.sh:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/bin/insert_test_data.sh
--------------------------------------------------------------------------------
/bin/question.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | function syntax {
4 | echo "Syntax:"
5 | echo " $0 #SUBJECT#"
6 | }
7 |
8 | if [[ $# != 1 ]]; then
9 | echo "Wrong number of parameters"
10 | syntax
11 | fi
12 |
13 | SUBJECT=$1
14 |
15 | echo "Question on subject $SUBJECT"
16 |
--------------------------------------------------------------------------------
/bin/remove_word.sh:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/bin/remove_word.sh
--------------------------------------------------------------------------------
/bin/setup_database.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | psql -c 'CREATE DATABASE words;'
4 | psql -c 'CREATE EXTENSION pg_trgm;' words
5 | psql -c 'CREATE EXTENSION pgcrypto;' words
6 | psql -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";' words
7 |
8 | sqitch deploy
9 |
10 | # test user username: testusername
11 | # test user password: testpassword
12 | # test user languages: testpassword
13 |
14 | INSERT INTO users (email, username, passpass, languages) VALUES ('donald.duck@brazaville.com', 'passpass', 'donald.duck', '{}');
15 | INSERT INTO users (email, username, passpass, languages) VALUES ('daisy.duck@brazaville.com', 'passpass', 'daisy.duck', '{"FR"}');
16 | INSERT INTO users (email, username, passpass, languages) VALUES ('huey.duck@brazaville.com', 'passpass', 'huey.duck', '{}');
17 | INSERT INTO users (email, username, passpass, languages) VALUES ('dewey.duck@brazaville.com', 'passpass', 'dewey.duck', '{}');
18 | INSERT INTO users (email, username, passpass, languages) VALUES ('louie.duck@brazaville.com', 'passpass', 'louie.duck', '{}');
19 | INSERT INTO users (email, username, passpass, languages) VALUES ('scrooge.mcduck@brazaville.com', 'passpass', 'scrooge.mcduck', '{"PL", "EN"}');
20 |
21 |
22 | insertUser dbconnection "donald.duck@brazaville.com" "passpass"
23 | insertUser dbconnection "daisy.duck@brazaville.com" "passpass"
24 | insertUser dbconnection "huey.duck@brazaville.com" "passpass"
25 | insertUser dbconnection "dewey.duck@brazaville.com" "passpass"
26 | insertUser dbconnection "louie.duck@brazaville.com" "passpass"
27 | insertUser dbconnection "scrooge.mcduck@brazaville.com" "passpass"
28 |
--------------------------------------------------------------------------------
/bin/tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | echo "test my yesod application"
4 | echo "please run yesod devel"
5 | curl http://127.0.0.1:3000/api/
6 |
--------------------------------------------------------------------------------
/config.yml.example:
--------------------------------------------------------------------------------
1 | ---
2 | base_url: "http://localhost:9000"
3 | production: False
4 | app_port: 9000
5 | pgservice: "flashcard"
6 | facebook_appid: ""
7 | facebook_appsecret: ""
8 | facebook_apptoken: ""
9 | ...
10 |
--------------------------------------------------------------------------------
/db_restore_data.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | . ./env
4 |
5 | psql -f db_dump.sql
6 |
--------------------------------------------------------------------------------
/deploy/0001-words.sql:
--------------------------------------------------------------------------------
1 | -- Deploy words:0001-words to pg
2 | BEGIN;
3 |
4 | -- Create Schema Words
5 | CREATE SEQUENCE words_id_seq;
6 |
7 | CREATE TABLE IF NOT EXISTS words (
8 | id BIGINT NOT NULL PRIMARY KEY DEFAULT nextval('words_id_seq'),
9 | last_query_at TIMESTAMPTZ NOT NULL default(now()),
10 | inserted_at TIMESTAMPTZ NOT NULL default(now()),
11 | language CHARACTER(2) NOT NULL,
12 | word VARCHAR(128) NOT NULL,
13 | keywords TEXT[] NOT NULL DEFAULT '{}',
14 | definition TEXT NOT NULL,
15 | difficulty INTEGER DEFAULT 1 NOT NULL,
16 | score FLOAT DEFAULT 0.5
17 | );
18 |
19 | ALTER SEQUENCE words_id_seq OWNED BY words.id;
20 |
21 | CREATE INDEX words_trgm ON words USING GIN(word gin_trgm_ops);
22 |
23 | COMMIT;
24 |
--------------------------------------------------------------------------------
/deploy/0002-users.sql:
--------------------------------------------------------------------------------
1 | -- Deploy users:0002-users to pg
2 |
3 | BEGIN;
4 |
5 | -- Create Users
6 | CREATE SEQUENCE users_id_seq;
7 |
8 | CREATE TABLE IF NOT EXISTS users (
9 | "id" BIGINT NOT NULL PRIMARY KEY DEFAULT nextval('users_id_seq'),
10 | "username" TEXT NOT NULL,
11 | "email" TEXT NULL,
12 | "passpass" TEXT NOT NULL,
13 | "languages" CHAR(2)[]
14 | );
15 |
16 | ALTER SEQUENCE users_id_seq OWNED BY users.id;
17 |
18 | CREATE UNIQUE INDEX users_username_index on users (lower(username));
19 |
20 | ALTER TABLE users ADD CONSTRAINT users_username_check CHECK (username <> ''::text);
21 |
22 | ALTER TABLE words ADD COLUMN userid BIGINT NOT NULL;
23 |
24 | ALTER TABLE words ADD CONSTRAINT userid_fk FOREIGN KEY (userid) REFERENCES users ON DELETE CASCADE;
25 |
26 | CREATE OR REPLACE FUNCTION user_hash_password ()
27 | RETURNS trigger
28 | AS $$
29 | BEGIN
30 | IF TG_OP = 'INSERT' OR (OLD.passpass != NEW.passpass) THEN
31 | -- Insert new user or Update password of the user, please encrypt it
32 | NEW.passpass := crypt(NEW.passpass, gen_salt('bf', 8));
33 | END IF;
34 |
35 | RETURN NEW;
36 | END
37 | $$ LANGUAGE plpgsql;
38 |
39 | CREATE TRIGGER tr_user_hash_password BEFORE INSERT OR UPDATE ON users
40 | FOR EACH ROW
41 | EXECUTE PROCEDURE user_hash_password();
42 |
43 | COMMIT;
44 |
--------------------------------------------------------------------------------
/deploy/0003-tokens.sql:
--------------------------------------------------------------------------------
1 | -- Deploy words:0003-tokens to pg
2 |
3 | BEGIN;
4 |
5 | -- Create token table
6 | CREATE SEQUENCE IF NOT EXISTS token_id_seq;
7 |
8 | CREATE TABLE IF NOT EXISTS token (
9 | "id" BIGINT NOT NULL PRIMARY KEY DEFAULT nextval('users_id_seq'),
10 | "token" UUID NOT NULL,
11 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT now()
12 | );
13 |
14 | ALTER SEQUENCE token_id_seq OWNED BY token.id;
15 |
16 | COMMIT;
17 |
--------------------------------------------------------------------------------
/deploy/0004-session.sql:
--------------------------------------------------------------------------------
1 | -- Deploy words:0004-session to pg
2 |
3 | BEGIN;
4 |
5 | -- Create Users
6 | CREATE SEQUENCE IF NOT EXISTS sessions_id_seq;
7 |
8 | CREATE UNLOGGED TABLE IF NOT EXISTS sessions (
9 | "id" BIGINT NOT NULL PRIMARY KEY DEFAULT nextval('sessions_id_seq'),
10 | "userid" BIGINT NOT NULL UNIQUE REFERENCES users (id) ON DELETE CASCADE,
11 | "secret" TEXT NOT NULL
12 | );
13 |
14 |
15 | ALTER SEQUENCE sessions_id_seq OWNED BY sessions.id;
16 |
17 | CREATE OR REPLACE FUNCTION auth_check_password (username TEXT, pass TEXT)
18 | RETURNS BOOLEAN AS $$
19 | DECLARE passed BOOLEAN;
20 | BEGIN
21 | SELECT (crypt($2, u.passpass) = u.passpass) INTO passed
22 | FROM users u
23 | WHERE u.username = $1;
24 |
25 | RETURN passed;
26 | END
27 | $$ LANGUAGE plpgsql
28 | SECURITY DEFINER;
29 |
30 | CREATE OR REPLACE FUNCTION auth_jwt_header ()
31 | RETURNS JSON AS $$
32 | BEGIN
33 | RETURN '{"alg": "HS256", "typ": "JWT"}'::json;
34 | END
35 | $$ LANGUAGE plpgsql;
36 |
37 | CREATE OR REPLACE FUNCTION auth_jwt_payload (user_id BIGINT)
38 | RETURNS JSON AS $$
39 | SELECT json_build_object(
40 | 'username', u.username,
41 | 'email', u.email
42 | )
43 | FROM users u
44 | WHERE u.id = $1
45 | LIMIT 1;
46 | $$ LANGUAGE sql;
47 |
48 | CREATE OR REPLACE FUNCTION auth_jwt_signature (header JSON, payload JSON, secret TEXT)
49 | RETURNS bytea AS $$
50 | SELECT hmac(
51 | ENCODE(CONVERT_TO(header::text, 'UTF-8'), 'base64') || '.' ||
52 | ENCODE(CONVERT_TO(payload::text, 'UTF-8'), 'base64'),
53 | secret,
54 | 'sha256');
55 | $$ LANGUAGE sql;
56 |
57 | CREATE OR REPLACE FUNCTION auth_jwt_new (user_id BIGINT)
58 | RETURNS TEXT AS $$
59 | DECLARE
60 | jwt_header JSON;
61 | jwt_payload JSON;
62 | jwt_secret TEXT;
63 | BEGIN
64 | jwt_header := auth_jwt_header();
65 | RAISE NOTICE 'JWT new header: %', jwt_header;
66 | jwt_payload := auth_jwt_payload($1);
67 | RAISE NOTICE 'JWT new payload: %', jwt_payload;
68 |
69 | -- Creating a new secret
70 | WITH user_secret_information (secret) AS (
71 | INSERT INTO
72 | sessions (userid, secret)
73 | VALUES
74 | (user_id, md5(random()::text))
75 | ON CONFLICT (userid) DO UPDATE
76 | SET secret = md5(random()::text)
77 | RETURNING
78 | secret
79 | )
80 | SELECT
81 | secret INTO jwt_secret
82 | FROM
83 | user_secret_information;
84 |
85 | -- Header (based 64 URL, and concat with the point)
86 | -- type of encryption ({"alg": "HS256", "typ": "JWT"})
87 | RETURN regexp_replace( ENCODE(CONVERT_TO(jwt_header::text, 'UTF-8'), 'base64') || '.' ||
88 | -- Payload (based 64 URL, and concat with the point)
89 | ENCODE(CONVERT_TO(jwt_payload::text, 'UTF-8'), 'base64') || '.' ||
90 | -- Signature (already in base64 URL)
91 | ENCODE(CONVERT_TO(auth_jwt_signature(jwt_header, jwt_payload, jwt_secret)::text, 'UTF-8'), 'base64'),
92 | -- Remove the newline and tabulation characters,
93 | -- this comes from an old base64 RFC that lines should be no more than 76 chars
94 | E'[\\n\\r]', '', 'g');
95 | END
96 | $$ LANGUAGE plpgsql;
97 |
98 | -- auth_jwt_decode
99 | -- decode a JWT token, and check the signature.
100 | -- If it match, then return the userid of the jwt
101 | CREATE OR REPLACE FUNCTION auth_jwt_decode (jwt TEXT)
102 | RETURNS BIGINT AS $$
103 | DECLARE
104 | jwt_parts TEXT[];
105 | jwt_header JSON;
106 | jwt_payload JSON;
107 | jwt_signature TEXT;
108 | jwt_username TEXT;
109 | jwt_user_id BIGINT;
110 | jwt_user_secret TEXT;
111 | jwt_signature_truth BYTEA;
112 | jwt_signature_to_be_verified BYTEA;
113 | BEGIN
114 | jwt_parts := regexp_split_to_array( jwt, '\.' );
115 | IF array_length(jwt_parts, 1) != 3 THEN
116 | -- The JWT is wrong
117 | RAISE EXCEPTION 'JWT Malformed' USING ERRCODE = 'P1002';
118 | ELSE
119 | jwt_header := CONVERT_FROM( DECODE( (jwt_parts)[1], 'base64') , 'UTF-8' )::JSON;
120 | jwt_payload := CONVERT_FROM( DECODE( (jwt_parts)[2], 'base64') , 'UTF-8' )::JSON;
121 | jwt_signature := (jwt_parts)[3];
122 |
123 | jwt_username := jwt_payload->>'username';
124 | SELECT u.id INTO jwt_user_id FROM users u WHERE u.username = jwt_username;
125 |
126 | SELECT secret INTO jwt_user_secret FROM users u join sessions s on u.id = s.userid WHERE u.username = jwt_username;
127 |
128 | SELECT DECODE(jwt_signature, 'base64') INTO jwt_signature_truth;
129 | SELECT auth_jwt_signature(jwt_header, jwt_payload, jwt_user_secret) INTO jwt_signature_to_be_verified;
130 | IF CONVERT_TO(jwt_signature_to_be_verified::text, 'UTF-8') = jwt_signature_truth THEN
131 | RAISE NOTICE 'JWT Signature recomputed and verified';
132 | -- TODO: if we want to retrieve the jwt payload…
133 | -- jwt_payload := ('{"user_id": "' || jwt_user_id || '"}')::jsonb || jwt_payload::jsonb;
134 | RETURN jwt_user_id;
135 | ELSE
136 | -- The JWT is wrong
137 | RAISE EXCEPTION 'JWT Incorrect' USING ERRCODE = 'P1003';
138 | RETURN 'f';
139 | END IF;
140 | END IF;
141 | END
142 | $$ LANGUAGE plpgsql;
143 |
144 | CREATE OR REPLACE FUNCTION auth_login (username TEXT, pass TEXT)
145 | RETURNS TEXT AS $$
146 | DECLARE
147 | auth_jwt TEXT;
148 | BEGIN
149 | IF auth_check_password(username, pass) THEN
150 | -- the credentials are correct, we can create JWT
151 | WITH user_info (id) AS (
152 | SELECT id
153 | FROM users u
154 | WHERE u.username = $1
155 | LIMIT 1
156 | )
157 | SELECT auth_jwt_new(user_info.id) INTO auth_jwt
158 | FROM user_info;
159 |
160 | RETURN auth_jwt;
161 | ELSE
162 | -- wrong credentials
163 | RAISE EXCEPTION 'Wrong credentials' USING ERRCODE = 'P1001';
164 | END IF;
165 | END
166 | $$ LANGUAGE plpgsql;
167 |
168 | COMMIT;
169 |
--------------------------------------------------------------------------------
/deploy/0005-scoring.sql:
--------------------------------------------------------------------------------
1 | -- Deploy words:0005-scoring to pg
2 |
3 | BEGIN;
4 |
5 | -- Create the word scoring system
6 | CREATE OR REPLACE FUNCTION get_word_score (last_query_at TIMESTAMPTZ, difficulty INT)
7 | RETURNS INT
8 | AS $$
9 | BEGIN
10 | RETURN (difficulty * 100) + date_part('day', (now() - last_query_at));
11 | END
12 | $$ LANGUAGE plpgsql;
13 |
14 |
15 | CREATE OR REPLACE FUNCTION verify_word_full(wordId INT, testDefinition TEXT)
16 | RETURNS BOOL
17 | AS $$
18 | DECLARE
19 | verified BOOL;
20 | diff INT;
21 | BEGIN
22 | -- arbitrary 0.4 found by example
23 | SELECT verify_word(wordId, testDefinition, 0.4) INTO verified;
24 | SELECT difficulty INTO diff FROM words WHERE id = wordId;
25 | IF verified = 't' AND diff > 0 THEN
26 | diff := diff - 1;
27 | ELSE
28 | IF verified = 'f' THEN
29 | diff := diff + 1;
30 | END IF;
31 | END IF;
32 | UPDATE words SET last_query_at = now(), difficulty = diff WHERE id = wordId;
33 | return verified;
34 | END
35 | $$ LANGUAGE plpgsql;
36 |
37 | CREATE OR REPLACE FUNCTION verify_word(wordId INT, testDefinition TEXT, threshold REAL)
38 | RETURNS BOOL
39 | AS $$
40 | DECLARE
41 | definition TEXT;
42 | def_curr TEXT;
43 | simi REAL;
44 | simi_max REAL;
45 | ind INT;
46 | nb_def INT;
47 | BEGIN
48 | SELECT w.definition INTO definition FROM words w WHERE id = wordId;
49 | RAISE NOTICE 'Definition to verify: %', testDefinition;
50 | RAISE NOTICE 'Against: %', definition;
51 |
52 | SELECT array_length(regexp_split_to_array(definition, ','), 1) INTO nb_def;
53 |
54 | ind := 1;
55 | simi_max := 0;
56 | LOOP
57 | SELECT split_part(definition, ',', ind) INTO def_curr;
58 | ind := ind + 1;
59 |
60 | SELECT similarity(unaccent(def_curr), unaccent(testDefinition)) INTO simi;
61 | IF simi > simi_max THEN
62 | simi_max := simi;
63 | END IF;
64 | EXIT WHEN ind > nb_def;
65 | END LOOP;
66 |
67 | RAISE NOTICE 'max similarity found to: %', simi_max;
68 | IF simi_max > threshold THEN
69 | return 't';
70 | ELSE
71 | return 'f';
72 | END IF;
73 | END
74 | $$ LANGUAGE plpgsql;
75 |
76 |
77 | COMMIT;
78 |
--------------------------------------------------------------------------------
/deploy/0006-update-user.sql:
--------------------------------------------------------------------------------
1 | -- Deploy words:0006-update-user to pg
2 |
3 | BEGIN;
4 |
5 | -- Create extensions
6 | CREATE EXTENSION IF NOT EXISTS CITEXT;
7 |
8 | -- Create
9 | ALTER TABLE users RENAME TO user_account;
10 |
11 | -- Create Domain
12 | CREATE DOMAIN email_d AS CITEXT
13 | CHECK(
14 | value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
15 | );
16 |
17 | DELETE FROM
18 | user_account
19 | WHERE
20 | email !~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$';
21 | ALTER TABLE user_account ALTER COLUMN email TYPE email_d ;
22 | DELETE FROM
23 | user_account
24 | WHERE
25 | email is NULL;
26 |
27 | ALTER TABLE user_account ALTER COLUMN email SET NOT NULL;
28 | ALTER TABLE user_account DROP CONSTRAINT users_username_check;
29 | ALTER TABLE user_account ALTER COLUMN username DROP NOT NULL;
30 | ALTER TABLE user_account ADD COLUMN first_name TEXT NULL;
31 | ALTER TABLE user_account ADD COLUMN last_name TEXT NULL;
32 | ALTER TABLE user_account ADD COLUMN email_verified BOOL DEFAULT 'f' NOT NULL;
33 | ALTER TABLE user_account ADD CONSTRAINT unique_user_account_email UNIQUE (email);
34 |
35 | -- Now for the session
36 | ALTER TABLE sessions RENAME TO session;
37 |
38 | ALTER TABLE session ADD COLUMN created_at TIMESTAMPTZ NOT NULL default now();
39 |
40 | -- Remove the unique constraint on the userid
41 | ALTER TABLE session DROP CONSTRAINT sessions_userid_key;
42 |
43 | -- Alter table words now
44 | DROP VIEW words_score;
45 |
46 | -- Alter table words to flashcard
47 | ALTER TABLE words RENAME TO flashcard;
48 |
49 | ALTER TABLE flashcard ALTER COLUMN word TYPE TEXT;
50 | ALTER TABLE flashcard RENAME word TO recto;
51 | ALTER TABLE flashcard RENAME definition TO verso;
52 | ALTER TABLE flashcard RENAME keywords TO tags;
53 | ALTER TABLE flashcard RENAME difficulty TO bucket;
54 | ALTER TABLE flashcard ALTER COLUMN bucket SET DEFAULT 0;
55 | ALTER TABLE flashcard DROP COLUMN language;
56 | ALTER TABLE flashcard ADD COLUMN side TEXT DEFAULT 'recto';
57 | ALTER TABLE flashcard ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
58 |
59 | CREATE OR REPLACE FUNCTION update_flashcard_updated_at()
60 | RETURNS TRIGGER AS $$
61 | BEGIN
62 | NEW.updated_at = now();
63 | RETURN NEW;
64 | END;
65 | $$ language 'plpgsql';
66 |
67 | CREATE TRIGGER flashcard_updated_at
68 | BEFORE UPDATE ON flashcard
69 | FOR EACH ROW
70 | EXECUTE PROCEDURE update_flashcard_updated_at();
71 |
72 | COMMIT;
73 |
--------------------------------------------------------------------------------
/deploysetup/deploysetup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 |
4 |
--------------------------------------------------------------------------------
/drop-ddl.sql:
--------------------------------------------------------------------------------
1 | -- Drop Tables
2 | DROP TABLE words;
3 |
--------------------------------------------------------------------------------
/front/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react", {'plugins': ['@babel/plugin-proposal-class-properties']}]
3 | }
4 |
--------------------------------------------------------------------------------
/front/FlashCardEdit.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | export function addTag(elt) {
5 | console.log('add tag');
6 | console.log(elt);
7 | var newDiv = document.createElement("div");
8 | elt.previousElementSibling.append(
9 | newDiv,
10 | );
11 | ReactDOM.render(
12 | ,
13 | newDiv
14 | );
15 | }
16 |
17 | export function removeTag(elt) {
18 | elt.parentElement.remove();
19 | }
20 |
21 | export class FlashCardTagInput extends React.Component {
22 |
23 | constructor(props) {
24 | super(props);
25 |
26 | this.state = {
27 | unmount: false,
28 | }
29 |
30 | this.onRemove = this.onRemove.bind(this);
31 | }
32 |
33 | onRemove = () => {
34 | this.setState({
35 | unmount: true,
36 | });
37 | }
38 |
39 | render() {
40 | const {unmount} = this.state;
41 |
42 | if (unmount) {
43 | return null;
44 | } else {
45 | return (
46 |
50 | )
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/front/FullscreenNavigation.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { render } from 'react-dom';
3 | import {closeFullScreenMenu} from './utils.js';
4 |
5 | export class FullscreenNavigation extends Component {
6 |
7 | constructor(props) {
8 | super(props);
9 |
10 | this.hideFullscreenMenu = this.hideFullscreenMenu.bind(this);
11 | }
12 |
13 | hideFullscreenMenu(e) {
14 | e.preventDefault();
15 | closeFullScreenMenu();
16 | }
17 |
18 | render() {
19 | return (
20 |
21 |
22 |
23 |
24 |
41 |
42 | );
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/front/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 | build:
3 | npm run build
4 |
5 | .PHONY: dev
6 | dev:
7 | npm run start
8 |
--------------------------------------------------------------------------------
/front/TopNavigation.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import ReactDOM, { render } from 'react-dom';
3 |
4 | import { FullscreenNavigation } from './FullscreenNavigation.jsx';
5 |
6 | export class TopNavigation extends Component {
7 |
8 | constructor(props) {
9 | super(props);
10 |
11 | this.onOpenFullscreenMenu = this.onOpenFullscreenMenu.bind(this);
12 | }
13 |
14 | componentDidMount() {
15 | }
16 |
17 | onOpenFullscreenMenu(e) {
18 | const {user} = this.props;
19 |
20 | e.preventDefault();
21 | ReactDOM.render(
22 | ,
23 | document.getElementById('fullscreen-menu')
24 | );
25 | openFullScreenMenu();
26 | }
27 |
28 | render() {
29 | return (
30 |
37 | );
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/front/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { TopNavigation } from "./TopNavigation.jsx";
5 |
6 | import {openFullScreenMenu, closeFullScreenMenu, newQuizzTimeout} from './utils.js';
7 | import {addTag, removeTag} from './FlashCardEdit.js';
8 |
9 | window.onload = function() {
10 | if (window.location.pathname === "/") {
11 | // pathname
12 | }
13 |
14 | // Top Navigation for user
15 | if (typeof backendVariables !== 'undefined' && typeof backendVariables.session !== 'undefined' && typeof backendVariables.session.user.id !== 'undefined') {
16 | ReactDOM.render(
17 | ,
18 | document.getElementById('top-navigation-user')
19 | );
20 | }
21 | }
22 |
23 |
24 | // export global context / window context the function needed
25 | // by the attribute onclick=
26 | window.openFullScreenMenu = openFullScreenMenu;
27 | window.closeFullScreenMenu = closeFullScreenMenu;
28 | window.addTag = addTag;
29 | window.removeTag = removeTag;
30 | window.newQuizzTimeout = newQuizzTimeout;
31 |
--------------------------------------------------------------------------------
/front/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "front",
3 | "version": "1.0.0",
4 | "description": "frontend for izidict",
5 | "main": "main.js",
6 | "scripts": {
7 | "start": "webpack --mode development --watch",
8 | "build": "webpack --mode production",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "@babel/core": "^7.1.5",
15 | "@babel/plugin-proposal-class-properties": "^7.1.0",
16 | "@babel/preset-env": "^7.1.5",
17 | "@babel/preset-react": "^7.0.0",
18 | "babel-loader": "^8.0.4",
19 | "webpack": "^4.25.1",
20 | "webpack-cli": "^3.1.2"
21 | },
22 | "dependencies": {
23 | "moment": "^2.22.2",
24 | "moment-timezone": "^0.5.23",
25 | "react": "^16.6.1",
26 | "react-dom": "^16.6.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/front/utils.js:
--------------------------------------------------------------------------------
1 | export function openFullScreenMenu() {
2 | document.getElementById('fullscreen-menu').style.visibility = "visible";
3 | }
4 |
5 | export function closeFullScreenMenu() {
6 | document.getElementById('fullscreen-menu').style.visibility = "hidden";
7 | }
8 |
9 | export function newQuizzTimeout() {
10 | setTimeout(
11 | () => {
12 | window.location = "/quizz";
13 | },
14 | 3000
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/front/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | entry: "./main.js",
5 | output: {
6 | path: path.join(__dirname, "/dist"),
7 | filename: "izidict.js"
8 | },
9 | module: {
10 | rules: [{
11 | test: /\.jsx?$/,
12 | exclude: /node_modules/,
13 | use: [{
14 | loader: "babel-loader"
15 | }]
16 | }]
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/izidict.cabal:
--------------------------------------------------------------------------------
1 | -- This file has been generated from package.yaml by hpack version 0.28.2.
2 | --
3 | -- see: https://github.com/sol/hpack
4 | --
5 | -- hash: 862007f62876066ccf6e24161edbdaf6ab1484c078e8b71963e2a05aee41c7d9
6 |
7 | name: izidict
8 | version: 0.1.0.0
9 | synopsis: IziDict HTTP backend server
10 | description: IziDict HTTP backend server
11 | category: Development
12 | homepage: https://github.com/aRkadeFR/izidict#readme
13 | bug-reports: https://github.com/aRkadeFR/izidict/issues
14 | maintainer: aRkadeFR
15 | license: MIT
16 | license-file: LICENSE
17 | build-type: Simple
18 | cabal-version: >= 1.10
19 |
20 | source-repository head
21 | type: git
22 | location: https://github.com/aRkadeFR/izidict
23 |
24 | executable backend-exe
25 | main-is: main.hs
26 | exposed-modules:
27 | API.Facebook
28 | Auth
29 | Cache
30 | Config
31 | Data
32 | Data.GrantUser
33 | Data.User
34 | Data.Message
35 | Data.Session
36 | Form
37 | HandlerM
38 | Mustache
39 | SharedEnv
40 | SQL
41 | Template
42 | User
43 | Word
44 | Paths_izidict
45 | hs-source-dirs:
46 | src
47 | app
48 | ghc-options: -Wall
49 | build-depends:
50 | base >=4.7 && <5
51 | , base64-bytestring
52 | , aeson
53 | , aeson-pretty
54 | , async
55 | , binary
56 | , binary-bits
57 | , blaze-html
58 | , blaze-markup
59 | , bytestring >=0.10.8.2
60 | , byteable
61 | , clay
62 | , cookie
63 | , containers
64 | , convertible
65 | , cryptohash
66 | , data-binary-ieee754
67 | , geojson
68 | , lens
69 | , lens-aeson
70 | , HaskellNet
71 | , HaskellNet-SSL
72 | , http-api-data
73 | , http-conduit
74 | , http-media
75 | , http-types
76 | , mtl
77 | , mime-mail
78 | , monad-logger
79 | , megaparsec
80 | , mustache
81 | , network
82 | , postgresql-libpq >= 0.9.4.2
83 | , protolude
84 | , random
85 | , resource-pool >=0.2.3.2
86 | , servant >=0.5
87 | , servant-blaze ==0.8
88 | , servant-multipart
89 | , servant-server ==0.14.1
90 | , scientific
91 | , semigroups
92 | , stm
93 | , swagger2
94 | , text
95 | , time
96 | , transformers
97 | , utf8-string
98 | , unordered-containers
99 | , uuid
100 | , vector
101 | , wai
102 | , wai-extra
103 | , warp
104 | , wkt-geom
105 | , yaml
106 | default-language: Haskell2010
107 | default-extensions: OverloadedStrings
108 | , DeriveGeneric
109 | , TemplateHaskell
110 | , TypeOperators
111 | , DataKinds
112 | , RankNTypes
113 | , GeneralizedNewtypeDeriving
114 | , QuasiQuotes
115 | , DeriveDataTypeable
116 | , FlexibleInstances
117 | , MultiParamTypeClasses
118 | , NoImplicitPrelude
119 | , InstanceSigs
120 |
--------------------------------------------------------------------------------
/package.yaml:
--------------------------------------------------------------------------------
1 | name: izidict
2 | version: 0.1.0.0
3 | synopsis: IziDict HTTP backend server
4 | description: IziDict HTTP backend server
5 | maintainer: aRkadeFR
6 | license: MIT
7 | github: aRkadeFR/izidict
8 | category: Development
9 |
10 | dependencies:
11 | - base >= 4.7 && < 5
12 | - aeson
13 | - aeson-pretty
14 | - convertible
15 | - utf8-string
16 | - elm-export
17 | - servant >= 0.5
18 | - servant-server
19 | - servant-swagger
20 | - servant-swagger-ui
21 | - servant-elm
22 | - wai-extra
23 | - wai-cors >= 0.2.6
24 | - swagger2
25 | - lens
26 | - wai
27 | - warp
28 | - containers
29 | - mtl
30 | - resource-pool >= 0.2.3.2
31 | - bytestring >= 0.10.8.2
32 | - HDBC >= 2.4.0.2
33 | - HDBC-postgresql >= 2.3.2.5
34 | - yeshql-hdbc >= 4.1.0.1
35 | - yeshql >= 4.1.0.1
36 | - yaml == 0.8.32
37 |
38 | source-dirs:
39 | - src
40 |
41 | ghc-options: -Wall
42 |
43 | executables:
44 | backend-exe:
45 | main: main.hs
46 | source-dirs: app
47 | generate-elm-api:
48 | main: generateElmAPI.hs
49 | source-dirs: app
50 |
51 | tests:
52 | spec:
53 | main: Spec.hs
54 | source-dirs: test
55 | dependencies:
56 | - hspec == 2.*
57 | - hspec-wai == 0.9.0
58 | - hspec-wai-json == 0.9.0
59 | - mockery
60 | - QuickCheck
61 |
--------------------------------------------------------------------------------
/queries.sql:
--------------------------------------------------------------------------------
1 | -- name: get-random-word
2 | -- Get random word from table
3 | WITH params AS (
4 | SELECT count(*) AS nb_rows
5 | FROM words
6 | )
7 | SELECT language, word, definition
8 | FROM words, params
9 | OFFSET floor(random() * (SELECT nb_rows FROM params))
10 | LIMIT 1;
11 |
12 | -- name: get-n-random-words
13 | -- Get N random word from table
14 | SELECT language, word, definition
15 | FROM words
16 | ORDER BY random()
17 | LIMIT %(n)s;
18 |
19 | -- name: get-similarity-word
20 | -- Check similarity word
21 | SELECT similarity(%(word_try)s, %(word)s);
22 |
23 | -- name: get-similarity-definition
24 | -- Check similarity word
25 | SELECT similarity(%(word_try)s, definition)
26 | FROM words
27 | WHERE word = %(word)s;
28 |
--------------------------------------------------------------------------------
/questionary.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import psycopg2
4 | import anosql
5 | import random
6 | import time
7 | import os
8 |
9 |
10 | def main():
11 | with psycopg2.connect('service=words') as db_conn:
12 | queries = anosql.load_queries('postgres', 'queries.sql')
13 |
14 | # Let's print 5 random words we want to work on,
15 | # with the definition
16 | random_words = queries.get_n_random_words(db_conn, n=5)
17 | print('Words to keep in mind are:')
18 | for random_word in random_words:
19 | print('{} "{}": {}'.format(random_word[0], random_word[1], random_word[2]))
20 |
21 | time.sleep(10)
22 | os.system('cls' if os.name == 'nt' else 'clear')
23 |
24 | # And now let's do a simple question
25 | random_word = random.choice(random_words)
26 |
27 | answer = input('Word in {} for {}:\n--> '.format(random_word[0], random_word[2]))
28 | similarity = queries.get_similarity_word(db_conn, word_try=answer, word=random_word[1])[0][0]
29 | if similarity > 0.05:
30 | print('Cool! Again?')
31 | else:
32 | print('Almost there… The good word was: {}\nKeep trying!'.format(random_word[1]))
33 | pass
34 |
35 |
36 | if __name__ == '__main__':
37 | main()
38 |
--------------------------------------------------------------------------------
/revert/0001-words.sql:
--------------------------------------------------------------------------------
1 | -- Revert words:0001-words from pg
2 |
3 | BEGIN;
4 |
5 | DROP TABLE "words";
6 |
7 | COMMIT;
8 |
--------------------------------------------------------------------------------
/revert/0002-users.sql:
--------------------------------------------------------------------------------
1 | -- Revert words:0002-users from pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add DDLs here.
6 |
7 | COMMIT;
8 |
--------------------------------------------------------------------------------
/revert/0003-tokens.sql:
--------------------------------------------------------------------------------
1 | -- Revert words:0003-tokens from pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add DDLs here.
6 |
7 | COMMIT;
8 |
--------------------------------------------------------------------------------
/revert/0004-session.sql:
--------------------------------------------------------------------------------
1 | -- Revert words:0004-session from pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add DDLs here.
6 |
7 | COMMIT;
8 |
--------------------------------------------------------------------------------
/revert/0005-scoring.sql:
--------------------------------------------------------------------------------
1 | -- Revert words:0005-scoring from pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add DDLs here.
6 |
7 | COMMIT;
8 |
--------------------------------------------------------------------------------
/revert/0006-update-user.sql:
--------------------------------------------------------------------------------
1 | -- Revert words:0006-update-user from pg
2 |
3 | BEGIN;
4 |
5 | -- Session
6 | -- Remove the unique constraint on the userid
7 | ALTER TABLE session ADD CONSTRAINT sessions_userid_key UNIQUE (userid);
8 | --
9 | ALTER TABLE session DROP COLUMN created_at;
10 | ALTER TABLE session RENAME TO sessions;
11 |
12 | -- Change the field email type back to text
13 | ALTER TABLE user_account ALTER COLUMN email TYPE TEXT ;
14 |
15 | -- Move the table back
16 | ALTER TABLE user_account RENAME TO users;
17 |
18 | -- Drop Domain
19 | DROP DOMAIN IF EXISTS email_d;
20 |
21 | -- Drop extension
22 | DROP EXTENSION IF EXISTS CITEXT;
23 |
24 | COMMIT;
25 |
--------------------------------------------------------------------------------
/sqitch.conf:
--------------------------------------------------------------------------------
1 | [core]
2 | engine = pg
3 | plan_file = sqitch.plan
4 | top_dir = .
5 | [deploy]
6 | verify = true
7 | [rebase]
8 | verify = true
9 | [target "dev"]
10 | uri = db:pg:words
11 | [target "test"]
12 | uri = db:pg:words
13 | [engine "pg"]
14 | target = test
15 |
--------------------------------------------------------------------------------
/sqitch.plan:
--------------------------------------------------------------------------------
1 | %syntax-version=1.0.0
2 | %project=words
3 |
4 | 0001-words 2018-02-05T21:59:14Z aRkadeFR # Schema for words
5 | 0002-users 2018-07-16T07:41:01Z aRkadeFR # Add 0002-users deploy change
6 | 0003-tokens 2018-08-03T07:06:48Z aRkadeFR # 0003-tokens temporary for authentication
7 | 0004-session 2018-08-05T09:57:37Z aRkadeFR # Add 0004-session handle / table
8 | 0005-scoring 2018-08-23T20:29:16Z aRkadeFR # Add 0005-scoring for words
9 | 0006-update-user 2019-01-05T06:09:38Z aRkadeFR # 0006-update-user
10 |
--------------------------------------------------------------------------------
/sql.load:
--------------------------------------------------------------------------------
1 | LOAD CSV
2 | FROM '/home/arkadefr/Projects/words/words.csv'
3 | HAVING FIELDS (
4 | language, word, definition
5 | )
6 | INTO postgresql:///words?tablename=words
7 | TARGET COLUMNS (
8 | language, word, definition
9 | )
10 |
11 | WITH truncate,
12 | skip header = 1,
13 | fields optionally enclosed by '"',
14 | fields escaped by double-quote,
15 | fields terminated by ','
16 |
17 | SET client_encoding to 'latin1',
18 | work_mem to '12MB',
19 | standard_conforming_strings to 'on'
20 | ;
21 |
--------------------------------------------------------------------------------
/sql/addFlashCard.sql:
--------------------------------------------------------------------------------
1 | -- Add FlashCard
2 | WITH flashcard_inserted AS (
3 | INSERT INTO
4 | flashcard
5 | (userid, recto, tags, verso, updated_at)
6 | VALUES
7 | (CAST($1 AS BIGINT), CAST($2 AS TEXT), CAST($3 AS TEXT[]), CAST($4 AS TEXT), now() - INTERVAL '1 day')
8 | RETURNING
9 | *
10 | )
11 | SELECT
12 | COUNT(*)
13 | FROM
14 | flashcard_inserted
15 | ;
16 |
--------------------------------------------------------------------------------
/sql/checkPassword.sql:
--------------------------------------------------------------------------------
1 | -- Password checked
2 | WITH user_checked AS (
3 | SELECT
4 | id
5 | FROM
6 | user_account
7 | WHERE
8 | email = CAST($1 AS CITEXT)
9 | AND CRYPT($2, passpass) = passpass
10 | )
11 | SELECT
12 | count(*)
13 | FROM
14 | user_checked
15 | ;
16 |
--------------------------------------------------------------------------------
/sql/decreaseBucketFlashCard.sql:
--------------------------------------------------------------------------------
1 | -- Decrease bucket flashcard .
2 | -- Not less than 0
3 | WITH flashcard_updated AS (
4 | UPDATE
5 | flashcard
6 | SET
7 | side = CASE WHEN side = 'recto' THEN 'verso'
8 | ELSE 'recto' END
9 | , bucket = CASE WHEN bucket = 6 THEN 5
10 | WHEN bucket = 5 THEN 4
11 | WHEN bucket = 4 THEN 3
12 | WHEN bucket = 3 THEN 2
13 | WHEN bucket = 2 THEN 1
14 | ELSE 0 END
15 | WHERE
16 | userid = CAST($1 AS BIGINT)
17 | AND id = CAST($2 AS BIGINT)
18 | RETURNING
19 | *
20 | )
21 | SELECT
22 | COUNT(*)
23 | FROM
24 | flashcard_updated
25 | ;
26 |
--------------------------------------------------------------------------------
/sql/deleteFlashCard.sql:
--------------------------------------------------------------------------------
1 | -- Delete FlashCard
2 | WITH flashcard_deleted AS (
3 | DELETE
4 | FROM
5 | flashcard
6 | WHERE
7 | userid = CAST($1 AS BIGINT)
8 | AND id = CAST($2 AS BIGINT)
9 | RETURNING
10 | *
11 | )
12 | SELECT
13 | COUNT(*)
14 | FROM
15 | flashcard_deleted
16 | ;
17 |
--------------------------------------------------------------------------------
/sql/getFlashCard.sql:
--------------------------------------------------------------------------------
1 | -- Get my FlashCards
2 | SELECT
3 | id,
4 | recto,
5 | tags,
6 | verso,
7 | bucket,
8 | side
9 | FROM
10 | flashcard
11 | WHERE
12 | userid = CAST($1 AS BIGINT)
13 | AND id = CAST($2 AS BIGINT)
14 | ;
15 |
--------------------------------------------------------------------------------
/sql/getFlashCards.sql:
--------------------------------------------------------------------------------
1 | -- Get my FlashCards
2 | SELECT
3 | id,
4 | recto,
5 | tags,
6 | verso,
7 | bucket,
8 | side
9 | FROM
10 | flashcard
11 | WHERE
12 | userid = CAST($1 AS BIGINT)
13 | ORDER BY
14 | inserted_at DESC
15 | LIMIT 50
16 | ;
17 |
--------------------------------------------------------------------------------
/sql/getNewRandomSession.sql:
--------------------------------------------------------------------------------
1 | -- Get new random session for user email
2 | WITH user_id AS (
3 | SELECT
4 | id
5 | FROM
6 | user_account
7 | WHERE
8 | email = CAST($1 AS CITEXT)
9 | AND CRYPT($2, passpass) = passpass
10 | )
11 | INSERT INTO
12 | session
13 | (userid, secret)
14 | SELECT
15 | uid.id, md5(random()::TEXT)
16 | FROM
17 | user_id uid
18 | RETURNING
19 | secret
20 | ;
21 |
--------------------------------------------------------------------------------
/sql/getNewRandomSessionWithoutPasswordCheck.sql:
--------------------------------------------------------------------------------
1 | -- Get new random session for user email
2 | WITH user_id AS (
3 | SELECT
4 | id
5 | FROM
6 | user_account
7 | WHERE
8 | email = CAST($1 AS CITEXT)
9 | )
10 | INSERT INTO
11 | session
12 | (userid, secret)
13 | SELECT
14 | uid.id, md5(random()::TEXT)
15 | FROM
16 | user_id uid
17 | RETURNING
18 | secret
19 | ;
20 |
--------------------------------------------------------------------------------
/sql/getQuizzFlashCard.sql:
--------------------------------------------------------------------------------
1 | -- Get a FlashCard for the quizz
2 | -- The rule to get a FlashCard is this one:
3 | -- - Bucket 0: Pick the flash card every day
4 | -- - Bucket 1: Pick the flash card every 2 days
5 | -- - Bucket 2: Pick the flash card every week
6 | -- - Bucket 3: Pick the flash card every month
7 | -- - Bucket 4: Pick the flash card every 3 months
8 | -- - Bucket 5: Pick the flash card every 6 months
9 | -- - Bucket 6: Pick the flash card every year
10 | SELECT
11 | id,
12 | recto,
13 | tags,
14 | verso,
15 | bucket,
16 | side
17 | FROM
18 | flashcard
19 | WHERE
20 | userid = CAST($1 AS BIGINT)
21 | AND date_part('day', updated_at) != date_part('day', now()) -- we remove all the cards already asked today
22 | AND ( bucket = 0
23 | OR bucket = 1 AND EXTRACT(DAY FROM (now() - updated_at)) > 2
24 | OR bucket = 2 AND EXTRACT(DAY FROM (now() - updated_at)) > 7
25 | OR bucket = 3 AND EXTRACT(DAY FROM (now() - updated_at)) > 30
26 | OR bucket = 4 AND EXTRACT(DAY FROM (now() - updated_at)) > 90
27 | OR bucket = 5 AND EXTRACT(DAY FROM (now() - updated_at)) > 180
28 | OR bucket = 6 AND EXTRACT(DAY FROM (now() - updated_at)) > 365
29 | )
30 | ORDER BY
31 | bucket DESC
32 | LIMIT
33 | 1
34 | ;
35 |
--------------------------------------------------------------------------------
/sql/increaseBucketFlashCard.sql:
--------------------------------------------------------------------------------
1 | -- Increase bucket flashcard .
2 | -- Not more than 5
3 | WITH flashcard_updated AS (
4 | UPDATE
5 | flashcard
6 | SET
7 | side = CASE WHEN side = 'recto' THEN 'verso'
8 | ELSE 'recto' END
9 | , bucket = CASE WHEN bucket = 0 THEN 1
10 | WHEN bucket = 1 THEN 2
11 | WHEN bucket = 2 THEN 3
12 | WHEN bucket = 3 THEN 4
13 | WHEN bucket = 4 THEN 5
14 | ELSE 6 END
15 | WHERE
16 | userid = CAST($1 AS BIGINT)
17 | AND id = CAST($2 AS BIGINT)
18 | RETURNING
19 | *
20 | )
21 | SELECT
22 | COUNT(*)
23 | FROM
24 | flashcard_updated
25 | ;
26 |
--------------------------------------------------------------------------------
/sql/insertDefaultFlashCards.sql:
--------------------------------------------------------------------------------
1 | -- Insert 10 Default FlashCards
2 | WITH flashcard_inserted AS (
3 | INSERT INTO
4 | flashcard
5 | (recto, tags, verso, userid, updated_at)
6 | VALUES
7 | ('Gravity Apple', CAST('{general knowledge}' AS TEXT[]), 'Object inspired Isaac Newton s theory', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
8 | ('NYC Yellow', CAST('{general knowledge}' AS TEXT[]), 'Color of NYC Taxi', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
9 | ('J. K. Rowling', CAST('{general knowledge}' AS TEXT[]), 'Author of the books Harry Potter', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
10 | ('Percy Spencer', CAST('{general knowledge}' AS TEXT[]), 'Inventor of the microwave', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
11 | ('Merci', CAST('{general knowledge}' AS TEXT[]), 'Thank you', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
12 | ('Yawn and stretch at the same time', CAST('{general knowledge}' AS TEXT[]), 'Pandiculating', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
13 | ('Pteronophobia', CAST('{general knowledge}' AS TEXT[]), 'Fear of being tickled by a feather', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
14 | ('Country with 50 percent of the total round about in the world', CAST('{general knowledge}' AS TEXT[]), 'France has 50 percent of the total number of…', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
15 | ('Pluto is smaller than which country?', CAST('{general knowledge}' AS TEXT[]), 'Russia is bigger than which planet', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
16 | ('Animal sleeping up to three years', CAST('{general knowledge}' AS TEXT[]), 'Action a snail can do during three years', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
17 | ('Animal without a brain', CAST('{general knowledge}' AS TEXT[]), 'Body part missing for starfish', CAST($1 AS BIGINT), now() - INTERVAL '1 day'),
18 | ('Year of creation of the video game character Mario', CAST('{general knowledge}' AS TEXT[]), '1981 Video Game', CAST($1 AS BIGINT), now() - INTERVAL '1 day')
19 | RETURNING
20 | *
21 | )
22 | SELECT
23 | COUNT(*)
24 | FROM
25 | flashcard_inserted
26 | ;
27 |
--------------------------------------------------------------------------------
/sql/insertUser.sql:
--------------------------------------------------------------------------------
1 | -- Facebook insert user
2 | -- the email is verified.
3 | WITH user_inserted AS (
4 | INSERT INTO
5 | user_account
6 | (username, email, passpass, email_verified)
7 | SELECT
8 | CAST($1 AS TEXT) || CAST($2 AS TEXT), CAST($3 AS TEXT), CAST($4 AS TEXT), 't'
9 | RETURNING
10 | *
11 | )
12 | SELECT
13 | id, username, email, languages
14 | FROM
15 | user_inserted
16 | ;
17 |
--------------------------------------------------------------------------------
/sql/insert_word.sql:
--------------------------------------------------------------------------------
1 | -- Insert sql word
2 | INSERT INTO
3 | words (language, word, definition)
4 | VALUES
5 | (:'language', :'word', :'definition')
6 | ;
7 |
--------------------------------------------------------------------------------
/sql/registerUser.sql:
--------------------------------------------------------------------------------
1 | -- Register new user
2 | WITH user_inserted AS (
3 | INSERT INTO
4 | user_account
5 | (email, passpass)
6 | SELECT
7 | CAST($1 AS email_d), CAST($2 AS TEXT)
8 | RETURNING
9 | *
10 | )
11 | SELECT
12 | id, username, email, languages
13 | FROM
14 | user_inserted
15 | ;
16 |
--------------------------------------------------------------------------------
/sql/searchFlashCards.sql:
--------------------------------------------------------------------------------
1 | -- Search my FlashCards
2 | SELECT
3 | id,
4 | recto,
5 | tags,
6 | verso,
7 | bucket,
8 | side
9 | FROM
10 | flashcard
11 | WHERE
12 | userid = CAST($1 AS BIGINT)
13 | AND to_tsvector('english', COALESCE(recto, '') || ' ' || COALESCE(verso, '')) @@ plainto_tsquery('english', CAST($2 AS TEXT))
14 | ORDER BY
15 | inserted_at DESC
16 | LIMIT 50
17 | ;
18 |
--------------------------------------------------------------------------------
/sql/test_data.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/sql/test_data.sql
--------------------------------------------------------------------------------
/sql/updateFlashCard.sql:
--------------------------------------------------------------------------------
1 | -- Update FlashCard
2 | WITH flashcard_updated AS (
3 | UPDATE
4 | flashcard
5 | SET
6 | recto = CAST($3 AS TEXT)
7 | , tags = CAST($4 AS TEXT[])
8 | , verso = CAST($5 AS TEXT)
9 | WHERE
10 | id = CAST($2 AS BIGINT)
11 | AND userid = CAST($1 AS BIGINT)
12 | RETURNING
13 | *
14 | )
15 | SELECT
16 | count(*)
17 | FROM
18 | flashcard_updated
19 | ;
20 |
--------------------------------------------------------------------------------
/sql/updatePassword.sql:
--------------------------------------------------------------------------------
1 | -- Update User Password
2 | WITH password_updated AS (
3 | UPDATE
4 | user_account
5 | SET
6 | passpass = CAST($2 AS TEXT)
7 | WHERE
8 | email = CAST($1 AS email_d)
9 | RETURNING
10 | *
11 | )
12 | SELECT
13 | count(*)
14 | FROM
15 | password_updated
16 | ;
17 |
--------------------------------------------------------------------------------
/sql/verifyFlashCardRectoAnswer.sql:
--------------------------------------------------------------------------------
1 | -- Verify Recto FlashCard answer
2 | SELECT
3 | to_tsvector('english', recto) @@ plainto_tsquery('english', CAST($3 AS TEXT)) AS verified
4 | FROM
5 | flashcard
6 | WHERE
7 | userid = CAST($1 AS BIGINT)
8 | AND id = CAST($2 AS BIGINT)
9 | ;
10 |
--------------------------------------------------------------------------------
/sql/verifyFlashCardVersoAnswer.sql:
--------------------------------------------------------------------------------
1 | -- Verify Verso FlashCard answer
2 | SELECT
3 | to_tsvector('english', verso) @@ plainto_tsquery('english', CAST($3 AS TEXT)) AS verified
4 | FROM
5 | flashcard
6 | WHERE
7 | userid = CAST($1 AS BIGINT)
8 | AND id = CAST($2 AS BIGINT)
9 | ;
10 |
--------------------------------------------------------------------------------
/src/API/Facebook.hs:
--------------------------------------------------------------------------------
1 | module API.Facebook where
2 |
3 | import Protolude
4 |
5 | import Control.Monad.Logger
6 | import Control.Exception
7 | import Data.Aeson
8 | import Network.HTTP.Conduit
9 |
10 | import Data.Text (unpack)
11 | import HandlerM
12 | import Data.Exception
13 | import Data.APIFacebook
14 |
15 |
16 | getFacebookAccessToken :: FBAppID -> FBAppSecret -> FBRedirectURI -> Text -> HandlerM FBAccessToken
17 | getFacebookAccessToken fbAppId fbAppSecret fbRedirectURI code = do
18 | returnBS <- simpleHttp . unpack $ ("https://graph.facebook.com/v3.2/oauth/access_token?client_id=" <> fbAppId <> "&redirect_uri=" <> fbRedirectURI <> "&client_secret=" <> fbAppSecret <> "&code=" <> code :: Text)
19 |
20 | let mfbAccessToken = decode returnBS :: Maybe FBAccessToken
21 | case mfbAccessToken of
22 | Just fbAccessToken -> return fbAccessToken
23 | Nothing -> throw $ ExceptionText "Cant decode the FB Access Token"
24 |
25 |
26 | getFacebookToken :: FBAppToken -> FBAccessToken -> HandlerM FBToken
27 | getFacebookToken fbAppToken fbAccessToken = do
28 | -- GET graph.facebook.com/debug_token?
29 | -- input_token={token-to-inspect}
30 | -- &access_token={app-token-or-admin-token}
31 | returnBS <- simpleHttp . unpack $ ("https://graph.facebook.com/debug_token?input_token=" <> access_token fbAccessToken <> "&access_token=" <> fbAppToken :: Text)
32 | $(logInfo) ("result: " <> show returnBS :: Text)
33 |
34 | let mfbToken = decode returnBS :: Maybe FBToken
35 | case mfbToken of
36 | Just fbToken -> return fbToken
37 | Nothing -> throw $ ExceptionText "Cant decode the FB Token"
38 |
39 | getFacebookPermissionList :: FBAppToken -> Text -> HandlerM FBPermissionList
40 | getFacebookPermissionList fbAppToken userId = do
41 | -- yes, userId is a Text in Facebook DB…
42 | returnBS <- simpleHttp . unpack $ ("https://graph.facebook.com/v3.2/" <> userId <> "/permissions?access_token=" <> fbAppToken :: Text)
43 |
44 | let mFBPermissions = decode returnBS :: Maybe FBPermissionList
45 |
46 | case mFBPermissions of
47 | Just fbPermissions -> return fbPermissions
48 | Nothing -> throw $ ExceptionText "Cant decode the FB Permission List"
49 |
50 | getFacebookUser :: FBAccessToken -> Text -> HandlerM FBUser
51 | getFacebookUser fbAppToken userId = do
52 | returnBS <- simpleHttp . unpack $ ("https://graph.facebook.com/v3.2/" <> userId <> "?access_token=" <> access_token fbAppToken <> "&fields=first_name,last_name,email" :: Text)
53 |
54 | let mFBUser = decode returnBS :: Maybe FBUser
55 |
56 | case mFBUser of
57 | Just fbUser -> return fbUser
58 | Nothing -> throw $ ExceptionText "Cant decode the FB User information"
59 |
--------------------------------------------------------------------------------
/src/Auth.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE TypeFamilies #-}
2 |
3 | module Auth where
4 |
5 | import Protolude
6 | import Data.List
7 | import Data.String
8 | import Data.Pool
9 | import Data.Exception
10 | import Control.Lens hiding ( Context )
11 | import Crypto.Hash hiding ( Context )
12 | import Data.Byteable
13 | import Data.Aeson.Lens
14 | import Database.Queries
15 | import Database.PostgreSQL.LibPQ
16 | import Servant
17 | import Servant.Server ( Context((:.), EmptyContext)
18 | , Handler
19 | )
20 | import Servant.API.Experimental.Auth ( AuthProtect )
21 | import Servant.Server.Experimental.Auth
22 | ( AuthHandler
23 | , AuthServerData
24 | , mkAuthHandler
25 | )
26 | import Network.Wai ( Request
27 | , requestHeaders
28 | )
29 | import qualified Data.ByteString as BS
30 | import qualified Data.ByteString.Base64 as BS64
31 | import Web.Cookie ( parseCookies )
32 |
33 | import Data.User
34 | import Data.Session
35 |
36 | -- | We need to specify the data returned after authentication
37 | type instance AuthServerData (AuthProtect "custom-auth") = Session
38 |
39 |
40 | -- | A method that, when given a JWT ByteString, will return a User
41 | -- This is our bespoke (and bad) authentication logic.
42 | sessionFromJWT :: Pool Connection -> ByteString -> Handler Session
43 | sessionFromJWT dbPool jwtPartsBS = do
44 | let jwtParts = BS.split 46 jwtPartsBS
45 | if length jwtParts /= 3
46 | then throwError $ err302 {
47 | errHeaders = [
48 | ("Location", "/")
49 | , ("Set-Cookie", "jwt=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT")
50 | ]
51 | }
52 | else do
53 | let jwtHeader = jwtParts!!0
54 | let jwtPayload = jwtParts!!1
55 | let jwtSignature = jwtParts!!2
56 |
57 | let eJwtPayloadBS = BS64.decode jwtPayload
58 | case eJwtPayloadBS of
59 | Right jwtPayloadBS -> do
60 | let mUserEmail = jwtPayloadBS ^? key "email"
61 | . _String
62 | case mUserEmail of
63 | Just userEmail' -> do
64 | eitherSecrets <- withResource dbPool $ \conn -> liftIO $ (try $ do
65 | secrets' <- queryFromText conn "SELECT s.secret FROM user_account uc JOIN session s ON s.userid = uc.id WHERE s.created_at > now() - INTERVAL '14 days' AND uc.email = CAST($1 AS CITEXT) ;"
66 | [ Just (Oid 25, encodeUtf8 userEmail', Binary) ] :: IO [Text]
67 | return $ secrets') :: Handler (Either ExceptionPostgreSQL [Text])
68 | case eitherSecrets of
69 | Right secretList -> do
70 | let signatureList = (\s -> BS64.encode $ toBytes $ hmacAlg SHA256 (encodeUtf8 $ s) (jwtHeader <> "." <> jwtPayload)) <$> secretList
71 |
72 | let signatureVerified = filter (\s -> s == jwtSignature) signatureList
73 | if length signatureVerified > 0
74 | then do
75 | -- the JWT has been verified at least against one
76 | -- secret. All good, we can retrieve and return the
77 | -- user
78 | eitherUserList <- withResource dbPool $ \conn -> liftIO $ (try $ do
79 | eitherUser' <- queryFromText conn ("SELECT uc.id , uc.username , uc.email , uc.languages "
80 | <> "FROM user_account uc "
81 | <> "WHERE uc.email = CAST($1 AS email_d) ;")
82 | [ Just (Oid 25, encodeUtf8 userEmail', Binary) ] :: IO [User]
83 | return $ eitherUser') :: Handler (Either ExceptionPostgreSQL [User])
84 | case eitherUserList of
85 | Right userList -> do
86 | if length userList > 0
87 | then return $ Session (userList!!0)
88 | else return AnonymousSession
89 | Left _ -> return AnonymousSession
90 | else return AnonymousSession
91 | Left _ -> return AnonymousSession
92 | Nothing -> return AnonymousSession
93 | Left _ -> return AnonymousSession
94 |
95 | --- | The auth handler wraps a function from Request -> Handler Session.
96 | --- We look for a token in the request headers that we expect to be in the cookie.
97 | authHandler :: Pool Connection -> AuthHandler Request Session
98 | authHandler dbPool = mkAuthHandler handler
99 | where handler :: Request -> Handler Session
100 | handler req = either returnAnonymousSession (sessionFromJWT dbPool) $ do
101 | cookie <- maybeToEither "Missing cookie header" $ lookup "cookie" $ requestHeaders req
102 | maybeToEither "Missing JWT cookie" $ lookup "jwt" $ parseCookies cookie
103 |
104 | returnAnonymousSession :: String -> Handler Session
105 | returnAnonymousSession _ = return AnonymousSession
106 |
107 | contextProxy :: Proxy '[(Servant.Server.Experimental.Auth.AuthHandler Request Session)]
108 | contextProxy = Proxy
109 |
110 | -- | The context that will be made available to request handlers. We supply the
111 | -- "cookie-auth"-tagged request handler defined above, so that the 'HasServer' instance
112 | -- of 'AuthProtect' can extract the handler and run it on the request.
113 | authServerContext :: Pool Connection -> Context (AuthHandler Request Session ': '[])
114 | authServerContext pool = (authHandler pool) :. EmptyContext
115 |
--------------------------------------------------------------------------------
/src/Cache.hs:
--------------------------------------------------------------------------------
1 | module Cache where
2 |
3 | import Protolude
4 |
5 | import Data.Text (pack)
6 | import Control.Concurrent.STM.TMVar
7 | import Data.HashMap.Strict
8 | import SharedEnv
9 | import Data.Settings
10 | import qualified Data.ByteString as BS
11 | import HandlerM
12 |
13 |
14 | readCacheOrFile :: FilePath -> HandlerM Text
15 | readCacheOrFile filePath = do
16 | sharedEnv <- ask
17 | if production . settings $ sharedEnv
18 | then do
19 | let tCache = cache sharedEnv
20 |
21 | cache' <- liftIO $ atomically $ readTMVar tCache
22 |
23 | let cacheKey = "FILE-" <> pack filePath :: Text
24 |
25 | let mCacheValue = lookup cacheKey cache'
26 |
27 | case mCacheValue of
28 | -- if the key exists in the cache', return it
29 | Just cacheValue -> return $ decodeUtf8 cacheValue
30 | -- if the key doesnt exists, read the file,
31 | -- save it in the cache, and return the content
32 | Nothing -> do
33 | contentTxt <- liftIO $ readFile filePath
34 | let newCache = insert cacheKey (encodeUtf8 contentTxt) cache'
35 | _ <- liftIO $ atomically $ swapTMVar tCache newCache
36 | return $ contentTxt
37 | else liftIO $ readFile filePath
38 |
39 |
40 | readCacheOrFileBS :: FilePath -> HandlerM ByteString
41 | readCacheOrFileBS filePath = do
42 | sharedEnv <- ask
43 | if production . settings $ sharedEnv
44 | then do
45 | let tCache = cache sharedEnv
46 |
47 | cache' <- liftIO $ atomically $ readTMVar tCache
48 |
49 | let cacheKey = "FILE-" <> pack filePath :: Text
50 |
51 | let mCacheValue = lookup cacheKey cache'
52 |
53 | case mCacheValue of
54 | -- if the key exists in the cache', return it
55 | Just cacheValue -> return $ cacheValue
56 | -- if the key doesnt exists, read the file,
57 | -- save it in the cache, and return the content
58 | Nothing -> do
59 | contentBS <- liftIO $ BS.readFile filePath
60 | let newCache = insert cacheKey contentBS cache'
61 | _ <- liftIO $ atomically $ swapTMVar tCache newCache
62 | return $ contentBS
63 | else liftIO $ BS.readFile filePath
64 |
--------------------------------------------------------------------------------
/src/Data.hs:
--------------------------------------------------------------------------------
1 | module Data
2 | ( module Data.APIFacebook
3 | , module Data.Exception
4 | , module Data.GrantUser
5 | , module Data.Message
6 | , module Data.Settings
7 | , module Data.Session
8 | , module Data.User
9 | , module Data.FlashCard
10 | ) where
11 |
12 | import Data.APIFacebook
13 | import Data.Exception
14 | import Data.GrantUser
15 | import Data.Message
16 | import Data.Settings
17 | import Data.Session
18 | import Data.User
19 | import Data.FlashCard
20 |
--------------------------------------------------------------------------------
/src/Data/APIFacebook.hs:
--------------------------------------------------------------------------------
1 | module Data.APIFacebook where
2 |
3 | import Protolude
4 | import Data.Aeson
5 |
6 |
7 | type FBAppID = Text
8 | type FBAppToken = Text
9 | type FBAppSecret = Text
10 | type FBRedirectURI = Text
11 |
12 |
13 | data FBUser = FBUser {
14 | first_name :: Text
15 | , last_name :: Text
16 | , email :: Text
17 | } deriving (Generic, Show)
18 |
19 | instance FromJSON FBUser
20 |
21 | data FBToken = FBToken {
22 | app_id :: Text
23 | , user_id :: Text
24 | } deriving (Generic, Show)
25 |
26 |
27 | instance FromJSON FBToken where
28 | parseJSON (Object o) = FBToken <$> ((o .: "data") >>= (.: "app_id"))
29 | <*> ((o .: "data") >>= (.: "user_id"))
30 | parseJSON _ = mzero
31 |
32 |
33 | data FBAccessToken = FBAccessToken {
34 | access_token :: Text
35 | , token_type :: Text
36 | , expires_in :: Integer
37 | } deriving (Generic, Show)
38 |
39 |
40 | instance FromJSON FBAccessToken
41 |
42 | data FBPermission = FBPermission {
43 | permission :: Text
44 | , status :: Text
45 | } deriving (Generic, Show)
46 |
47 | instance FromJSON FBPermission where
48 | parseJSON (Object o) =
49 | FBPermission <$> (o .: "permission")
50 | <*> (o .: "status")
51 | parseJSON _ = mzero
52 |
53 | newtype FBPermissionList = FBPermissionList { fBPermissionList :: [FBPermission] }
54 | deriving (Show)
55 |
56 | instance FromJSON FBPermissionList where
57 | parseJSON (Object o) = FBPermissionList <$> o .: "data"
58 | parseJSON _ = mzero
59 |
--------------------------------------------------------------------------------
/src/Data/Exception.hs:
--------------------------------------------------------------------------------
1 | module Data.Exception where
2 |
3 | import Protolude
4 |
5 |
6 | newtype ExceptionText = ExceptionText Text
7 | deriving (Show, Eq)
8 | instance Exception ExceptionText
9 |
10 | data ExceptionPostgreSQL = ExceptionPGUniqueViolation
11 | | ExceptionPGJWTMalformed
12 | | ExceptionPGJWTInvalid
13 | deriving (Show, Eq)
14 | instance Exception ExceptionPostgreSQL
15 |
--------------------------------------------------------------------------------
/src/Data/FlashCard.hs:
--------------------------------------------------------------------------------
1 | module Data.FlashCard where
2 |
3 | import Protolude
4 |
5 | import Data.Aeson
6 |
7 |
8 | data FlashCard = FlashCard
9 | { flashCardId :: Integer
10 | , flashCardRecto :: Text
11 | , flashCardTags :: [Text]
12 | , flashCardVerso :: Text
13 | , flashCardBucket :: Int
14 | , flashCardSide :: Text
15 | } deriving (Eq, Generic, Show)
16 |
17 |
18 | instance ToJSON FlashCard
19 | instance FromJSON FlashCard
20 |
--------------------------------------------------------------------------------
/src/Data/GrantUser.hs:
--------------------------------------------------------------------------------
1 | module Data.GrantUser where
2 |
3 | import Protolude
4 |
5 | import Data.Aeson
6 |
7 | data GrantUser = GrantUser
8 | { grantUsername :: Text
9 | , grantPassword :: Text
10 | } deriving (Eq, Generic, Show)
11 |
12 | instance ToJSON GrantUser
13 | instance FromJSON GrantUser
14 |
--------------------------------------------------------------------------------
/src/Data/Message.hs:
--------------------------------------------------------------------------------
1 | module Data.Message where
2 |
3 | import Protolude
4 |
5 | data Message = Message {
6 | description :: Text
7 | } deriving (Eq, Show)
8 |
--------------------------------------------------------------------------------
/src/Data/NewUser.hs:
--------------------------------------------------------------------------------
1 | module Data.NewUser
2 | ( NewUser(..)
3 | , newuserUsername
4 | , newuserPassword
5 | , newuserEmail
6 | , newuserLanguages
7 | )
8 | where
9 |
10 | import Protolude
11 |
12 | import Data.Aeson
13 |
14 |
15 | data NewUser = NewUser
16 | { username :: String
17 | , password :: String
18 | , email :: MaybeString
19 | , languages :: StringArray
20 | } deriving (Eq, Generic, Show)
21 |
22 |
23 | newuserUsername :: NewUser -> String
24 | newuserUsername = username
25 |
26 | newuserPassword :: NewUser -> String
27 | newuserPassword = password
28 |
29 | newuserEmail :: NewUser -> MaybeString
30 | newuserEmail = email
31 |
32 | newuserLanguages :: NewUser -> StringArray
33 | newuserLanguages = languages
34 |
35 | instance ToSchema NewUser
36 | instance ToJSON NewUser
37 | instance FromJSON NewUser
38 | instance ElmType NewUser
39 |
--------------------------------------------------------------------------------
/src/Data/Session.hs:
--------------------------------------------------------------------------------
1 | module Data.Session where
2 |
3 | import Protolude
4 | import Data.Aeson
5 | import Data.User
6 |
7 | data Session = Session
8 | { sessionUser :: User
9 | } | AnonymousSession
10 | deriving (Generic, Show)
11 |
12 |
13 | instance ToJSON Session where
14 | toJSON (Session u) = object ["user" .= u]
15 | toJSON AnonymousSession = object []
16 |
17 | toEncoding (Session u) = pairs ("user" .= u)
18 | toEncoding AnonymousSession = genericToEncoding defaultOptions AnonymousSession
19 |
20 |
21 | data SessionSecret = SessionSecret
22 | { sessionSecret :: ByteString
23 | } deriving (Generic, Show)
24 |
--------------------------------------------------------------------------------
/src/Data/Settings.hs:
--------------------------------------------------------------------------------
1 | module Data.Settings where
2 |
3 |
4 | import Protolude
5 | import Control.Monad
6 | import qualified Data.Text as T
7 | import qualified Data.Yaml as Y
8 | import Data.APIFacebook
9 |
10 | data Settings = Settings
11 | { base_url :: T.Text
12 | , production :: Bool
13 | , app_port :: Int
14 | , pgservice :: T.Text
15 | , facebook_appid :: FBAppID
16 | , facebook_appsecret :: FBAppSecret
17 | , facebook_apptoken :: FBAppToken
18 | } deriving (Show, Generic, Eq)
19 |
20 | instance Y.ToJSON Settings
21 | instance Y.FromJSON Settings
22 |
23 | loadSettings :: T.Text -> IO (Settings)
24 | loadSettings pathFileCfg = do
25 | maybeSettings <- Y.decodeFileEither . T.unpack $ pathFileCfg
26 | case maybeSettings of
27 | Right config -> return config
28 | Left err -> fail ("impossible to load the config.yml file" ++ show err)
29 |
--------------------------------------------------------------------------------
/src/Data/User.hs:
--------------------------------------------------------------------------------
1 | module Data.User where
2 |
3 | import Protolude
4 | import Data.Aeson
5 |
6 |
7 | data User = User
8 | { userId :: Integer
9 | , userUsername :: Maybe Text
10 | , userEmail :: Text
11 | } deriving (Eq, Generic, Show)
12 |
13 |
14 | instance ToJSON User where
15 | toJSON (User id' username' email') =
16 | object [
17 | "id" .= id'
18 | , "username" .= username'
19 | , "email" .= email'
20 | ]
21 | instance FromJSON User
22 |
--------------------------------------------------------------------------------
/src/Data/Word.hs:
--------------------------------------------------------------------------------
1 | module Data.Word where
2 |
3 | import Protolude hiding (Word)
4 |
5 | import Data.Aeson
6 |
7 |
8 | data Word = Word
9 | { wordId :: Int
10 | , wordRecto :: Text
11 | , wordTags :: [Text]
12 | , wordVerso :: Text
13 | , wordBucket :: Integer
14 | } deriving (Eq, Generic, Show)
15 |
16 | instance ToJSON Word
17 | instance FromJSON Word
18 |
--------------------------------------------------------------------------------
/src/Database.hs:
--------------------------------------------------------------------------------
1 | module Database
2 | ( DBConnectionString
3 | , initConnectionPool
4 | ) where
5 |
6 | import Protolude
7 |
8 | import Database.PostgreSQL.LibPQ (Connection, connectdb, finish)
9 | import Data.Pool ( Pool
10 | , createPool
11 | )
12 |
13 | type DBConnectionString = Text
14 |
15 | initConnectionPool :: DBConnectionString -> IO (Pool Connection)
16 | initConnectionPool connTxt =
17 | createPool (connectdb . encodeUtf8 $ (connTxt))
18 | finish
19 | 2 -- stripes
20 | 60 -- unused connection are kept open for a minute
21 | 10 -- max. 10 connections open per stripe
22 |
--------------------------------------------------------------------------------
/src/Database/Queries.hs:
--------------------------------------------------------------------------------
1 | module Database.Queries where
2 |
3 | import Protolude hiding (FatalError)
4 |
5 | import Data.Pool
6 |
7 | import Database.PostgreSQL.LibPQ
8 | import qualified Data.ByteString.Internal as B
9 | ( ByteString(..) )
10 |
11 | import Control.Exception
12 | import SharedEnv
13 | import Database.SqlRow
14 | import Data.Exception
15 | import Cache
16 | import HandlerM
17 |
18 | -- PG_TYPE_BOOL 16
19 | -- PG_TYPE_BYTEA 17
20 | -- PG_TYPE_CHAR 18
21 | -- PG_TYPE_NAME 19
22 | -- PG_TYPE_INT8 20
23 | -- PG_TYPE_INT2 21
24 | -- PG_TYPE_INT2VECTOR 22
25 | -- PG_TYPE_INT4 23
26 | -- PG_TYPE_REGPROC 24
27 | -- PG_TYPE_TEXT 25
28 | -- PG_TYPE_OID 26
29 | -- PG_TYPE_TID 27
30 | -- PG_TYPE_XID 28
31 | -- PG_TYPE_CID 29
32 | -- PG_TYPE_OIDVECTOR 30
33 | -- PG_TYPE_SET 32
34 | -- PG_TYPE_XML 142
35 | -- PG_TYPE_XMLARRAY 143
36 | -- PG_TYPE_CHAR2 409
37 | -- PG_TYPE_CHAR4 410
38 | -- PG_TYPE_CHAR8 411
39 | -- PG_TYPE_POINT 600
40 | -- PG_TYPE_LSEG 601
41 | -- PG_TYPE_PATH 602
42 | -- PG_TYPE_BOX 603
43 | -- PG_TYPE_POLYGON 604
44 | -- PG_TYPE_FILENAME 605
45 | -- PG_TYPE_CIDR 650
46 | -- PG_TYPE_FLOAT4 700
47 | -- PG_TYPE_FLOAT8 701
48 | -- PG_TYPE_ABSTIME 702
49 | -- PG_TYPE_RELTIME 703
50 | -- PG_TYPE_TINTERVAL 704
51 | -- PG_TYPE_UNKNOWN 705
52 | -- PG_TYPE_MONEY 790
53 | -- PG_TYPE_OIDINT2 810
54 | -- PG_TYPE_MACADDR 829
55 | -- PG_TYPE_INET 869
56 | -- PG_TYPE_OIDINT4 910
57 | -- PG_TYPE_OIDNAME 911
58 | -- PG_TYPE_TEXTARRAY 1009
59 | -- PG_TYPE_BPCHARARRAY 1014
60 | -- PG_TYPE_VARCHARARRAY 1015
61 | -- PG_TYPE_BPCHAR 1042
62 | -- PG_TYPE_VARCHAR 1043
63 | -- PG_TYPE_DATE 1082
64 | -- PG_TYPE_TIME 1083
65 | -- PG_TYPE_TIMESTAMP_NO_TMZONE 1114 /* since 7.2 */
66 | -- PG_TYPE_DATETIME 1184
67 | -- PG_TYPE_TIME_WITH_TMZONE 1266 /* since 7.1 */
68 | -- PG_TYPE_TIMESTAMP 1296 /* deprecated since 7.0 */
69 | -- PG_TYPE_NUMERIC 1700
70 | -- PG_TYPE_RECORD 2249
71 | -- PG_TYPE_VOID 2278
72 |
73 | queryFromText :: (SqlRow a) => Connection -> Text -> [Maybe (Oid, B.ByteString, Format)] -> IO [a]
74 | queryFromText conn sqlQuery params = do
75 | mresult <- execParams conn (encodeUtf8 sqlQuery) params Binary
76 | case mresult of
77 | Just result -> do
78 | rStatus <- resultStatus result
79 | case rStatus of
80 | TuplesOk -> fromSqlResultRows result
81 | FatalError -> do
82 | diagSqlstate <- resultErrorField result DiagSqlstate
83 |
84 | diagSeverity <- resultErrorField result DiagSeverity
85 | putStrLn $ ("DiagSeverity : " <> (show diagSeverity) :: Text)
86 | diagMessagePrimary <- resultErrorField result DiagMessagePrimary
87 | putStrLn $ ("DiagMessagePrimary : " <> (show diagMessagePrimary) :: Text)
88 | diagMessageDetail <- resultErrorField result DiagMessageDetail
89 | putStrLn $ ("DiagMessageDetail : " <> (show diagMessageDetail) :: Text)
90 | diagMessageHint <- resultErrorField result DiagMessageHint
91 | putStrLn $ ("DiagMessageHint : " <> (show diagMessageHint) :: Text)
92 | diagStatementPosition <- resultErrorField result DiagStatementPosition
93 | putStrLn $ ("DiagStatementPosition : " <> (show diagStatementPosition) :: Text)
94 | diagInternalPosition <- resultErrorField result DiagInternalPosition
95 | putStrLn $ ("DiagInternalPosition : " <> (show diagInternalPosition) :: Text)
96 | diagInternalQuery <- resultErrorField result DiagInternalQuery
97 | putStrLn $ ("DiagInternalQuery : " <> (show diagInternalQuery) :: Text)
98 | diagContext <- resultErrorField result DiagContext
99 | putStrLn $ ("DiagContext : " <> (show diagContext) :: Text)
100 | diagSourceFile <- resultErrorField result DiagSourceFile
101 | putStrLn $ ("DiagSourceFile : " <> (show diagSourceFile) :: Text)
102 | diagSourceLine <- resultErrorField result DiagSourceLine
103 | putStrLn $ ("DiagSourceLine : " <> (show diagSourceLine) :: Text)
104 | diagSourceFunction <- resultErrorField result DiagSourceFunction
105 | putStrLn $ ("DiagSourceFunction : " <> (show diagSourceFunction) :: Text)
106 | putStrLn $ ("DiagSqlstate : " <> (show diagSqlstate) :: Text)
107 |
108 | case diagSqlstate of
109 | Just sqlStateBS -> do
110 | let sqlState = show sqlStateBS :: [Char]
111 | case sqlState of
112 | -- https://www.postgresql.org/docs/current/errcodes-appendix.html
113 | "\"23505\"" -> throw ExceptionPGUniqueViolation
114 | "\"P1002\"" -> throw ExceptionPGJWTMalformed
115 | "\"P1003\"" -> throw ExceptionPGJWTInvalid
116 | _ -> throw $ ExceptionText "Dont know this PG exception"
117 | Nothing -> throw $ ExceptionText "Cant retrieve the PostgreSQL error field SqlState"
118 | otherStatus -> do
119 | putStrLn $ ("PG Status : " <> (show otherStatus) :: Text)
120 | throw $ ExceptionText "Wrong PostgreSQL result status, please check"
121 | Nothing -> throw $ ExceptionText "Didnt receive a PostgreSQL result"
122 |
123 | runSqlFile :: (SqlRow a) => FilePath -> [Maybe (Oid, B.ByteString, Format)] -> HandlerM (Either ExceptionPostgreSQL [a])
124 | runSqlFile sqlFilePath params = do
125 | -- fetch the events
126 | sharedEnv <- ask
127 | let dbPool = dbConnectionPool sharedEnv
128 | sqlQuery <- readCacheOrFile sqlFilePath
129 | withResource dbPool $ \conn -> liftIO $ try $ do
130 | queryFromText conn sqlQuery params
131 |
132 | runSqlFile' :: (SqlRow a) => FilePath -> [Maybe (Oid, B.ByteString, Format)] -> HandlerM [a]
133 | runSqlFile' sqlFilePath params = do
134 | -- Unsafe function that can throw exception
135 | eitherResult <- runSqlFile sqlFilePath params :: (SqlRow a) => HandlerM (Either ExceptionPostgreSQL [a])
136 | case eitherResult of
137 | Right result -> return result
138 | Left err -> throw $ toException err
139 |
--------------------------------------------------------------------------------
/src/Database/SqlRow.hs:
--------------------------------------------------------------------------------
1 | module Database.SqlRow where
2 |
3 | import Protolude hiding (Location, get)
4 |
5 | import Database.PostgreSQL.LibPQ
6 | import Foreign.C.Types
7 |
8 | import Database.Types
9 |
10 | import Data
11 |
12 |
13 | class SqlRow a where
14 | fromSqlResultRow :: Result -> CInt -> IO a
15 |
16 | fromSqlResultRows :: Result -> IO [a]
17 | fromSqlResultRows sqlResult = do
18 | nbRows <- ntuples sqlResult
19 | fromSqlResultRows' sqlResult 0 nbRows
20 | where fromSqlResultRows' res nRow (Row nbRows) =
21 | if nbRows == 0
22 | then return []
23 | else
24 | if nRow >= (nbRows -1)
25 | then do
26 | -- last value to return
27 | rowValue <- fromSqlResultRow res nRow
28 | return $ [rowValue]
29 | else do
30 | rowValue <- fromSqlResultRow res nRow
31 | rowValues <- fromSqlResultRows' res (nRow+1) (Row nbRows)
32 | return $ rowValue:rowValues
33 |
34 |
35 | instance SqlRow Text where
36 | fromSqlResultRow :: Result -> CInt -> IO Text
37 | fromSqlResultRow res nRow = do
38 | getPGTypeValue res nRow 0 :: IO Text
39 |
40 | instance SqlRow Integer where
41 | fromSqlResultRow :: Result -> CInt -> IO Integer
42 | fromSqlResultRow res nRow = do
43 | getPGTypeValue res nRow 0 :: IO Integer
44 |
45 | instance SqlRow Bool where
46 | fromSqlResultRow :: Result -> CInt -> IO Bool
47 | fromSqlResultRow res nRow = do
48 | getPGTypeValue res nRow 0 :: IO Bool
49 |
50 | instance SqlRow User where
51 | fromSqlResultRow :: Result -> CInt -> IO User
52 | fromSqlResultRow res nRow = do
53 | User <$> (getPGTypeValue res nRow 0 :: IO Integer)
54 | <*> (getPGTypeValue res nRow 1 :: IO (Maybe Text))
55 | <*> (getPGTypeValue res nRow 2 :: IO Text)
56 |
57 | instance SqlRow FlashCard where
58 | fromSqlResultRow :: Result -> CInt -> IO FlashCard
59 | fromSqlResultRow res nRow = do
60 | FlashCard <$> (getPGTypeValue res nRow 0 :: IO Integer)
61 | <*> (getPGTypeValue res nRow 1 :: IO Text)
62 | <*> (getPGTypeValue res nRow 2 :: IO [Text])
63 | <*> (getPGTypeValue res nRow 3 :: IO Text)
64 | <*> (getPGTypeValue res nRow 4 :: IO Int)
65 | <*> (getPGTypeValue res nRow 5 :: IO Text)
66 |
--------------------------------------------------------------------------------
/src/Database/Types.hs:
--------------------------------------------------------------------------------
1 | module Database.Types where
2 |
3 | import Protolude hiding (empty, Location, get)
4 |
5 | import Database.PostgreSQL.LibPQ
6 | import Foreign.C.Types
7 | import Control.Exception
8 | import System.IO.Error
9 |
10 | import Data.Binary
11 | import Data.Binary.Get
12 | import Data.Time
13 | import Data.UUID
14 | import qualified Data.ByteString.Lazy as BSL
15 | import qualified Data.ByteString as BS
16 |
17 | getPGBool :: Get Bool
18 | getPGBool = do
19 | w <- getWord8
20 | return $ (fromIntegral w :: Integer) /= 0
21 |
22 | getRemainingWord8 :: Get [Word8]
23 | getRemainingWord8 = do
24 | empty <- isEmpty
25 | if empty
26 | then return []
27 | else do current <- getWord8
28 | remainingWords <- getRemainingWord8
29 | return (current:remainingWords)
30 |
31 | data PGRange = PGRange
32 | { pgH :: !Word8
33 | , pgLengthStart :: !Word32
34 | , pgRangeStartData :: ![Word8]
35 | , pgLengthStop :: !Word32
36 | , pgRangeStopData :: ![Word8]
37 | } deriving (Show)
38 |
39 | getPGRange :: Get PGRange
40 | getPGRange = do
41 | pgH' <- getWord8 -- get the first byte
42 | pgLengthStart' <- getWord32be
43 | pgRangeStartData' <- getNbPGArrayData pgLengthStart'
44 | pgLengthStop' <- getWord32be
45 | pgRangeStopData' <- getNbPGArrayData pgLengthStop'
46 |
47 | return $! PGRange pgH' pgLengthStart' pgRangeStartData' pgLengthStop' pgRangeStopData'
48 |
49 | -- Please refer to the documentation of array.h in postgresql repository
50 | -- https://doxygen.postgresql.org/array_8h_source.html
51 | -- * - standard varlena header word
52 | -- * - number of dimensions of the array
53 | -- * - offset to stored data, or 0 if no nulls bitmap
54 | -- * - element type OID
55 | -- * - length of each array axis (C array of int)
56 | -- * - lower boundary of each dimension (C array of int)
57 | -- * - bitmap showing locations of nulls (OPTIONAL)
58 | -- * - whatever is the stored data
59 | data PGArray = PGArray
60 | { pgArrayVl_len_ :: !Word32
61 | , pgArrayNbDim :: !(Maybe Word16)
62 | , pgArrayDataOffset :: !(Maybe Word32)
63 | , pgArrayElemType :: !(Maybe Word16)
64 | , pgArrayDimensions :: !(Maybe Word32)
65 | , pgArrayLowerBound :: !(Maybe Word32)
66 | , pgArrayData :: ![PGArrayData]
67 | } deriving (Show)
68 |
69 | data PGArrayData = PGArrayData
70 | { pgArrayDataLength :: !Word32
71 | , pgArrayDataData :: ![Word8]
72 | } deriving (Show)
73 |
74 | getNb16PGArrayData :: Word16 -> Get [Word8]
75 | getNb16PGArrayData 0 = return []
76 | getNb16PGArrayData nbWords = do
77 | currentWord <- getWord8
78 | remainingWords <- getNb16PGArrayData (nbWords - 1)
79 | return (currentWord:remainingWords)
80 |
81 | getNbPGArrayData :: Word32 -> Get [Word8]
82 | getNbPGArrayData 0 = return []
83 | getNbPGArrayData nbWords = do
84 | currentWord <- getWord8
85 | remainingWords <- getNbPGArrayData (nbWords - 1)
86 | return (currentWord:remainingWords)
87 |
88 | getPGArrayData :: Get PGArrayData
89 | getPGArrayData = do
90 | textLength <- getWord32be
91 | pgWord8' <- getNbPGArrayData textLength
92 | return $ PGArrayData textLength pgWord8'
93 |
94 | getPGArrayDataList :: Get [PGArrayData]
95 | getPGArrayDataList = do
96 | empty <- isEmpty
97 | if empty
98 | then return []
99 | else do pgArrayData' <- getPGArrayData
100 | pgArrayDatas' <- getPGArrayDataList
101 | return (pgArrayData':pgArrayDatas')
102 |
103 | getPGArray :: Get PGArray
104 | getPGArray = do
105 | vl_len_' <- getWord32be
106 | if vl_len_' == 0
107 | then do
108 | return $! PGArray vl_len_' Nothing Nothing Nothing Nothing Nothing []
109 | else do
110 | nbDim' <- getWord16be
111 | dataOffset' <- getWord32be
112 | elemType' <- getWord16be
113 | dimensions' <- getWord32be
114 | lowerBound' <- getWord32be
115 |
116 | -- A text is represented by the length of the next words
117 | -- then appended the text
118 | pgArrayData' <- getPGArrayDataList
119 |
120 | return $! PGArray vl_len_' (Just nbDim') (Just dataOffset') (Just elemType') (Just dimensions') (Just lowerBound') pgArrayData'
121 |
122 | class PGType a where
123 | decodePGType :: BSL.ByteString -> IO a
124 |
125 | getPGTypeValue :: Result -> CInt -> CInt -> IO a
126 | getPGTypeValue res nRow nCol = do
127 | mValueBS <- getvalue res (Row nRow) (Col nCol)
128 | case mValueBS of
129 | Just valueBS -> decodePGType . BSL.fromStrict $ valueBS
130 | Nothing -> throw $ userError "Impossible to get the value bytestring"
131 |
132 |
133 | instance PGType [Text] where
134 | decodePGType bs = do
135 | return $ (decodeUtf8 . BS.pack) <$> words8
136 | where words8 = fmap pgArrayDataData $ pgArrayData pgArray
137 | pgArray = runGet getPGArray bs
138 |
139 |
140 | instance PGType (Maybe Text) where
141 | decodePGType bs = return $ Just (decodeUtf8 . BSL.toStrict $ bs)
142 | getPGTypeValue res nRow nCol = do
143 | mValueBS <- getvalue res (Row nRow) (Col nCol)
144 | return $ decodeUtf8 <$> mValueBS
145 |
146 | instance PGType Text where
147 | decodePGType bs = return $ decodeUtf8 . BSL.toStrict $ bs
148 |
149 | instance PGType UUID where
150 | decodePGType bs = do
151 | let mValue = fromByteString bs
152 | case mValue of
153 | Just value' -> return value'
154 | Nothing -> throw $ userError "Impossible to decode expected UUID"
155 |
156 | instance PGType ByteString where
157 | decodePGType bs = return $ BSL.toStrict bs
158 |
159 | instance PGType BSL.ByteString where
160 | decodePGType bs = return $ bs
161 |
162 | instance PGType Bool where
163 | decodePGType bs = return $ runGet getPGBool bs
164 |
165 | instance PGType Double where
166 | decodePGType bs = return $ runGet getDoublebe bs
167 |
168 | instance PGType Int where
169 | decodePGType bs = return $ fromIntegral $ runGet getInt32be bs
170 |
171 | instance PGType Integer where
172 | decodePGType bs = return $ fromIntegral $ runGet getInt64be bs
173 |
174 | getTupleWord64 :: Get (Word64, Word64)
175 | getTupleWord64 = do
176 | firstWord64 <- getWord64be
177 | secondWord64 <- getWord64be
178 | _ <- getWord8 -- 1 byte unconsume for flag
179 | return (firstWord64,secondWord64)
180 |
181 | getDate :: Get (Integer, Integer)
182 | getDate = do
183 | totalSeconds <- get :: Get Int64
184 | let relDays = (totalSeconds `div` 86400000000) + 51544
185 | let relSeconds = (totalSeconds `mod` 86400000000) `div` 1000000
186 | return (fromIntegral relDays, fromIntegral relSeconds)
187 |
188 |
189 | instance PGType UTCTime where
190 | decodePGType bs = return $ UTCTime (ModifiedJulianDay relDays) (secondsToDiffTime relSeconds)
191 | where (relDays, relSeconds) = runGet getDate bs
192 |
193 | instance PGType Float where
194 | decodePGType bs = return $ runGet getFloatbe bs
195 |
--------------------------------------------------------------------------------
/src/Form.hs:
--------------------------------------------------------------------------------
1 | module Form where
2 |
3 | import Protolude
4 |
5 | import Servant.Multipart
6 |
7 |
8 | -- | Lookup a textual input with the given @name@ attribute.
9 | lookupInputs :: Text -> MultipartData tag -> Maybe [Text]
10 | lookupInputs iname = Just . fmap iValue . filter ((==iname) . iName) . inputs
11 |
12 |
13 | data UserLoginForm = UserLoginForm {
14 | userLoginFormEmail :: Text
15 | , userLoginFormPassword :: Text
16 | } deriving (Show)
17 | instance FromMultipart Mem UserLoginForm where
18 | fromMultipart multipartData =
19 | UserLoginForm <$> lookupInput "email" multipartData
20 | <*> lookupInput "password" multipartData
21 |
22 | data RegisterForm = RegisterForm {
23 | registerFormEmail :: Text
24 | , registerFormPassword :: Text
25 | } deriving (Show)
26 | instance FromMultipart Mem RegisterForm where
27 | fromMultipart multipartData =
28 | RegisterForm <$> lookupInput "email" multipartData
29 | <*> lookupInput "password" multipartData
30 |
31 | data UserUpdateForm = UserUpdateForm {
32 | userUpdateFormEmail :: Text
33 | , userUpdateFormUsername :: Text
34 | , userUpdateFormPassword :: Text
35 | , userUpdateFormNewPassword :: Text
36 | } deriving (Show)
37 | instance FromMultipart Mem UserUpdateForm where
38 | fromMultipart multipartData =
39 | UserUpdateForm <$> lookupInput "email" multipartData
40 | <*> lookupInput "username" multipartData
41 | <*> lookupInput "password" multipartData
42 | <*> lookupInput "new-password" multipartData
43 |
44 | data FlashCardForm = FlashCardForm {
45 | flashCardFormId :: Text
46 | , flashCardFormRecto :: Text
47 | , flashCardFormTags :: [Text]
48 | , flashCardFormVerso :: Text
49 | } deriving (Show)
50 | instance FromMultipart Mem FlashCardForm where
51 | fromMultipart multipartData =
52 | FlashCardForm <$> lookupInput "id" multipartData
53 | <*> lookupInput "recto" multipartData
54 | <*> lookupInputs "tag" multipartData
55 | <*> lookupInput "verso" multipartData
56 |
57 | data SearchForm = SearchForm {
58 | searchFormText :: Text
59 | } deriving (Show)
60 | instance FromMultipart Mem SearchForm where
61 | fromMultipart multipartData =
62 | SearchForm <$> lookupInput "search-text" multipartData
63 |
64 | data QuizzForm = QuizzForm {
65 | quizzFormId :: Text
66 | , quizzFormRecto :: Maybe Text
67 | , quizzFormVerso :: Maybe Text
68 | } deriving (Show)
69 | instance FromMultipart Mem QuizzForm where
70 | fromMultipart multipartData =
71 | QuizzForm <$> lookupInput "id" multipartData
72 | <*> Just (lookupInput "recto" multipartData)
73 | <*> Just (lookupInput "verso" multipartData)
74 |
--------------------------------------------------------------------------------
/src/HandlerM.hs:
--------------------------------------------------------------------------------
1 | module HandlerM where
2 |
3 | import Control.Monad.Trans.Reader (runReaderT, ReaderT)
4 | import Control.Monad.Logger
5 | import SharedEnv
6 | import Servant
7 |
8 |
9 | -- hoistServer :: HasServer api '[] => Proxy api -> (forall x. m x -> n x) -> ServerT api m -> ServerT api n
10 | -- ^^^^^^^^^^^^^^^^^^^^^^
11 | nt :: SharedEnv -> HandlerM a -> Handler a
12 | -- runReaderT :: conf -> m a
13 | nt c x = runStderrLoggingT (runReaderT x c)
14 |
15 | -- newtype ReaderT conf m a
16 | type HandlerM = ReaderT SharedEnv (LoggingT Handler)
17 |
--------------------------------------------------------------------------------
/src/Mustache.hs:
--------------------------------------------------------------------------------
1 | module Mustache where
2 |
3 | import Protolude
4 |
5 | import Text.Mustache
6 | import Text.Mustache.Types
7 | import qualified Data.Vector as V
8 | import Data
9 |
10 |
11 |
12 | newtype MessageMustache = MessageMustache Text
13 | instance ToMustache MessageMustache where
14 | toMustache (MessageMustache messageText) = object
15 | [ "message" ~> (String $ messageText)
16 | ]
17 |
18 | newtype FlashCardTagMustache = FlashCardTagMustache Text
19 | instance ToMustache FlashCardTagMustache where
20 | toMustache (FlashCardTagMustache tag) = object
21 | [ "tag" ~> (String tag) ]
22 |
23 | newtype FlashCardMustache = FlashCardMustache FlashCard
24 | instance ToMustache FlashCardMustache where
25 | toMustache (FlashCardMustache flashCard@FlashCard{}) = object
26 | [ "id" ~> (Number . fromInteger $ flashCardId flashCard)
27 | , "recto" ~> (String $ flashCardRecto flashCard)
28 | , "recto-value" ~> (String $ (if (flashCardSide flashCard) == "recto" then (flashCardRecto flashCard) else ""))
29 | , "recto-disabled" ~> (String $ (if (flashCardSide flashCard) == "recto" then "disabled" else ""))
30 | , "verso" ~> (String $ flashCardVerso flashCard)
31 | , "verso-value" ~> (String $ (if (flashCardSide flashCard) == "verso" then (flashCardVerso flashCard) else ""))
32 | , "verso-disabled" ~> (String $ (if (flashCardSide flashCard) == "verso" then "disabled" else ""))
33 | , "tags" ~> (V.fromList (toMustache . FlashCardTagMustache <$> flashCardTags flashCard))
34 | ]
35 |
36 | newtype UserMustache = UserMustache User
37 | instance ToMustache UserMustache where
38 | toMustache (UserMustache user@User{}) = object
39 | [ "username" ~> (String $ fromMaybe "" (userUsername user))
40 | , "email" ~> (String $ userEmail user)
41 | ]
42 |
--------------------------------------------------------------------------------
/src/Queries.sql:
--------------------------------------------------------------------------------
1 | -- name:updateFullUser :: User
2 | -- :user :: User
3 | -- :fullUser :: FullUser
4 | UPDATE
5 | users
6 | SET
7 | email = :fullUser.fullUserEmail
8 | , languages = :fullUser.fullUserLanguages
9 | WHERE
10 | users.id = :user.userId
11 | RETURNING
12 | id
13 | , username
14 | , email
15 | , languages
16 | ;;;
17 | -- name:insertUser :: (String, String)
18 | -- :user :: NewUser
19 | INSERT INTO
20 | users
21 | (username, passpass, email, languages)
22 | VALUES
23 | (:user.newuserUsername, :user.newuserPassword, :user.newuserEmail, :user.newuserLanguages)
24 | RETURNING
25 | id
26 | , username
27 | ;;;
28 | -- name:getNewToken :: (String)
29 | INSERT INTO
30 | token
31 | (token)
32 | VALUES
33 | (uuid_generate_v4())
34 | RETURNING
35 | token
36 | ;;;
37 | -- name:verifyToken :: (Bool)
38 | -- :uuid :: String
39 | SELECT
40 | created_at > (now() - INTERVAL '5 minutes')
41 | FROM
42 | token
43 | WHERE
44 | token = :uuid
45 | ;;;
46 | -- name:getSessionJWT :: (String)
47 | -- :username :: String
48 | -- :pass :: String
49 | SELECT auth_login(:username, :pass)
50 | ;;;
51 | -- name:verifyJWT :: User
52 | -- :jwt :: String
53 | WITH user_id AS (
54 | SELECT auth_jwt_decode(:jwt) AS user_id
55 | )
56 | SELECT
57 | id
58 | , username
59 | , email
60 | , languages
61 | FROM
62 | users
63 | JOIN user_id
64 | ON users.id = user_id.user_id
65 | ;;;
66 | -- name:updatePassword :: User
67 | -- :user :: User
68 | -- :password :: String
69 | UPDATE
70 | users
71 | SET
72 | passpass = :password
73 | WHERE
74 | users.id = :user.userId
75 | RETURNING
76 | id
77 | , username
78 | , email
79 | , languages
80 | ;;;
81 | -- name:getUser :: (Int, String)
82 | -- :userName :: String
83 | -- :userPassword :: String
84 | SELECT
85 | id, username
86 | FROM
87 | users
88 | WHERE
89 | username = :userName
90 | AND passpass = crypt(:userPassword, passpass)
91 | ;;;
92 | -- name:getUserById :: (Int, String)
93 | -- :userName :: String
94 | SELECT
95 | id, username
96 | FROM
97 | users
98 | WHERE
99 | username = :userName
100 | ;;;
101 | -- name:getAllWords :: [Word]
102 | -- :user :: User
103 | SELECT
104 | id, language, word, definition, keywords, difficulty
105 | FROM
106 | words
107 | WHERE
108 | userid = :user.userId
109 | ;;;
110 | -- name:getQuizzWordsLang :: [Word]
111 | -- :user :: User
112 | -- :lang :: String
113 | WITH possible_words AS (
114 | SELECT
115 | id
116 | FROM
117 | words
118 | WHERE
119 | userid = :user.userId AND
120 | language = :lang
121 | ORDER BY
122 | get_word_score(last_query_at, difficulty) DESC
123 | LIMIT
124 | 100
125 | )
126 | SELECT
127 | w.id, w.language, w.word, w.definition, w.keywords, w.difficulty
128 | FROM
129 | possible_words p join words w ON p.id = w.id
130 | ORDER BY
131 | random()
132 | LIMIT
133 | 5
134 | ;;;
135 | -- name:getQuizzResponse :: (Bool)
136 | -- :wordId :: WordId
137 | -- :testResponse :: String
138 | SELECT * FROM verify_word_full(:wordId, :testResponse);
139 | ;;;
140 | -- name:getLastWords :: [Word]
141 | -- :user :: User
142 | SELECT
143 | id, language, word, definition, keywords, difficulty
144 | FROM
145 | words
146 | WHERE
147 | userid = :user.userId
148 | ORDER BY
149 | inserted_at DESC
150 | LIMIT 20
151 | ;;;
152 | -- name:getWordsByKeyword :: [Word]
153 | -- :user :: User
154 | -- :keyword :: String
155 | SELECT
156 | id, language, word, definition, keywords, difficulty
157 | FROM
158 | words
159 | WHERE
160 | userid = :user.userId AND
161 | :keyword = ANY ( keywords )
162 | LIMIT 20
163 | ;;;
164 | -- name:getAllKeywords :: [(String)]
165 | -- :user :: User
166 | SELECT
167 | DISTINCT ( UNNEST ( keywords ) )
168 | FROM
169 | words
170 | WHERE
171 | userid = :user.userId
172 | ;;;
173 | -- name:getSearchWords :: [Word]
174 | -- :user :: User
175 | -- :searchWord :: String
176 | SELECT
177 | id, language, word, definition, keywords, difficulty
178 | FROM
179 | words
180 | WHERE
181 | userid = :user.userId AND
182 | word LIKE '%' || :searchWord || '%'
183 | LIMIT
184 | 20
185 | ;;;
186 | -- name:getSearchWordsUser :: [Word]
187 | -- :user :: User
188 | SELECT
189 | id, language, word, definition, keywords, difficulty
190 | FROM
191 | words
192 | WHERE
193 | userid = :user.userId
194 | ORDER BY
195 | id DESC
196 | LIMIT
197 | 20
198 | ;;;
199 | -- name:getSearchKeyword :: [Word]
200 | -- :user :: User
201 | -- :keyword :: String
202 | SELECT
203 | id, language, word, definition, keywords, difficulty
204 | FROM
205 | words
206 | WHERE
207 | userid = :user.userId AND
208 | :keyword = ANY (keywords)
209 | LIMIT
210 | 20
211 | ;;;
212 | -- name:getSearchWordsKeyword :: [Word]
213 | -- :user :: User
214 | -- :searchWord :: String
215 | -- :keyword :: String
216 | SELECT
217 | id, language, word, definition, keywords, difficulty
218 | FROM
219 | words
220 | WHERE
221 | userid = :user.userId AND
222 | word LIKE '%' || :searchWord || '%' AND
223 | :keyword = ANY (keywords)
224 | LIMIT
225 | 20
226 | ;;;
227 | -- name:getWordById :: Word
228 | -- :user :: User
229 | -- :wordId :: WordId
230 | SELECT
231 | id, language, word, definition, keywords, difficulty
232 | FROM
233 | words
234 | WHERE
235 | id = :wordId
236 | AND userid = :user.userId
237 | ;;;
238 | -- name:updateWordById :: Word
239 | -- :user :: User
240 | -- :wordId :: WordId
241 | -- :newWord :: Word
242 | UPDATE
243 | words
244 | SET
245 | "language" = :newWord.language
246 | , "word" = :newWord.word
247 | , "definition" = :newWord.definition
248 | , "keywords" = :newWord.keywords
249 | , "difficulty" = :newWord.difficulty
250 | WHERE
251 | "id" = :wordId
252 | AND "userid" = :user.userId
253 | RETURNING
254 | "id"
255 | , "language"
256 | , "word"
257 | , "definition"
258 | , "keywords"
259 | , "difficulty"
260 | ;;;
261 | -- name:insertWord :: (Integer)
262 | -- :user :: User
263 | -- :newWord :: Word
264 | INSERT INTO
265 | words
266 | (userid, language, word, keywords, definition)
267 | VALUES
268 | (:user.userId, :newWord.language, :newWord.word, :newWord.keywords, :newWord.definition)
269 | ;;;
270 | -- name:deleteWordById :: (Integer)
271 | -- :user :: User
272 | -- :wordId :: Int
273 | DELETE
274 | FROM
275 | words
276 | WHERE
277 | id = :wordId
278 | AND userid = :user.userId
279 | ;;;;
280 | -- find similar words:
281 | -- SELECT
282 | -- w1.word, w2.word, levenshtein(w1.word, w2.word)
283 | -- FROM
284 | -- words w1,
285 | -- words w2
286 | -- WHERE
287 | -- w1.userid = 1
288 | -- AND w2.userid = 1
289 | -- AND w1.id != w2.id
290 | -- AND w1.word = 'to grate'
291 | -- AND levenshtein(w1.word, w2.word) < 3
292 | -- ;
293 |
--------------------------------------------------------------------------------
/src/Server.hs:
--------------------------------------------------------------------------------
1 | module Server where
2 |
3 | import Protolude
4 | import Control.Monad.Logger
5 | import Network.HTTP.Media ((//))
6 | import Network.Wai
7 | import Network.Wai.Handler.Warp ( run )
8 | import Network.Wai.Middleware.RequestLogger
9 | ( logStdoutDev )
10 |
11 | import Servant
12 | import qualified Data.Vector as V
13 | import qualified Data.ByteString.Base64 as BS64
14 | import Servant.Multipart
15 | import Text.Read hiding (String)
16 | import Data.Text (unpack)
17 | import Data.Byteable
18 | import Mustache
19 | import Crypto.Hash
20 | import Data.List ((!!))
21 | import HandlerM
22 | import SharedEnv
23 | import Text.Mustache.Types
24 | import qualified Data.Aeson as A
25 | import qualified Data.HashMap.Strict as HM
26 | import qualified Data.ByteString.Lazy as BSL
27 | import Servant.HTML.Blaze
28 | import Text.Blaze.Html5 as H
29 | ( Html )
30 | import Data.Settings
31 | import Database.PostgreSQL.LibPQ hiding (status)
32 | import Database.Queries
33 | import API.Facebook
34 | import Template
35 | import Utils
36 | import Cache
37 | import Data
38 | import Css
39 | import Form
40 | import Auth
41 |
42 | data OctetStreamFavico
43 |
44 | instance Accept OctetStreamFavico where
45 | contentType _ = "image" // "x-icon"
46 |
47 | instance MimeRender OctetStreamFavico ByteString where
48 | mimeRender _ = BSL.fromStrict
49 |
50 | type API = "static" :> Raw
51 | :<|> AuthProtect "custom-auth" :> FrontAPI
52 |
53 | server :: ServerT API HandlerM
54 | server =
55 | staticServer
56 | :<|> frontServer
57 | where
58 | staticServer = serveDirectoryFileServer "www"
59 |
60 |
61 | type FrontAPI = Get '[HTML] H.Html
62 | :<|> "register" :> Get '[HTML] H.Html
63 | :<|> "register" :> MultipartForm Mem RegisterForm :> Post '[HTML] H.Html
64 | :<|> "logout" :> Post '[HTML] H.Html
65 | :<|> "login" :> Get '[HTML] H.Html
66 | :<|> "login" :> MultipartForm Mem UserLoginForm :> Post '[HTML] H.Html
67 | :<|> "login" :> "facebook" :> QueryParam "code" Text :> QueryParam "state" Text :> QueryParam "error" Text :> QueryParam "error_code" Integer :> QueryParam "error_description" Text :> Get '[HTML] H.Html
68 | :<|> "account" :> Get '[HTML] H.Html
69 | :<|> "account" :> MultipartForm Mem UserUpdateForm :> Post '[HTML] H.Html
70 | :<|> "dashboard" :> Get '[HTML] H.Html
71 | :<|> "dashboard" :> MultipartForm Mem SearchForm :> Post '[HTML] H.Html
72 | :<|> "flashcard" :> "add" :> Get '[HTML] H.Html
73 | :<|> "flashcard" :> "add" :> MultipartForm Mem FlashCardForm :> Post '[HTML] H.Html
74 | :<|> "flashcard" :> "edit" :> Capture "flashCardId" Integer :> Get '[HTML] H.Html
75 | :<|> "flashcard" :> "edit" :> Capture "flashCardId" Integer :> MultipartForm Mem FlashCardForm :> Post '[HTML] H.Html
76 | :<|> "flashcard" :> "delete" :> Capture "flashCardId" Integer :> Get '[HTML] H.Html
77 | :<|> "quizz" :> Get '[HTML] H.Html
78 | :<|> "quizz" :> "finish" :> Get '[HTML] H.Html
79 | :<|> "quizz" :> "answer" :> Capture "flashCardId" Integer :> Get '[HTML] H.Html
80 | :<|> "quizz" :> MultipartForm Mem QuizzForm :> Post '[HTML] H.Html
81 | :<|> "favicon.ico" :> Get '[OctetStreamFavico] ByteString
82 | :<|> "sitemap.xml" :> Get '[PlainText] Text
83 | :<|> "robots.txt" :> Get '[PlainText] Text
84 |
85 |
86 |
87 | frontServer :: Session -> ServerT FrontAPI HandlerM
88 | frontServer session =
89 | getHomePage
90 | :<|> getRegisterPage []
91 | :<|> postRegisterPage
92 | :<|> postLogoutPage
93 | :<|> getLoginPage []
94 | :<|> postLoginPage
95 | :<|> loginFacebookCallback
96 | :<|> getAccountPage []
97 | :<|> postAccountPage
98 | :<|> getDashboardPage [] Nothing
99 | :<|> postDashboardSearchPage
100 | :<|> getFlashCardAddPage []
101 | :<|> postFlashCardAddPage
102 | :<|> getFlashCardEditPage []
103 | :<|> postFlashCardEditPage
104 | :<|> getFlashCardDeletePage
105 | :<|> getQuizzPage []
106 | :<|> getQuizzFinishPage
107 | :<|> getQuizzAnswerPage []
108 | :<|> postQuizzPage
109 | :<|> faviconIco
110 | :<|> sitemapXml
111 | :<|> robotsTxt
112 |
113 | where
114 | getHomePage :: HandlerM H.Html
115 | getHomePage = do
116 | css <- izidictCSSText
117 | facebookUrlLogin' <- facebookUrlLogin
118 | baseUrl' <- baseUrl
119 | let frontEndVariables = A.Object $ HM.fromList [
120 | ("session", A.toJSON session)
121 | ]
122 |
123 | let frontContext = Object $ HM.fromList [
124 | ("css", String $ css)
125 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
126 | , ("facebookLoginUrl", String facebookUrlLogin')
127 | , ("base_url", String baseUrl')
128 | ]
129 | $(logInfo) ("homepage" ::Text)
130 | template <- compileTemplate "home.html"
131 | preEscapedToMarkupSubstituteTemplate template frontContext
132 |
133 | getRegisterPage :: [Message] -> HandlerM H.Html
134 | getRegisterPage messages = do
135 | css <- izidictCSSText
136 | facebookUrlLogin' <- facebookUrlLogin
137 | baseUrl' <- baseUrl
138 | let frontEndVariables = A.Object $ HM.fromList [
139 | ("session", A.toJSON session)
140 | ]
141 |
142 | let frontContext = Object $ HM.fromList [
143 | ("css", String $ css)
144 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
145 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
146 | , ("facebookLoginUrl", String facebookUrlLogin')
147 | , ("base_url", String baseUrl')
148 | ]
149 | $(logInfo) ("homepage" ::Text)
150 | template <- compileTemplate "register.html"
151 | preEscapedToMarkupSubstituteTemplate template frontContext
152 |
153 | postRegisterPage :: RegisterForm -> HandlerM H.Html
154 | postRegisterPage form' = do
155 | eitherUserInserted <- runSqlFile
156 | "sql/registerUser.sql"
157 | [ Just (Oid 25, encodeUtf8 . registerFormEmail $ form', Binary)
158 | , Just (Oid 25, encodeUtf8 . registerFormPassword $ form', Binary)
159 | ] :: HandlerM (Either ExceptionPostgreSQL [User])
160 |
161 | case eitherUserInserted of
162 | Left pgErr -> do
163 | case pgErr of
164 | ExceptionPGUniqueViolation -> do
165 | $(logInfo) ("User already in database")
166 | getRegisterPage [(Message "User already registered")]
167 | _ -> do
168 | $(logInfo) ("Unknown PG Exception: " <> (show pgErr) :: Text)
169 | throwError $ err302 {
170 | errHeaders = [
171 | ("Location", "/login")
172 | ]
173 | }
174 | Right userInserted' -> do
175 | -- Send a welcoming email if new user
176 | $(logInfo) ("log: " <> (show userInserted') :: Text)
177 | $(logInfo) ("Need to send a welcoming email to the user")
178 | $(logInfo) ("Inserting the 10 default general culture flashcard for this user")
179 | _ <- runSqlFile'
180 | "sql/insertDefaultFlashCards.sql"
181 | [ Just (Oid 25, show . userId $ (userInserted'!!0), Text)
182 | ] :: HandlerM [Integer]
183 | throwError $ err302 {
184 | errHeaders = [
185 | ("Location", "/login")
186 | ]
187 | }
188 |
189 | postLogoutPage :: HandlerM H.Html
190 | postLogoutPage = do
191 | throwError $ err302 {
192 | errHeaders = [
193 | ("Location", "/")
194 | , ("Set-Cookie", "jwt=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT")
195 | ]
196 | }
197 |
198 | getLoginPage :: [Message] -> HandlerM H.Html
199 | getLoginPage messages = do
200 | css <- izidictCSSText
201 | baseUrl' <- baseUrl
202 | let frontEndVariables = A.Object $ HM.fromList [
203 | ("session", A.toJSON session)
204 | ]
205 |
206 | let frontContext = Object $ HM.fromList [
207 | ("css", String $ css)
208 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
209 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
210 | , ("base_url", String baseUrl')
211 | ]
212 | template <- compileTemplate "login.html"
213 | preEscapedToMarkupSubstituteTemplate template frontContext
214 |
215 | postLoginPage :: UserLoginForm -> HandlerM H.Html
216 | postLoginPage form = do
217 | jwtSecrets <- runSqlFile'
218 | "sql/getNewRandomSession.sql"
219 | [ Just (Oid 25, encodeUtf8 . userLoginFormEmail $ form, Text)
220 | , Just (Oid 25, encodeUtf8 . userLoginFormPassword $ form, Text)
221 | ] :: HandlerM [Text]
222 |
223 | if length jwtSecrets > 0
224 | then do
225 | let jwtHeader = BS64.encode $ "{\"alg\": \"HS256\", \"typ\": \"JWT\"}"
226 | let jwtPayload = BS64.encode $ "{\"email\": \"" <> (encodeUtf8 . userLoginFormEmail $ form) <> "\"}"
227 | let jwtSignature = BS64.encode $ toBytes $ hmacAlg SHA256 (encodeUtf8 $ jwtSecrets!!0) (jwtHeader <> "." <> jwtPayload)
228 |
229 | let jwt = jwtHeader <> "." <> jwtPayload <> "." <> jwtSignature
230 |
231 | throwError $ err302 {
232 | errHeaders = [
233 | ("Location", "/dashboard")
234 | , ("Set-Cookie", "jwt=" <> jwt <> "; path=/; expires=Thu, 01 Jan 2200 00:00:00 GMT")
235 | ]
236 | }
237 | else getLoginPage [Message "Wrong credentials"]
238 |
239 | getAccountPage :: [Message] -> HandlerM H.Html
240 | getAccountPage messages = do
241 | loggedInOr302 session
242 | css <- izidictCSSText
243 | baseUrl' <- baseUrl
244 | let frontEndVariables = A.Object $ HM.fromList [
245 | ("session", A.toJSON session)
246 | ]
247 |
248 | let frontContext = Object $ HM.fromList [
249 | ("css", String $ css)
250 | , ("account", (toMustache . UserMustache . sessionUser $ session))
251 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
252 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
253 | , ("base_url", String baseUrl')
254 | ]
255 | template <- compileTemplate "account.html"
256 | preEscapedToMarkupSubstituteTemplate template frontContext
257 |
258 | postAccountPage :: UserUpdateForm -> HandlerM H.Html
259 | postAccountPage form' = do
260 | loggedInOr302 session
261 | $(logInfo) ("log: " <> (show form') :: Text)
262 | -- check password
263 | passwordChecked <- runSqlFile'
264 | "sql/checkPassword.sql"
265 | [ Just (Oid 25, show . userEmail . sessionUser $ session, Text) -- User Email
266 | , Just (Oid 25, encodeUtf8 . userUpdateFormPassword $ form', Text) -- Password
267 | ] :: HandlerM [Integer]
268 | _ <- if length passwordChecked > 1
269 | then return mempty
270 | else getAccountPage [(Message "Wrong credentials")]
271 |
272 | -- update password
273 | _ <- runSqlFile'
274 | "sql/updatePassword.sql"
275 | [ Just (Oid 25, encodeUtf8 . userEmail . sessionUser $ session, Text) -- User Email
276 | , Just (Oid 25, encodeUtf8 . userUpdateFormNewPassword $ form', Text) -- Password
277 | ] :: HandlerM [Integer]
278 |
279 | throwError $ err302 {
280 | errHeaders = [
281 | ("Location", "/account")
282 | ]
283 | }
284 |
285 | getDashboardPage :: [Message] -> Maybe Text -> HandlerM H.Html
286 | getDashboardPage messages mSearchText = do
287 | loggedInOr302 session
288 | flashcards <- case mSearchText of
289 | Just searchText -> runSqlFile'
290 | "sql/searchFlashCards.sql"
291 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User ID
292 | , Just (Oid 25, encodeUtf8 searchText, Text)
293 | ] :: HandlerM [FlashCard]
294 | Nothing -> runSqlFile'
295 | "sql/getFlashCards.sql"
296 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User ID
297 | ] :: HandlerM [FlashCard]
298 | css <- izidictCSSText
299 | baseUrl' <- baseUrl
300 | let frontEndVariables = A.Object $ HM.fromList [
301 | ("session", A.toJSON session)
302 | ]
303 |
304 | let frontContext = Object $ HM.fromList [
305 | ("flashcards", Array . V.fromList $ map (toMustache . FlashCardMustache) flashcards)
306 | , ("css", String $ css)
307 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
308 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
309 | , ("base_url", String baseUrl')
310 | ]
311 | template <- compileTemplate "dashboard.html"
312 | preEscapedToMarkupSubstituteTemplate template frontContext
313 |
314 | postDashboardSearchPage :: SearchForm -> HandlerM H.Html
315 | postDashboardSearchPage form' = do
316 | getDashboardPage [] (Just $ searchFormText form')
317 |
318 | getFlashCardAddPage :: [Message] -> HandlerM H.Html
319 | getFlashCardAddPage messages = do
320 | loggedInOr302 session
321 | css <- izidictCSSText
322 | baseUrl' <- baseUrl
323 | let frontEndVariables = A.Object $ HM.fromList [
324 | ("session", A.toJSON session)
325 | ]
326 |
327 | let frontContext = Object $ HM.fromList [
328 | ("css", String $ css)
329 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
330 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
331 | , ("base_url", String baseUrl')
332 | ]
333 | template <- compileTemplate "flashcard.add.html"
334 | preEscapedToMarkupSubstituteTemplate template frontContext
335 |
336 | postFlashCardAddPage :: FlashCardForm -> HandlerM H.Html
337 | postFlashCardAddPage form' = do
338 | loggedInOr302 session
339 | _ <- runSqlFile'
340 | "sql/addFlashCard.sql"
341 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User Id
342 | , Just (Oid 25, encodeUtf8 . flashCardFormRecto $ form', Text)
343 | , Just (Oid 25, encodeUtf8 $ "{" <> (mconcat . intersperse "," $ flashCardFormTags form') <> "}", Text)
344 | , Just (Oid 25, encodeUtf8 . flashCardFormVerso $ form', Text)
345 | ] :: HandlerM [Integer]
346 | throwError $ err302 {
347 | errHeaders = [
348 | ("Location", "/dashboard")
349 | ]
350 | }
351 |
352 | getFlashCardEditPage :: [Message] -> Integer -> HandlerM H.Html
353 | getFlashCardEditPage messages flashCardId' = do
354 | loggedInOr302 session
355 | css <- izidictCSSText
356 | baseUrl' <- baseUrl
357 | flashcards <- runSqlFile'
358 | "sql/getFlashCard.sql"
359 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User ID
360 | , Just (Oid 25, show flashCardId', Text) -- FlashCard ID
361 | ] :: HandlerM [FlashCard]
362 | $(logInfo) ("log: " <> (show flashcards) :: Text)
363 | if length flashcards < 1
364 | then
365 | throwError $ err302 {
366 | errHeaders = [
367 | ("Location", "/")
368 | ]
369 | }
370 | else return ()
371 | let frontEndVariables = A.Object $ HM.fromList [
372 | ("session", A.toJSON session)
373 | ]
374 |
375 | let frontContext = Object $ HM.fromList [
376 | ("css", String $ css)
377 | , ("flashcard", (toMustache . FlashCardMustache) (flashcards!!0))
378 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
379 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
380 | , ("base_url", String baseUrl')
381 | ]
382 | template <- compileTemplate "flashcard.edit.html"
383 | preEscapedToMarkupSubstituteTemplate template frontContext
384 |
385 | postFlashCardEditPage :: Integer -> FlashCardForm -> HandlerM H.Html
386 | postFlashCardEditPage flashCardId' form' = do
387 | _ <- runSqlFile'
388 | "sql/updateFlashCard.sql"
389 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User Id
390 | , Just (Oid 25, show flashCardId', Text) -- Word Id
391 | , Just (Oid 25, encodeUtf8 . flashCardFormRecto $ form', Text)
392 | , Just (Oid 25, encodeUtf8 $ "{" <> (mconcat . intersperse "," $ flashCardFormTags form') <> "}", Text)
393 | , Just (Oid 25, encodeUtf8 . flashCardFormVerso $ form', Text)
394 | ] :: HandlerM [Integer]
395 | throwError $ err302 {
396 | errHeaders = [
397 | ("Location", "/dashboard")
398 | ]
399 | }
400 |
401 | getFlashCardDeletePage :: Integer -> HandlerM H.Html
402 | getFlashCardDeletePage flashCardId' = do
403 | loggedInOr302 session
404 | _ <- runSqlFile'
405 | "sql/deleteFlashCard.sql"
406 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User Id
407 | , Just (Oid 25, show flashCardId', Text) -- Word Id
408 | ] :: HandlerM [Integer]
409 | throwError $ err302 {
410 | errHeaders = [
411 | ("Location", "/dashboard")
412 | ]
413 | }
414 |
415 | getQuizzPage :: [Message] -> HandlerM H.Html
416 | getQuizzPage messages = do
417 | loggedInOr302 session
418 | css <- izidictCSSText
419 | baseUrl' <- baseUrl
420 | flashcards <- runSqlFile'
421 | "sql/getQuizzFlashCard.sql"
422 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User Id
423 | ] :: HandlerM [FlashCard]
424 |
425 | if length flashcards < 1
426 | then throwError $ err302 {errHeaders = [("Location", "/quizz/finish")]}
427 | else return ()
428 |
429 | let frontEndVariables = A.Object $ HM.fromList [
430 | ("session", A.toJSON session)
431 | ]
432 |
433 | let frontContext = Object $ HM.fromList [
434 | ("css", String $ css)
435 | , ("flashcard", (toMustache . FlashCardMustache) (flashcards!!0))
436 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
437 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
438 | , ("base_url", String baseUrl')
439 | ]
440 | template <- compileTemplate "quizz.html"
441 | preEscapedToMarkupSubstituteTemplate template frontContext
442 |
443 | getQuizzFinishPage :: HandlerM H.Html
444 | getQuizzFinishPage = do
445 | loggedInOr302 session
446 | css <- izidictCSSText
447 | baseUrl' <- baseUrl
448 | let frontEndVariables = A.Object $ HM.fromList [
449 | ("session", A.toJSON session)
450 | ]
451 | let frontContext = Object $ HM.fromList [
452 | ("css", String $ css)
453 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
454 | , ("base_url", String baseUrl')
455 | ]
456 | template <- compileTemplate "quizz.finish.html"
457 | preEscapedToMarkupSubstituteTemplate template frontContext
458 |
459 | getQuizzAnswerPage :: [Message] -> Integer -> HandlerM H.Html
460 | getQuizzAnswerPage messages flashCardId' = do
461 | loggedInOr302 session
462 | flashcards <- runSqlFile'
463 | "sql/getFlashCard.sql"
464 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User ID
465 | , Just (Oid 25, show flashCardId', Text) -- FlashCard ID
466 | ] :: HandlerM [FlashCard]
467 |
468 | css <- izidictCSSText
469 | baseUrl' <- baseUrl
470 | let frontEndVariables = A.Object $ HM.fromList [
471 | ("session", A.toJSON session)
472 | ]
473 |
474 | let frontContext = Object $ HM.fromList [
475 | ("css", String $ css)
476 | , ("flashcard", (toMustache . FlashCardMustache) (flashcards!!0))
477 | , ("messages", Array . V.fromList $ map (toMustache . MessageMustache . description) messages)
478 | , ("frontEndVariables", String . decodeUtf8 . BSL.toStrict . A.encode $ frontEndVariables)
479 | , ("base_url", String baseUrl')
480 | ]
481 |
482 | template <- compileTemplate "quizz.answer.html"
483 | preEscapedToMarkupSubstituteTemplate template frontContext
484 |
485 | postQuizzPage :: QuizzForm -> HandlerM H.Html
486 | postQuizzPage form' = do
487 | loggedInOr302 session
488 |
489 | let sqlFile = case quizzFormRecto form' of
490 | Just _ -> "sql/verifyFlashCardRectoAnswer.sql"
491 | Nothing -> "sql/verifyFlashCardVersoAnswer.sql"
492 | -- verify the word
493 | flashCardVerified <- runSqlFile'
494 | sqlFile
495 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User Id
496 | , Just (Oid 25, encodeUtf8 . quizzFormId $ form', Text) -- FlashCard Id
497 | , Just (Oid 25, encodeUtf8 ((fromMaybe "" $ quizzFormRecto form') <> (fromMaybe "" $ quizzFormVerso form')), Text) -- Answer
498 | ] :: HandlerM [Bool]
499 | _ <- if (flashCardVerified!!0)
500 | then do
501 | runSqlFile'
502 | "sql/increaseBucketFlashCard.sql"
503 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User Id
504 | , Just (Oid 25, encodeUtf8 . quizzFormId $ form', Text) -- FlashCard Id
505 | ] :: HandlerM [Integer]
506 | else do
507 | runSqlFile'
508 | "sql/decreaseBucketFlashCard.sql"
509 | [ Just (Oid 25, show . userId . sessionUser $ session, Text) -- User Id
510 | , Just (Oid 25, encodeUtf8 . quizzFormId $ form', Text) -- FlashCard Id
511 | ] :: HandlerM [Integer]
512 |
513 | getQuizzAnswerPage [(Message (if (flashCardVerified!!0) then "Right answer :) " else "Wrong answer :( "))] (read . unpack . quizzFormId $ form' :: Integer)
514 |
515 | faviconIco :: HandlerM ByteString
516 | faviconIco = do
517 | fileBS <- readCacheOrFileBS "www/favicon.ico"
518 | return $ fileBS
519 |
520 | sitemapXml :: HandlerM Text
521 | sitemapXml = do
522 | baseUrl' <- baseUrl
523 | let frontContext = Object $ HM.fromList [
524 | ("base_url", String baseUrl')
525 | ]
526 |
527 | template <- compileTemplate "sitemap.xml"
528 | return $ substituteTemplate template frontContext
529 |
530 | robotsTxt :: HandlerM Text
531 | robotsTxt = do
532 | baseUrl' <- baseUrl
533 | let frontContext = Object $ HM.fromList [
534 | ("base_url", String baseUrl')
535 | ]
536 |
537 | template <- compileTemplate "robots.txt"
538 | return $ substituteTemplate template frontContext
539 |
540 |
541 | loginFacebookCallback :: Maybe Text -> Maybe Text -> Maybe Text -> Maybe Integer -> Maybe Text -> HandlerM H.Html
542 | loginFacebookCallback mCode _ _ _ _ = do
543 | code <- case mCode of
544 | Nothing -> do
545 | $(logInfo) "didnt receive the code & state"
546 | throwError $ err302 {errHeaders = [("Location", "/")]}
547 | Just code' -> return code'
548 |
549 | sharedEnv <- ask
550 | let facebookAppId = facebook_appid . settings $ sharedEnv
551 | let facebookRedirectUri = (base_url . settings $ sharedEnv) <> "/login/facebook"
552 | let facebookAppSecret = facebook_appsecret . settings $ sharedEnv
553 | let facebookAppToken = facebook_apptoken . settings $ sharedEnv
554 |
555 | fbAccessToken <- getFacebookAccessToken facebookAppId facebookAppSecret facebookRedirectUri code
556 |
557 | fbToken <- getFacebookToken facebookAppToken fbAccessToken
558 |
559 | let userId' = user_id fbToken
560 |
561 | fbPermissions <- getFacebookPermissionList facebookAppToken userId'
562 |
563 | let nbDeclinedPermissions = length $ filter (\p -> (status p) == "declined") (fBPermissionList fbPermissions)
564 |
565 | if nbDeclinedPermissions > 0
566 | then do
567 | $(logInfo) ("Some Facebook permission were rejected, need to re-ask" :: Text)
568 | let facebookClientId = facebook_appid . settings $ sharedEnv
569 | throwError $ err302 {errHeaders = [("Location", encodeUtf8 $ "https://www.facebook.com/v3.2/dialog/oauth?client_id=" <> facebookClientId <> "&redirect_uri=" <> facebookRedirectUri <> "&auth_type=rerequest&scope=public_profile,email")]}
570 | else return ()
571 |
572 | -- Retrieving email address + personnal information
573 | fbUser <- getFacebookUser fbAccessToken userId'
574 | randomPassword <- liftIO $ generateRandomPassword
575 |
576 | -- Create a new account
577 | eitherNBUserInserted <- runSqlFile
578 | "sql/insertUser.sql"
579 | [ Just (Oid 25, encodeUtf8 . first_name $ fbUser, Text)
580 | , Just (Oid 25, encodeUtf8 . last_name $ fbUser, Binary)
581 | , Just (Oid 25, encodeUtf8 . email $ fbUser, Binary)
582 | , Just (Oid 25, encodeUtf8 randomPassword, Binary)]
583 | :: HandlerM (Either ExceptionPostgreSQL [User])
584 |
585 | $(logInfo) ("either nb user inserted: " <> (show eitherNBUserInserted) :: Text)
586 |
587 | case eitherNBUserInserted of
588 | Right userInserted' -> do
589 | let userInserted = userInserted'!!0
590 | -- Send a welcoming email if new user
591 | $(logInfo) ("Need to send a welcoming email to the user " <> (show userInserted) :: Text)
592 | -- Insert the default FlashCards
593 | _ <- runSqlFile'
594 | "sql/insertDefaultFlashCards.sql"
595 | [ Just (Oid 25, show . userId $ userInserted, Text)
596 | ] :: HandlerM [Integer]
597 | return ()
598 | Left pgErr -> do
599 | case pgErr of
600 | ExceptionPGUniqueViolation -> $(logInfo) ("User already in database")
601 | _ -> $(logInfo) ("Unknown PG Exception: " <> (show pgErr) :: Text)
602 |
603 | jwtSecrets <- runSqlFile'
604 | "sql/getNewRandomSessionWithoutPasswordCheck.sql"
605 | [ Just (Oid 25, encodeUtf8 . email $ fbUser, Text)
606 | ] :: HandlerM [Text]
607 |
608 | let jwtHeader = BS64.encode $ "{\"alg\": \"HS256\", \"typ\": \"JWT\"}"
609 | let jwtPayload = BS64.encode $ "{\"email\": \"" <> (encodeUtf8 . email $ fbUser) <> "\"}"
610 |
611 | let jwtSignature = BS64.encode $ toBytes $ hmacAlg SHA256 (encodeUtf8 $ jwtSecrets!!0) (jwtHeader <> "." <> jwtPayload)
612 |
613 | let jwt = jwtHeader <> "." <> jwtPayload <> "." <> jwtSignature
614 |
615 | throwError $ err302 {
616 | errHeaders = [
617 | ("Location", "/dashboard")
618 | , ("Set-Cookie", "jwt=" <> jwt <> "; path=/; expires=Thu, 01 Jan 2200 00:00:00 GMT")
619 | ]
620 | }
621 |
622 |
623 | startApp :: SharedEnv -> IO ()
624 | startApp sharedEnv =
625 | run (app_port . settings $ sharedEnv) (logStdoutDev (app sharedEnv) )
626 |
627 | app :: SharedEnv -> Application
628 | -- serveWithContext :: (HasServer api context) => Proxy api -> Context context -> Server api -> Application
629 | -- hoistServer :: HasServer api '[] => Proxy api -> (forall x. m x -> n x) -> ServerT api m -> ServerT api n
630 | -- hoistServerWithContext :: HasServer api context => Proxy api -> Proxy context -> (forall x . m x -> n x) -> ServerT api m -> ServerT api n
631 | app c = serveWithContext api (authServerContext (dbConnectionPool c)) $ hoistServerWithContext api contextProxy (nt c) server
632 |
633 | api :: Proxy API
634 | api = Proxy
635 |
--------------------------------------------------------------------------------
/src/SharedEnv.hs:
--------------------------------------------------------------------------------
1 | module SharedEnv where
2 |
3 | import Protolude
4 |
5 | import qualified Data.Text as T
6 | import qualified Control.Concurrent.STM.TMVar as STMVar
7 | import Data.HashMap.Strict as HM
8 | import Database.PostgreSQL.LibPQ (Connection)
9 | import Database (initConnectionPool)
10 | import Data.Pool ( Pool )
11 | import Text.Mustache.Types
12 | import Data
13 |
14 |
15 | data SharedEnv = SharedEnv
16 | { settings :: Settings
17 | , cache :: STMVar.TMVar (HashMap Text ByteString)
18 | , cacheTemplate :: STMVar.TMVar (HashMap FilePath Template)
19 | , dbConnectionPool :: Pool Connection
20 | } deriving (Generic)
21 |
22 |
23 | initSharedEnv :: T.Text -> STMVar.TMVar (HashMap Text ByteString) -> STMVar.TMVar TemplateCache -> IO (SharedEnv)
24 | initSharedEnv pathFileCfg tCache tCachetemplate = do
25 | settingsLoaded <- loadSettings pathFileCfg
26 |
27 | -- Setup the DB pools connection
28 | dbConnectionPool' <- initConnectionPool $ "service=" <> (pgservice settingsLoaded)
29 |
30 | return (SharedEnv settingsLoaded tCache tCachetemplate dbConnectionPool')
31 |
--------------------------------------------------------------------------------
/src/Template.hs:
--------------------------------------------------------------------------------
1 | module Template where
2 |
3 | import Protolude
4 |
5 | import Data.HashMap.Strict
6 | import Control.Concurrent.STM.TMVar
7 | import Control.Exception
8 | import Text.Mustache
9 | import Text.Blaze.Html
10 | import SharedEnv
11 | import Data.Settings
12 | import HandlerM
13 |
14 | data LCException = TemplateParseException
15 | deriving (Show)
16 |
17 | instance Exception LCException
18 |
19 | compileTemplate' :: FilePath -> IO Template
20 | compileTemplate' templateName = do
21 | let searchSpace = ["src/templates/", "templates/", "emails/"]
22 | compiled <- automaticCompile searchSpace templateName
23 | case compiled of
24 | Left err -> do
25 | putStrLn $ ("TemplateParseException: " <> (show err) :: Text)
26 | throw TemplateParseException
27 | Right template -> return template
28 |
29 |
30 | compileTemplate :: FilePath -> HandlerM Template
31 | compileTemplate templateName = do
32 | sharedEnv <- ask
33 | case production . settings $ sharedEnv of
34 | True -> do
35 | let tCacheTemplate = cacheTemplate sharedEnv
36 |
37 | templateCache' <- liftIO $ atomically $ readTMVar tCacheTemplate
38 |
39 | let mCacheValue = lookup templateName templateCache'
40 | case mCacheValue of
41 | -- if the key exists in the templateCache', return it
42 | Just template -> return template
43 | -- if the template doesnt exists, load and compile the
44 | -- template with the partials, then save it into the
45 | -- cache
46 | Nothing -> do
47 | -- check the cache
48 | template <- liftIO $ compileTemplate' templateName
49 | let newCache = insert templateName template templateCache'
50 | _ <- liftIO $ atomically $ swapTMVar tCacheTemplate newCache
51 | return $ template
52 | False -> liftIO $ compileTemplate' templateName
53 |
54 |
55 | -- substituteTemplate template context = substitute template (object ["ctx" ~= context])
56 | substituteTemplate :: ToMustache ctx => Template -> ctx -> Text
57 | substituteTemplate template context = substitute template context
58 |
59 | preEscapedToMarkupSubstituteTemplate :: ToMustache ctx => Template -> ctx -> HandlerM Html
60 | preEscapedToMarkupSubstituteTemplate template context = do
61 | return $ preEscapedToMarkup $ substitute template context
62 |
--------------------------------------------------------------------------------
/src/Utils.hs:
--------------------------------------------------------------------------------
1 | module Utils where
2 |
3 | import Protolude
4 |
5 | import Data.Text (pack)
6 | import System.Random
7 | import Servant
8 | import HandlerM
9 | import SharedEnv
10 | import Data
11 |
12 |
13 | facebookUrlLogin :: HandlerM Text
14 | facebookUrlLogin = do
15 | sharedEnv <- ask
16 | let facebookState = "CSRF1"
17 | let facebookRedirectUri = (base_url . settings $ sharedEnv ) <> "/login/facebook"
18 | let facebookClientId = facebook_appid . settings $ sharedEnv
19 | return $ "https://www.facebook.com/v3.2/dialog/oauth?response_type=code&client_id=" <> facebookClientId <> "&redirect_uri=" <> facebookRedirectUri <> "&state=" <> facebookState <> "&scope=public_profile,email"
20 |
21 | baseUrl :: HandlerM Text
22 | baseUrl = do
23 | sharedEnv <- ask
24 | return $ base_url . settings $ sharedEnv
25 |
26 |
27 | loggedInOr302 :: Session -> HandlerM ()
28 | loggedInOr302 AnonymousSession = do
29 | throwError $ err302 {
30 | errHeaders = [
31 | ("Location", encodeUtf8 "/")
32 | ]
33 | }
34 | loggedInOr302 _ = return ()
35 |
36 |
37 | generateRandomPassword :: IO Text
38 | generateRandomPassword = do
39 | newPassword <- passwordString []
40 | return $ pack newPassword
41 | where passwordString :: [Char] -> IO [Char]
42 | passwordString fullPassword@[_,_,_,_,_,_,_,_] = return fullPassword
43 | passwordString currPassword = do
44 | newIntR <- getStdRandom (randomR (48, 122)) :: IO Int
45 | if (48 < newIntR && newIntR < 57) || (65 < newIntR && newIntR < 90) || (97 < newIntR && newIntR < 122)
46 | then passwordString (chr(newIntR):currPassword)
47 | else passwordString currPassword
48 |
--------------------------------------------------------------------------------
/src/templates/account.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
22 |
23 |
My account
24 | {{#account}}
25 |
38 | {{/account}}
39 |
40 |
41 |
42 | {{> footer.html}}
43 |
44 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/templates/dashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
22 |
23 |
Your FlashCard dashboard
24 |
25 |
26 |
27 |
30 |
33 |
Search a flashcard
34 |
38 |
39 |
My FlashCards
40 | {{#flashcards}}
41 |
42 |
46 |
47 |
{{recto}}
48 |
49 |
50 |
{{verso}}
51 |
52 |
53 | {{/flashcards}}
54 |
55 |
56 |
57 | {{> footer.html}}
58 |
59 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/templates/flashcard.add.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
22 |
23 |
Add a flashcard
24 |
49 |
50 |
51 |
52 | {{> footer.html}}
53 |
54 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/templates/flashcard.edit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
22 |
23 |
Edit a flashcard
24 | {{#flashcard}}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{recto}}
33 |
34 |
35 | {{verso}}
36 |
37 |
38 |
39 |
49 |
50 | {{/flashcard}}
51 |
52 |
53 |
54 | {{> footer.html}}
55 |
56 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/templates/footer.html:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/src/templates/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | IziDict - All Your FlashCards Online!
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/templates/header-logo.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
22 |
23 |
All your FlashCard - Online!
24 |
25 |
26 |
27 |
Never forget anything!
28 |
FlashCards
29 | are mostly used to consolidate your vocabulary in any languages,
30 | but as this method is generic, you can use it to remember
31 | absolutely anything. Remembering and increasing your general
32 | culture becomes easy.
33 |
Your memory uses a two way binding. You link an idea or word in
34 | two way.
35 |
For instance if you're a video game fan, and I'm asking you what
36 | represents the year 1981, you will probably never guess. But if I'm
37 | asking to you when the character Mario was created, maybe you'll
38 | retrieve the information much faster.
39 |
The method used at IziDict.com
40 | heavily rely on how the memory works.
41 |
You create your FlashCards online (recto and verso), and can
42 | take quizzes about them. If you pass the quizz about a FlashCard,
43 | it will move to a higher bucket of FlashCard, that will be asked to
44 | you later. If you fail, the FlashCard will be moved to a lower
45 | bucket of FlashCard, that will be asked to you sooner.
46 |
47 |
48 |
49 | Recto: English most common verb
50 |
51 |
52 | Verso: to be
53 |
54 |
55 |
56 |
This method has been proven to be very effective for various
57 | subject like:
58 |
59 | increase your general knowledge
60 | increase your vocabulary
61 | revised for your next test
62 | learn a new language
63 | …
64 |
65 |
Still not convinced? Give it a try or contact us.
66 |
67 |
Bonus: Why this font?
68 |
Sans Forgetica is a downloadable font that is scientifically designed to help you remember your study notes.
69 |
70 |
71 |
72 |
73 | {{> footer.html}}
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
42 |
43 | {{> footer.html}}
44 |
45 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/templates/nav.fullscreen.html:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/templates/nav.login.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/src/templates/quizz.answer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{> header-logo.html}}
11 |
12 |
17 |
18 | {{> nav.login.html}}
19 |
20 |
21 | {{> nav.fullscreen.html}}
22 |
23 |
24 |
25 |
Answer
26 |
27 |
28 | {{#messages}}
29 | {{{message}}}
30 | {{/messages}}
31 |
32 |
33 |
36 |
37 |
38 | {{#flashcard}}
39 |
40 |
44 |
45 |
{{recto}}
46 |
47 |
48 |
{{verso}}
49 |
50 |
51 | {{/flashcard}}
52 |
53 |
54 |
55 | {{> footer.html}}
56 |
57 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/templates/quizz.finish.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{> header-logo.html}}
11 |
12 |
17 |
18 | {{> nav.login.html}}
19 |
20 |
21 | {{> nav.fullscreen.html}}
22 |
23 |
24 |
25 |
Congratulation
26 |
You can rest until tomorrow :)
27 |
You don't have anymore FlashCard to do for today.
28 |
See ya!
29 |
30 |
31 |
32 | {{> footer.html}}
33 |
34 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/templates/quizz.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
22 |
23 |
Take a quizz
24 | {{#flashcard}}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{recto-value}}
33 |
34 |
35 | {{verso-value}}
36 |
37 |
38 |
39 | {{/flashcard}}
40 |
41 |
42 |
43 | {{> footer.html}}
44 |
45 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/templates/register.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> head.html}}
4 |
5 |
6 |
7 |
8 | {{> header-logo.html}}
9 |
10 |
15 |
16 | {{> nav.login.html}}
17 |
18 |
19 | {{> nav.fullscreen.html}}
20 |
21 |
35 |
36 | {{> footer.html}}
37 |
38 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/templates/robots.txt:
--------------------------------------------------------------------------------
1 | # Disallow
2 | User-agent: *
3 | Disallow: /dashboard
4 | User-agent: *
5 | Disallow: /account
6 | User-agent: *
7 | Disallow: /quizz
8 | User-agent: *
9 | Disallow: /flashcard/*
10 |
11 | # Allow
12 | User-agent: *
13 | Allow: /
14 |
15 | Sitemap: {{base_url}}/sitemap.xml
16 |
--------------------------------------------------------------------------------
/src/templates/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{base_url}}/
5 | monthly
6 | 1
7 |
8 |
9 | {{base_url}}/register
10 | monthly
11 | 0.1
12 |
13 |
14 | {{base_url}}/login
15 | monthly
16 | 0.1
17 |
18 |
19 |
--------------------------------------------------------------------------------
/stack.yaml:
--------------------------------------------------------------------------------
1 | # This file was automatically generated by 'stack init'
2 | #
3 | # Some commonly used options have been documented as comments in this file.
4 | # For advanced use and comprehensive documentation of the format, please see:
5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/
6 |
7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version.
8 | # A snapshot resolver dictates the compiler version and the set of packages
9 | # to be used for project dependencies. For example:
10 | #
11 | # resolver: lts-3.5
12 | # resolver: nightly-2015-09-21
13 | # resolver: ghc-7.10.2
14 | # resolver: ghcjs-0.1.0_ghc-7.10.2
15 | #
16 | # The location of a snapshot can be provided as a file or url. Stack assumes
17 | # a snapshot provided as a file might change, whereas a url resource does not.
18 | #
19 | # resolver: ./custom-snapshot.yaml
20 | # resolver: https://example.com/snapshots/2018-01-01.yaml
21 | resolver: lts-12.6
22 |
23 | # User packages to be built.
24 | # Various formats can be used as shown in the example below.
25 | #
26 | # packages:
27 | # - some-directory
28 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz
29 | # - location:
30 | # git: https://github.com/commercialhaskell/stack.git
31 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
32 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
33 | # subdirs:
34 | # - auto-update
35 | # - wai
36 | packages:
37 | - .
38 | # Dependency packages to be pulled from upstream that are not in the resolver
39 | # using the same syntax as the packages field.
40 | # (e.g., acme-missiles-0.3)
41 | #
42 | extra-deps:
43 | - HDBC-postgresql-2.3.2.5
44 |
45 | # Override default flag values for local packages and extra-deps
46 | # flags: {}
47 |
48 | # Extra package databases containing global packages
49 | # extra-package-dbs: []
50 |
51 | # Control whether we use the GHC we find on the path
52 | # system-ghc: true
53 | #
54 | # Require a specific version of stack, using version ranges
55 | # require-stack-version: -any # Default
56 | # require-stack-version: ">=1.7"
57 | #
58 | # Override the architecture used by stack, especially useful on Windows
59 | # arch: i386
60 | # arch: x86_64
61 | #
62 | # Extra directories used by stack for building
63 | # extra-include-dirs: [/path/to/dir]
64 | # extra-lib-dirs: [/path/to/dir]
65 | #
66 | # Allow a newer minor version of GHC than the snapshot specifies
67 | # compiler-check: newer-minor
68 |
--------------------------------------------------------------------------------
/static/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/static/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/static/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/static/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/static/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/static/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/static/tmp/autogen-FQn2C_-6.js:
--------------------------------------------------------------------------------
1 | document.getElementById("hident4").innerHTML="This text was added by the Javascript part of the homepage widget.";$(function(){$("#js-commentForm").submit(function(event){event.preventDefault();var message=$("#js-createCommentTextarea").val();if(!message){alert("Please fill out the comment form first.");return};$.ajax({url:'http://127.1:3000/comments',type:'POST',contentType:"application/json",data:JSON.stringify({message:message}),success:function(data){var newNode=$(" ");newNode.text(data.message);console.log(data);$("#js-commentList").append(newNode)},error:function(data){console.log("Error creating comment: "+data)}})})})
--------------------------------------------------------------------------------
/static/tmp/autogen-PE46a7-4.css:
--------------------------------------------------------------------------------
1 | h2#hident4{color:#990
2 | }li{line-height:2em;font-size:16px
3 | }#js-createCommentTextarea{width:400px;height:100px}.masthead,.navbar{background-color:rgb(27, 28, 29)}.navbar-default .navbar-nav > .active > a{background-color:transparent;border-bottom:2px solid white}.navbar-nav{padding-bottom:1em}.masthead{margin-top:-21px;color:white;text-align:center;min-height:500px}.masthead .header{max-width:700px;margin:0 auto;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif}.masthead h1.header{margin-top:1em;margin-bottom:0em;font-size:4.5em;line-height:1.2em;font-weight:normal}.masthead h2{font-size:1.7em;font-weight:normal}.masthead .btn{margin:1em 0}.bs-callout{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-radius:3px}.bs-callout p:last-child{margin-bottom:0}.bs-callout-info{border-left-color:#1b809e}.bs-docs-section{margin-bottom:60px}.bs-docs-section:last-child{margin-bottom:0}#message{margin-bottom:40px}
--------------------------------------------------------------------------------
/static/tmp/autogen-dAT6QVJx.js:
--------------------------------------------------------------------------------
1 | document.getElementById("hident4").innerHTML="This text was added by the Javascript part of the homepage widget.";$(function(){$("#js-commentForm").submit(function(event){event.preventDefault();var message=$("#js-createCommentTextarea").val();if(!message){alert("Please fill out the comment form first.");return};$.ajax({url:'/comments',type:'POST',contentType:"application/json",data:JSON.stringify({message:message}),success:function(data){var newNode=$(" ");newNode.text(data.message);console.log(data);$("#js-commentList").append(newNode)},error:function(data){console.log("Error creating comment: "+data)}})})})
--------------------------------------------------------------------------------
/static/tmp/autogen-f9mZBX8C.js:
--------------------------------------------------------------------------------
1 | document.getElementById("hident4").innerHTML="This text was added by the Javascript part of the homepage widget.";$(function(){$("#js-commentForm").submit(function(event){event.preventDefault();var message=$("#js-createCommentTextarea").val();if(!message){alert("Please fill out the comment form first.");return};$.ajax({url:'http://127.0.0.1:3000/comments',type:'POST',contentType:"application/json",data:JSON.stringify({message:message}),success:function(data){var newNode=$(" ");newNode.text(data.message);console.log(data);$("#js-commentList").append(newNode)},error:function(data){console.log("Error creating comment: "+data)}})})})
--------------------------------------------------------------------------------
/templates/default-layout-wrapper.hamlet:
--------------------------------------------------------------------------------
1 | $newline never
2 | \
3 | \
4 | \
5 | \
6 | \
7 |
8 |
9 |
10 |
11 | #{pageTitle pc}
12 |
13 |
14 |
15 |
16 |
17 | ^{pageHead pc}
18 |
19 | \
22 |
12 |
13 |
14 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | curl -i -v http://127.1:8080/words
4 | curl -i -v -X POST -H 'Content-Type: application/json' -d '{ "wordId": 12 , "wordLanguage": "EN" , "wordWord": "wordWord" , "wordKeywords": [] , "wordDefinition": "wordDefinition" , "wordDifficulty": 2 }' http://127.1:8080/words
5 |
--------------------------------------------------------------------------------
/verify/0001-words.sql:
--------------------------------------------------------------------------------
1 | -- Verify words:0001-words on pg
2 |
3 | BEGIN;
4 |
5 | SELECT "id", "language", "words", "keywords", "definition", "difficulty"
6 | FROM words
7 | WHERE false;
8 |
9 | ROLLBACK;
10 |
--------------------------------------------------------------------------------
/verify/0002-users.sql:
--------------------------------------------------------------------------------
1 | -- Verify words:0002-users on pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add verifications here.
6 |
7 | ROLLBACK;
8 |
--------------------------------------------------------------------------------
/verify/0003-tokens.sql:
--------------------------------------------------------------------------------
1 | -- Verify words:0003-tokens on pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add verifications here.
6 |
7 | ROLLBACK;
8 |
--------------------------------------------------------------------------------
/verify/0004-session.sql:
--------------------------------------------------------------------------------
1 | -- Verify words:0004-session on pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add verifications here.
6 |
7 | ROLLBACK;
8 |
--------------------------------------------------------------------------------
/verify/0005-scoring.sql:
--------------------------------------------------------------------------------
1 | -- Verify words:0005-scoring on pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add verifications here.
6 |
7 | ROLLBACK;
8 |
--------------------------------------------------------------------------------
/verify/0006-update-user.sql:
--------------------------------------------------------------------------------
1 | -- Verify words:0006-update-user on pg
2 |
3 | BEGIN;
4 |
5 | -- XXX Add verifications here.
6 |
7 | ROLLBACK;
8 |
--------------------------------------------------------------------------------
/www/favico.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/favico.png
--------------------------------------------------------------------------------
/www/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/favicon.ico
--------------------------------------------------------------------------------
/www/fonts/Roboto-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-Black.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-BlackItalic.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-BoldItalic.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-Italic.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-Light.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-LightItalic.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-MediumItalic.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-Thin.ttf
--------------------------------------------------------------------------------
/www/fonts/Roboto-ThinItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/Roboto-ThinItalic.ttf
--------------------------------------------------------------------------------
/www/fonts/RobotoSlab-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/RobotoSlab-Bold.ttf
--------------------------------------------------------------------------------
/www/fonts/RobotoSlab-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/RobotoSlab-Light.ttf
--------------------------------------------------------------------------------
/www/fonts/RobotoSlab-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/RobotoSlab-Regular.ttf
--------------------------------------------------------------------------------
/www/fonts/RobotoSlab-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/RobotoSlab-Thin.ttf
--------------------------------------------------------------------------------
/www/fonts/SansForgetica-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/fonts/SansForgetica-Regular.otf
--------------------------------------------------------------------------------
/www/home.html:
--------------------------------------------------------------------------------
1 | index.html
--------------------------------------------------------------------------------
/www/images/dictionnary.logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/images/dictionnary.logo.png
--------------------------------------------------------------------------------
/www/images/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/images/icons.png
--------------------------------------------------------------------------------
/www/images/icons.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/images/icons.xcf
--------------------------------------------------------------------------------
/www/images/idea.1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/images/idea.1.jpg
--------------------------------------------------------------------------------
/www/images/memory.1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/images/memory.1.jpg
--------------------------------------------------------------------------------
/www/images/memory.2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/images/memory.2.jpg
--------------------------------------------------------------------------------
/www/images/memory.3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlogFr/FlashCard/17029f9b84f23c43f67702b77bd7418cdf0a9d28/www/images/memory.3.jpg
--------------------------------------------------------------------------------
/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | IziDict - My Dictionary Online (1)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/www/login.html:
--------------------------------------------------------------------------------
1 | index.html
--------------------------------------------------------------------------------
/www/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Correct the font size and margin on `h1` elements within `section` and
29 | * `article` contexts in Chrome, Firefox, and Safari.
30 | */
31 |
32 | h1 {
33 | font-size: 2em;
34 | margin: 0.67em 0;
35 | }
36 |
37 | /* Grouping content
38 | ========================================================================== */
39 |
40 | /**
41 | * 1. Add the correct box sizing in Firefox.
42 | * 2. Show the overflow in Edge and IE.
43 | */
44 |
45 | hr {
46 | box-sizing: content-box; /* 1 */
47 | height: 0; /* 1 */
48 | overflow: visible; /* 2 */
49 | }
50 |
51 | /**
52 | * 1. Correct the inheritance and scaling of font size in all browsers.
53 | * 2. Correct the odd `em` font sizing in all browsers.
54 | */
55 |
56 | pre {
57 | font-family: monospace, monospace; /* 1 */
58 | font-size: 1em; /* 2 */
59 | }
60 |
61 | /* Text-level semantics
62 | ========================================================================== */
63 |
64 | /**
65 | * Remove the gray background on active links in IE 10.
66 | */
67 |
68 | a {
69 | background-color: transparent;
70 | }
71 |
72 | /**
73 | * 1. Remove the bottom border in Chrome 57-
74 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
75 | */
76 |
77 | abbr[title] {
78 | border-bottom: none; /* 1 */
79 | text-decoration: underline; /* 2 */
80 | text-decoration: underline dotted; /* 2 */
81 | }
82 |
83 | /**
84 | * Add the correct font weight in Chrome, Edge, and Safari.
85 | */
86 |
87 | b,
88 | strong {
89 | font-weight: bolder;
90 | }
91 |
92 | /**
93 | * 1. Correct the inheritance and scaling of font size in all browsers.
94 | * 2. Correct the odd `em` font sizing in all browsers.
95 | */
96 |
97 | code,
98 | kbd,
99 | samp {
100 | font-family: monospace, monospace; /* 1 */
101 | font-size: 1em; /* 2 */
102 | }
103 |
104 | /**
105 | * Add the correct font size in all browsers.
106 | */
107 |
108 | small {
109 | font-size: 80%;
110 | }
111 |
112 | /**
113 | * Prevent `sub` and `sup` elements from affecting the line height in
114 | * all browsers.
115 | */
116 |
117 | sub,
118 | sup {
119 | font-size: 75%;
120 | line-height: 0;
121 | position: relative;
122 | vertical-align: baseline;
123 | }
124 |
125 | sub {
126 | bottom: -0.25em;
127 | }
128 |
129 | sup {
130 | top: -0.5em;
131 | }
132 |
133 | /* Embedded content
134 | ========================================================================== */
135 |
136 | /**
137 | * Remove the border on images inside links in IE 10.
138 | */
139 |
140 | img {
141 | border-style: none;
142 | }
143 |
144 | /* Forms
145 | ========================================================================== */
146 |
147 | /**
148 | * 1. Change the font styles in all browsers.
149 | * 2. Remove the margin in Firefox and Safari.
150 | */
151 |
152 | button,
153 | input,
154 | optgroup,
155 | select,
156 | textarea {
157 | font-family: inherit; /* 1 */
158 | font-size: 100%; /* 1 */
159 | line-height: 1.15; /* 1 */
160 | margin: 0; /* 2 */
161 | }
162 |
163 | /**
164 | * Show the overflow in IE.
165 | * 1. Show the overflow in Edge.
166 | */
167 |
168 | button,
169 | input { /* 1 */
170 | overflow: visible;
171 | }
172 |
173 | /**
174 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
175 | * 1. Remove the inheritance of text transform in Firefox.
176 | */
177 |
178 | button,
179 | select { /* 1 */
180 | text-transform: none;
181 | }
182 |
183 | /**
184 | * Correct the inability to style clickable types in iOS and Safari.
185 | */
186 |
187 | button,
188 | [type="button"],
189 | [type="reset"],
190 | [type="submit"] {
191 | -webkit-appearance: button;
192 | }
193 |
194 | /**
195 | * Remove the inner border and padding in Firefox.
196 | */
197 |
198 | button::-moz-focus-inner,
199 | [type="button"]::-moz-focus-inner,
200 | [type="reset"]::-moz-focus-inner,
201 | [type="submit"]::-moz-focus-inner {
202 | border-style: none;
203 | padding: 0;
204 | }
205 |
206 | /**
207 | * Restore the focus styles unset by the previous rule.
208 | */
209 |
210 | button:-moz-focusring,
211 | [type="button"]:-moz-focusring,
212 | [type="reset"]:-moz-focusring,
213 | [type="submit"]:-moz-focusring {
214 | outline: 1px dotted ButtonText;
215 | }
216 |
217 | /**
218 | * Correct the padding in Firefox.
219 | */
220 |
221 | fieldset {
222 | padding: 0.35em 0.75em 0.625em;
223 | }
224 |
225 | /**
226 | * 1. Correct the text wrapping in Edge and IE.
227 | * 2. Correct the color inheritance from `fieldset` elements in IE.
228 | * 3. Remove the padding so developers are not caught out when they zero out
229 | * `fieldset` elements in all browsers.
230 | */
231 |
232 | legend {
233 | box-sizing: border-box; /* 1 */
234 | color: inherit; /* 2 */
235 | display: table; /* 1 */
236 | max-width: 100%; /* 1 */
237 | padding: 0; /* 3 */
238 | white-space: normal; /* 1 */
239 | }
240 |
241 | /**
242 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
243 | */
244 |
245 | progress {
246 | vertical-align: baseline;
247 | }
248 |
249 | /**
250 | * Remove the default vertical scrollbar in IE 10+.
251 | */
252 |
253 | textarea {
254 | overflow: auto;
255 | }
256 |
257 | /**
258 | * 1. Add the correct box sizing in IE 10.
259 | * 2. Remove the padding in IE 10.
260 | */
261 |
262 | [type="checkbox"],
263 | [type="radio"] {
264 | box-sizing: border-box; /* 1 */
265 | padding: 0; /* 2 */
266 | }
267 |
268 | /**
269 | * Correct the cursor style of increment and decrement buttons in Chrome.
270 | */
271 |
272 | [type="number"]::-webkit-inner-spin-button,
273 | [type="number"]::-webkit-outer-spin-button {
274 | height: auto;
275 | }
276 |
277 | /**
278 | * 1. Correct the odd appearance in Chrome and Safari.
279 | * 2. Correct the outline style in Safari.
280 | */
281 |
282 | [type="search"] {
283 | -webkit-appearance: textfield; /* 1 */
284 | outline-offset: -2px; /* 2 */
285 | }
286 |
287 | /**
288 | * Remove the inner padding in Chrome and Safari on macOS.
289 | */
290 |
291 | [type="search"]::-webkit-search-decoration {
292 | -webkit-appearance: none;
293 | }
294 |
295 | /**
296 | * 1. Correct the inability to style clickable types in iOS and Safari.
297 | * 2. Change font properties to `inherit` in Safari.
298 | */
299 |
300 | ::-webkit-file-upload-button {
301 | -webkit-appearance: button; /* 1 */
302 | font: inherit; /* 2 */
303 | }
304 |
305 | /* Interactive
306 | ========================================================================== */
307 |
308 | /*
309 | * Add the correct display in Edge, IE 10+, and Firefox.
310 | */
311 |
312 | details {
313 | display: block;
314 | }
315 |
316 | /*
317 | * Add the correct display in all browsers.
318 | */
319 |
320 | summary {
321 | display: list-item;
322 | }
323 |
324 | /* Misc
325 | ========================================================================== */
326 |
327 | /**
328 | * Add the correct display in IE 10+.
329 | */
330 |
331 | template {
332 | display: none;
333 | }
334 |
335 | /**
336 | * Add the correct display in IE 10.
337 | */
338 |
339 | [hidden] {
340 | display: none;
341 | }
342 |
--------------------------------------------------------------------------------
/www/profile.html:
--------------------------------------------------------------------------------
1 | index.html
--------------------------------------------------------------------------------
/www/register.html:
--------------------------------------------------------------------------------
1 | index.html
--------------------------------------------------------------------------------