|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
import asyncdispatch, base64, jester, json, jwt, logging, options, sequtils,
|
|
|
|
|
strutils, times, uuids
|
|
|
|
|
from unicode import capitalize
|
|
|
|
|
import timeutils except `<`
|
|
|
|
|
|
|
|
|
|
import ./db, ./configuration, ./models, ./service, ./version
|
|
|
|
@ -47,6 +48,17 @@ template json500Resp(ex: ref Exception, details: string = ""): void =
|
|
|
|
|
error details & ":\n" & ex.msg
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
# internal JSON parsing utils
|
|
|
|
|
proc getIfExists(n: JsonNode, key: string): JsonNode =
|
|
|
|
|
## convenience method to get a key from a JObject or return null
|
|
|
|
|
result = if n.hasKey(key): n[key]
|
|
|
|
|
else: newJNull()
|
|
|
|
|
|
|
|
|
|
proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
|
|
|
|
|
## convenience method to get a key from a JObject or raise an exception
|
|
|
|
|
if not n.hasKey(key): raiseEx BadRequestError, objName & " missing key '" & key & "'"
|
|
|
|
|
return n[key]
|
|
|
|
|
|
|
|
|
|
proc toJWT*(ctx: PMApiContext, session: Session): string =
|
|
|
|
|
## Make a JST token for this session.
|
|
|
|
|
var jwt = JWT(
|
|
|
|
@ -78,25 +90,21 @@ proc fromJWT*(ctx: PMApiContext, strTok: string): Session =
|
|
|
|
|
expires: fromUnix(jwt.claims["exp"].node.num))
|
|
|
|
|
|
|
|
|
|
proc fromApiToken*(ctx: PMApiContext, strTok: string): Session =
|
|
|
|
|
info "fromApiToken"
|
|
|
|
|
let pairs = strTok.decode.split(":")
|
|
|
|
|
let email = pairs[0]
|
|
|
|
|
let apiTokVal = pairs[1]
|
|
|
|
|
|
|
|
|
|
info "email: " & email & "\tapiTokVal: " & apiTokVal
|
|
|
|
|
# Look up the user
|
|
|
|
|
var user: User
|
|
|
|
|
try:
|
|
|
|
|
let users = ctx.db.getUsersByEmail(email)
|
|
|
|
|
let users = ctx.db.findUsersByEmail(email)
|
|
|
|
|
if users.len != 1: raiseEx ""
|
|
|
|
|
user = users[0]
|
|
|
|
|
info "Found user: " & $user
|
|
|
|
|
except: raiseEx "invalid username or password"
|
|
|
|
|
|
|
|
|
|
# look for the ApiToken record, hashing the token provided with the user's salt
|
|
|
|
|
let hashedToken = hashPwd(apiTokVal, user.salt)
|
|
|
|
|
echo "Hashed token: " & hashedToken
|
|
|
|
|
let foundTokens = ctx.db.getApiTokensByHashedToken(hashedToken)
|
|
|
|
|
let hashed = hashWithSalt(apiTokVal, user.salt)
|
|
|
|
|
let foundTokens = ctx.db.findApiTokensByHashedToken(hashed.hash)
|
|
|
|
|
if foundTokens.len != 1: raiseEx "invalid username or password"
|
|
|
|
|
|
|
|
|
|
let apiToken = foundTokens[0]
|
|
|
|
@ -137,23 +145,23 @@ proc makeAuthToken*(ctx: PMApiContext, email, pwd: string): string =
|
|
|
|
|
## token string.
|
|
|
|
|
|
|
|
|
|
if email.len == 0 or pwd.len == 0:
|
|
|
|
|
raiseEx "fields 'username' and 'password' required"
|
|
|
|
|
raiseEx AuthError, "fields 'username' and 'password' required"
|
|
|
|
|
|
|
|
|
|
# find the user record
|
|
|
|
|
var user: User
|
|
|
|
|
try:
|
|
|
|
|
let users = ctx.db.getUsersByEmail(email)
|
|
|
|
|
let users = ctx.db.findUsersByEmail(email)
|
|
|
|
|
if users.len != 1: raiseEx ""
|
|
|
|
|
user = users[0]
|
|
|
|
|
except: raiseEx "invalid username or password"
|
|
|
|
|
except: raiseEx AuthError, "invalid username or password"
|
|
|
|
|
|
|
|
|
|
if not validatePwd(user, pwd): raiseEx "invalid username or password"
|
|
|
|
|
if not validatePwd(user, pwd): raiseEx AuthError, "invalid username or password"
|
|
|
|
|
|
|
|
|
|
let session = newSession(user)
|
|
|
|
|
|
|
|
|
|
result = toJWT(ctx, session)
|
|
|
|
|
|
|
|
|
|
template checkAuth() =
|
|
|
|
|
template checkAuth(requiresAdmin = false) =
|
|
|
|
|
## Check this request for authentication and authorization information.
|
|
|
|
|
## Injects the session into the running context. If the request is not
|
|
|
|
|
## authorized, this template returns an appropriate 401 response.
|
|
|
|
@ -165,6 +173,9 @@ template checkAuth() =
|
|
|
|
|
debug "Auth failed: " & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http401, "Unauthorized", @{"WWW-Authenticate": "Bearer"})
|
|
|
|
|
|
|
|
|
|
if requiresAdmin and not session.user.isAdmin:
|
|
|
|
|
jsonResp(Http401, "Unauthorized", @{"WWW-Authenticate": "Bearer"})
|
|
|
|
|
|
|
|
|
|
proc start*(ctx: PMApiContext): void =
|
|
|
|
|
|
|
|
|
|
if ctx.cfg.debug: setLogFilter(lvlDebug)
|
|
|
|
@ -181,48 +192,128 @@ proc start*(ctx: PMApiContext): void =
|
|
|
|
|
resp($(%("personal_measure_api v" & PM_API_VERSION)), JSON)
|
|
|
|
|
|
|
|
|
|
post "/auth-token":
|
|
|
|
|
var email, pwd: string
|
|
|
|
|
try:
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
email = jsonBody["email"].getStr
|
|
|
|
|
pwd = jsonBody["password"].getStr
|
|
|
|
|
except: jsonResp(Http400)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
let email = jsonBody.getOrFail("email").getStr
|
|
|
|
|
let pwd = jsonBody.getOrFail("password").getStr
|
|
|
|
|
let authToken = makeAuthToken(ctx, email, pwd)
|
|
|
|
|
resp($(%authToken), JSON)
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except: jsonResp(Http401, getCurrentExceptionMsg())
|
|
|
|
|
|
|
|
|
|
post "/change-pwd":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
|
|
|
|
|
if not validatePwd(session.user, jsonBody.getOrFail("oldPassword").getStr):
|
|
|
|
|
raiseEx AuthError, "old password is incorrect"
|
|
|
|
|
|
|
|
|
|
let newHash = hashWithSalt(jsonBody.getOrFail("newPassword").getStr, session.user.salt)
|
|
|
|
|
session.user.hashedPwd = newHash.hash
|
|
|
|
|
if ctx.db.updateUser(session.user): jsonResp(Http200)
|
|
|
|
|
else: jsonResp(Http500, "unable to change pwd")
|
|
|
|
|
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except AuthError: jsonResp(Http401, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "internal error changing password: " & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
post "/change-pwd/@userId":
|
|
|
|
|
checkAuth(true)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
|
|
|
|
|
var user = ctx.db.getUser(parseUUID(@"userId"))
|
|
|
|
|
let newHash = hashWithSalt(jsonBody.getOrFail("newPassword").getStr, user.salt)
|
|
|
|
|
user.hashedPwd = newHash.hash
|
|
|
|
|
if ctx.db.updateUser(user): jsonResp(Http200)
|
|
|
|
|
else: jsonResp(Http500, "unable to change pwd")
|
|
|
|
|
|
|
|
|
|
except ValueError: jsonResp(Http400, "invalid UUID")
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except AuthError: jsonResp(Http401, getCurrentExceptionMsg())
|
|
|
|
|
except NotFoundError: jsonResp(Http404, "no such user")
|
|
|
|
|
except:
|
|
|
|
|
error "internal error changing password: " & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
get "/user":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
resp(Http200, $(%session.user), JSON)
|
|
|
|
|
|
|
|
|
|
post "/users":
|
|
|
|
|
checkAuth(true)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
|
|
|
|
|
let pwdAndSalt = jsonBody.getOrFail("password").getStr.hashPwd(ctx.cfg.pwdCost)
|
|
|
|
|
|
|
|
|
|
let newUser = User(
|
|
|
|
|
displayName: jsonBody.getOrFail("displayName").getStr,
|
|
|
|
|
email: jsonBody.getOrFail("email").getStr,
|
|
|
|
|
hashedPwd: pwdAndSalt.pwd,
|
|
|
|
|
salt: pwdAndSalt.salt,
|
|
|
|
|
isAdmin: false)
|
|
|
|
|
|
|
|
|
|
resp($(%ctx.db.createUser(newUser)), JSON)
|
|
|
|
|
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "Could not create new user:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
delete "/users/@userId":
|
|
|
|
|
checkAuth(true)
|
|
|
|
|
|
|
|
|
|
var user: User
|
|
|
|
|
try:
|
|
|
|
|
let userId = parseUUID(@"userId")
|
|
|
|
|
user = ctx.db.getUser(userId)
|
|
|
|
|
except: jsonResp(Http404)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if not ctx.db.deleteUser(user): raiseEx "unable to delete user"
|
|
|
|
|
except: jsonResp(Http500, getCurrentExceptionMsg())
|
|
|
|
|
|
|
|
|
|
get "/api-tokens":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
resp(Http200, $(%ctx.db.getApiTokensByUserId($session.user.id)))
|
|
|
|
|
resp(Http200, $(%ctx.db.findApiTokensByUserId($session.user.id)))
|
|
|
|
|
|
|
|
|
|
post "/api-tokens":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
var newToken: ApiToken
|
|
|
|
|
try:
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
newToken = ApiToken(
|
|
|
|
|
var newToken = ApiToken(
|
|
|
|
|
userId: session.user.id,
|
|
|
|
|
name: jsonBody["name"].getStr,
|
|
|
|
|
name: jsonBody.getOrFail("name").getStr,
|
|
|
|
|
expires: none[DateTime](),
|
|
|
|
|
hashedToken: "")
|
|
|
|
|
except: jsonResp(Http400)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let tokenValue = randomString(ctx.cfg.pwdCost)
|
|
|
|
|
newToken.hashedToken = hashPwd(tokenValue, session.user.salt)
|
|
|
|
|
let hashed = hashWithSalt(tokenValue, session.user.salt)
|
|
|
|
|
|
|
|
|
|
newToken.hashedToken = hashed.hash
|
|
|
|
|
newToken = ctx.db.createApiToken(newToken)
|
|
|
|
|
|
|
|
|
|
let respToken = %newToken
|
|
|
|
|
respToken["value"] = %tokenValue
|
|
|
|
|
resp($respToken, JSON)
|
|
|
|
|
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except AuthError: jsonResp(Http401, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
debug getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
@ -237,6 +328,148 @@ proc start*(ctx: PMApiContext): void =
|
|
|
|
|
else: jsonResp(Http500)
|
|
|
|
|
except: jsonResp(Http404)
|
|
|
|
|
|
|
|
|
|
get "/measures":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try: resp($(%ctx.db.findMeasuresByUserId($session.user.id)), JSON)
|
|
|
|
|
except:
|
|
|
|
|
error "unable to retrieve measures for user:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
post "/measures":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
|
|
|
|
|
if not (jsonBody.hasKey("slug") or jsonBody.hasKey("name")):
|
|
|
|
|
raiseEx BadRequestError, "body must contain either the 'slug' field (short name), or the 'name' field, or both"
|
|
|
|
|
|
|
|
|
|
let slug =
|
|
|
|
|
if jsonBody.hasKey("slug"): jsonBody["slug"].getStr.nameToSlug
|
|
|
|
|
else: jsonBody["slug"].getStr.nameToSlug
|
|
|
|
|
|
|
|
|
|
let name =
|
|
|
|
|
if jsonBody.hasKey("name"): jsonBody["name"].getStr
|
|
|
|
|
else: jsonBody["slug"].getStr.capitalize
|
|
|
|
|
|
|
|
|
|
var newMeasure = Measure(
|
|
|
|
|
userId: session.user.id,
|
|
|
|
|
slug: slug,
|
|
|
|
|
name: name,
|
|
|
|
|
description: jsonBody.getIfExists("description").getStr(""),
|
|
|
|
|
domainSource:
|
|
|
|
|
if jsonBody.hasKey("domainSource"): some(jsonBody["domainSource"].getStr)
|
|
|
|
|
else: none[string](),
|
|
|
|
|
domainUnits: jsonBody.getIfExists("domainUnits").getStr(""),
|
|
|
|
|
rangeSource:
|
|
|
|
|
if jsonBody.hasKey("rangeSource"): some(jsonBody["rangeSource"].getStr)
|
|
|
|
|
else: none[string](),
|
|
|
|
|
rangeUnits: jsonBody.getIfExists("rangeUnits").getStr(""),
|
|
|
|
|
analysis: @[])
|
|
|
|
|
|
|
|
|
|
if jsonBody.hasKey("analysis"):
|
|
|
|
|
for a in jsonBody["analysis"].getElems:
|
|
|
|
|
newMeasure.analysis.add(a.getStr)
|
|
|
|
|
|
|
|
|
|
resp($(%ctx.db.createMeasure(newMeasure)), JSON)
|
|
|
|
|
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "unable to create new measure:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
get "/measures/@slug":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try: resp($(%ctx.getMeasureForSlug(session.user.id, @"slug")), JSON)
|
|
|
|
|
except NotFoundError: jsonResp(Http404, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "unable to look up a measure by id:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
delete "/measures/@slug":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let measure = ctx.getMeasureForSlug(session.user.id, @"slug")
|
|
|
|
|
if ctx.db.deleteMeasure(measure): jsonResp(Http200)
|
|
|
|
|
else: raiseEx ""
|
|
|
|
|
except NotFoundError: jsonResp(Http404, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "unable to delete a measure:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
get "/measure/@slug":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let measure = ctx.getMeasureForSlug(session.user.id, @"slug")
|
|
|
|
|
resp($(%ctx.db.findMeasurementsByMeasureId($measure.id)), JSON)
|
|
|
|
|
except NotFoundError: jsonResp(Http404, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "unable to list measurements:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
post "/measure/@slug":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let measure = ctx.getMeasureForSlug(session.user.id, @"slug")
|
|
|
|
|
let jsonBody = parseJson(request.body)
|
|
|
|
|
|
|
|
|
|
let newMeasurement = Measurement(
|
|
|
|
|
measureId: measure.id,
|
|
|
|
|
value: jsonBody.getOrFail("value").getInt,
|
|
|
|
|
timestamp:
|
|
|
|
|
if jsonBody.hasKey("timestamp"): jsonBody["timestamp"].getStr.parseIso8601.utc
|
|
|
|
|
else: getTime().utc,
|
|
|
|
|
extData:
|
|
|
|
|
if jsonBody.hasKey("extData"): jsonBody["extData"]
|
|
|
|
|
else: newJObject())
|
|
|
|
|
|
|
|
|
|
resp($(%ctx.db.createMeasurement(newMeasurement)), JSON)
|
|
|
|
|
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except NotFoundError: jsonResp(Http404, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "unable to add measurement:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
get "/measure/@slug/@id":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let measure = ctx.getMeasureForSlug(session.user.id, @"slug")
|
|
|
|
|
resp($(%ctx.getMeasurementForMeasure(measure.id, parseUUID(@"id"))), JSON)
|
|
|
|
|
|
|
|
|
|
except ValueError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except NotFoundError: jsonResp(Http404, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "unable to retrieve measurement:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
delete "/measure/@slug/@id":
|
|
|
|
|
checkAuth()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let measure = ctx.getMeasureForSlug(session.user.id, @"slug")
|
|
|
|
|
let measurement = ctx.getMeasurementForMeasure(measure.id, parseUUID(@"id"))
|
|
|
|
|
if ctx.db.deleteMeasurement(measurement): jsonResp(Http200)
|
|
|
|
|
else: raiseEx ""
|
|
|
|
|
|
|
|
|
|
except ValueError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except JsonParsingError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except BadRequestError: jsonResp(Http400, getCurrentExceptionMsg())
|
|
|
|
|
except NotFoundError: jsonResp(Http404, getCurrentExceptionMsg())
|
|
|
|
|
except:
|
|
|
|
|
error "unable to delete measurement:\n\t" & getCurrentExceptionMsg()
|
|
|
|
|
jsonResp(Http500)
|
|
|
|
|
|
|
|
|
|
post "/service/debug/stop":
|
|
|
|
|
if not ctx.cfg.debug: jsonResp(Http404)
|
|
|
|
|
else:
|
|
|
|
|