You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

847 lines
29 KiB
Lua

--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