Compare commits
No commits in common. "main" and "1.0.0" have entirely different histories.
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"originHash" : "d54afbeb9301d3df649cfb4bef5794fb7abe1c6aa826c3651df7253a5656b124",
|
"originHash" : "39da4f005434f990c0dece54faba957e7edc27104875be011d48221ad538739d",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "async-http-client",
|
"identity" : "async-http-client",
|
||||||
|
|
@ -28,33 +28,6 @@
|
||||||
"version" : "4.15.2"
|
"version" : "4.15.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "fluent",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/fluent.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "223b27d04ab2b51c25503c9922eecbcdf6c12f89",
|
|
||||||
"version" : "4.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "fluent-kit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/fluent-kit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "8baacd7e8f7ebf68886c496b43bbe6cdcc5b57e0",
|
|
||||||
"version" : "1.52.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "fluent-postgres-driver",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/fluent-postgres-driver.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "095bc5a17ab3363167f4becb270b6f8eb790481c",
|
|
||||||
"version" : "2.10.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "leaf",
|
"identity" : "leaf",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "ManageableUsers",
|
name: "ManagableUsers",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v13)
|
.macOS(.v13)
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "ManageableUsers",
|
name: "ManagableUsers",
|
||||||
targets: ["ManageableUsers"]
|
targets: ["ManagableUsers"]
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
@ -28,7 +28,7 @@ let package = Package(
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "ManageableUsers",
|
name: "ManagableUsers",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "Fluent", package: "fluent"),
|
.product(name: "Fluent", package: "fluent"),
|
||||||
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
|
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
|
||||||
|
|
@ -43,7 +43,7 @@ let package = Package(
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "SampleApp",
|
name: "SampleApp",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.target(name: "ManageableUsers"),
|
.target(name: "ManagableUsers"),
|
||||||
],
|
],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ async function loadData (block) {
|
||||||
const response = await fetch (source);
|
const response = await fetch (source);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const items = await response.json();
|
const items = await response.json();
|
||||||
var itemLoaders = [];
|
|
||||||
|
|
||||||
if (Array.isArray (items)) {
|
if (Array.isArray (items)) {
|
||||||
const template = block.querySelector ("template");
|
const template = block.querySelector ("template");
|
||||||
|
|
@ -13,25 +12,15 @@ async function loadData (block) {
|
||||||
tableBody.innerHTML = "";
|
tableBody.innerHTML = "";
|
||||||
items.forEach (item => {
|
items.forEach (item => {
|
||||||
const clone = template.content.cloneNode (true);
|
const clone = template.content.cloneNode (true);
|
||||||
const loader = window["populate" + block.dataset.item] (clone, item);
|
window["populate" + block.dataset.item] (clone, item);
|
||||||
if (loader != null) {
|
|
||||||
itemLoaders.push (loader);
|
|
||||||
}
|
|
||||||
tableBody.appendChild (clone);
|
tableBody.appendChild (clone);
|
||||||
tableBody.lastElementChild.dataset.itemid = item.id;
|
tableBody.lastElementChild.dataset.itemid = item.id;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const loader = window["populate" + block.dataset.item] (block, items);
|
window["populate" + block.dataset.item] (block, items);
|
||||||
if (loader != null) {
|
|
||||||
itemLoaders.push (loader);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
block.classList.add ("loaded");
|
block.classList.add ("loaded");
|
||||||
|
|
||||||
for (const loader of itemLoaders) {
|
|
||||||
await loader();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if ((response.status == 401) || (response.status == 403)) {
|
if ((response.status == 401) || (response.status == 403)) {
|
||||||
document.location.href = document.body.dataset.baseurl;
|
document.location.href = document.body.dataset.baseurl;
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -1,4 +1,4 @@
|
||||||
# ManageableUsers
|
# ManagableUsers
|
||||||
|
|
||||||
A package providing basic user management when using the Vapor web framework and a PostgreSQL database.
|
A package providing basic user management when using the Vapor web framework and a PostgreSQL database.
|
||||||
|
|
||||||
|
|
@ -76,34 +76,34 @@ Create a new Vapor project, or reuse an existing one.
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
Add dependency on `manageable-users` by adding the package to `Package.swift` as
|
Add dependency on `managable-users` by adding the package to `Package.swift` as
|
||||||
```swift
|
```swift
|
||||||
.package(url: "https://git.carlberg.org/public/manageable-users.git", from: "1.0.0"),
|
.package(url: "https://git.carlberg.org/public/manageable-users.git", from: "1.0.0"),
|
||||||
```
|
```
|
||||||
|
|
||||||
…and adding to the executable target's dependencies as
|
…and adding to the executable target's dependencies as
|
||||||
```swift
|
```swift
|
||||||
.product(name: "ManageableUsers", package: "manageable-users"),
|
.product(name: "ManagableUsers", package: "managable-users"),
|
||||||
``
|
``
|
||||||
|
|
||||||
Copy the contents of the `Resources/Views` folder from the `manageable-users` into the `Resources/Views` folder
|
Copy the contents of the `Resources/Views` folder from the `managable-users` into the `Resources/Views` folder
|
||||||
in the new project, and also copy the contents of the `Public` folder into the `Public` folder.
|
in the new project, and also copy the contents of the `Public` folder into the `Public` folder.
|
||||||
|
|
||||||
In the `configure.swift` file, add the import
|
In the `configure.swift` file, add the import
|
||||||
```swift
|
```swift
|
||||||
import ManageableUsers
|
import ManagableUsers
|
||||||
```
|
```
|
||||||
|
|
||||||
…and add
|
…and add
|
||||||
```swift
|
```swift
|
||||||
try await ManageableUsers.configure (app)
|
try await ManagableUsers.configure (app)
|
||||||
```
|
```
|
||||||
|
|
||||||
into the `configure()` function.
|
into the `configure()` function.
|
||||||
|
|
||||||
In the `routes.swift` file, add the import
|
In the `routes.swift` file, add the import
|
||||||
```swift
|
```swift
|
||||||
import ManageableUsers
|
import ManagableUsers
|
||||||
```
|
```
|
||||||
|
|
||||||
…and replace everything in the `routes()` function with
|
…and replace everything in the `routes()` function with
|
||||||
|
|
@ -137,13 +137,13 @@ To use a consistent look, use `base.leaf` in your Leaf templates for pages.
|
||||||
To add items to the main menu, do that in `configure.swift` by replacing
|
To add items to the main menu, do that in `configure.swift` by replacing
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
try await ManageableUsers.configure (app)
|
try await ManagableUsers.configure (app)
|
||||||
```
|
```
|
||||||
|
|
||||||
with something like
|
with something like
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
try await ManageableUsers.configure(app, mainMenu: [MenuItem (section: "inventory", path: "inventory", name: "Inventory")])
|
try await ManagableUsers.configure(app, mainMenu: [MenuItem (section: "inventory", path: "inventory", name: "Inventory")])
|
||||||
```
|
```
|
||||||
|
|
||||||
For `MenuItem`
|
For `MenuItem`
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ CREATE TABLE "user_tokens" (
|
||||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
"user_id" UUID NOT NULL,
|
"user_id" UUID NOT NULL,
|
||||||
"token" CHARACTER VARYING (1000) NOT NULL,
|
"token" CHARACTER VARYING (1000) NOT NULL,
|
||||||
"realm" CHARACTER VARYING (10) NOT NULL CHECK ("realm" IN ('invite', 'forgot')),
|
|
||||||
"insert_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"insert_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
CONSTRAINT "user_tokens_user_fk" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
CONSTRAINT "user_tokens_user_fk" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,9 @@ WITH "created" AS (
|
||||||
SELECT "id", 'admin'
|
SELECT "id", 'admin'
|
||||||
FROM "created"
|
FROM "created"
|
||||||
)
|
)
|
||||||
INSERT INTO "user_tokens" ("user_id", "token", "realm")
|
INSERT INTO "user_tokens" ("user_id", "token")
|
||||||
SELECT "id",
|
SELECT "id", (SELECT string_agg (substr (c, (random() * length (c) + 1)::integer, 1), '') AS "token"
|
||||||
(SELECT string_agg (substr (c, (random() * length (c) + 1)::integer, 1), '') AS "token"
|
|
||||||
FROM (VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')) AS x(c),
|
FROM (VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')) AS x(c),
|
||||||
generate_series (1, 32)),
|
generate_series (1, 32))
|
||||||
'invite'
|
|
||||||
FROM "created"
|
FROM "created"
|
||||||
RETURNING 'https://example.com/auth/password/' || "token" AS "Password URL"
|
RETURNING 'https://example.com/auth/password/' || "token" AS "Password URL"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
Welcome to #(host).
|
Välkommen till #(host).
|
||||||
|
|
||||||
To activate your account, go to #baseURL/auth/password/#(token) and enter a password.
|
För att aktivera ditt konto, gå till #baseURL/auth/password/#(token) och skriv in ett lösenord.
|
||||||
|
|
||||||
The link is valid until #date(expiration, "yyyy-MM-dd HH:mm (z)").
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
You have requested a new password on #(host).
|
Du har begärt ett nytt lösenord på #(host).
|
||||||
|
|
||||||
To change your password, go to #baseURL/auth/password/#(token) and enter a password.
|
För att ändra ditt lösenord, gå till #baseURL/auth/password/#(token) och skriv in ett lösenord.
|
||||||
|
|
||||||
The link is valid until #date(expiration, "yyyy-MM-dd HH:mm (z)").
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import Vapor
|
||||||
struct UserAuthenticator<User: ManagedUser>: AsyncBasicAuthenticator where User.SessionID == ExpiringUserId {
|
struct UserAuthenticator<User: ManagedUser>: AsyncBasicAuthenticator where User.SessionID == ExpiringUserId {
|
||||||
|
|
||||||
func authenticate (basic: BasicAuthorization, for request: Request) async throws {
|
func authenticate (basic: BasicAuthorization, for request: Request) async throws {
|
||||||
if let user = try await request.db.withSQLConnection ({ connection in
|
if let user = try await request.db.withSQLConnection { connection in
|
||||||
return try await User.find (email: basic.username, password: basic.password, on: connection)
|
return try await User.find (email: basic.username, password: basic.password, on: connection)
|
||||||
}) {
|
} {
|
||||||
request.auth.login (user)
|
request.auth.login (user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,9 @@ public struct UserSessionAuthenticator<SessionUser: ManagedUser>: AsyncSessionAu
|
||||||
public typealias User = SessionUser
|
public typealias User = SessionUser
|
||||||
|
|
||||||
public func authenticate (sessionID: SessionUser.SessionID, for request: Request) async throws {
|
public func authenticate (sessionID: SessionUser.SessionID, for request: Request) async throws {
|
||||||
if let user = try await request.db.withSQLConnection ({ connection in
|
if let user = try await request.db.withSQLConnection { connection in
|
||||||
return try await SessionUser.authenticate (sessionID: sessionID, on: connection)
|
return try await SessionUser.authenticate (sessionID: sessionID, on: connection)
|
||||||
}) {
|
} {
|
||||||
request.logger.info ("Seeing user \(user)")
|
request.logger.info ("Seeing user \(user)")
|
||||||
request.auth.login (user)
|
request.auth.login (user)
|
||||||
request.logger.info ("Saw user \(user)")
|
request.logger.info ("Saw user \(user)")
|
||||||
|
|
@ -88,7 +88,7 @@ public struct AdminController<User: ManagedUser>: Sendable where User.SessionID
|
||||||
let token = try await UserToken.create (connection: connection).token
|
let token = try await UserToken.create (connection: connection).token
|
||||||
try await User.create (email: invitation.email, fullname: invitation.fullname, roles: invitation.roles, token: token, on: connection)
|
try await User.create (email: invitation.email, fullname: invitation.fullname, roles: invitation.roles, token: token, on: connection)
|
||||||
let host = try Environment.baseURL.host() ?? ""
|
let host = try Environment.baseURL.host() ?? ""
|
||||||
let body = try await request.view.render ("email/invite", AuthenticationController<User>.TokenEmailContext (token: token, host: host, expiration: Calendar.current.date (byAdding: .day, value: 1, to: Date()) ?? Date()))
|
let body = try await request.view.render ("email/invite", ["token": token, "host": host])
|
||||||
.data
|
.data
|
||||||
let message = Email (sender: Email.Contact (emailAddress: try Environment.emailSender),
|
let message = Email (sender: Email.Contact (emailAddress: try Environment.emailSender),
|
||||||
recipients: [Email.Contact(emailAddress: invitation.email)],
|
recipients: [Email.Contact(emailAddress: invitation.email)],
|
||||||
|
|
@ -116,8 +116,8 @@ public struct AdminController<User: ManagedUser>: Sendable where User.SessionID
|
||||||
}
|
}
|
||||||
let save = try request.content.decode(Save.self)
|
let save = try request.content.decode(Save.self)
|
||||||
|
|
||||||
return try await request.db.withSQLConnection (user: try request.auth.require (BasicUser.self)) { connection in
|
return try await request.db.withSQLConnection { connection in
|
||||||
guard (try await User.fetch (userId, on: connection)) != nil else {
|
guard (try await User.find (userId, on: connection)) != nil else {
|
||||||
throw Abort (.notFound)
|
throw Abort (.notFound)
|
||||||
}
|
}
|
||||||
guard !save.email.isEmpty && !save.fullname.isEmpty else {
|
guard !save.email.isEmpty && !save.fullname.isEmpty else {
|
||||||
|
|
@ -91,12 +91,6 @@ public struct AuthenticationController<User: ManagedUser>: Sendable where User.S
|
||||||
let email: String
|
let email: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TokenEmailContext: Encodable {
|
|
||||||
let token: String
|
|
||||||
let host: String
|
|
||||||
let expiration: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
func forgotPassword (request: Request) async throws -> Response {
|
func forgotPassword (request: Request) async throws -> Response {
|
||||||
|
|
||||||
let input = try request.content.decode (Input.self)
|
let input = try request.content.decode (Input.self)
|
||||||
|
|
@ -107,7 +101,7 @@ public struct AuthenticationController<User: ManagedUser>: Sendable where User.S
|
||||||
let token = try await UserToken.create (connection: connection).token
|
let token = try await UserToken.create (connection: connection).token
|
||||||
try await User.store (token: token, userId: user.id, on: connection)
|
try await User.store (token: token, userId: user.id, on: connection)
|
||||||
let host = try Environment.baseURL.host() ?? ""
|
let host = try Environment.baseURL.host() ?? ""
|
||||||
let body = try await request.view.render ("email/reset", TokenEmailContext (token: token, host: host, expiration: Calendar.current.date (byAdding: .hour, value: 1, to: Date()) ?? Date()))
|
let body = try await request.view.render ("email/reset", ["token": token, "host": host, "section": "login"])
|
||||||
.data
|
.data
|
||||||
let message = Email (sender: Email.Contact (emailAddress: try Environment.emailSender),
|
let message = Email (sender: Email.Contact (emailAddress: try Environment.emailSender),
|
||||||
recipients: [Email.Contact(emailAddress: input.email)],
|
recipients: [Email.Contact(emailAddress: input.email)],
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import Vapor
|
||||||
|
import Fluent
|
||||||
|
import FluentPostgresDriver
|
||||||
|
import SwiftSMTPVapor
|
||||||
|
|
||||||
|
public struct ManagableUsers {
|
||||||
|
public static func configure (_ app: Application, mainMenu: [MenuItem] = [], homePageName: String = "Home", userAdminPageName: String = "User Administration") async throws {
|
||||||
|
app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init(
|
||||||
|
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
|
||||||
|
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
|
||||||
|
username: Environment.get("DATABASE_USERNAME") ?? "sampleapp",
|
||||||
|
password: Environment.get("DATABASE_PASSWORD") ?? "sampleapp_password",
|
||||||
|
database: Environment.get("DATABASE_NAME") ?? "sampleapp",
|
||||||
|
tls: .prefer(try .init(configuration: .clientDefault)))
|
||||||
|
), as: .psql)
|
||||||
|
|
||||||
|
_ = try Environment.baseURL
|
||||||
|
_ = try Environment.emailSender
|
||||||
|
|
||||||
|
app.lifecycle.use(SMTPInitializer(configuration: .fromEnvironment()))
|
||||||
|
|
||||||
|
app.views.use(.leaf)
|
||||||
|
app.leaf.tags["baseURL"] = BaseURLTag()
|
||||||
|
app.leaf.tags["hasRole"] = HasRoleTag()
|
||||||
|
app.leaf.tags["mainmenuItems"] = MenuItemsTag (mainMenu + [MenuItem (section: "useradmin", path: "admin", name: userAdminPageName, role: "admin")], homePageName: homePageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,8 +49,8 @@ extension ManagedUser {
|
||||||
TRUE)
|
TRUE)
|
||||||
RETURNING "id"
|
RETURNING "id"
|
||||||
)
|
)
|
||||||
INSERT INTO "user_tokens" ("user_id", "token", "realm")
|
INSERT INTO "user_tokens" ("user_id", "token")
|
||||||
SELECT "id", \(bind: token), 'invite'
|
SELECT "id", \(bind: token)
|
||||||
FROM created
|
FROM created
|
||||||
""")
|
""")
|
||||||
.run()
|
.run()
|
||||||
|
|
@ -72,8 +72,8 @@ extension ManagedUser {
|
||||||
FROM "created"
|
FROM "created"
|
||||||
CROSS JOIN unnest (\(bind: roles)) AS "role_name"
|
CROSS JOIN unnest (\(bind: roles)) AS "role_name"
|
||||||
)
|
)
|
||||||
INSERT INTO "user_tokens" ("user_id", "token", "realm")
|
INSERT INTO "user_tokens" ("user_id", "token")
|
||||||
SELECT "id", \(bind: token), 'invite'
|
SELECT "id", \(bind: token)
|
||||||
FROM "created"
|
FROM "created"
|
||||||
""")
|
""")
|
||||||
.run()
|
.run()
|
||||||
|
|
@ -121,8 +121,8 @@ extension ManagedUser {
|
||||||
|
|
||||||
public static func store (token: String, userId: UUID, on connection: any SQLDatabase) async throws {
|
public static func store (token: String, userId: UUID, on connection: any SQLDatabase) async throws {
|
||||||
try await connection.raw("""
|
try await connection.raw("""
|
||||||
INSERT INTO "user_tokens" ("user_id", "token", "realm")
|
INSERT INTO "user_tokens" ("user_id", "token")
|
||||||
VALUES (\(bind: userId), \(bind: token), 'forgot')
|
VALUES (\(bind: userId), \(bind: token))
|
||||||
""")
|
""")
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
@ -30,9 +30,7 @@ struct UserToken: Decodable {
|
||||||
JOIN "users"
|
JOIN "users"
|
||||||
ON "users"."id" = "user_tokens"."user_id"
|
ON "users"."id" = "user_tokens"."user_id"
|
||||||
WHERE "token" = \(bind: token)
|
WHERE "token" = \(bind: token)
|
||||||
AND "insert_time" >= CURRENT_TIMESTAMP - CASE WHEN "realm" = 'invite'
|
AND "insert_time" >= CURRENT_TIMESTAMP - INTERVAL '1 HOUR'
|
||||||
THEN INTERVAL '1 DAY'
|
|
||||||
ELSE INTERVAL '1 HOUR' END
|
|
||||||
""")
|
""")
|
||||||
.first (decoding: Token.self)
|
.first (decoding: Token.self)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Fluent
|
||||||
|
import SQLKit
|
||||||
|
|
||||||
|
extension Database {
|
||||||
|
public func withSQLConnection<T: Sendable>(_ closure: @escaping @Sendable (SQLDatabase) async throws -> T) async throws -> T {
|
||||||
|
return try await self.withConnection { database in
|
||||||
|
guard let connection = database as? SQLDatabase else {
|
||||||
|
throw NoSQLDatabaseError (database: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await closure (connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import Vapor
|
|
||||||
import Fluent
|
|
||||||
import FluentPostgresDriver
|
|
||||||
import SwiftSMTPVapor
|
|
||||||
|
|
||||||
public struct ManageableUsers {
|
|
||||||
public static func configure (_ app: Application,
|
|
||||||
mainMenu: [MenuItem] = [],
|
|
||||||
homePageName: String = "Home",
|
|
||||||
userAdminPageName: String = "User Administration",
|
|
||||||
maxConnectionsPerEventLoop: Int = 1,
|
|
||||||
connectionPoolTimeout: TimeAmount = .seconds(10),
|
|
||||||
sqlLogLevel: Logger.Level = .debug) async throws {
|
|
||||||
app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init (hostname: Environment.get("DATABASE_HOST") ?? "localhost",
|
|
||||||
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
|
|
||||||
username: Environment.get("DATABASE_USERNAME") ?? "sampleapp",
|
|
||||||
password: Environment.get("DATABASE_PASSWORD") ?? "sampleapp_password",
|
|
||||||
database: Environment.get("DATABASE_NAME") ?? "sampleapp",
|
|
||||||
tls: .prefer(try .init(configuration: .clientDefault))),
|
|
||||||
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
|
|
||||||
connectionPoolTimeout: connectionPoolTimeout,
|
|
||||||
sqlLogLevel: sqlLogLevel
|
|
||||||
), as: .psql)
|
|
||||||
|
|
||||||
_ = try Environment.baseURL
|
|
||||||
_ = try Environment.emailSender
|
|
||||||
|
|
||||||
app.lifecycle.use(SMTPInitializer(configuration: .fromEnvironment()))
|
|
||||||
|
|
||||||
app.views.use(.leaf)
|
|
||||||
app.leaf.tags["baseURL"] = BaseURLTag()
|
|
||||||
app.leaf.tags["hasRole"] = HasRoleTag()
|
|
||||||
app.leaf.tags["mainmenuItems"] = MenuItemsTag (mainMenu + [MenuItem (section: "useradmin", path: "admin", name: userAdminPageName, role: "admin")], homePageName: homePageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import Fluent
|
|
||||||
import SQLKit
|
|
||||||
|
|
||||||
extension Database {
|
|
||||||
public func withSQLConnection<T: Sendable>(user: BasicUser? = nil, _ closure: @escaping @Sendable (any SQLDatabase) async throws -> T) async throws -> T {
|
|
||||||
return try await self.withConnection { database in
|
|
||||||
guard let connection = database as? (any SQLDatabase) else {
|
|
||||||
throw NoSQLDatabaseError (database: database)
|
|
||||||
}
|
|
||||||
try await connection.raw ("""
|
|
||||||
SELECT pg_catalog.set_config ('manageable_users.active_user', \(bind: user?.fullName), FALSE)
|
|
||||||
""")
|
|
||||||
.run()
|
|
||||||
|
|
||||||
let result = try await closure (connection)
|
|
||||||
|
|
||||||
try await connection.raw ("""
|
|
||||||
SELECT pg_catalog.set_config ('manageable_users.active_user', NULL, FALSE)
|
|
||||||
""")
|
|
||||||
.run()
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import ManageableUsers
|
import ManagableUsers
|
||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
public func configure (_ app: Application) async throws {
|
public func configure (_ app: Application) async throws {
|
||||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||||
|
|
||||||
try await ManageableUsers.configure (app)
|
try await ManagableUsers.configure (app)
|
||||||
|
|
||||||
try routes(app)
|
try routes(app)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import ManageableUsers
|
import ManagableUsers
|
||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
func routes(_ app: Application) throws {
|
func routes(_ app: Application) throws {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import ManageableUsers
|
import ManagableUsers
|
||||||
@testable import SampleApp
|
@testable import SampleApp
|
||||||
import VaporTesting
|
import VaporTesting
|
||||||
import SQLKit
|
import SQLKit
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
@testable import SampleApp
|
@testable import SampleApp
|
||||||
import ManageableUsers
|
import ManagableUsers
|
||||||
import VaporTesting
|
import VaporTesting
|
||||||
import SQLKit
|
import SQLKit
|
||||||
import Testing
|
import Testing
|
||||||
|
|
@ -9,8 +9,6 @@ struct AuthorizationTests {
|
||||||
private func withApp(_ test: (Application) async throws -> ()) async throws {
|
private func withApp(_ test: (Application) async throws -> ()) async throws {
|
||||||
let app = try await Application.make (.testing)
|
let app = try await Application.make (.testing)
|
||||||
do {
|
do {
|
||||||
setenv ("BASE_URL", "http://localhost", 0)
|
|
||||||
setenv ("EMAIL_SENDER", "nobody@example.com", 0)
|
|
||||||
try await SampleApp.configure (app)
|
try await SampleApp.configure (app)
|
||||||
let mockDatabase = MockDatabase (eventLoop: app.eventLoopGroup.next())
|
let mockDatabase = MockDatabase (eventLoop: app.eventLoopGroup.next())
|
||||||
app.storage[Application.MockDatabaseKey.self] = mockDatabase
|
app.storage[Application.MockDatabaseKey.self] = mockDatabase
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue