Refactor to add API layer.
This commit is contained in:
parent
03da2e9cd9
commit
0d4827453a
141
private/api.nim
141
private/api.nim
@ -2,14 +2,17 @@
|
||||
## ===================================
|
||||
|
||||
import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging,
|
||||
ospaths, sequtils, strutils, os
|
||||
ospaths, sequtils, strutils, os, times, uuids
|
||||
|
||||
import nre except toSeq
|
||||
|
||||
import ./models
|
||||
import ./util
|
||||
import ./version
|
||||
|
||||
type
|
||||
PtkUser* = object
|
||||
username*, salt*, pwdhash*, timelinePath: string
|
||||
username*, salt*, pwdhash*, timelinePath*: string
|
||||
isAdmin*: bool
|
||||
|
||||
PtkApiCfg* = object
|
||||
@ -20,7 +23,19 @@ type
|
||||
const TXT = "text/plain"
|
||||
const JSON = "application/json"
|
||||
|
||||
proc raiseEx(reason: string): void = raise newException(Exception, reason)
|
||||
proc parseUser(json: JsonNode): PtkUser =
|
||||
result = Ptkuser(
|
||||
username: json.getOrFail("username").getStr,
|
||||
salt: json.getOrFail("salt").getStr,
|
||||
pwdHash: json.getOrFail("pwdhash").getStr,
|
||||
timelinePath: json.getOrFail("timelinePath").getStr,
|
||||
isAdmin: json.getIfExists("isAdmin").getBool(false))
|
||||
|
||||
proc loadApiConfig*(json: JsonNode): PtkApiCfg =
|
||||
result = PtkApiCfg(
|
||||
port: parseInt(json.getIfExists("port").getStr("3280")),
|
||||
dataDir: json.getOrFail("dataDir").getStr,
|
||||
users: json.getIfExists("users").getElems(@[]).mapIt(parseUser(it)))
|
||||
|
||||
template checkAuth(cfg: PtkApiCfg) =
|
||||
## Check this request for authentication and authorization information.
|
||||
@ -69,18 +84,8 @@ proc parseAndRun(user: PtkUser, cmd: string, params: StringTableRef): string =
|
||||
if execResult[2] != 0: raiseEx(stripAnsi($execResult[0] & "\n" & $execResult[1]))
|
||||
else: return stripAnsi(execResult[0])
|
||||
|
||||
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 objName & " missing key '" & key & "'"
|
||||
return n[key]
|
||||
|
||||
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 parseUser(json: JsonNode): PtkUser =
|
||||
let salt = gensalt(12)
|
||||
proc apiParseUser(json: JsonNode): PtkUser =
|
||||
let salt = genSalt(12)
|
||||
|
||||
return PtkUser(
|
||||
username: json.getOrFail("username").getStr,
|
||||
@ -89,7 +94,24 @@ proc parseUser(json: JsonNode): PtkUser =
|
||||
timelinePath: json.getIfExists("timelinePath").getStr(""),
|
||||
isAdmin: false)
|
||||
|
||||
proc start*(cfg: PtkApiCfg) =
|
||||
proc apiParseMark(json: JsonNode): Mark =
|
||||
|
||||
if not json.hasKey("id"): json["id"] = %($genUUID())
|
||||
if not json.hasKey("summary"): raiseEx "cannot parse mark: missing 'summary'"
|
||||
if not json.hasKey("time"): json["time"] = %(getTime().local.format(ISO_TIME_FORMAT))
|
||||
|
||||
return parseMark(json)
|
||||
|
||||
proc patchMark(m: Mark, j: JsonNode): Mark =
|
||||
result = m
|
||||
|
||||
if j.hasKey("summary"): result.summary = j["summary"].getStr
|
||||
if j.hasKey("notes"): result.notes = j["notes"].getStr
|
||||
if j.hasKey("tags"):
|
||||
result.tags = j["tags"].getElems(@[]).map(proc (t: JsonNode): string = t.getStr())
|
||||
if j.hasKey("time"): result.time = parse(j["time"].getStr(), ISO_TIME_FORMAT)
|
||||
|
||||
proc start_api*(cfg: PtkApiCfg) =
|
||||
|
||||
var stopFuture = newFuture[void]()
|
||||
|
||||
@ -99,7 +121,7 @@ proc start*(cfg: PtkApiCfg) =
|
||||
|
||||
routes:
|
||||
|
||||
get "/ping": resp("pong", TXT)
|
||||
get "/version": resp("ptk v" & PTK_VERSION, TXT)
|
||||
|
||||
get "/marks":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
@ -107,21 +129,98 @@ proc start*(cfg: PtkApiCfg) =
|
||||
try: resp(parseAndRun(user, "list", request.params), TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/continue":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
|
||||
try: resp(parseAndRun(user, "continue", request.params), TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/sum-time":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
|
||||
try: resp(parseAndRun(user, "sum-time", request.params), TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/mark":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
|
||||
var newMark: Mark
|
||||
try: newMark = parseMark(parseJson(request.body))
|
||||
try: newMark = apiParseMark(parseJson(request.body))
|
||||
except: resp(Http400, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
var params = newStringTable
|
||||
try:
|
||||
var timeline = loadTimeline(user.timelinePath)
|
||||
timeline.marks.add(newMark)
|
||||
saveTimeline(timeline, user.timelinePath)
|
||||
resp(Http201, "ok", TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/stop":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
|
||||
var newMark: Mark
|
||||
try:
|
||||
var json = parseJson(request.body)
|
||||
json["summary"] = %STOP_MSG
|
||||
json["id"] = %($genUUID())
|
||||
newMark = apiParseMark(json)
|
||||
except: resp(Http400, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
try:
|
||||
var timeline = loadTimeline(user.timelinePath)
|
||||
timeline.marks.add(newMark)
|
||||
saveTimeline(timeline, user.timelinePath)
|
||||
resp(Http201, "ok", TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/resume/@id":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
|
||||
var timeline: Timeline
|
||||
try: timeline = loadTimeline(user.timelinePath)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
var newMark: Mark
|
||||
try:
|
||||
let origMarkIdx = timeline.marks.findById(@"id")
|
||||
if origMarkIdx < 0: resp(Http404, "no mark for id: " & @"id", TXT)
|
||||
let origMark = timeline.marks[origMarkIdx]
|
||||
|
||||
newMark = origMark
|
||||
newMark.id = genUUID()
|
||||
newMark.time = getTime().local
|
||||
newMark = newMark.patchMark(parseJson(request.body))
|
||||
except: resp(Http400, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
try:
|
||||
timeline.marks.add(newMark)
|
||||
timeline.saveTimeline(user.timelinePath)
|
||||
resp(Http201, "ok", TXT)
|
||||
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/amend/@id":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
|
||||
try:
|
||||
var timeline = loadTimeline(user.timelinePath)
|
||||
|
||||
let idx = timeline.marks.findById(@"id")
|
||||
if idx < 0: resp(Http404, "no mark for id: " & @"id", TXT)
|
||||
|
||||
timeline.marks[idx] = timeline.marks[idx].patchMark(parseJson(request.body))
|
||||
|
||||
timeline.saveTimeline(user.timelinePath)
|
||||
resp(Http202, $(%timeline.marks[idx]), JSON)
|
||||
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/users":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
if not user.isAdmin: resp(Http403, "insufficient permission", TXT)
|
||||
|
||||
var newUser: PtkUser
|
||||
try: newUser = parseUser(parseJson(request.body))
|
||||
try: newUser = apiParseUser(parseJson(request.body))
|
||||
except: resp(Http400, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
if cfg.users.anyIt(it.username == newUser.username):
|
||||
@ -136,3 +235,5 @@ proc start*(cfg: PtkApiCfg) =
|
||||
resp(Http200, "ok", TXT)
|
||||
|
||||
except: resp(Http500, "could not init new user timeline", TXT)
|
||||
|
||||
waitFor(stopFuture)
|
||||
|
@ -1,5 +1,7 @@
|
||||
import algorithm, json, sequtils, strutils, times, timeutils, uuids
|
||||
|
||||
import ./util
|
||||
|
||||
type
|
||||
Mark* = tuple[id: UUID, time: DateTime, summary: string, notes: string, tags: seq[string]]
|
||||
## Representation of a single mark on the timeline.
|
||||
@ -25,9 +27,18 @@ const TIME_FORMATS* = @[
|
||||
"HH:mm:ss", "H:mm:ss", "H:mm", "HH:mm" ]
|
||||
## Other time formats that PTK will accept as input.
|
||||
|
||||
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 objName & " missing key '" & key & "'"
|
||||
return n[key]
|
||||
|
||||
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 parseTime*(timeStr: string): DateTime =
|
||||
## Helper to parse time strings trying multiple known formats.
|
||||
## Helper to parse time strings trying multiple known formats.
|
||||
for fmt in TIME_FORMATS:
|
||||
try: return parse(timeStr, fmt)
|
||||
except: discard nil
|
||||
@ -36,16 +47,9 @@ proc parseTime*(timeStr: string): DateTime =
|
||||
|
||||
proc parseMark*(json: JsonNode): Mark =
|
||||
|
||||
# TODO: an incorrect time format that was used in version 0.6 and prior.
|
||||
# Version 0.7 between 1.0 support this format on read only and will write
|
||||
# out the correct format (so they can be used to convert older timelines).
|
||||
var time: DateTime
|
||||
try: time = parse(json["time"].getStr(), ISO_TIME_FORMAT)
|
||||
except: time = parse(json["time"].getStr(), "yyyy:MM:dd'T'HH:mm:ss")
|
||||
|
||||
return (
|
||||
id: parseUUID(json["id"].getStr()),
|
||||
time: time, #parse(json["time"].getStr(), ISO_TIME_FORMAT),
|
||||
time: parse(json["time"].getStr(), ISO_TIME_FORMAT),
|
||||
summary: json["summary"].getStr(),
|
||||
notes: json["notes"].getStr(),
|
||||
tags: json["tags"].getElems(@[]).map(proc (t: JsonNode): string = t.getStr()))
|
||||
|
@ -6,3 +6,6 @@ proc flatten*[T](a: seq[seq[T]]): seq[T] =
|
||||
result = @[]
|
||||
for subseq in a:
|
||||
result.add(subseq)
|
||||
|
||||
|
||||
proc raiseEx*(reason: string): void = raise newException(Exception, reason)
|
||||
|
1
private/version.nim
Normal file
1
private/version.nim
Normal file
@ -0,0 +1 @@
|
||||
const PTK_VERSION* = "1.0.0"
|
26
ptk.nim
26
ptk.nim
@ -9,6 +9,7 @@ import algorithm, docopt, json, langutils, logging, os, nre, sequtils,
|
||||
import private/util
|
||||
import private/api
|
||||
import private/models
|
||||
import private/version
|
||||
|
||||
#proc `$`*(mark: Mark): string =
|
||||
#return (($mark.uuid)[
|
||||
@ -255,6 +256,7 @@ Usage:
|
||||
ptk current
|
||||
ptk sum-time --ids <ids>...
|
||||
ptk sum-time [options] [<firstId>] [<lastId>]
|
||||
ptk serve-api <svcCfg> [--port <port>]
|
||||
ptk (-V | --version)
|
||||
ptk (-h | --help)
|
||||
|
||||
@ -287,7 +289,7 @@ Options:
|
||||
let now = getTime().local
|
||||
|
||||
# Parse arguments
|
||||
let args = docopt(doc, version = "ptk 0.12.4")
|
||||
let args = docopt(doc, version = PTK_VERSION)
|
||||
|
||||
if args["--echo-args"]: echo $args
|
||||
|
||||
@ -377,7 +379,7 @@ Options:
|
||||
summary: STOP_MSG,
|
||||
notes: args["--notes"] ?: "",
|
||||
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
|
||||
|
||||
|
||||
timeline.marks.add(newMark)
|
||||
|
||||
timeline.writeMarks(
|
||||
@ -442,7 +444,7 @@ Options:
|
||||
markToResumeIdx = timeline.marks.getLastIndex()
|
||||
if markToResumeIdx < 0: exitErr "No mark to resume."
|
||||
var markToResume = timeline.marks[markToResumeIdx]
|
||||
|
||||
|
||||
var newMark: Mark = (
|
||||
id: genUUID(),
|
||||
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
||||
@ -456,7 +458,7 @@ Options:
|
||||
timeline.writeMarks(
|
||||
indices = sequtils.toSeq(markToResumeIdx..<timeline.marks.len),
|
||||
includeNotes = args["--verbose"])
|
||||
|
||||
|
||||
saveTimeline(timeline, timelineLocation)
|
||||
|
||||
if args["amend"]:
|
||||
@ -472,7 +474,7 @@ Options:
|
||||
if markIdx < 0: exitErr "No mark to amend."
|
||||
|
||||
var mark = timeline.marks[markIdx]
|
||||
|
||||
|
||||
if args["<summary>"]: mark.summary = $args["<summary>"]
|
||||
if args["--notes"]: mark.notes = $args["<notes>"]
|
||||
if args["--tags"]:
|
||||
@ -530,7 +532,7 @@ Options:
|
||||
includeNotes = true)
|
||||
|
||||
if args["sum-time"]:
|
||||
|
||||
|
||||
var intervals: seq[TimeInterval] = @[]
|
||||
|
||||
if args["--ids"]:
|
||||
@ -558,7 +560,17 @@ Options:
|
||||
else:
|
||||
let total = intervals.foldl(a + b)
|
||||
echo flexFormat(total)
|
||||
|
||||
|
||||
if args["serve-api"]:
|
||||
|
||||
if not fileExists($args["<svcCfg>"]):
|
||||
exitErr "cannot find service config file: '" & $args["<svcCfg>"]
|
||||
|
||||
var apiCfg = loadApiConfig(parseFile($args["<svcCfg>"]))
|
||||
if args["--port"]: apiCfg.port = parseInt($args["--port"])
|
||||
|
||||
start_api(apiCfg)
|
||||
|
||||
except:
|
||||
fatal "ptk: " & getCurrentExceptionMsg()
|
||||
quit(QuitFailure)
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Package
|
||||
include "private/version.nim"
|
||||
|
||||
version = "0.12.4"
|
||||
version = PTK_VERSION
|
||||
author = "Jonathan Bernard"
|
||||
description = "Personal Time Keeper"
|
||||
license = "MIT"
|
||||
@ -8,5 +9,5 @@ bin = @["ptk"]
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires @["nim >= 0.18.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile", "timeutils >= 0.2.2", "isaac >= 0.1.2", "bcrypt", "cliutils", "jester" ]
|
||||
requires @["nim >= 0.18.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile", "timeutils >= 0.2.2", "isaac >= 0.1.2", "bcrypt", "cliutils >= 0.5.0", "jester 0.2.0"]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user