--local xxhash = require( "xxhash" ) --local uuid = require( 'resty.jit-uuid' ) local uuid = require( 'lua_uuid' ) local DB = require( "./piazza-lmdb" ) local db = DB.init() local user = require'./models/user' local item = require'./models/item' local image = require'./models/image' local thumbnail = require'./models/thumbnail' local cart = require'./models/cart' local lineitem = require'./models/lineitem' local order = require'./models/order' local customprice = require'./models/customprice' local application = require'./models/application' local util = require( './piazza-util' ) local models = {user, item, image, thumbnail, cart, lineitem, order, customprice, application} for _, model in ipairs(models) do model.setdb(db) end -- password hash: local hash = util.hash local sessions = {} -- keys are session_hashes, values are whatever the server wants to store local validate = util.validate local api = {}; api.post = function() return {message="I did a post."} end api.attemptlogin = function(req) local validation_errors = validate(req, { username = {'string', required=true}, password = {'string', required=true} }) if #validation_errors > 0 then return {errors=validation_errors} else local user_record = db:findOne(function(record) return record.type == 'user' and record.username == req.username end) if user_record == nil then return {success = false, error="no such user"} elseif user_record.password_hash ~= hash(req.password) then return {success = false, error="wrong password"} -- TODO: add 'elseif' to make sure user isn't already logged-in, so two sessions aren't made for the same user who clicked 'login' button twice; although this will make it impossible to sign-in from two different computers else local session_hash = uuid() sessions[session_hash] = {user_id = user_record.id} if user_record.current_cart_id == nil then cart.newForUser(user_record) end return { success = true, -- user = db:preparecopy(user_record, {'password_hash'}), -- cart = db:preparecopy(user_record.current_cart), user = user_record:copyandtransform({ id = 'copy', username = 'copy', firstname = 'copy', tier = 'copy', is_admin = 'copy' }), cart = db:getrecord(user_record.current_cart_id):copyandtransform({ id = 'copy', lineitem_ids = {'list', { id = 'copy', quantity = 'copy', price = 'copy', item_id = { id = 'copy', name = 'copy', brand = 'copy', model = 'copy', color = 'copy' } }} }), session_hash = session_hash } end end end api.logout = function(req) local session_hash = req.session_hash if session_hash ~= nil and sessions[session_hash] ~= nil then sessions[session_hash] = nil return {success=true} else return {error="no such session_hash"} -- TODO: this could happen if user logged-in, then fastcgi was restarted (thus clearing the sessions table), and then logged-out end end api.getone = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true}, ['type'] = {'string', required=true}, id = {'db_ref', to=req['type'], required=true, load_as='record'}, structure = {'table', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local record_copy = loaded_records.record:copyandtransform(req.structure) return {success = true, record = record_copy} end end --[[ -- too big of a security concern. it could return the whole DB to a clever attacker api.getall = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true}, filter = {'table', required=true}, -- TODO: ensure 'filter' is a non-empty table, for security reasons, because then the whole DB will be returned structure = {'table', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local records = db:findByStructure(req.filter) local record_copies = db:copyandtransformall(records, req.structure) return {success=true, records = record_copies} end end ]] api.setproperty = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, record_type = {'string', required=true}, record_id = {'db_ref', to=req.record_type, required=true, load_as='record'}, key = {'string', required=true} -- value = {'string', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local record = loaded_records.record record[req.key] = req.value record:save() return {success=true} end end api.insertrecord = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true}, record = {'table', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local id = db:insertrecord(req.record) return {success=true, id = id} end end -- admin function: add item api.additem = function(req) local validation_errors = validate(req, { session_hash = {'session', required=true, is_admin=true}, item = {'table', required=true} }, nil, sessions) if #validation_errors > 0 then return {errors=validation_errors} else item.new(req.item) -- if saving succeeds, return success message return {success=true} end end -- admin function: delete item api.deleteitem = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, item_id = {'db_ref', to='item', required=true, load_as='item_record'} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else -- delete item --loaded_records.item_record:delete() loaded_records.item_record.deleted = true loaded_records.item_record:save() -- if deletion succeeds, return success message return {success=true} end end -- admin function: update item -- session_hash, item_id, properties_to_update api.updateitem = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, item_id = {'db_ref', to='item', required=true, load_as='item_record'}, properties_to_update = {'table', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else util.merge(loaded_records.item_record, req.properties_to_update) loaded_records.item_record:save() --db:update(loaded_records.item_record, req.properties_to_update) -- if update succeeds, reply with success return {success=true} end end api.fulliteminfo = function(req) local validation_errors, loaded_records = validate(req, { item_id = {'db_ref', to='item', required=true, load_as='item_record'} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local item_record_copy = loaded_records.item_record:copyandtransform({ id = 'copy', ['type'] = 'copy', description_short = 'copy', size = 'copy', material = 'copy', color_description = 'copy' }) -- if update succeeds, reply with success return {success=true, item=item_record_copy} end end --[[ api.newimage = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, image_base64 = {'base64', required=true, load_as = 'image_base64'}, original_filename = {'string', required=true} -- so we can tell for which frame it is in retrospect }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else image.new_from_base64(loaded_records.image_base64, req.original_filename, db) return {success=true} end end ]] -- admin function: create new empty frame record -- session_hash api.createnewframe = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local new_frame_record = item.new({ subtype='frame', brand='', model='', color = '', color_description = '', name = '', description_short = '', size = '', material = '', price_silver = 0, price_gold = 0, inventory = 0 }) local new_frame_record_copy = new_frame_record:copyandtransform({ id = 'copy', ['type'] = 'copy', subtype = 'copy', brand = 'copy', model = 'copy', color = 'copy', color_description = 'copy', name = 'copy', description_short = 'copy', size = 'copy', material = 'copy', price_silver = 'copy', price_gold = 'copy', inventory = 'copy' }) -- if update succeeds, reply with success return {success=true, new_frame=new_frame_record_copy} end end -- admin function: add image to item -- session_hash, item_id, image_base64, original_filename api.addimagetoitem = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, item_id = {'db_ref', to='item', required=true, load_as='item_record'}, image_base64 = {'base64', required=true, load_as='image_base64'}, original_filename = {'string', required=true} -- so we can tell for which frame it is in retrospect }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local image_record = image.new_from_base64(loaded_records.image_base64, req.original_filename) local item_record = loaded_records.item_record -- image_record.item = item_record -- not all images have a `.item` property, so we should rather link the image to the item by `item.images` if item_record.image_ids == nil then item_record.image_ids = {} end table.insert(item_record.image_ids, image_record.id) item_record:save() local image_record_copy = image_record:copyandtransform({ id = 'copy', ['type'] = 'copy', original_filename = 'copy', thumbnail_960_id = 'copy', thumbnail_180_id = 'copy', thumbnail_original_id = 'copy' }) -- if update succeeds, reply with success return {success=true, image=image_record_copy} end end api.deleteitemimage = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, item_id = {'db_ref', to='item', required=true, load_as='item_record'}, image_id = {'db_ref', to='image', required=true, load_as='image_record'} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local image_record = loaded_records.image_record local item_record = loaded_records.item_record -- image_record.item = item_record -- not all images have a `.item` property, so we should rather link the image to the item by `item.images` if item_record.image_ids == nil then item_record.image_ids = {} end local image_index = 0 repeat image_index = image_index + 1 until item_record.image_ids[image_index] == image_record.id or item_record.image_ids[image_index] == nil table.remove(item_record.image_ids, image_index) item_record:save() -- also delete the image_record, and the files it points to image_record:delete() db:delete(image_record.thumbnail_original_id) db:delete(image_record.thumbnail_960_id) db:delete(image_record.thumbnail_180_id) -- if update succeeds, reply with success return {success=true} end end api.itemsbystring = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true}, search_string = {'string', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local item_records = db:findAll(function(record) return record.type == 'item' and string.find(record.model, req.search_string) ~= nil end) item_records = db:copyandtransformall(item_records, { id = 'copy', ['type'] = 'copy', brand = 'copy', model = 'copy', color = 'copy' }) return {success=true, results = item_records} end end api.framesbystring = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true}, search_string = {'string', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local item_records = db:findAll(function(record) return record.subtype == 'frame' and string.find(record.model, req.search_string) ~= nil end) item_records = db:copyandtransformall(item_records, { id = 'copy', ['type'] = 'copy', subtype = 'copy', name = 'copy', brand = 'copy', model = 'copy', color = 'copy', color_description = 'copy', description_short = 'copy', size = 'copy', material = 'copy', price_silver = 'copy', price_gold = 'copy', inventory = 'copy' }) return {success=true, results = item_records} end end api.itemimages = function(req) local validation_errors, loaded_records = validate(req, { --session_hash = {'session', required=true}, item_id = {'db_ref', to='item', required=true, load_as='item_record'} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local item_record = loaded_records.item_record if item_record.image_ids == nil then item_record.image_ids = {} item_record:save() end local image_records = db:copyandtransformall(item_record.image_ids, { id = 'copy', ['type'] = 'copy', original_filename = 'copy', thumbnail_960_id = 'copy', thumbnail_180_id = 'copy', thumbnail_original_id = 'copy' }) return {success=true, images = image_records} end end -- admin function: change user's tier -- session_hash, user_id, tier api.changeusertier = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, user_id = {'db_ref', to='user', required=true, load_as='user_record'}, tier = {'string', required=true, is_any_of={'gold','silver'}} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else --db:update(loaded_records.user_record, {tier=req.tier}) loaded_records.user_record.tier = req.tier loaded_records.user_record:save() -- if update succeeds, reply with success return {success=true} end end -- admin function: give custom price to user for certain item -- session_hash, user_id, item_id, price api.setuseritemprice = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, user_id = {'db_ref', to='user', required=true, load_as='user_record'}, item_id = {'db_ref', to='item', required=true, load_as='item_record'}, price = {'number', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else -- find previously-existing special price between this user and this item local custom_price_record = db:findOne(function(record) return record.type == 'custom_price' and record.user_id == loaded_records.user_record.id and record.item_id == loaded_records.item_record_id end) -- if no existing special price is found, insert one into DB if custom_price_record == nil then customprice.new({user_id=loaded_records.user_record.id, item_id=loaded_records.item_record.id, price=req.price}) -- if insert succeeds, reply with success return {success=true} -- if insert fails, reply with error -- if existing special price is found, update it else --db:update(custom_price_record, {price = req.price}) custom_price_record.price = req.price custom_price_record:save() -- if update succeeds, reply with success return {success=true} -- if update fails, reply with error end end end -- function: update item quantity in current cart; if zero, remove; if was zero, add to cart api.updateitemquantityincart = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true}, item_id = {'db_ref', to='item', required=true, load_as='item_record'}, quantity = {'number', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else -- find cart based on session_hash local current_user_record = db:getrecord( loaded_records.session.user_id ) local cart_record = db:getrecord(current_user_record.current_cart_id) -- if cart not found, create one if cart_record == nil then cart_record = cart.newForUser(current_user_record) end -- find lineitem for this item in cart local lineitem_record_id = util.findOne(cart_record.lineitem_ids, function(lineitem_id) local lineitem = db:getrecord(lineitem_id) return lineitem.item_id == loaded_records.item_record.id end) local lineitem_record -- if lineitem not found, create one if lineitem_record_id == nil then if req.quantity > 0 then lineitem_record = lineitem.new({cart=cart_record, item=loaded_records.item_record, quantity=req.quantity}) end else lineitem_record = db:getrecord(lineitem_record_id) if req.quantity > 0 then -- update quantity in lineitem --db:update(lineitem_record, {quantity = req.quantity}) lineitem_record.quantity = req.quantity lineitem_record:save() -- if the item is being removed from the cart elseif req.quantity == 0 then -- remove item from cart util.removefromtable(cart_record.lineitem_ids, lineitem_record.id) --db:remove(lineitem_record) lineitem_record:delete() cart_record:save() end end local lineitem_records_copy = db:copyandtransformall(cart_record.lineitem_ids, { id = 'copy', ['type'] = 'copy', item_id = { id = 'copy', ['type'] = 'copy', name = 'copy', brand = 'copy', model = 'copy', color = 'copy', price_silver = 'copy', price_gold = 'copy', inventory = 'copy', price = 'copy', }, quantity = 'copy', price = 'copy' }) return {success=true, lineitems = lineitem_records_copy} end end -- function: place order (i.e. current cart) api.placeorder = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else -- find cart based on session_hash local current_user_record = db:getrecord( loaded_records.session.user_id ) local cart_record = db:getrecord( current_user_record.current_cart_id ) -- if cart not found, reply with error if cart_record == nil then return {error="No cart for current user."} elseif #cart_record.lineitem_ids == 0 then return {error="Cart is empty."} else -- if cart found, create order out of it local order_record = order.new({cart=cart_record}) -- if order creation successful, create new empty cart for user and reply with success for _, lineitem_id in ipairs(cart_record.lineitem_ids) do local lineitem_record = db:getrecord(lineitem_id) local item_record = db:getrecord(lineitem_record.item_id) item_record.inventory = item_record.inventory - lineitem_record.quantity item_record:save() end local new_cart_record = cart.newForUser(current_user_record) return {success=true, order_id = order_record.id, new_cart_record = new_cart_record} end end end -- admin function: list open orders api.listopenorders = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else -- find open orders local open_order_records = db:findAll(function(record) return record.type == 'order' and record.status == 'open' end) -- local open_orders = db:preparecopies(open_order_records) local open_orders = db:copyandtransformall(open_order_records, { id = 'copy', ['type'] = 'copy', cart_id = 'copy', user_id = { id = 'copy', ['type'] = 'copy', firstname = 'copy', lastname = 'copy', store = 'copy' }, date_ordered = 'copy', status = 'copy', progress = 'copy' }) return {success=true, orders=open_orders} end end -- admin function: list open orders api.loadorder = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, order_id = {'db_ref', to='order', load_as='order_record'} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local order_record_copy = loaded_records.order_record:copyandtransform({ id = 'copy', ['type'] = 'copy', progress = 'copy', cart_id = { id = 'copy', ['type'] = 'copy', lineitem_ids = { 'list', { id = 'copy', ['type'] = 'copy', price = 'copy', quantity = 'copy', item_id = { id = 'copy', ['type'] = 'copy', name = 'copy' } } } } }) return {success=true, order=order_record_copy} end end -- admin function: mark order as packed, shipped, and delivered api.updateorderstatus = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, order_id = {'db_ref', to='order', required=true, load_as='order_record'}, status = {'string', required=true, is_any_of={'open','packed','shipped','paid'}} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else -- if order found, update status --db:update(loaded_records.order_record, {status = req.status}) loaded_records.order_record.status = req.status loaded_records.order_record:save() -- if update succeeds, reply with success return {success=true} end end -- function: find items according to privileges and characteristics api.itemsbyfilter = function(req) local filter = req.filter local item_records = db:findAll(function(record) if record.type ~= 'item' then return false end --if record.inventory == nil then record.inventory = 0; record:save() end if record.inventory < 1 then return false end -- if user is not signed-in, he cannot see all items: if req.session_hash == nil then for k,v in pairs(filter) do if record[k] ~= v then return false end if item.restricted == true then return false end end -- if user is signed-in, he may see all items: else for k,v in pairs(filter) do if record[k] ~= v then return false end end end return true end) local item_records_copy = db:copyandtransformall(item_records, { id = 'copy', name = 'copy', brand = 'copy', model = 'copy', color = 'copy', color_description = 'copy', size = 'copy', material = 'copy', description_short = 'copy', price_gold = 'copy', price_silver = 'copy', inventory = 'copy', image_ids = { 'list', { id = 'copy', ['type'] = 'copy', thumbnail_180_id = 'copy', thumbnail_960_id = 'copy', thumbnail_original_id = 'copy' } } }) -- TODO: only send the price for each item according to the user's tier, or his own special pricing. don't send price_silver and price_gold return {success=true, items = item_records_copy} end -- function: submit new user application, to be accepted or declined by admin api.submitnewuserapplication = function(req) local validation_errors, loaded_records = validate(req, { username = {'string', required=true}, salutation = {'string', required=true, is_any_of={'mr','ms','mrs','dr'}}, firstname = {'string', required=true}, lastname = {'string', required=true}, position = {'string', required=true}, store = {'string', required=true}, address = {'string', required=true}, practice_type = {'string', required=true}, phone_office = {'string', required=true}, phone_cell = {'string', required=true}, email = {'string', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else application.new({ ['type'] = 'application', username = req.username, salutation = req.salutation, firstname = req.firstname, lastname = req.lastname, position = req.position, store = req.store, address = req.address, practice_type = req.practice_type, phone_office = req.phone_office, phone_cell = req.phone_cell, email = req.email }) -- if update succeeds, reply with success return {success=true} end end api.acceptnewuserapplication = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, application_id = {'db_ref', to='application', required=true, load_as='application_record'}, password = {'string', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else local application_record = loaded_records.application_record user.new({ password = req.password, username = application_record.username, salutation = application_record.salutation, firstname = application_record.firstname, lastname = application_record.lastname, position = application_record.position, store = application_record.store, address = application_record.address, practice_type = application_record.practice_type, phone_office = application_record.phone_office, phone_cell = application_record.phone_cell, email = application_record.email, tier='silver' }) --db:update(application_record, {status = 'accepted', accepted_at = os.time()}) application_record.status = 'accepted' application_record.accepted_at = os.time() application_record:save() -- if update succeeds, reply with success return {success=true} end end -- admin function: list open application records api.listnewuserapplications = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else -- find open applications local open_application_records = db:findAll(function(record) return record.type == 'application' and record.status == 'open' end) local open_applications = db:copyandtransformall(open_application_records, { id='copy', ['type'] = 'copy', username = 'copy', salutation = 'copy', firstname = 'copy', lastname = 'copy', position = 'copy', store = 'copy', address = 'copy', practice_type = 'copy', phone_office = 'copy', phone_cell = 'copy', email = 'copy', submitted_at = 'copy' }) return {success=true, open_applications=open_applications} end end api.declinenewuserapplication = function(req) local validation_errors, loaded_records = validate(req, { session_hash = {'session', required=true, is_admin=true}, application_id = {'db_ref', to='application', required=true, load_as='application_record'}, password = {'string', required=true} }, db, sessions) if #validation_errors > 0 then return {errors=validation_errors} else --db:update(loaded_records.application_record, {status = 'declined', declined_at = os.time()}) local application_record = loaded_records.application_record application_record.status = 'declined' application_record.declined_at = os.time() application_record:save() -- if update succeeds, reply with success return {success=true} end end api.getthumbnail = function(thumbnail_id) return db:getrecord(thumbnail_id).src end return api