From e3450d5f8fa088b17eb07f27ba2d0d60b5a8423a Mon Sep 17 00:00:00 2001
From: Jonathan Bernard <jonathan@jdbernard.com>
Date: Tue, 19 Feb 2019 02:50:07 -0600
Subject: [PATCH] Beginning implementation of the planned API endpoints.

---
 api/api.txt                                   | 16 +++--
 ....json => personal_measure_api.config.json} |  0
 api/src/main/nim/personal_measure_api.nim     | 31 ++++++---
 .../main/nim/personal_measure_apipkg/api.nim  | 69 ++++++++++++++++---
 .../nim/personal_measure_apipkg/models.nim    | 43 +++++++++++-
 .../20190214122514-initial-schema-up.sql      |  2 +-
 6 files changed, 130 insertions(+), 31 deletions(-)
 rename api/{personal_measure.config.json => personal_measure_api.config.json} (100%)

diff --git a/api/api.txt b/api/api.txt
index 67ac8d3..395404c 100644
--- a/api/api.txt
+++ b/api/api.txt
@@ -3,10 +3,10 @@ Personal Measure API
 
 ### Measure
 
-☐   GET     /measure                  Get a list of all defined measures for this user.
-☐   POST    /measure                  Create a new measure (post the definition).
-☐   GET     /measure/<measure-slug>   Get the definition for a specific measure.
-☐   DELETE  /measure/<measure-slug>   Delete a measure (and all values associated with it).
+☐   GET     /measures                 Get a list of all defined measures for this user.
+☐   POST    /measures                 Create a new measure (post the definition).
+☐   GET     /measures/<measure-slug>  Get the definition for a specific measure.
+☐   DELETE  /measures/<measure-slug>  Delete a measure (and all values associated with it).
 
 ### Values
 
@@ -18,9 +18,11 @@ Personal Measure API
 
 ### Auth
 
-☐   GET     /auth-token               Given a valid username/password combo, get an auth token.
-☐   GET     /user                     Given a valid auth token, return the user details.
-☐   PSOT    /app-token                With a valid session, create a new app token.
+☑   GET     /auth-token               Given a valid username/password combo, get an auth token.
+☑   GET     /user                     Given a valid auth token, return the user details.
+☐   GET     /api-tokens               List api tokens.
+☐   DELETE  /api-tokens/<id>          Delete a specific api token.
+☐   POST    /api-tokens               With a valid session, create a new api token.
 
 Legend
 ------
diff --git a/api/personal_measure.config.json b/api/personal_measure_api.config.json
similarity index 100%
rename from api/personal_measure.config.json
rename to api/personal_measure_api.config.json
diff --git a/api/src/main/nim/personal_measure_api.nim b/api/src/main/nim/personal_measure_api.nim
index 6b904cf..ba61b68 100644
--- a/api/src/main/nim/personal_measure_api.nim
+++ b/api/src/main/nim/personal_measure_api.nim
@@ -1,8 +1,12 @@
 import cliutils, docopt, logging, jester, json, os, strutils, tables
 
-import personal_measure_apipkg/configuration
-import personal_measure_apipkg/version
 import personal_measure_apipkg/api
