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,
|
import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging,
|
||||||
ospaths, sequtils, strutils, os
|
ospaths, sequtils, strutils, os, times, uuids
|
||||||
|
|
||||||
import nre except toSeq
|
import nre except toSeq
|
||||||
|
|
||||||
import ./models
|
import ./models
|
||||||
|
import ./util
|
||||||
|
import ./version
|
||||||
|
|
||||||
type
|
type
|
||||||
PtkUser* = object
|
PtkUser* = object
|
||||||
username*, salt*, pwdhash*, timelinePath: string
|
username*, salt*, pwdhash*, timelinePath*: string
|
||||||
isAdmin*: bool
|
isAdmin*: bool
|
||||||
|
|
||||||
PtkApiCfg* = object
|
PtkApiCfg* = object
|
||||||
@ -20,7 +23,19 @@ type
|
|||||||
const TXT = "text/plain"
|
const TXT = "text/plain"
|
||||||
const JSON = "application/json"
|
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) =
|
template checkAuth(cfg: PtkApiCfg) =
|
||||||
## Check this request for authentication and authorization information.
|
## 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]))
|
if execResult[2] != 0: raiseEx(stripAnsi($execResult[0] & "\n" & $execResult[1]))
|
||||||
else: return stripAnsi(execResult[0])
|
else: return stripAnsi(execResult[0])
|
||||||
|
|
||||||
proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
|
proc apiParseUser(json: JsonNode): PtkUser =
|
||||||
## convenience method to get a key from a JObject or raise an exception
|
let salt = genSalt(12)
|
||||||
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)
|
|
||||||
|
|
||||||
return PtkUser(
|
return PtkUser(
|
||||||
username: json.getOrFail("username").getStr,
|
username: json.getOrFail("username").getStr,
|
||||||
@ -89,7 +94,24 @@ proc parseUser(json: JsonNode): PtkUser =
|
|||||||
timelinePath: json.getIfExists("timelinePath").getStr(""),
|
timelinePath: json.getIfExists("timelinePath").getStr(""),
|
||||||
isAdmin: false)
|
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]()
|
var stopFuture = newFuture[void]()
|
||||||
|
|
||||||
@ -99,7 +121,7 @@ proc start*(cfg: PtkApiCfg) =
|
|||||||
|
|
||||||
routes:
|
routes:
|
||||||
|
|
||||||
get "/ping": resp("pong", TXT)
|
get "/version": resp("ptk v" & PTK_VERSION, TXT)
|
||||||
|
|
||||||
get "/marks":
|
get "/marks":
|
||||||
checkAuth(cfg); if not authed: return true
|
checkAuth(cfg); if not authed: return true
|
||||||
@ -107,21 +129,98 @@ proc start*(cfg: PtkApiCfg) =
|
|||||||
try: resp(parseAndRun(user, "list", request.params), TXT)
|
try: resp(parseAndRun(user, "list", request.params), TXT)
|
||||||
except: resp(Http500, getCurrentExceptionMsg(), 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":
|
post "/mark":
|
||||||
checkAuth(cfg); if not authed: return true
|
checkAuth(cfg); if not authed: return true
|
||||||
|
|
||||||
var newMark: Mark
|
var newMark: Mark
|
||||||
try: newMark = parseMark(parseJson(request.body))
|
try: newMark = apiParseMark(parseJson(request.body))
|
||||||
except: resp(Http400, getCurrentExceptionMsg(), TXT)
|
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":
|
post "/users":
|
||||||
checkAuth(cfg); if not authed: return true
|
checkAuth(cfg); if not authed: return true
|
||||||
if not user.isAdmin: resp(Http403, "insufficient permission", TXT)
|
if not user.isAdmin: resp(Http403, "insufficient permission", TXT)
|
||||||
|
|
||||||
var newUser: PtkUser
|
var newUser: PtkUser
|
||||||
try: newUser = parseUser(parseJson(request.body))
|
try: newUser = apiParseUser(parseJson(request.body))
|
||||||
except: resp(Http400, getCurrentExceptionMsg(), TXT)
|
except: resp(Http400, getCurrentExceptionMsg(), TXT)
|
||||||
|
|
||||||
if cfg.users.anyIt(it.username == newUser.username):
|
if cfg.users.anyIt(it.username == newUser.username):
|
||||||
@ -136,3 +235,5 @@ proc start*(cfg: PtkApiCfg) =
|
|||||||
resp(Http200, "ok", TXT)
|
resp(Http200, "ok", TXT)
|
||||||
|
|
||||||
except: resp(Http500, "could not init new user timeline", 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 algorithm, json, sequtils, strutils, times, timeutils, uuids
|
||||||
|
|
||||||
|
import ./util
|
||||||
|
|
||||||
type
|
type
|
||||||
Mark* = tuple[id: UUID, time: DateTime, summary: string, notes: string, tags: seq[string]]
|
Mark* = tuple[id: UUID, time: DateTime, summary: string, notes: string, tags: seq[string]]
|
||||||
## Representation of a single mark on the timeline.
|
## 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" ]
|
"HH:mm:ss", "H:mm:ss", "H:mm", "HH:mm" ]
|
||||||
## Other time formats that PTK will accept as input.
|
## 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 =
|
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:
|
for fmt in TIME_FORMATS:
|
||||||
try: return parse(timeStr, fmt)
|
try: return parse(timeStr, fmt)
|
||||||
except: discard nil
|
except: discard nil
|
||||||
@ -36,16 +47,9 @@ proc parseTime*(timeStr: string): DateTime =
|
|||||||
|
|
||||||
proc parseMark*(json: JsonNode): Mark =
|
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 (
|
return (
|
||||||
id: parseUUID(json["id"].getStr()),
|
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(),
|
summary: json["summary"].getStr(),
|
||||||
notes: json["notes"].getStr(),
|
notes: json["notes"].getStr(),
|
||||||
tags: json["tags"].getElems(@[]).map(proc (t: JsonNode): string = t.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 = @[]
|
result = @[]
|
||||||
for subseq in a:
|
for subseq in a:
|
||||||
result.add(subseq)
|
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/util
|
||||||
import private/api
|
import private/api
|
||||||
import private/models
|
import private/models
|
||||||
|
import private/version
|
||||||
|
|
||||||
#proc `$`*(mark: Mark): string =
|
#proc `$`*(mark: Mark): string =
|
||||||
#return (($mark.uuid)[
|
#return (($mark.uuid)[
|
||||||
@ -255,6 +256,7 @@ Usage:
|
|||||||
ptk current
|
ptk current
|
||||||
ptk sum-time --ids <ids>...
|
ptk sum-time --ids <ids>...
|
||||||
ptk sum-time [options] [<firstId>] [<lastId>]
|
ptk sum-time [options] [<firstId>] [<lastId>]
|
||||||
|
ptk serve-api <svcCfg> [--port <port>]
|
||||||
ptk (-V | --version)
|
ptk (-V | --version)
|
||||||
ptk (-h | --help)
|
ptk (-h | --help)
|
||||||
|
|
||||||
@ -287,7 +289,7 @@ Options:
|
|||||||
let now = getTime().local
|
let now = getTime().local
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
let args = docopt(doc, version = "ptk 0.12.4")
|
let args = docopt(doc, version = PTK_VERSION)
|
||||||
|
|
||||||
if args["--echo-args"]: echo $args
|
if args["--echo-args"]: echo $args
|
||||||
|
|
||||||
@ -377,7 +379,7 @@ Options:
|
|||||||
summary: STOP_MSG,
|
summary: STOP_MSG,
|
||||||
notes: args["--notes"] ?: "",
|
notes: args["--notes"] ?: "",
|
||||||
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
|
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
|
||||||
|
|
||||||
timeline.marks.add(newMark)
|
timeline.marks.add(newMark)
|
||||||
|
|
||||||
timeline.writeMarks(
|
timeline.writeMarks(
|
||||||
@ -442,7 +444,7 @@ Options:
|
|||||||
markToResumeIdx = timeline.marks.getLastIndex()
|
markToResumeIdx = timeline.marks.getLastIndex()
|
||||||
if markToResumeIdx < 0: exitErr "No mark to resume."
|
if markToResumeIdx < 0: exitErr "No mark to resume."
|
||||||
var markToResume = timeline.marks[markToResumeIdx]
|
var markToResume = timeline.marks[markToResumeIdx]
|
||||||
|
|
||||||
var newMark: Mark = (
|
var newMark: Mark = (
|
||||||
id: genUUID(),
|
id: genUUID(),
|
||||||
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
||||||
@ -456,7 +458,7 @@ Options:
|
|||||||
timeline.writeMarks(
|
timeline.writeMarks(
|
||||||
indices = sequtils.toSeq(markToResumeIdx..<timeline.marks.len),
|
indices = sequtils.toSeq(markToResumeIdx..<timeline.marks.len),
|
||||||
includeNotes = args["--verbose"])
|
includeNotes = args["--verbose"])
|
||||||
|
|
||||||
saveTimeline(timeline, timelineLocation)
|
saveTimeline(timeline, timelineLocation)
|
||||||
|
|
||||||
if args["amend"]:
|
if args["amend"]:
|
||||||
@ -472,7 +474,7 @@ Options:
|
|||||||
if markIdx < 0: exitErr "No mark to amend."
|
if markIdx < 0: exitErr "No mark to amend."
|
||||||
|
|
||||||
var mark = timeline.marks[markIdx]
|
var mark = timeline.marks[markIdx]
|
||||||
|
|
||||||
if args["<summary>"]: mark.summary = $args["<summary>"]
|
if args["<summary>"]: mark.summary = $args["<summary>"]
|
||||||
if args["--notes"]: mark.notes = $args["<notes>"]
|
if args["--notes"]: mark.notes = $args["<notes>"]
|
||||||
if args["--tags"]:
|
if args["--tags"]:
|
||||||
@ -530,7 +532,7 @@ Options:
|
|||||||
includeNotes = true)
|
includeNotes = true)
|
||||||
|
|
||||||
if args["sum-time"]:
|
if args["sum-time"]:
|
||||||
|
|
||||||
var intervals: seq[TimeInterval] = @[]
|
var intervals: seq[TimeInterval] = @[]
|
||||||
|
|
||||||
if args["--ids"]:
|
if args["--ids"]:
|
||||||
@ -558,7 +560,17 @@ Options:
|
|||||||
else:
|
else:
|
||||||
let total = intervals.foldl(a + b)
|
let total = intervals.foldl(a + b)
|
||||||
echo flexFormat(total)
|
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:
|
except:
|
||||||
fatal "ptk: " & getCurrentExceptionMsg()
|
fatal "ptk: " & getCurrentExceptionMsg()
|
||||||
quit(QuitFailure)
|
quit(QuitFailure)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Package
|
# Package
|
||||||
|
include "private/version.nim"
|
||||||
|
|
||||||
version = "0.12.4"
|
version = PTK_VERSION
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Personal Time Keeper"
|
description = "Personal Time Keeper"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -8,5 +9,5 @@ bin = @["ptk"]
|
|||||||
|
|
||||||
# Dependencies
|
# 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