diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/michielcarman.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/michielcarman.xcuserdatad/UserInterfaceState.xcuserstate
index 406ac5f..27743b9 100644
Binary files a/.swiftpm/xcode/package.xcworkspace/xcuserdata/michielcarman.xcuserdatad/UserInterfaceState.xcuserstate and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/michielcarman.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/.swiftpm/xcode/xcuserdata/michielcarman.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/.swiftpm/xcode/xcuserdata/michielcarman.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
new file mode 100644
index 0000000..75f58b2
--- /dev/null
+++ b/.swiftpm/xcode/xcuserdata/michielcarman.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -0,0 +1,6 @@
+
+
+
diff --git a/Sources/App/Controllers/FuelEntryV2Controller.swift b/Sources/App/Controllers/FuelEntryV2Controller.swift
index 7dbaaa7..6802b6f 100644
--- a/Sources/App/Controllers/FuelEntryV2Controller.swift
+++ b/Sources/App/Controllers/FuelEntryV2Controller.swift
@@ -12,6 +12,7 @@ struct FuelEntryV2Controller: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let fuelEntries = routes.grouped("fuel_entries")
fuelEntries.get(use: index)
+ fuelEntries.get("test", use: filtered)
// fuelEntries.get("with-images", use: getAllEntriesWithImages)
fuelEntries.post(use: create)
// fuelEntries.post("upload-image", use: uploadImage)
@@ -24,6 +25,90 @@ struct FuelEntryV2Controller: RouteCollection {
return try await FuelEntry.query(on: req.db).all()
}
+ struct filteredreturn: Content{
+ var fuelentries: [FuelEntry]
+ var pages: Int
+ }
+
+ @Sendable
+ func filtered(req: Request) async throws -> Response {
+
+ var image: Bool
+ var serial: String
+ var tank: String
+ let qstring = req.query
+ if let qimage = try? qstring.get(Bool.self, at: "image"){
+ image = qimage
+ } else {
+ image = false
+ }
+ if let qserial = try? qstring.get(String.self, at: "serial"){
+ serial = qserial
+ } else {
+ serial = ""
+ }
+ if let qtank = try? qstring.get(String.self, at: "tank"){
+ tank = qtank
+ } else {
+ tank = ""
+ }
+
+ var query = FuelEntry.query(on: req.db)
+ if(image){
+ query = query
+ .field(\.$id)
+ .field(\.$timestamp)
+ .field(\.$userID)
+ .field(\.$product)
+ .field(\.$make)
+ .field(\.$model)
+ .field(\.$serialNumber)
+ .field(\.$tankID)
+ .field(\.$fuelAmount)
+ .field(\.$fuelType)
+ .field(\.$isOverride)
+ .field(\.$latitude)
+ .field(\.$longitude)
+ .field(\.$image)
+ } else {
+ query = query
+ .field(\.$id)
+ .field(\.$timestamp)
+ .field(\.$userID)
+ .field(\.$product)
+ .field(\.$make)
+ .field(\.$model)
+ .field(\.$serialNumber)
+ .field(\.$tankID)
+ .field(\.$fuelAmount)
+ .field(\.$fuelType)
+ .field(\.$isOverride)
+ .field(\.$latitude)
+ .field(\.$longitude)
+ }
+ if(serial.count > 0){
+ query = query
+ .filter(\.$serialNumber == serial)
+ }
+ if(tank.count > 0){
+ query = query
+ .filter(\.$tankID == tank)
+ }
+ if(image){
+ let wimage = try await query.paginate(for: req).map{
+ img in FuelEntry.wimage(id: img.id, timestamp: img.timestamp, userID: img.userID, latitude: img.latitude, longitutde: img.longitude, product: img.product, make: img.make, model: img.model, serialNumber: img.serialNumber, tankID: img.tankID, fuelAmount: img.fuelAmount, fuelType: img.fuelType, isOverride: img.isOverride, image: img.image!)
+ }
+ return try await wimage.encodeResponse(for: req)
+ } else {
+ let woimage = try await query.paginate(for: req).map{
+ img in FuelEntry.woimage(id: img.id, timestamp: img.timestamp, userID: img.userID, latitude: img.latitude, longitutde: img.longitude, product: img.product, make: img.make, model: img.model, serialNumber: img.serialNumber, tankID: img.tankID, fuelAmount: img.fuelAmount, fuelType: img.fuelType, isOverride: img.isOverride)
+ }
+ return try await woimage.encodeResponse(for: req)
+ }
+// let result = try await query.paginate(for: req)
+// return try await result.encodeResponse(for: req)
+ }
+
// func getAllEntriesWithImages(req: Request) async throws -> [FuelEntry] {
// let fuelEntries = try await FuelEntry.query(on: req.db).all()
//
diff --git a/Sources/App/Controllers/UserController.swift b/Sources/App/Controllers/UserController.swift
index cc4702c..1ede7b2 100644
--- a/Sources/App/Controllers/UserController.swift
+++ b/Sources/App/Controllers/UserController.swift
@@ -5,4 +5,78 @@
// Created by Michiel Carman on 9/26/24.
//
-import Foundation
+import Vapor
+import Fluent
+
+struct UserSignup: Content {
+ let username: String
+ let password: String
+}
+
+struct NewSession: Content {
+ let token: String
+ let user: User.Public
+}
+
+extension UserSignup: Validatable {
+ static func validations(_ validations: inout Validations) {
+ validations.add("username", as: String.self, is: !.empty)
+ validations.add("password", as: String.self, is: .count(6...))
+ }
+}
+
+struct UserController: RouteCollection {
+ func boot(routes: RoutesBuilder) throws {
+ let usersRoute = routes.grouped("users")
+ usersRoute.post("signup", use: create)
+
+ let tokenProtected = usersRoute.grouped(Token.authenticator())
+ tokenProtected.get("me", use: getMyOwnUser)
+
+ let passwordProtected = usersRoute.grouped(User.authenticator())
+ passwordProtected.post("login", use: login)
+ }
+
+ fileprivate func create(req: Request) throws -> EventLoopFuture {
+ try UserSignup.validate(content: req)
+ let userSignup = try req.content.decode(UserSignup.self)
+ let user = try User.create(from: userSignup)
+ var token: Token!
+
+ return checkIfUserExists(userSignup.username, req: req).flatMap { exists in
+ guard !exists else {
+ return req.eventLoop.future(error: UserError.usernameTaken)
+ }
+
+ return user.save(on: req.db)
+ }.flatMap {
+ guard let newToken = try? user.createToken(source: .signup) else {
+ return req.eventLoop.future(error: Abort(.internalServerError))
+ }
+ token = newToken
+ return token.save(on: req.db)
+ }.flatMapThrowing {
+ NewSession(token: token.value, user: try user.asPublic())
+ }
+ }
+
+ fileprivate func login(req: Request) throws -> EventLoopFuture {
+ let user = try req.auth.require(User.self)
+ let token = try user.createToken(source: .login)
+
+ return token.save(on: req.db).flatMapThrowing {
+ NewSession(token: token.value, user: try user.asPublic())
+ }
+ }
+
+ func getMyOwnUser(req: Request) throws -> User.Public {
+ try req.auth.require(User.self).asPublic()
+ }
+
+ private func checkIfUserExists(_ username: String, req: Request) -> EventLoopFuture {
+ User.query(on: req.db)
+ .filter(\.$username == username)
+ .first()
+ .map { $0 != nil }
+ }
+}
diff --git a/Sources/App/Errors/UserErrors.swift b/Sources/App/Errors/UserErrors.swift
index cc4702c..a3e4ee4 100644
--- a/Sources/App/Errors/UserErrors.swift
+++ b/Sources/App/Errors/UserErrors.swift
@@ -6,3 +6,26 @@
//
import Foundation
+import Vapor
+
+enum UserError {
+ case usernameTaken
+}
+
+extension UserError: AbortError {
+ var description: String {
+ reason
+ }
+
+ var status: HTTPResponseStatus {
+ switch self {
+ case .usernameTaken: return .conflict
+ }
+ }
+
+ var reason: String {
+ switch self {
+ case .usernameTaken: return "Username already taken"
+ }
+ }
+}
diff --git a/Sources/App/Migrations/CreateFuelEntry.swift b/Sources/App/Migrations/CreateFuelEntry.swift
index ad0151d..197121d 100755
--- a/Sources/App/Migrations/CreateFuelEntry.swift
+++ b/Sources/App/Migrations/CreateFuelEntry.swift
@@ -82,3 +82,17 @@ struct StoreImageDataInDatabase: AsyncMigration {
}
}
+struct AddDownloadedBool: AsyncMigration {
+ func prepare(on database: Database) async throws {
+ try await database.schema("fuel_entries")
+ .field("downloaded", .bool)
+ .update()
+ }
+
+ func revert(on database: Database) async throws {
+ try await database.schema("fuel_entries")
+ .deleteField("downloaded")
+ .update()
+ }
+}
+
diff --git a/Sources/App/Migrations/CreateToken.swift b/Sources/App/Migrations/CreateToken.swift
index cc4702c..3f7228f 100644
--- a/Sources/App/Migrations/CreateToken.swift
+++ b/Sources/App/Migrations/CreateToken.swift
@@ -6,3 +6,26 @@
//
import Foundation
+import Vapor
+import Fluent
+
+struct CreateTokens: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ // 2
+ database.schema(Token.schema)
+ // 3
+ .id()
+ .field("user_id", .uuid, .references("user", "id"))
+ .field("value", .string, .required)
+ .unique(on: "value")
+ .field("source", .int, .required)
+ .field("created_at", .datetime, .required)
+ .field("expires_at", .datetime)
+ .create()
+ }
+
+ // 5
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(Token.schema).delete()
+ }
+}
diff --git a/Sources/App/Migrations/CreateUser.swift b/Sources/App/Migrations/CreateUser.swift
index cc4702c..bd3990b 100644
--- a/Sources/App/Migrations/CreateUser.swift
+++ b/Sources/App/Migrations/CreateUser.swift
@@ -6,3 +6,22 @@
//
import Foundation
+import Vapor
+import Fluent
+
+struct CreateUser: AsyncMigration {
+ func prepare(on database: Database) async throws {
+ try await database.schema(User.schema)
+ .id()
+ .field("username", .string, .required)
+ .unique(on: "username")
+ .field("password_hash", .string, .required)
+ .field("created_at", .datetime, .required)
+ .field("updated_at", .datetime, .required)
+ .create()
+ }
+
+ func revert(on database: Database) async throws {
+ try await database.schema(User.schema).delete()
+ }
+}
diff --git a/Sources/App/Models/FuelEntry.swift b/Sources/App/Models/FuelEntry.swift
index 9d60f10..2a8df86 100755
--- a/Sources/App/Models/FuelEntry.swift
+++ b/Sources/App/Models/FuelEntry.swift
@@ -10,6 +10,39 @@ import Vapor
final class FuelEntry: Model, Content {
static let schema = "fuel_entries"
+
+ struct woimage: Content{
+ let id: UUID?
+ let timestamp: Date
+ let userID: String
+ let latitude: Double
+ let longitutde: Double
+ let product: String
+ let make: String
+ let model: String
+ let serialNumber: String
+ let tankID: String
+ let fuelAmount: Double
+ let fuelType: String
+ let isOverride: Bool
+ }
+
+ struct wimage: Content{
+ let id: UUID?
+ let timestamp: Date
+ let userID: String
+ let latitude: Double
+ let longitutde: Double
+ let product: String
+ let make: String
+ let model: String
+ let serialNumber: String
+ let tankID: String
+ let fuelAmount: Double
+ let fuelType: String
+ let isOverride: Bool
+ let image: Data?
+ }
@ID(key: .id)
var id: UUID?
@@ -53,17 +86,58 @@ final class FuelEntry: Model, Content {
@Field(key: "is_override")
var isOverride: Bool
+ @Field(key: "downloaded")
+ var downloaded: Bool?
+
// var imageData: Data?
init() {}
+ init(id: UUID? = nil, timestamp: Date, userID: String, latitude: Double, longitude: Double, image: Data?, product: String, make: String, model: String, serialNumber: String, tankID: String, fuelAmount: Double, fuelType: String, isOverride: Bool, downloaded: Bool?) { //imageData: Data?
+ self.id = id
+ self.timestamp = timestamp
+ self.userID = userID
+ self.latitude = latitude
+ self.longitude = longitude
+ self.image = image ?? nil
+ self.product = product
+ self.make = make
+ self.model = model
+ self.serialNumber = serialNumber
+ self.tankID = tankID
+ self.fuelAmount = fuelAmount
+ self.fuelType = fuelType
+ self.isOverride = isOverride
+ self.downloaded = downloaded ?? nil
+ self.image = image ?? nil
+// self.imageData = imageData
+ }
init(id: UUID? = nil, timestamp: Date, userID: String, latitude: Double, longitude: Double, image: Data?, product: String, make: String, model: String, serialNumber: String, tankID: String, fuelAmount: Double, fuelType: String, isOverride: Bool) { //imageData: Data?
self.id = id
self.timestamp = timestamp
self.userID = userID
self.latitude = latitude
self.longitude = longitude
- self.image = image
+ self.image = image ?? nil
+ self.product = product
+ self.make = make
+ self.model = model
+ self.serialNumber = serialNumber
+ self.tankID = tankID
+ self.fuelAmount = fuelAmount
+ self.fuelType = fuelType
+ self.isOverride = isOverride
+ self.downloaded = downloaded ?? nil
+ self.image = image ?? nil
+// self.imageData = imageData
+ }
+ init(id: UUID? = nil, timestamp: Date, userID: String, latitude: Double, longitude: Double, product: String, make: String, model: String, serialNumber: String, tankID: String, fuelAmount: Double, fuelType: String, isOverride: Bool) { //imageData: Data?
+ self.id = id
+ self.timestamp = timestamp
+ self.userID = userID
+ self.latitude = latitude
+ self.longitude = longitude
+ self.image = image ?? nil
self.product = product
self.make = make
self.model = model
diff --git a/Sources/App/Models/Token.swift b/Sources/App/Models/Token.swift
index b2a1ea5..8b7e109 100644
--- a/Sources/App/Models/Token.swift
+++ b/Sources/App/Models/Token.swift
@@ -5,4 +5,56 @@
// Created by Michiel Carman on 9/24/24.
//
-import Foundation
+import Vapor
+import Fluent
+
+enum SessionSource: Int, Content {
+ case signup
+ case login
+}
+
+final class Token: Model {
+ static let schema = "token"
+
+ @ID()
+ var id: UUID?
+
+ @Parent(key: "user_id")
+ var user: User
+
+ @Field(key: "value")
+ var value: String
+
+ @Field(key: "source")
+ var source: SessionSource
+
+ @Field(key: "expires_at")
+ var expiresAt: Date?
+
+ @Timestamp(key: "created_at", on: .create)
+ var createdAt: Date?
+
+ init() {}
+
+ init(id: UUID? = nil, userId: User.IDValue, token: String,
+ source: SessionSource, expiresAt: Date?) {
+ self.id = id
+ self.$user.id = userId
+ self.value = token
+ self.source = source
+ self.expiresAt = expiresAt
+ }
+}
+
+extension Token: ModelTokenAuthenticatable {
+ static let valueKey = \Token.$value
+ static let userKey = \Token.$user
+
+ var isValid: Bool {
+ guard let expiryDate = expiresAt else {
+ return true
+ }
+
+ return expiryDate > Date()
+ }
+}
diff --git a/Sources/App/Models/User.swift b/Sources/App/Models/User.swift
index caa91d5..be1cbf2 100644
--- a/Sources/App/Models/User.swift
+++ b/Sources/App/Models/User.swift
@@ -5,12 +5,68 @@
// Created by Michiel Carman on 9/24/24.
//
-import Foundation
-import Vapor
import Fluent
+import Vapor
final class User: Model {
- @ID var id: UUID?
-
- @Field(key: "name") var name: String
+ struct Public: Content {
+ let username: String
+ let id: UUID
+ let createdAt: Date?
+ let updatedAt: Date?
+ }
+
+ static let schema = "user"
+
+ @ID()
+ var id: UUID?
+
+ @Field(key: "username")
+ var username: String
+
+ @Field(key: "password_hash")
+ var passwordHash: String
+
+ @Timestamp(key: "created_at", on: .create)
+ var createdAt: Date?
+
+ @Timestamp(key: "updated_at", on: .update)
+ var updatedAt: Date?
+
+ init() {}
+
+ init(id: UUID? = nil, username: String, passwordHash: String) {
+ self.id = id
+ self.username = username
+ self.passwordHash = passwordHash
+ }
+}
+
+extension User {
+ static func create(from userSignup: UserSignup) throws -> User {
+ User(username: userSignup.username, passwordHash: try Bcrypt.hash(userSignup.password))
+ }
+
+ func createToken(source: SessionSource) throws -> Token {
+ let calendar = Calendar(identifier: .gregorian)
+ let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date())
+ return try Token(userId: requireID(),
+ token: [UInt8].random(count: 16).base64, source: source, expiresAt: expiryDate)
+ }
+
+ func asPublic() throws -> Public {
+ Public(username: username,
+ id: try requireID(),
+ createdAt: createdAt,
+ updatedAt: updatedAt)
+ }
+}
+
+extension User: ModelAuthenticatable {
+ static let usernameKey = \User.$username
+ static let passwordHashKey = \User.$passwordHash
+
+ func verify(password: String) throws -> Bool {
+ try Bcrypt.verify(password, created: self.passwordHash)
+ }
}
diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift
index 06fe51f..177900a 100755
--- a/Sources/App/configure.swift
+++ b/Sources/App/configure.swift
@@ -4,6 +4,7 @@ import Vapor
// configures your application
public func configure(_ app: Application) async throws {
+ app.middleware.use(ErrorMiddleware.default(environment: app.environment))
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.routes.defaultMaxBodySize = "128mb"
@@ -15,6 +16,9 @@ public func configure(_ app: Application) async throws {
app.migrations.add(AddLatitudeAndLongitudeAndRemoveGpsLocationAndImage())
app.migrations.add(AddImageBackIn())
app.migrations.add(StoreImageDataInDatabase())
+ app.migrations.add(AddDownloadedBool())
+ app.migrations.add(CreateUser())
+ app.migrations.add(CreateTokens())
try await app.autoMigrate().get()
// register routes
diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift
index cd36958..787b8cc 100755
--- a/Sources/App/routes.swift
+++ b/Sources/App/routes.swift
@@ -1,6 +1,12 @@
import Vapor
func routes(_ app: Application) throws {
+
+ app.routes.caseInsensitive = true
+ app.routes.defaultMaxBodySize = "64mb";
+ let v2 = app.grouped("api").grouped("v2")
+ try v2.register(collection: FuelEntryV2Controller())
+ try v2.register(collection: UserController())
let fuelEntryController = FuelEntryController()
app.get("fuel-entries") { req in
@@ -15,30 +21,33 @@ func routes(_ app: Application) throws {
try await fuelEntryController.delete(req: req)
}
+
+
+
// V2 API routes with image upload
- let v2Routes = app.group("api", "v2") { v2 in
- let fuelEntryV2Controller = FuelEntryV2Controller()
-
- v2.get("fuel-entries") {req in
- try await fuelEntryV2Controller.index(req: req)
- }
-
-// v2.get("fuel-entries", "with-images") {req in
-// try await fuelEntryV2Controller.getAllEntriesWithImages(req: req)
+// let v2Routes: () = app.group("api", "v2") { v2 in
+// let fuelEntryV2Controller = FuelEntryV2Controller()
+//
+// v2.get("fuel-entries") {req in
+// try await fuelEntryV2Controller.index(req: req)
// }
-
- v2.post("fuel-entries") {req in
- try await fuelEntryV2Controller.create(req: req)
- }
-
-// v2.post("fuel-entries", ":id", "upload-image") { req -> FuelEntry in
-// return try await fuelEntryV2Controller.uploadImage(req: req)
+//
+//// v2.get("fuel-entries", "with-images") {req in
+//// try await fuelEntryV2Controller.getAllEntriesWithImages(req: req)
+//// }
+//
+// v2.post("fuel-entries") {req in
+// try await fuelEntryV2Controller.create(req: req)
// }
-
- v2.delete("fuel-entries", ":fuelEntryID") { req in
- try await fuelEntryV2Controller.delete(req: req)
- }
- }
+//
+//// v2.post("fuel-entries", ":id", "upload-image") { req -> FuelEntry in
+//// return try await fuelEntryV2Controller.uploadImage(req: req)
+//// }
+//
+// v2.delete("fuel-entries", ":fuelEntryID") { req in
+// try await fuelEntryV2Controller.delete(req: req)
+// }
+// }
}