+import personal_measure_apipkg/configuration
+import personal_measure_apipkg/service
+import personal_measure_apipkg/version
+
+import personal_measure_apipkg/db
+import personal_measure_apipkg/models
 
 const DEFAULT_CONFIG = PMApiConfig(
   authSecret: "change me",
@@ -15,7 +19,7 @@ proc loadConfig*(args: Table[string, docopt.Value] = initTable[string, docopt.Va
 
   let filePath =
       if args["--config"]: $args["--config"]
-      else: "personal_measure.config.json"
+      else: "personal_measure_api.config.json"
 
   var json: JsonNode
   try: json = parseFile(filePath)
@@ -57,6 +61,7 @@ when isMainModule:
 Usage:
   personal_measure_api test [options]
   personal_measure_api serve [options]
+  personal_measure_api hashpwd <password> [<cost>] [options]
 
 Options:
 
@@ -68,15 +73,19 @@ Options:
     let args = docopt(doc, version = PM_API_VERSION)
     let ctx = initContext(args)
 
-    if args["test"]:
-      echo "Test"
+    if args["hashpwd"]:
+      let cost =
+        if args["<cost>"]: parseInt($args["<cost>"])
+        else: 11
+
+      echo hashPwd($args["<password>"], cast[int8](cost))
+
+    if args["test"]:
+      echo "test"
+      echo ctx.db.getUserByEmail("jonathan@jdbernard.com")
+
+    if args["serve"]: start(ctx)
 
-    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()
diff --git a/api/src/main/nim/personal_measure_apipkg/api.nim b/api/src/main/nim/personal_measure_apipkg/api.nim
index e794adb..0df45c2 100644
--- a/api/src/main/nim/personal_measure_apipkg/api.nim
+++ b/api/src/main/nim/personal_measure_apipkg/api.nim
@@ -1,9 +1,6 @@
-import asyncdispatch, jester, json, jwt, strutils, times, timeutils, uuids
-
-import ./db
-import ./configuration
-import ./models
-import ./service
+import asyncdispatch, jester, json, jwt, logging, options, sequtils, strutils,
+  times, timeutils, uuids
+import ./db, ./configuration, ./models, ./service, ./version
 
 const JSON = "application/json"
 
@@ -101,7 +98,10 @@ proc makeAuthToken*(ctx: PMApiContext, email, pwd: string): string =
 
   # find the user record
   var user: User
-  try: user = ctx.db.getUserByEmail(email)
+  try:
+    let users = ctx.db.getUserByEmail(email)
+    if users.len != 1: raiseEx ""
+    user = users[0]
   except: raiseEx "invalid username or password"
 
   if not validatePwd(user, pwd): raiseEx "invalid username or password"
@@ -117,17 +117,19 @@ template checkAuth() =
 
   var session {.inject.}: Session
 
-  try: session = extractSession(cfg, request)
+  try: session = extractSession(ctx, request)
   except:
     debug "Auth failed: " & getCurrentExceptionMsg()
     jsonResp(Http401, "Unauthorized", @{"WWW-Authenticate": "Bearer"})
 
-proc start*(cfg: PMApiConfig): void =
+proc start*(ctx: PMApiContext): void =
+
+  if ctx.cfg.debug: setLogFilter(lvlDebug)
 
   var stopFuture = newFuture[void]()
 
   settings:
-    port = Port(cfg.port)
+    port = Port(ctx.cfg.port)
     appName = "/api"
 
   routes:
@@ -135,12 +137,57 @@ proc start*(cfg: PMApiConfig): void =
     get "/version":
       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 authToken = makeAuthToken(ctx, email, pwd)
+        resp($(%authToken), JSON)
+      except: jsonResp(Http401, getCurrentExceptionMsg())
+
+    get "/user":
+      checkAuth()
+
+      resp(Http200, $(%session.user), JSON)
+
     post "/service/debug/stop":
-      if not cfg.debug: jsonResp(Http404)
+      if not ctx.cfg.debug: jsonResp(Http404)
       else:
         let shutdownFut = sleepAsync(100)
         shutdownFut.callback = proc(): void = complete(stopFuture)
         resp($(%"shutting down"), JSON)
 
+    get "/api-tokens":
+      checkAuth()
+
+      resp(Http200, $(%ctx.db.getApiTokenByUserId($session.user.id)))
+
+    post "/api-tokens":
+      checkAuth()
+
+      var newToken: ApiToken
+      try:
+        let jsonBody = parseJson(request.body)
+        newToken = ApiToken(
+          id: genUUID(),
+          userId: session.user.id,
+          name: jsonBody["name"].getStr,
+          expires: none[DateTime](),
+          hashedToken:  "")
+      except: jsonResp(Http400)
+
+      try:
+        let tokenValue = "" # TODO
+        newToken.hashedToken = hashPwd(tokenValue)
+        ctx.db.createApiToken(token)
+        let respToken = %newToken
+        newToken["value"] = tokenValue
+        resp($newToken, JSON)
+
   waitFor(stopFuture)
 
diff --git a/api/src/main/nim/personal_measure_apipkg/models.nim b/api/src/main/nim/personal_measure_apipkg/models.nim
index c57877f..f4b3677 100644
--- a/api/src/main/nim/personal_measure_apipkg/models.nim
+++ b/api/src/main/nim/personal_measure_apipkg/models.nim
@@ -1,4 +1,4 @@
-import options, times, timeutils, uuids
+import json, options, times, timeutils, uuids
 
 type
   User* = object
@@ -46,3 +46,44 @@ proc `$`*(m: Measure): string =
 
 proc `$`*(v: Value): string =
   return "Value " & ($v.id)[0..6] & " - " & ($v.measureId)[0..6] & " = " & $v.value
+
+proc `%`*(u: User): JsonNode =
+  result = %*{
+    "id": $u.id,
+    "email": u.email,
+    "displayName": u.displayName
+  }
+
+proc `%`*(tok: ApiToken): JsonNode =
+  result = %*{
+    "id": $tok.id,
+    "userId": $tok.userId,
+    "name": tok.name
+  }
+
+  if tok.expires.isSome:
+    result["expires"] = %(tok.expires.get.formatIso8601)
+
+proc `%`*(m: Measure): JsonNode =
+  result = %*{
+    "id": $m.id,
+    "userId": $m.userId,
+    "slug": m.slug,
+    "name": m.name,
+    "description": m.description,
+    "domainUnits": m.domainUnits,
+    "rangeUnits": m.rangeUnits,
+    "analysis": m.analysis
+  }
+
+  if m.domainSource.isSome: result["domainSource"] = %(m.domainSource.get)
+  if m.rangeSource.isSome: result["rangeSource"] = %(m.rangeSource.get)
+
+proc `%`*(v: Value): JsonNode =
+  result = %*{
+    "id": $v.id,
+    "measureId": $v.measureId,
+    "value": v.value,
+    "timestampe": v.timestamp.formatIso8601,
+    "extData": v.extData
+  }
diff --git a/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql b/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql
index 0208ea8..28b329c 100644
--- a/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql
+++ b/api/src/main/sql/migrations/20190214122514-initial-schema-up.sql
@@ -4,7 +4,7 @@ create extension if not exists "uuid-ossp";
 create table "users" (
   id uuid default uuid_generate_v4() primary key,
   display_name varchar not null,
-  email varchar not null,
+  email varchar not null unique,
   hashed_pwd varchar not null
 );