WIP Refactor API into multiple sub-modules.
This commit is contained in:
		
							
								
								
									
										7
									
								
								api/personal_measure.config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								api/personal_measure.config.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "authSecret":"change me",
 | 
			
		||||
  "dbConnString":"host=localhost port=5500 dbname=personal_measure user=postgres password=password",
 | 
			
		||||
  "debug":true,
 | 
			
		||||
  "port":8080,
 | 
			
		||||
  "pwdCost":11
 | 
			
		||||
}
 | 
			
		||||
@@ -8,9 +8,11 @@ description   = "JDB\'s Personal Measures API"
 | 
			
		||||
license       = "MIT"
 | 
			
		||||
srcDir        = "src/main/nim"
 | 
			
		||||
bin           = @["personal_measure_api"]
 | 
			
		||||
skipExt       = @["nim"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Dependencies
 | 
			
		||||
 | 
			
		||||
requires @["nim >= 0.19.4", "bcrypt", "docopt >= 0.6.8", "isaac >= 0.1.3", "jester >= 0.4.1", "jwt", "tempfile",
 | 
			
		||||
  "timeutils >= 0.4.0", "uuids >= 0.1.10" ]
 | 
			
		||||
requires @["nim >= 0.19.4", "bcrypt", "cliutils >= 0.6.3", "docopt >= 0.6.8",
 | 
			
		||||
  "isaac >= 0.1.3", "jester >= 0.4.1", "jwt", "tempfile", "timeutils >= 0.4.0",
 | 
			
		||||
  "uuids >= 0.1.10" ]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,192 +1,83 @@
 | 
			
		||||
import asyncdispatch, bcrypt, docopt, jester, json, jwt, options, times, timeutils
 | 
			
		||||
import cliutils, docopt, logging, jester, json, os, strutils, tables
 | 
			
		||||
 | 
			
		||||
import personal_measure_apipkg/models
 | 
			
		||||
import personal_measure_apipkg/db
 | 
			
		||||
import personal_measure_apipkg/configuration
 | 
			
		||||
import personal_measure_apipkg/version
 | 
			
		||||
import personal_measure_apipkg/api
 | 
			
		||||
 | 
			
		||||
const JSON = "application/json"
 | 
			
		||||
const DEFAULT_CONFIG = PMApiConfig(
 | 
			
		||||
  authSecret: "change me",
 | 
			
		||||
  dbConnString: "",
 | 
			
		||||
  debug: false,
 | 
			
		||||
  port: 8080,
 | 
			
		||||
  pwdCost: 11)
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  PersonalMeasureApiConfig = object
 | 
			
		||||
    authSecret*: string
 | 
			
		||||
    dbConnString*: string
 | 
			
		||||
    debug*: bool
 | 
			
		||||
    port*: int
 | 
			
		||||
    pwdCost*: int
 | 
			
		||||
proc loadConfig*(args: Table[string, docopt.Value] = initTable[string, docopt.Value]()): PMApiConfig  =
 | 
			
		||||
 | 
			
		||||
  Session = object
 | 
			
		||||
    user*: User
 | 
			
		||||
    issuedAt*, expires*: Time
 | 
			
		||||
  let filePath =
 | 
			
		||||
      if args["--config"]: $args["--config"]
 | 
			
		||||
      else: "personal_measure.config.json"
 | 
			
		||||
 | 
			
		||||
proc newSession*(user: User): Session =
 | 
			
		||||
  result = Session(
 | 
			
		||||
    user: user,
 | 
			
		||||
    issuedAt: getTime().utc.trimNanoSec.toTime,
 | 
			
		||||
    expires: daysForward(1).trimNanoSec.toTime)
 | 
			
		||||
 | 
			
		||||
proc raiseEx*(reason: string): void =
 | 
			
		||||
  raise newException(Exception, reason)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
template halt(code: HttpCode,
 | 
			
		||||
              headers: RawHeaders,
 | 
			
		||||
              content: string): typed =
 | 
			
		||||
  ## Immediately replies with the specified request. This means any further
 | 
			
		||||
  ## code will not be executed after calling this template in the current
 | 
			
		||||
  ## route.
 | 
			
		||||
  bind TCActionSend, newHttpHeaders
 | 
			
		||||
  result[0] = CallbackAction.TCActionSend
 | 
			
		||||
  result[1] = code
 | 
			
		||||
  result[2] = some(headers)
 | 
			
		||||
  result[3] = content
 | 
			
		||||
  result.matched = true
 | 
			
		||||
  break allRoutes
 | 
			
		||||
 | 
			
		||||
template jsonResp(code: HttpCode, details: string = "", headers: RawHeaders = @{:} ) =
 | 
			
		||||
  halt(
 | 
			
		||||
    code,
 | 
			
		||||
    headers & @{"Content-Type": JSON},
 | 
			
		||||
    $(%* {
 | 
			
		||||
      "statusCode": code.int,
 | 
			
		||||
      "status": $code,
 | 
			
		||||
      "details": details
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
template json500Resp(ex: ref Exception, details: string = ""): void =
 | 
			
		||||
  when not defined(release): debug ex.getStackTrace()
 | 
			
		||||
  error details & ":\n" & ex.msg
 | 
			
		||||
  jsonResp(Http500)
 | 
			
		||||
 | 
			
		||||
proc toJWT*(cfg: PersonalMeasureApiConfig, session: Session): string =
 | 
			
		||||
  ## Make a JST token for this session.
 | 
			
		||||
  var jwt = JWT(
 | 
			
		||||
    header: JOSEHeader(alg: HS256, typ: "jwt"),
 | 
			
		||||
    claims: toClaims(%*{
 | 
			
		||||
      "sub": $(session.user.id),
 | 
			
		||||
      "iat": session.issuedAt.toUnix.int,
 | 
			
		||||
      "exp": session.expires.toUnix.int }))
 | 
			
		||||
 | 
			
		||||
  jwt.sign(cfg.authSecret)
 | 
			
		||||
  result = $jwt
 | 
			
		||||
 | 
			
		||||
proc fromJWT*(cfg: PersonalMeasureApiConfig, strTok: string): Session =
 | 
			
		||||
  ## Validate a given JWT and extract the session data.
 | 
			
		||||
  let jwt = toJWT(strTok)
 | 
			
		||||
  var secret = cfg.authSecret
 | 
			
		||||
  if not jwt.verify(secret): raiseEx "Unable to verify auth token."
 | 
			
		||||
  jwt.verifyTimeClaims()
 | 
			
		||||
 | 
			
		||||
  # Find the user record (if authenticated)
 | 
			
		||||
  let userId = jwt.claims["sub"].node.str
 | 
			
		||||
  let user = db.getUser(userId)
 | 
			
		||||
  if users.len != 1: raiseEx "Could not find session user."
 | 
			
		||||
 | 
			
		||||
  result = Session(
 | 
			
		||||
    user: user,
 | 
			
		||||
    issuedAt: fromUnix(jwt.claims["iat"].node.num),
 | 
			
		||||
    expires: fromUnix(jwt.claims["exp"].node.num))
 | 
			
		||||
 | 
			
		||||
proc extractSession(cfg: PersonalMeasureApiConfig, request: Request): Session =
 | 
			
		||||
  ## Helper to extract a session from a reqest.
 | 
			
		||||
 | 
			
		||||
  # Find the auth header
 | 
			
		||||
  if not request.headers.hasKey("Authorization"):
 | 
			
		||||
    raiseEx "No auth token."
 | 
			
		||||
 | 
			
		||||
  # Read and verify the JWT token
 | 
			
		||||
  let headerVal = request.headers["Authorization"]
 | 
			
		||||
  if not headerVal.startsWith("Bearer "):
 | 
			
		||||
    raiseEx "Invalid Authentication type (only 'Bearer' is supported)."
 | 
			
		||||
 | 
			
		||||
  result = fromJWT(cfg, headerVal[7..^1])
 | 
			
		||||
 | 
			
		||||
proc hashPwd*(pwd: string, cost: int8): string =
 | 
			
		||||
  let salt = genSalt(cost)
 | 
			
		||||
  result = hash(pwd, salt)
 | 
			
		||||
 | 
			
		||||
proc validatePwd*(u: ref User, givenPwd: string): bool =
 | 
			
		||||
  let salt = u.hashedPwd[0..28] # TODO: magic numbers
 | 
			
		||||
  result = compare(u.hashedPwd, hash(givenPwd, salt))
 | 
			
		||||
 | 
			
		||||
proc makeAuthToken*(cfg: PersonalMeasureApiConfig, uname, pwd: string): string =
 | 
			
		||||
  ## Given a username and pwd, validate the combination and generate a JWT
 | 
			
		||||
  ## token string.
 | 
			
		||||
 | 
			
		||||
  if uname.len == 0 or pwd.len == 0:
 | 
			
		||||
    raiseEx "fields 'username' and 'password' required"
 | 
			
		||||
 | 
			
		||||
  # find the user record
 | 
			
		||||
  let users = cfg.users.filterIt(it.name == uname)
 | 
			
		||||
  if users.len != 1: raiseEx "invalid username or password"
 | 
			
		||||
 | 
			
		||||
  let user = users[0]
 | 
			
		||||
 | 
			
		||||
  if not validatePwd(user, pwd): raiseEx "invalid username or password"
 | 
			
		||||
 | 
			
		||||
  let session = newSession(user)
 | 
			
		||||
 | 
			
		||||
  result = toJWT(cfg, session)
 | 
			
		||||
 | 
			
		||||
proc makeApiKey*(cfg: PersonalMeasureApiConfig, uname: string): string =
 | 
			
		||||
  ## Given a username, make an API token (JWT token string that does not
 | 
			
		||||
  ## expire). Note that this does not validate the username/pwd combination. It
 | 
			
		||||
  ## is not intended to be exposed publicly via the API, but serve as a utility
 | 
			
		||||
  ## function for an administrator to setup a unsupervised account (git access
 | 
			
		||||
  ## for example).
 | 
			
		||||
 | 
			
		||||
  if uname.len == 0: raiseEx "no username given"
 | 
			
		||||
 | 
			
		||||
  # find the user record
 | 
			
		||||
  let users = cfg.users.filterIt(it.name == uname)
 | 
			
		||||
  if users.len != 1: raiseEx "invalid username"
 | 
			
		||||
 | 
			
		||||
  let session = Session(
 | 
			
		||||
    user: users[0],
 | 
			
		||||
    issuedAt: getTime(),
 | 
			
		||||
    expires: daysForward(365 * 1000).toTime())
 | 
			
		||||
 | 
			
		||||
  result = toJWT(cfg, session);
 | 
			
		||||
 | 
			
		||||
template checkAuth() =
 | 
			
		||||
  ## 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.
 | 
			
		||||
 | 
			
		||||
  var session {.inject.}: Session
 | 
			
		||||
 | 
			
		||||
  try: session = extractSession(cfg, request)
 | 
			
		||||
  var json: JsonNode
 | 
			
		||||
  try: json = parseFile(filePath)
 | 
			
		||||
  except:
 | 
			
		||||
    debug "Auth failed: " & getCurrentExceptionMsg()
 | 
			
		||||
    jsonResp(Http401, "Unauthorized", @{"WWW-Authenticate": "Bearer"})
 | 
			
		||||
    json = %DEFAULT_CONFIG
 | 
			
		||||
    if not existsFile(filePath):
 | 
			
		||||
      info "created new configuration file \"" & filePath & "\""
 | 
			
		||||
      filePath.writeFile($json)
 | 
			
		||||
    else:
 | 
			
		||||
      warn "Cannot read configuration file \"" & filePath & "\":\n\t" &
 | 
			
		||||
        getCurrentExceptionMsg()
 | 
			
		||||
 | 
			
		||||
  let cfg = CombinedConfig(docopt: args, json: json)
 | 
			
		||||
 | 
			
		||||
proc start*(cfg: PersonalMeasureApiConfig): void =
 | 
			
		||||
  result = PMApiConfig(
 | 
			
		||||
    authSecret: cfg.getVal("authSecret"),
 | 
			
		||||
    dbConnString: cfg.getVal("dbConnString"),
 | 
			
		||||
    debug: "true".startsWith(cfg.getVal("debug", "false").toLower()),
 | 
			
		||||
    port: parseInt(cfg.getVal("port", "8080")),
 | 
			
		||||
    pwdCost: parseInt(cfg.getVal("pwdCost", "11")))
 | 
			
		||||
    
 | 
			
		||||
proc initContext(args: Table[string, docopt.Value]): PMApiContext =
 | 
			
		||||
 | 
			
		||||
  var stopFuture = newFuture[void]()
 | 
			
		||||
  var cfg: PMApiConfig
 | 
			
		||||
  var db: PMApiDb
 | 
			
		||||
 | 
			
		||||
  settings:
 | 
			
		||||
    port = Port(cfg.port)
 | 
			
		||||
    appName = "/api"
 | 
			
		||||
  try: cfg = loadConfig(args)
 | 
			
		||||
  except: raiseEx "Unable to load configuration: \n\t" & getCurrentExceptionMsg()
 | 
			
		||||
 | 
			
		||||
  routes:
 | 
			
		||||
 | 
			
		||||
    get "/version":
 | 
			
		||||
      resp($(%("personal_measure_api v" & PM_API_VERSION)), JSON)
 | 
			
		||||
 | 
			
		||||
    post "/service/debug/stop":
 | 
			
		||||
      if not cfg.debug: jsonResp(Http404)
 | 
			
		||||
      else:
 | 
			
		||||
        let shutdownFut = sleepAsync(100)
 | 
			
		||||
        shutdownFut.callback = proc(): void = complete(stopFuture)
 | 
			
		||||
        resp($(%"shutting down"), JSON)
 | 
			
		||||
 | 
			
		||||
  waitFor(stopFuture)
 | 
			
		||||
  try: db = connect(cfg.dbConnString)
 | 
			
		||||
  except: raiseEx "Unable to connect to the database:\n\t" & getCurrentExceptionMsg()
 | 
			
		||||
 | 
			
		||||
  result = PMApiContext(cfg: cfg, db: db)
 | 
			
		||||
 | 
			
		||||
when isMainModule:
 | 
			
		||||
  start(PersonalMeasureApiConfig(
 | 
			
		||||
    debug: true,
 | 
			
		||||
    port: 8090,
 | 
			
		||||
    pwdCost: 11,
 | 
			
		||||
    dbConnString: "host=localhost port=5500 username=postgres password=password dbname=personal_measure"))
 | 
			
		||||
 | 
			
		||||
  try:
 | 
			
		||||
    let doc = """
 | 
			
		||||
Usage:
 | 
			
		||||
  personal_measure_api test [options]
 | 
			
		||||
  personal_measure_api serve [options]
 | 
			
		||||
 | 
			
		||||
Options:
 | 
			
		||||
 | 
			
		||||
  -C, --config <cfgFile>    Location of the config file (defaults to personal_measure.config.json)
 | 
			
		||||
"""
 | 
			
		||||
    logging.addHandler(newConsoleLogger())
 | 
			
		||||
 | 
			
		||||
    # Initialize our service context
 | 
			
		||||
    let args = docopt(doc, version = PM_API_VERSION)
 | 
			
		||||
    let ctx = initContext(args)
 | 
			
		||||
 | 
			
		||||
    if args["test"]:
 | 
			
		||||
      echo "Test"
 | 
			
		||||
 | 
			
		||||
    if args["serve"]:
 | 
			
		||||
      start(PMApiConfig(
 | 
			
		||||
        debug: true,
 | 
			
		||||
        port: 8090,
 | 
			
		||||
        pwdCost: 11,
 | 
			
		||||
        dbConnString: "host=localhost port=5500 username=postgres password=password dbname=personal_measure"))
 | 
			
		||||
  except:
 | 
			
		||||
    fatal "pit: " & getCurrentExceptionMsg()
 | 
			
		||||
    #raise getCurrentException()
 | 
			
		||||
    quit(QuitFailure)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										146
									
								
								api/src/main/nim/personal_measure_apipkg/api.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								api/src/main/nim/personal_measure_apipkg/api.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
			
		||||
import asyncdispatch, jester, json, jwt, strutils, times, timeutils, uuids
 | 
			
		||||
 | 
			
		||||
import ./db
 | 
			
		||||
import ./configuration
 | 
			
		||||
import ./models
 | 
			
		||||
import ./service
 | 
			
		||||
 | 
			
		||||
const JSON = "application/json"
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  Session* = object
 | 
			
		||||
    user*: User
 | 
			
		||||
    issuedAt*, expires*: Time
 | 
			
		||||
 | 
			
		||||
proc newSession*(user: User): Session =
 | 
			
		||||
  result = Session(
 | 
			
		||||
    user: user,
 | 
			
		||||
    issuedAt: getTime().utc.trimNanoSec.toTime,
 | 
			
		||||
    expires: daysForward(1).trimNanoSec.toTime)
 | 
			
		||||
 | 
			
		||||
template halt(code: HttpCode,
 | 
			
		||||
              headers: RawHeaders,
 | 
			
		||||
              content: string): typed =
 | 
			
		||||
  ## Immediately replies with the specified request. This means any further
 | 
			
		||||
  ## code will not be executed after calling this template in the current
 | 
			
		||||
  ## route.
 | 
			
		||||
  bind TCActionSend, newHttpHeaders
 | 
			
		||||
  result[0] = CallbackAction.TCActionSend
 | 
			
		||||
  result[1] = code
 | 
			
		||||
  result[2] = some(headers)
 | 
			
		||||
  result[3] = content
 | 
			
		||||
  result.matched = true
 | 
			
		||||
  break allRoutes
 | 
			
		||||
 | 
			
		||||
template jsonResp(code: HttpCode, details: string = "", headers: RawHeaders = @{:} ) =
 | 
			
		||||
  halt(
 | 
			
		||||
    code,
 | 
			
		||||
    headers & @{"Content-Type": JSON},
 | 
			
		||||
    $(%* {
 | 
			
		||||
      "statusCode": code.int,
 | 
			
		||||
      "status": $code,
 | 
			
		||||
      "details": details
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
template json500Resp(ex: ref Exception, details: string = ""): void =
 | 
			
		||||
  when not defined(release): debug ex.getStackTrace()
 | 
			
		||||
  error details & ":\n" & ex.msg
 | 
			
		||||
  jsonResp(Http500)
 | 
			
		||||
 | 
			
		||||
proc toJWT*(ctx: PMApiContext, session: Session): string =
 | 
			
		||||
  ## Make a JST token for this session.
 | 
			
		||||
  var jwt = JWT(
 | 
			
		||||
    header: JOSEHeader(alg: HS256, typ: "jwt"),
 | 
			
		||||
    claims: toClaims(%*{
 | 
			
		||||
      "sub": $(session.user.id),
 | 
			
		||||
      "iat": session.issuedAt.toUnix.int,
 | 
			
		||||
      "exp": session.expires.toUnix.int }))
 | 
			
		||||
 | 
			
		||||
  jwt.sign(ctx.cfg.authSecret)
 | 
			
		||||
  result = $jwt
 | 
			
		||||
 | 
			
		||||
proc fromJWT*(ctx: PMApiContext, strTok: string): Session =
 | 
			
		||||
  ## Validate a given JWT and extract the session data.
 | 
			
		||||
  let jwt = toJWT(strTok)
 | 
			
		||||
  var secret = ctx.cfg.authSecret
 | 
			
		||||
  if not jwt.verify(secret): raiseEx "Unable to verify auth token."
 | 
			
		||||
  jwt.verifyTimeClaims()
 | 
			
		||||
 | 
			
		||||
  # Find the user record (if authenticated)
 | 
			
		||||
  let userId = jwt.claims["sub"].node.str
 | 
			
		||||
  var user: User
 | 
			
		||||
  try: user = ctx.db.getUser(parseUUID(userId))
 | 
			
		||||
  except: raiseEx "unknown user"
 | 
			
		||||
 | 
			
		||||
  result = Session(
 | 
			
		||||
    user: user,
 | 
			
		||||
    issuedAt: fromUnix(jwt.claims["iat"].node.num),
 | 
			
		||||
    expires: fromUnix(jwt.claims["exp"].node.num))
 | 
			
		||||
 | 
			
		||||
proc extractSession(ctx: PMApiContext, request: Request): Session =
 | 
			
		||||
  ## Helper to extract a session from a reqest.
 | 
			
		||||
 | 
			
		||||
  # Find the auth header
 | 
			
		||||
  if not request.headers.hasKey("Authorization"):
 | 
			
		||||
    raiseEx "No auth token."
 | 
			
		||||
 | 
			
		||||
  # Read and verify the JWT token
 | 
			
		||||
  let headerVal = request.headers["Authorization"]
 | 
			
		||||
  if not headerVal.startsWith("Bearer "):
 | 
			
		||||
    raiseEx "Invalid Authentication type (only 'Bearer' is supported)."
 | 
			
		||||
 | 
			
		||||
  result = fromJWT(ctx, headerVal[7..^1])
 | 
			
		||||
 | 
			
		||||
proc makeAuthToken*(ctx: PMApiContext, email, pwd: string): string =
 | 
			
		||||
  ## Given a user's email and pwd, validate the combination and generate a JWT
 | 
			
		||||
  ## token string.
 | 
			
		||||
 | 
			
		||||
  if email.len == 0 or pwd.len == 0:
 | 
			
		||||
    raiseEx "fields 'username' and 'password' required"
 | 
			
		||||
 | 
			
		||||
  # find the user record
 | 
			
		||||
  var user: User
 | 
			
		||||
  try: user = ctx.db.getUserByEmail(email)
 | 
			
		||||
  except: raiseEx "invalid username or password"
 | 
			
		||||
 | 
			
		||||
  if not validatePwd(user, pwd): raiseEx "invalid username or password"
 | 
			
		||||
 | 
			
		||||
  let session = newSession(user)
 | 
			
		||||
 | 
			
		||||
  result = toJWT(ctx, session)
 | 
			
		||||
 | 
			
		||||
template checkAuth() =
 | 
			
		||||
  ## 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.
 | 
			
		||||
 | 
			
		||||
  var session {.inject.}: Session
 | 
			
		||||
 | 
			
		||||
  try: session = extractSession(cfg, request)
 | 
			
		||||
  except:
 | 
			
		||||
    debug "Auth failed: " & getCurrentExceptionMsg()
 | 
			
		||||
    jsonResp(Http401, "Unauthorized", @{"WWW-Authenticate": "Bearer"})
 | 
			
		||||
 | 
			
		||||
proc start*(cfg: PMApiConfig): void =
 | 
			
		||||
 | 
			
		||||
  var stopFuture = newFuture[void]()
 | 
			
		||||
 | 
			
		||||
  settings:
 | 
			
		||||
    port = Port(cfg.port)
 | 
			
		||||
    appName = "/api"
 | 
			
		||||
 | 
			
		||||
  routes:
 | 
			
		||||
 | 
			
		||||
    get "/version":
 | 
			
		||||
      resp($(%("personal_measure_api v" & PM_API_VERSION)), JSON)
 | 
			
		||||
 | 
			
		||||
    post "/service/debug/stop":
 | 
			
		||||
      if not cfg.debug: jsonResp(Http404)
 | 
			
		||||
      else:
 | 
			
		||||
        let shutdownFut = sleepAsync(100)
 | 
			
		||||
        shutdownFut.callback = proc(): void = complete(stopFuture)
 | 
			
		||||
        resp($(%"shutting down"), JSON)
 | 
			
		||||
 | 
			
		||||
  waitFor(stopFuture)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								api/src/main/nim/personal_measure_apipkg/configuration.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/src/main/nim/personal_measure_apipkg/configuration.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
import ./db
 | 
			
		||||
 | 
			
		||||
type
 | 
			
		||||
  PMApiConfig* = object
 | 
			
		||||
    authSecret*: string
 | 
			
		||||
    dbConnString*: string
 | 
			
		||||
    debug*: bool
 | 
			
		||||
    port*: int
 | 
			
		||||
    pwdCost*: int
 | 
			
		||||
 | 
			
		||||
  PMApiContext* = object
 | 
			
		||||
    cfg*: PMApiConfig
 | 
			
		||||
    db*: PMApiDb
 | 
			
		||||
 | 
			
		||||
proc `%`*(cfg: PMApiConfig): JsonNode =
 | 
			
		||||
  result = %* {
 | 
			
		||||
    "authSecret": cfg.authSecret,
 | 
			
		||||
    "dbConnString": cfg.dbConnString,
 | 
			
		||||
    "debug": cfg.debug,
 | 
			
		||||
    "port": cfg.port,
 | 
			
		||||
    "pwdCost": cfg.pwdCost }
 | 
			
		||||
 | 
			
		||||
proc raiseEx*(reason: string): void =
 | 
			
		||||
  raise newException(Exception, reason)
 | 
			
		||||
 | 
			
		||||
@@ -4,14 +4,32 @@ import db_postgres, macros, options, postgres, sequtils, strutils, times,
 | 
			
		||||
import ./models
 | 
			
		||||
import ./db_common
 | 
			
		||||
 | 
			
		||||
# proc create: Typed create methods for specific records
 | 
			
		||||
proc createUser*(db: DbConn, user: User): User = return db.createRecord(user)
 | 
			
		||||
proc createApiToken*(db: DbConn, token: ApiToken): ApiToken = return db.createRecord(token)
 | 
			
		||||
proc createMeasure*(db: DbConn, measure: Measure): Measure = return db.createRecord(measure)
 | 
			
		||||
proc createValue*(db: DbConn, value: Value): Value = return db.createRecord(value)
 | 
			
		||||
type
 | 
			
		||||
  PMApiDb* = ref object
 | 
			
		||||
    conn: DbConn
 | 
			
		||||
 | 
			
		||||
proc getUser*(db: DbConn, id: UUID): User = return db.getRecord(User, id)
 | 
			
		||||
proc getUser*(db: DbConn, id: string): User = return db.getRecord(User, parseUUID(id))
 | 
			
		||||
proc getApiToken*(db: DbConn, id: UUID): ApiToken = return db.getRecord(ApiToken, id)
 | 
			
		||||
proc getMeasure*(db: DbConn, id: UUID): Measure = return db.getRecord(Measure, id)
 | 
			
		||||
proc getValue*(db: DbConn, id: UUID): Value = return db.getRecord(Value, id)
 | 
			
		||||
  
 | 
			
		||||
proc connect*(connString: string): PMApiDb =
 | 
			
		||||
  result = PMApiDb(conn: open("", "", "", connString))
 | 
			
		||||
 | 
			
		||||
macro makeGetRecord(modelType: type): untyped =
 | 
			
		||||
  echo modelType.getType.treeRepr
 | 
			
		||||
#[
 | 
			
		||||
  let procIdent = ident("get" & $modelType.getType[1])
 | 
			
		||||
  return quote do:
 | 
			
		||||
    proc `procIdent`*(db: PMApiDb, id: UUID): `modelType` = return db.conn.getRecord(`modelType`, id)
 | 
			
		||||
]#
 | 
			
		||||
 | 
			
		||||
proc createUser*(db: PMApiDb, user: User): User = return db.conn.createRecord(user)
 | 
			
		||||
proc createApiToken*(db: PMApiDb, token: ApiToken): ApiToken = return db.conn.createRecord(token)
 | 
			
		||||
proc createMeasure*(db: PMApiDb, measure: Measure): Measure = return db.conn.createRecord(measure)
 | 
			
		||||
proc createValue*(db: PMApiDb, value: Value): Value = return db.conn.createRecord(value)
 | 
			
		||||
 | 
			
		||||
proc getUser*(db: PMApiDb, id: UUID): User = return db.conn.getRecord(User, id)
 | 
			
		||||
proc getUser*(db: PMApiDb, id: string): User = return db.conn.getRecord(User, parseUUID(id))
 | 
			
		||||
proc getApiToken*(db: PMApiDb, id: UUID): ApiToken = return db.conn.getRecord(ApiToken, id)
 | 
			
		||||
proc getMeasure*(db: PMApiDb, id: UUID): Measure = return db.conn.getRecord(Measure, id)
 | 
			
		||||
proc getValue*(db: PMApiDb, id: UUID): Value = return db.conn.getRecord(Value, id)
 | 
			
		||||
 | 
			
		||||
#proc getUsersWhere*(db: PMApiDb, whereClause: string, values: varargs[string, dbFormat]): User =
 | 
			
		||||
#  return db.conn.getRecordsWhere(User, whereClause, values)
 | 
			
		||||
 
 | 
			
		||||
@@ -40,8 +40,19 @@ template getRecord*(db: DbConn, modelType: type, id: UUID): untyped =
 | 
			
		||||
    "SELECT " & columnNamesForModel(modelType).join(",") &
 | 
			
		||||
    " FROM " & tableName(modelType) &
 | 
			
		||||
    " WHERE id = ?"), @[$id])
 | 
			
		||||
 | 
			
		||||
  if row.allIt(it.len == 0):
 | 
			
		||||
    raise newException(KeyError, "no record for id " & $id)
 | 
			
		||||
 | 
			
		||||
  rowToModel(modelType, row)
 | 
			
		||||
 | 
			
		||||
template getRecordsWhere*(db: DbConn, modelType: type, whereClause: string, values: varargs[string, dbFormat]): untyped =
 | 
			
		||||
  db.getAllRows(sql(
 | 
			
		||||
    "SELECT " & columnNamesForModel(modelType).join(",") &
 | 
			
		||||
    " FROM " & tableName(modelType) &
 | 
			
		||||
    " WHERE " & whereClause), values)
 | 
			
		||||
    .mapIt(rowToModel(modelType, it))
 | 
			
		||||
 | 
			
		||||
template getAllRecords*(db: DbConn, modelType: type): untyped =
 | 
			
		||||
  db.getAllRows(sql(
 | 
			
		||||
    "SELECT " & columnNamesForModel(modelType).join(",") &
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								api/src/main/nim/personal_measure_apipkg/service.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								api/src/main/nim/personal_measure_apipkg/service.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
import bcrypt
 | 
			
		||||
 | 
			
		||||
import ./configuration
 | 
			
		||||
import ./db
 | 
			
		||||
import ./models
 | 
			
		||||
 | 
			
		||||
proc hashPwd*(pwd: string, cost: int8): string =
 | 
			
		||||
  let salt = genSalt(cost)
 | 
			
		||||
  result = hash(pwd, salt)
 | 
			
		||||
 | 
			
		||||
proc validatePwd*(u: User, givenPwd: string): bool =
 | 
			
		||||
  let salt = u.hashedPwd[0..28] # TODO: magic numbers
 | 
			
		||||
  result = compare(u.hashedPwd, hash(givenPwd, salt))
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user