WIP Refactor DB module to separate out common from PM_API=specific code. Continued work on parsing.
This commit is contained in:
parent
77084565a1
commit
7bba4a0ad7
@ -1,17 +1,169 @@
|
|||||||
import asyncdispatch, bcrypt, docopt, jester, json, jwt
|
import asyncdispatch, bcrypt, docopt, jester, json, jwt, options, times, timeutils
|
||||||
|
|
||||||
|
import personal_measure_apipkg/models
|
||||||
import personal_measure_apipkg/db
|
import personal_measure_apipkg/db
|
||||||
|
import personal_measure_apipkg/version
|
||||||
|
|
||||||
|
const JSON = "application/json"
|
||||||
|
|
||||||
type
|
type
|
||||||
PersonalMeasureApiConfig = object
|
PersonalMeasureApiConfig = object
|
||||||
|
authSecret*: string
|
||||||
|
dbConnString*: string
|
||||||
|
debug*: bool
|
||||||
port*: int
|
port*: int
|
||||||
pwdCost*: int
|
pwdCost*: int
|
||||||
dbConnString*: string
|
|
||||||
|
|
||||||
proc start*(cfg: StrawBossConfig): void =
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
except:
|
||||||
|
debug "Auth failed: " & getCurrentExceptionMsg()
|
||||||
|
jsonResp(Http401, "Unauthorized", @{"WWW-Authenticate": "Bearer"})
|
||||||
|
|
||||||
|
|
||||||
|
proc start*(cfg: PersonalMeasureApiConfig): void =
|
||||||
|
|
||||||
var stopFuture = newFuture[void]()
|
var stopFuture = newFuture[void]()
|
||||||
var workers: seq[Worker] = @[]
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
port = Port(cfg.port)
|
port = Port(cfg.port)
|
||||||
@ -20,6 +172,21 @@ proc start*(cfg: StrawBossConfig): void =
|
|||||||
routes:
|
routes:
|
||||||
|
|
||||||
get "/version":
|
get "/version":
|
||||||
resp($(%("strawboss v" & SB_VERSION)), JSON)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
start(PersonalMeasureApiConfig(
|
||||||
|
debug: true,
|
||||||
|
port: 8090,
|
||||||
|
pwdCost: 11,
|
||||||
|
dbConnString: "host=localhost port=5500 username=postgres password=password dbname=personal_measure"))
|
||||||
|
@ -2,53 +2,7 @@ import db_postgres, macros, options, postgres, sequtils, strutils, times,
|
|||||||
timeutils, unicode, uuids
|
timeutils, unicode, uuids
|
||||||
|
|
||||||
import ./models
|
import ./models
|
||||||
import ./db_util
|
import ./db_common
|
||||||
|
|
||||||
proc newMutateClauses(): MutateClauses =
|
|
||||||
return MutateClauses(
|
|
||||||
columns: @[],
|
|
||||||
placeholders: @[],
|
|
||||||
values: @[])
|
|
||||||
|
|
||||||
proc createRecord*[T](db: DbConn, rec: T): T =
|
|
||||||
var mc = newMutateClauses()
|
|
||||||
populateMutateClauses(rec, true, mc)
|
|
||||||
|
|
||||||
# Confusingly, getRow allows inserts and updates. We use it to get back the ID
|
|
||||||
# we want from the row.
|
|
||||||
let newIdStr = db.getValue(sql(
|
|
||||||
"INSERT INTO " & tableName(rec) &
|
|
||||||
" (" & mc.columns.join(",") & ") " &
|
|
||||||
" VALUES (" & mc.placeholders.join(",") & ") " &
|
|
||||||
" RETURNING id"), mc.values)
|
|
||||||
|
|
||||||
result = rec
|
|
||||||
result.id = parseUUID(newIdStr)
|
|
||||||
|
|
||||||
proc updateRecord*[T](db: DbConn, rec: T): bool =
|
|
||||||
var mc = newMutateClauses()
|
|
||||||
populateMutateClauses(rec, false, mc)
|
|
||||||
|
|
||||||
let setClause = zip(mc.columns, mc.placeholders).mapIt(it.a & " = " it.b).join(',')
|
|
||||||
let numRowsUpdated = db.execAffectedRows(sql(
|
|
||||||
"UPDATE " & tableName(rec) &
|
|
||||||
" SET " & setClause &
|
|
||||||
" WHERE id = ? "), mc.values.concat(@[rec.id]))
|
|
||||||
|
|
||||||
return numRowsUpdated > 0;
|
|
||||||
|
|
||||||
template getRecord*(db: DbConn, modelType: type, id: UUID): untyped =
|
|
||||||
let row = db.getRow(sql(
|
|
||||||
"SELECT " & columnNamesForModel(modelType).join(",") &
|
|
||||||
" FROM " & tableName(modelType) &
|
|
||||||
" WHERE id = ?"), @[$id])
|
|
||||||
rowToModel(modelType, row)
|
|
||||||
|
|
||||||
template getAllRecords*(db: DbConn, modelType: type): untyped =
|
|
||||||
db.getAllRows(sql(
|
|
||||||
"SELECT " & columnNamesForModel(modelType).join(",") &
|
|
||||||
" FROM " & tableName(modelType)))
|
|
||||||
.mapIt(rowToModel(modelType, it))
|
|
||||||
|
|
||||||
# proc create: Typed create methods for specific records
|
# proc create: Typed create methods for specific records
|
||||||
proc createUser*(db: DbConn, user: User): User = return db.createRecord(user)
|
proc createUser*(db: DbConn, user: User): User = return db.createRecord(user)
|
||||||
@ -57,35 +11,7 @@ proc createMeasure*(db: DbConn, measure: Measure): Measure = return db.createRec
|
|||||||
proc createValue*(db: DbConn, value: Value): Value = return db.createRecord(value)
|
proc createValue*(db: DbConn, value: Value): Value = return db.createRecord(value)
|
||||||
|
|
||||||
proc getUser*(db: DbConn, id: UUID): User = return db.getRecord(User, id)
|
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 getApiToken*(db: DbConn, id: UUID): ApiToken = return db.getRecord(ApiToken, id)
|
||||||
proc getMeasure*(db: DbConn, id: UUID): Measure = return db.getRecord(Measure, 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 getValue*(db: DbConn, id: UUID): Value = return db.getRecord(Value, id)
|
||||||
|
|
||||||
when isMainModule:
|
|
||||||
|
|
||||||
let db = open("", "", "", "host=localhost port=5500 dbname=personal_measure user=postgres password=password")
|
|
||||||
|
|
||||||
echo "Users:"
|
|
||||||
echo $db.getAllRecords(User)
|
|
||||||
|
|
||||||
echo "\nApiTokens:"
|
|
||||||
echo $db.getAllRecords(ApiToken)
|
|
||||||
|
|
||||||
echo "\nMeasures:"
|
|
||||||
let measures = db.getAllRecords(Measure)
|
|
||||||
echo $measures
|
|
||||||
echo "\tanalysis: ", measures[0].analysis[0]
|
|
||||||
|
|
||||||
#[
|
|
||||||
#echo tableName(ApiToken)
|
|
||||||
#echo $rowToModel(ApiToken, @["47400441-5c3a-4119-8acf-f616ae25c16c", "9e5460dd-b580-4071-af97-c1cbdedaae12", "Test Token", "5678", ""])
|
|
||||||
for row in db.fastRows(sql"SELECT * FROM api_tokens"):
|
|
||||||
echo $rowToModel(ApiToken, row);
|
|
||||||
|
|
||||||
let u = User(
|
|
||||||
displayName: "Bob",
|
|
||||||
email: "bob@bobsco.com",
|
|
||||||
hashedPwd: "test")
|
|
||||||
|
|
||||||
]#
|
|
||||||
|
|
||||||
|
50
api/src/main/nim/personal_measure_apipkg/db_common.nim
Normal file
50
api/src/main/nim/personal_measure_apipkg/db_common.nim
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import db_postgres, macros, options, sequtils, uuids
|
||||||
|
|
||||||
|
import ./db_util
|
||||||
|
|
||||||
|
proc newMutateClauses(): MutateClauses =
|
||||||
|
return MutateClauses(
|
||||||
|
columns: @[],
|
||||||
|
placeholders: @[],
|
||||||
|
values: @[])
|
||||||
|
|
||||||
|
proc createRecord*[T](db: DbConn, rec: T): T =
|
||||||
|
var mc = newMutateClauses()
|
||||||
|
populateMutateClauses(rec, true, mc)
|
||||||
|
|
||||||
|
# Confusingly, getRow allows inserts and updates. We use it to get back the ID
|
||||||
|
# we want from the row.
|
||||||
|
let newIdStr = db.getValue(sql(
|
||||||
|
"INSERT INTO " & tableName(rec) &
|
||||||
|
" (" & mc.columns.join(",") & ") " &
|
||||||
|
" VALUES (" & mc.placeholders.join(",") & ") " &
|
||||||
|
" RETURNING id"), mc.values)
|
||||||
|
|
||||||
|
result = rec
|
||||||
|
result.id = parseUUID(newIdStr)
|
||||||
|
|
||||||
|
proc updateRecord*[T](db: DbConn, rec: T): bool =
|
||||||
|
var mc = newMutateClauses()
|
||||||
|
populateMutateClauses(rec, false, mc)
|
||||||
|
|
||||||
|
let setClause = zip(mc.columns, mc.placeholders).mapIt(it.a & " = " it.b).join(',')
|
||||||
|
let numRowsUpdated = db.execAffectedRows(sql(
|
||||||
|
"UPDATE " & tableName(rec) &
|
||||||
|
" SET " & setClause &
|
||||||
|
" WHERE id = ? "), mc.values.concat(@[rec.id]))
|
||||||
|
|
||||||
|
return numRowsUpdated > 0;
|
||||||
|
|
||||||
|
template getRecord*(db: DbConn, modelType: type, id: UUID): untyped =
|
||||||
|
let row = db.getRow(sql(
|
||||||
|
"SELECT " & columnNamesForModel(modelType).join(",") &
|
||||||
|
" FROM " & tableName(modelType) &
|
||||||
|
" WHERE id = ?"), @[$id])
|
||||||
|
rowToModel(modelType, row)
|
||||||
|
|
||||||
|
template getAllRecords*(db: DbConn, modelType: type): untyped =
|
||||||
|
db.getAllRows(sql(
|
||||||
|
"SELECT " & columnNamesForModel(modelType).join(",") &
|
||||||
|
" FROM " & tableName(modelType)))
|
||||||
|
.mapIt(rowToModel(modelType, it))
|
||||||
|
|
@ -140,10 +140,11 @@ proc createParseStmt*(t, value: NimNode): NimNode =
|
|||||||
elif t.typeKind == ntySequence:
|
elif t.typeKind == ntySequence:
|
||||||
let innerType = t[1]
|
let innerType = t[1]
|
||||||
|
|
||||||
|
let parseStmts = createParseStmt(innerType, ident("it"))
|
||||||
|
|
||||||
|
echo parseStmts.treeRepr
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
# TODO: for each value call the type-specific parsing logic.
|
parseDbArray(`value`).mapIt(`parseStmts`)
|
||||||
#parseDbArray(`value`).mapIt(createParseStmt(it))
|
|
||||||
parseDbArray(`value`)
|
|
||||||
|
|
||||||
elif t.typeKind == ntyString:
|
elif t.typeKind == ntyString:
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user