user bin listing
This commit is contained in:
+20
-14
@@ -16,12 +16,12 @@ import {
|
|||||||
|
|
||||||
|
|
||||||
function App(vnode_init){
|
function App(vnode_init){
|
||||||
const {state, dispatch} = vnode_init.attrs;
|
const {dispatch} = vnode_init.attrs;
|
||||||
const o = function(handler){ return handler.bind(null, state, dispatch); }
|
|
||||||
//load_notes(state, dispatch);
|
//load_notes(state, dispatch);
|
||||||
return {
|
return {
|
||||||
view: function(vnode){
|
view: function(vnode){
|
||||||
const s = vnode.attrs.state;
|
const s = vnode.attrs.state;
|
||||||
|
const o = function(handler){ return handler.bind(null, s, dispatch); }
|
||||||
return m('.app', {key: 'app'}, [
|
return m('.app', {key: 'app'}, [
|
||||||
m('.top', {key: 'top'}, [
|
m('.top', {key: 'top'}, [
|
||||||
m('.top-left', {key: 'top-left'}, [
|
m('.top-left', {key: 'top-left'}, [
|
||||||
@@ -33,16 +33,19 @@ function App(vnode_init){
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
m('.top-right', {key: 'top-right'}, [
|
m('.top-right', {key: 'top-right'}, [
|
||||||
(s.login.is_logged_in
|
(s.login.showing
|
||||||
? m('.login-info', {key:'login-info'}, [
|
? (s.login.is_logged_in
|
||||||
m('.text', 'Logged in as '+s.login.user.username),
|
? m('.login-info', {key:'login-info'}, [
|
||||||
m('button', {onclick: o(logout_request_handler)}, 'Logout')
|
m('.text', 'Logged in as '+s.login.user.username),
|
||||||
])
|
m('button', {onclick: o(logout_request_handler)}, 'Logout')
|
||||||
: m('.login-form', {key:'login-form'}, [
|
])
|
||||||
m('input.username', {key:'username', value: s.login.username, onchange: o(username_change_handler)}),
|
: m('.login-form', {key:'login-form'}, [
|
||||||
m('input.password[type=password]', {key:'password', value: s.login.password, onchange: o(password_change_handler)}),
|
m('input.username', {key:'username', value: s.login.username, onchange: o(username_change_handler)}),
|
||||||
m('button', {key:'login-button', onclick: o(login_request_handler)}, 'Login/Auto-Register')
|
m('input.password[type=password]', {key:'password', value: s.login.password, onchange: o(password_change_handler)}),
|
||||||
])
|
m('button', {key:'login-button', onclick: o(login_request_handler)}, 'Login/Auto-Register')
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: m('.empty', {key:'empty'})
|
||||||
),
|
),
|
||||||
m('.buttons', {key:'buttons'}, [
|
m('.buttons', {key:'buttons'}, [
|
||||||
(s.is_editing_bin_name
|
(s.is_editing_bin_name
|
||||||
@@ -54,12 +57,15 @@ function App(vnode_init){
|
|||||||
? m('button', {key:'save-bin-name-button', onclick: o(bin_name_commit_handler)}, 'Save')
|
? m('button', {key:'save-bin-name-button', onclick: o(bin_name_commit_handler)}, 'Save')
|
||||||
: m('button', {key:'rename-bin-button', onclick: o(bin_name_editing_toggle_button_handler)}, 'Rename')
|
: m('button', {key:'rename-bin-button', onclick: o(bin_name_editing_toggle_button_handler)}, 'Rename')
|
||||||
)
|
)
|
||||||
])
|
]),
|
||||||
|
m('ul.bin-list', {key:'bin-list'}, s.login.bins.map(b=>
|
||||||
|
m('li.bin', b.name)
|
||||||
|
))
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
m('.main', {key: 'main'}, [
|
m('.main', {key: 'main'}, [
|
||||||
m('.notes', s.notes.map(note_state =>
|
m('.notes', s.notes.map(note_state =>
|
||||||
m(Note, {state, note_state, dispatch, key: note_state.note.id})
|
m(Note, {state:s, note_state, dispatch, key: note_state.note.id})
|
||||||
))
|
))
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ const api = {};
|
|||||||
|
|
||||||
// not `/api` (relative to domain root) nor `//api` (results in https:///api/...)
|
// not `/api` (relative to domain root) nor `//api` (results in https:///api/...)
|
||||||
let prefix = "api";
|
let prefix = "api";
|
||||||
|
let store = null;
|
||||||
|
|
||||||
|
api.setStore = function(s){
|
||||||
|
store=s;
|
||||||
|
};
|
||||||
|
|
||||||
api.post = function(url, body){
|
api.post = function(url, body){
|
||||||
|
body = body || {};
|
||||||
|
body.session_id = store.getState().login.session_id;
|
||||||
return m.request({
|
return m.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: prefix+url,
|
url: prefix+url,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const new_bin_handler = function(state, dispatch){
|
|||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
// change browser location in address bar:
|
// change browser location in address bar:
|
||||||
window.history.pushState(null,'','#'+id);
|
window.history.pushState(null,'','#'+id);
|
||||||
dispatch('new-bin', {id});
|
dispatch('new-bin', {id, name:id});
|
||||||
};
|
};
|
||||||
const username_change_handler = function(state, dispatch, e){
|
const username_change_handler = function(state, dispatch, e){
|
||||||
dispatch('update-username', e.target.value);
|
dispatch('update-username', e.target.value);
|
||||||
@@ -45,7 +45,8 @@ const login_request_handler = function(state, dispatch, e){
|
|||||||
api.post('/login', {username: state.login.username, password: state.login.password})
|
api.post('/login', {username: state.login.username, password: state.login.password})
|
||||||
.then(res=>{
|
.then(res=>{
|
||||||
if(res.success===true){
|
if(res.success===true){
|
||||||
dispatch('login-succeeded', res.user, res.session_id);
|
dispatch('login-succeeded', {user:res.user, session_id:res.session_id});
|
||||||
|
dispatch('user-bin-list-loaded', res.bins);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
dispatch('login-failed');
|
dispatch('login-failed');
|
||||||
@@ -54,8 +55,9 @@ const login_request_handler = function(state, dispatch, e){
|
|||||||
.catch(e=>{ dispatch('login-failed'); });
|
.catch(e=>{ dispatch('login-failed'); });
|
||||||
};
|
};
|
||||||
const logout_request_handler = function(state, dispatch, e){
|
const logout_request_handler = function(state, dispatch, e){
|
||||||
|
api.post('/logout', {});
|
||||||
|
// we dispatch after the API call because the logout action clears the session_id, which is needed to logout
|
||||||
dispatch('logout-requested');
|
dispatch('logout-requested');
|
||||||
api.post('/logout', {session_id: state.login.session_id});
|
|
||||||
};
|
};
|
||||||
const bin_name_editing_toggle_button_handler = function(state, dispatch){
|
const bin_name_editing_toggle_button_handler = function(state, dispatch){
|
||||||
dispatch('update-bin-name-editing', !state.is_editing_bin_name);
|
dispatch('update-bin-name-editing', !state.is_editing_bin_name);
|
||||||
@@ -64,6 +66,7 @@ const bin_name_change_handler = function(state, dispatch, e){
|
|||||||
dispatch('update-bin-name', e.target.value);
|
dispatch('update-bin-name', e.target.value);
|
||||||
};
|
};
|
||||||
const bin_name_commit_handler = function(state, dispatch){
|
const bin_name_commit_handler = function(state, dispatch){
|
||||||
|
api.post('/bin-rename', {bin_id:state.bin.id, name:state.temp_bin_name});
|
||||||
dispatch('commit-bin-name');
|
dispatch('commit-bin-name');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import api from '../api.js';
|
import api from '../api.js';
|
||||||
|
|
||||||
const load_bin_handler = function(state, dispatch, bin_id){
|
const initial_load_bin_handler = function(state, dispatch, bin_id){
|
||||||
/*
|
api.post('/load-bin', {bin_id})
|
||||||
api.post('/load-bin', {id: bin_id})
|
|
||||||
.then(res => {
|
.then(res => {
|
||||||
dispatch('bin-loaded', {bin:res.bin, _notes:res.notes});
|
dispatch('bin-loaded', res.bin);
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
api.post('/load-notes', {bin_id})
|
api.post('/load-notes', {bin_id})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
dispatch('notes-loaded', res.notes);
|
dispatch('notes-loaded', res.notes);
|
||||||
@@ -22,6 +20,10 @@ const hash_change_handler = function(state, dispatch, e){
|
|||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
dispatch('bin-requested', bin_id);
|
dispatch('bin-requested', bin_id);
|
||||||
|
api.post('/load-bin', {bin_id})
|
||||||
|
.then(res=>{
|
||||||
|
dispatch('bin-loaded', res.bin);
|
||||||
|
});
|
||||||
api.post('/load-notes', {bin_id})
|
api.post('/load-notes', {bin_id})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
dispatch('notes-loaded', res.notes);
|
dispatch('notes-loaded', res.notes);
|
||||||
@@ -29,4 +31,4 @@ const hash_change_handler = function(state, dispatch, e){
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export {load_bin_handler, hash_change_handler};
|
export {initial_load_bin_handler, hash_change_handler};
|
||||||
+27
-10
@@ -1,6 +1,7 @@
|
|||||||
|
import api from './api.js';
|
||||||
import App from './App.js';
|
import App from './App.js';
|
||||||
import nanoid from './nanoid.min.js';
|
import nanoid from './nanoid.min.js';
|
||||||
import {load_bin_handler, hash_change_handler} from './handlers/index.js';
|
import {initial_load_bin_handler, hash_change_handler} from './handlers/index.js';
|
||||||
import {bin, note} from './models.js';
|
import {bin, note} from './models.js';
|
||||||
const produce = immer.produce;
|
const produce = immer.produce;
|
||||||
immer.setAutoFreeze(false); // needed for high-frequency updated values, like onkeyup->note.temp_text; only once 'save' is called will it produce a new immutable state tree
|
immer.setAutoFreeze(false); // needed for high-frequency updated values, like onkeyup->note.temp_text; only once 'save' is called will it produce a new immutable state tree
|
||||||
@@ -23,14 +24,25 @@ if(bin_id === ''){
|
|||||||
}
|
}
|
||||||
// the actual loading from server is done later, after store and dispatch are defined
|
// the actual loading from server is done later, after store and dispatch are defined
|
||||||
|
|
||||||
|
function addToBinListIfLoggedIn(state){
|
||||||
|
const s = state;
|
||||||
|
// if user is logged in:
|
||||||
|
if(s.login.is_logged_in){
|
||||||
|
// if bin is not already in the list:
|
||||||
|
if(s.login.bins.filter(b=>b.id===s.bin.id).length===0){
|
||||||
|
s.login.bins.push({id: s.bin.id, name: s.bin.name});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const reducer = handleActions({
|
const reducer = handleActions({
|
||||||
'new-bin': (s, bin) => { s.bin=bin; s.notes=[]; },
|
'new-bin': (s, bin) => { s.bin=bin; s.notes=[]; s.temp_bin_name=bin.id; },
|
||||||
'bin-requested': (s, bin_id) => { s.bin={id:bin_id}; s.notes = []; },
|
'bin-requested': (s, bin_id) => { s.bin={id:bin_id}; s.notes = []; },
|
||||||
'bin-loaded': (s, {bin, _notes}) => { s.bin=bin; s.temp_bin_name=bin.name; s.notes=_notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: bin.id, note:n})); },
|
'bin-loaded': (s, bin) => { s.bin=bin; s.temp_bin_name=bin.name; },
|
||||||
'update-search-term': (s, search_term) => { s.search_term=search_term; },
|
'update-search-term': (s, search_term) => { s.search_term=search_term; },
|
||||||
'update-search-results': (s, _notes) => { s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin.id, note:n})); },
|
'update-search-results': (s, _notes) => { s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin.id, note:n})); },
|
||||||
'add-note': (s, {id, date}) => { s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin.id, is_focused:true, note: {id: id, text: '', modified: date}}); },
|
'add-note': (s, {id, date}) => { s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin.id, is_focused:true, note: {id: id, text: '', modified: date}}); },
|
||||||
'notes-loaded': (s, _notes) => { s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin_id, note:n})); },
|
'notes-loaded': (s, _notes) => { s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin.id, note:n})); },
|
||||||
'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below
|
'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below
|
||||||
'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.is_editing=is_editing; note_s.temp_text=note_s.note.text; },
|
'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.is_editing=is_editing; note_s.temp_text=note_s.note.text; },
|
||||||
'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; note_s.temp_text=text; note_s.is_editing=false; },
|
'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; note_s.temp_text=text; note_s.is_editing=false; },
|
||||||
@@ -38,12 +50,13 @@ const reducer = handleActions({
|
|||||||
'update-username': (s, username) => { s.login.username=username; },
|
'update-username': (s, username) => { s.login.username=username; },
|
||||||
'update-password': (s, password) => { s.login.password=password; },
|
'update-password': (s, password) => { s.login.password=password; },
|
||||||
'login-requested': (s) => { s.login.showing=false; },
|
'login-requested': (s) => { s.login.showing=false; },
|
||||||
'login-succeeded': (s, user, session_id) => { s.login.is_logged_in=true; s.login.showing=false; s.login.password=''; s.login.user=user; s.login.session_id=session_id; },
|
'login-succeeded': (s, {user, session_id}) => { s.login.is_logged_in=true; s.login.showing=true; s.login.password=''; s.login.user=user; s.login.session_id=session_id; },
|
||||||
'login-failed': (s) => { s.login.showing=true; s.login.password=''; },
|
'login-failed': (s) => { s.login.showing=true; s.login.password=''; },
|
||||||
'logout-requested': (s) => { s.login.is_logged_in=false; s.login.showing=true; s.login.username=''; s.login.password=''; s.login.session_id=''; },
|
'logout-requested': (s) => { s.login.is_logged_in=false; s.login.showing=true; s.login.username=''; s.login.password=''; s.login.session_id=''; s.login.bins=[]; },
|
||||||
|
'user-bin-list-loaded': (s, bins) => { s.login.bins=bins; },
|
||||||
'update-bin-name-editing': (s, is_editing) => { s.is_editing_bin_name=is_editing; },
|
'update-bin-name-editing': (s, is_editing) => { s.is_editing_bin_name=is_editing; },
|
||||||
'update-bin-name': (s, name) => { s.temp_bin_name=name; },
|
'update-bin-name': (s, name) => { s.temp_bin_name=name; },
|
||||||
'commit-bin-name': (s) => { s.bin.name=s.temp_bin_name; s.is_editing_bin_name=false; }
|
'commit-bin-name': (s) => { s.bin.name=s.temp_bin_name; s.is_editing_bin_name=false; addToBinListIfLoggedIn(s); }
|
||||||
}, {
|
}, {
|
||||||
bin: {id: bin_id, name: bin_id, user_id: ''},
|
bin: {id: bin_id, name: bin_id, user_id: ''},
|
||||||
is_editing_bin_name: false,
|
is_editing_bin_name: false,
|
||||||
@@ -62,7 +75,8 @@ const reducer = handleActions({
|
|||||||
id: '',
|
id: '',
|
||||||
username: ''
|
username: ''
|
||||||
}*/,
|
}*/,
|
||||||
session_id: ''
|
session_id: '',
|
||||||
|
bins: []
|
||||||
}
|
}
|
||||||
//search_result_notes: []
|
//search_result_notes: []
|
||||||
});
|
});
|
||||||
@@ -73,13 +87,16 @@ const store = Redux.createStore(reducer, /* preloadedState, */
|
|||||||
const dispatch = function(action_type, payload){ store.dispatch({type:action_type, payload}); };
|
const dispatch = function(action_type, payload){ store.dispatch({type:action_type, payload}); };
|
||||||
|
|
||||||
store.subscribe(()=>{
|
store.subscribe(()=>{
|
||||||
m.render(root, m(App, {state: store.getState(), dispatch}));
|
const state = store.getState();
|
||||||
|
m.render(root, m(App, {state, dispatch}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.setStore(store);
|
||||||
|
|
||||||
window.addEventListener("hashchange", (e)=>hash_change_handler(store.getState(), dispatch, e), false);
|
window.addEventListener("hashchange", (e)=>hash_change_handler(store.getState(), dispatch, e), false);
|
||||||
|
|
||||||
let state = store.getState();
|
let state = store.getState();
|
||||||
load_bin_handler(state, dispatch, bin_id);
|
initial_load_bin_handler(state, dispatch, bin_id);
|
||||||
|
|
||||||
// we don't want Mithril auto-redraw system in place, since Redux will manually re-render when necessary with store.subscribe():
|
// we don't want Mithril auto-redraw system in place, since Redux will manually re-render when necessary with store.subscribe():
|
||||||
// m.mount(root, App);
|
// m.mount(root, App);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function note(o){
|
|||||||
n.id = o.id;
|
n.id = o.id;
|
||||||
n.text = o.text || '';
|
n.text = o.text || '';
|
||||||
n.modified = o.modified || 0;
|
n.modified = o.modified || 0;
|
||||||
|
n.bin_id = o.bin_id || '';
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+83
-16
@@ -14,19 +14,17 @@ const db = new Pool({
|
|||||||
const app = express();
|
const app = express();
|
||||||
const port = 80;
|
const port = 80;
|
||||||
|
|
||||||
db.query("CREATE TABLE IF NOT EXISTS bin (id VARCHAR PRIMARY KEY);"
|
db.query("CREATE TABLE IF NOT EXISTS bin (id VARCHAR PRIMARY KEY, name VARCHAR);"
|
||||||
+ "CREATE TABLE IF NOT EXISTS note (id VARCHAR PRIMARY KEY, text TEXT, modified TIMESTAMPTZ);"
|
+ "CREATE TABLE IF NOT EXISTS note (id VARCHAR PRIMARY KEY, text TEXT, modified TIMESTAMPTZ);"
|
||||||
+ "CREATE TABLE IF NOT EXISTS bin_note (bin_id VARCHAR REFERENCES bin (id), note_id VARCHAR REFERENCES note (id), PRIMARY KEY (bin_id, note_id));"
|
+ "CREATE TABLE IF NOT EXISTS bin_note (bin_id VARCHAR REFERENCES bin (id), note_id VARCHAR REFERENCES note (id), PRIMARY KEY (bin_id, note_id));"
|
||||||
+ "CREATE TABLE IF NOT EXISTS user_ (id VARCHAR PRIMARY KEY, username TEXT, password TEXT);" // table 'user' is already taken
|
+ "CREATE TABLE IF NOT EXISTS user_ (id VARCHAR PRIMARY KEY, username TEXT, password TEXT);" // table 'user' is already taken
|
||||||
|
+ "CREATE TABLE IF NOT EXISTS bin_user (bin_id VARCHAR REFERENCES bin (id), user_id VARCHAR REFERENCES user_ (id), PRIMARY KEY (bin_id, user_id));"
|
||||||
|
+ "CREATE TABLE IF NOT EXISTS session (id VARCHAR PRIMARY KEY, user_id VARCHAR REFERENCES user_ (id));"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/', (req, res)=>{
|
|
||||||
res.send('Feh')
|
|
||||||
});
|
|
||||||
|
|
||||||
const load_notes_stmt =
|
const load_notes_stmt =
|
||||||
"SELECT n.id, n.text, n.modified FROM bin_note AS bn"
|
"SELECT n.id, n.text, n.modified FROM bin_note AS bn"
|
||||||
+" JOIN note AS n"
|
+" JOIN note AS n"
|
||||||
@@ -37,7 +35,26 @@ router.post('/load-notes', (req, res)=>{
|
|||||||
const bin_id = req.body.bin_id;
|
const bin_id = req.body.bin_id;
|
||||||
db.query(load_notes_stmt, [bin_id])
|
db.query(load_notes_stmt, [bin_id])
|
||||||
.then(result => {
|
.then(result => {
|
||||||
res.json({status: 'ok', bin:{id:bin_id}, notes:result.rows})
|
res.json({success:true, notes:result.rows})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const load_bin_stmt =
|
||||||
|
"SELECT b.id, b.name FROM bin AS b"
|
||||||
|
+" WHERE b.id = $1";
|
||||||
|
// {bin_id}
|
||||||
|
router.post('/load-bin', (req, res)=>{
|
||||||
|
const bin_id = req.body.bin_id;
|
||||||
|
db.query(load_bin_stmt, [bin_id])
|
||||||
|
.then(result => {
|
||||||
|
const bin = result.rows[0];
|
||||||
|
// if a bin with given id was found:
|
||||||
|
if(result.rows.length>0){
|
||||||
|
res.json({success:true, bin:{id:bin.id, name:bin.name}});
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
res.json({success:false, bin:{id:bin_id, name:bin_id}});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// {status: 'ok', bin:{id:bin.id}, notes: bin.notes}
|
// {status: 'ok', bin:{id:bin.id}, notes: bin.notes}
|
||||||
});
|
});
|
||||||
@@ -61,24 +78,33 @@ const upsert_note_stmt =
|
|||||||
+" ON CONFLICT (id)"
|
+" ON CONFLICT (id)"
|
||||||
+" DO UPDATE SET text = EXCLUDED.text, modified = EXCLUDED.modified";
|
+" DO UPDATE SET text = EXCLUDED.text, modified = EXCLUDED.modified";
|
||||||
const upsert_bin_stmt =
|
const upsert_bin_stmt =
|
||||||
"INSERT INTO bin (id) VALUES ($1)"
|
"INSERT INTO bin (id, name) VALUES ($1, $1)"
|
||||||
+" ON CONFLICT (id)"
|
+" ON CONFLICT (id)"
|
||||||
+" DO NOTHING";
|
+" DO NOTHING";
|
||||||
const upsert_bin_note_stmt =
|
const upsert_bin_note_stmt =
|
||||||
"INSERT INTO bin_note (bin_id, note_id) VALUES ($1, $2)"
|
"INSERT INTO bin_note (bin_id, note_id) VALUES ($1, $2)"
|
||||||
+" ON CONFLICT (bin_id, note_id)"
|
+" ON CONFLICT (bin_id, note_id)"
|
||||||
+" DO NOTHING";
|
+" DO NOTHING";
|
||||||
|
const upsert_bin_user_stmt =
|
||||||
|
"INSERT INTO bin_user (bin_id, user_id)"
|
||||||
|
+" (SELECT $1, s.user_id FROM session AS s WHERE s.id = $2)"
|
||||||
|
+" ON CONFLICT (bin_id, user_id)"
|
||||||
|
+" DO NOTHING";
|
||||||
// {bin_id, note_id, text}
|
// {bin_id, note_id, text}
|
||||||
router.post('/save', (req, res)=>{
|
router.post('/save', (req, res)=>{
|
||||||
const {bin_id, note_id, text} = req.body;
|
const {bin_id, note_id, text, session_id} = req.body;
|
||||||
db.connect().then(async (client)=>{
|
db.connect().then(async (client)=>{
|
||||||
|
// TODO: make the following into a transaction:
|
||||||
await client.query(upsert_note_stmt, [note_id, text]);
|
await client.query(upsert_note_stmt, [note_id, text]);
|
||||||
await client.query(upsert_bin_stmt, [bin_id]);
|
await client.query(upsert_bin_stmt, [bin_id]);
|
||||||
await client.query(upsert_bin_note_stmt, [bin_id, note_id]);
|
await client.query(upsert_bin_note_stmt, [bin_id, note_id]);
|
||||||
|
// if the user is signed-in, ensure the bin and user are associated:
|
||||||
|
if(session_id !== ''){
|
||||||
|
await client.query(upsert_bin_user_stmt, [bin_id, session_id]);
|
||||||
|
}
|
||||||
// don't forget to release back into the Pool:
|
// don't forget to release back into the Pool:
|
||||||
client.release();
|
client.release();
|
||||||
res.json({status: 'ok'});
|
res.json({success:true});
|
||||||
});
|
});
|
||||||
// {status: 'ok'}
|
// {status: 'ok'}
|
||||||
});
|
});
|
||||||
@@ -88,17 +114,32 @@ const login_check_stmt =
|
|||||||
+" WHERE u.username = $1";
|
+" WHERE u.username = $1";
|
||||||
const register_stmt =
|
const register_stmt =
|
||||||
"INSERT INTO user_ (id, username, password) VALUES ($1, $2, $3)";
|
"INSERT INTO user_ (id, username, password) VALUES ($1, $2, $3)";
|
||||||
|
const session_create_stmt =
|
||||||
|
"INSERT INTO session (id, user_id) VALUES ($1, $2)";
|
||||||
|
const user_bin_list_stmt =
|
||||||
|
"SELECT b.id, b.name FROM bin_user AS bu"
|
||||||
|
+" INNER JOIN bin AS b"
|
||||||
|
+" ON bu.bin_id = b.id"
|
||||||
|
+" WHERE bu.user_id = $1";
|
||||||
router.post('/login', (req, res)=>{
|
router.post('/login', (req, res)=>{
|
||||||
const {username, password_from_client} = req.body;
|
const {username, password} = req.body;
|
||||||
|
const password_from_client=password;
|
||||||
db.query(login_check_stmt, [username])
|
db.query(login_check_stmt, [username])
|
||||||
.then(result=>{
|
.then(result=>{
|
||||||
// if there is such a username:
|
// if there is such a username:
|
||||||
if(result.rows.length > 0){
|
if(result.rows.length > 0){
|
||||||
const row = result.rows[0];
|
const row = result.rows[0];
|
||||||
const id=row.id, password_from_db=row.password;
|
const user_id=row.id, password_from_db=row.password;
|
||||||
// if the passwords match:
|
// if the passwords match:
|
||||||
|
// TODO: replace `===` with a constant-time comparison function to prevent timing attacks; or better, store passwords as salted hashes and use `===` on that:
|
||||||
if(password_from_client === password_from_db){
|
if(password_from_client === password_from_db){
|
||||||
res.json({success:true, user:{id, username}, session_id:nanoid()});
|
const session_id = nanoid();
|
||||||
|
db.query(session_create_stmt, [session_id, user_id]);
|
||||||
|
db.query(user_bin_list_stmt, [user_id])
|
||||||
|
.then(result=>{
|
||||||
|
const bins = result.rows;
|
||||||
|
res.json({success:true, user:{id:user_id, username}, session_id, bins});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
res.json({success: false});
|
res.json({success: false});
|
||||||
@@ -106,15 +147,41 @@ router.post('/login', (req, res)=>{
|
|||||||
}
|
}
|
||||||
// if no such user exists:
|
// if no such user exists:
|
||||||
else{
|
else{
|
||||||
const id = nanoid();
|
const user_id = nanoid();
|
||||||
db.query(register_stmt, [id, username, password_from_client])
|
const session_id = nanoid();
|
||||||
|
db.query(register_stmt, [user_id, username, password_from_client])
|
||||||
.then(r=>{
|
.then(r=>{
|
||||||
res.json({success: true, user:{id, username}, session_id:nanoid()});
|
db.query(session_create_stmt, [session_id, user_id]);
|
||||||
|
res.json({success: true, user:{id:user_id, username}, session_id, bins:[]});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const delete_session_stmt =
|
||||||
|
"DELETE FROM session WHERE id = $1";
|
||||||
|
router.post('/logout', (req, res)=>{
|
||||||
|
db.query(delete_session_stmt, [req.body.session_id]);
|
||||||
|
res.json({success:true});
|
||||||
|
});
|
||||||
|
|
||||||
|
const rename_bin_stmt =
|
||||||
|
"INSERT INTO bin (id, name) VALUES ($1, $2)"
|
||||||
|
+" ON CONFLICT (id)"
|
||||||
|
+" DO UPDATE SET name = EXCLUDED.name;";
|
||||||
|
router.post('/bin-rename', (req,res)=>{
|
||||||
|
const {bin_id, name, session_id} = req.body;
|
||||||
|
db.query(rename_bin_stmt, [bin_id, name])
|
||||||
|
.then(x=>{
|
||||||
|
// ensure user and bin are associated if necessary:
|
||||||
|
if(session_id !== ''){
|
||||||
|
// `upsert_bin_user_stmt` is defined above:
|
||||||
|
db.query(upsert_bin_user_stmt, [bin_id, session_id]);
|
||||||
|
}
|
||||||
|
res.json({success:true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.use('/', express.json(), router);
|
app.use('/', express.json(), router);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user