Compare commits

..

No commits in common. "main" and "1.1.1" have entirely different histories.
main ... 1.1.1

11 changed files with 27 additions and 63 deletions

View File

@ -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;

View File

@ -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
); );

View File

@ -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"

View File

@ -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)").

View File

@ -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)").

View File

@ -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)],

View File

@ -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)],

View File

@ -4,22 +4,14 @@ import FluentPostgresDriver
import SwiftSMTPVapor import SwiftSMTPVapor
public struct ManageableUsers { public struct ManageableUsers {
public static func configure (_ app: Application, public static func configure (_ app: Application, mainMenu: [MenuItem] = [], homePageName: String = "Home", userAdminPageName: String = "User Administration") async throws {
mainMenu: [MenuItem] = [], app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init(
homePageName: String = "Home", hostname: Environment.get("DATABASE_HOST") ?? "localhost",
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, port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "sampleapp", username: Environment.get("DATABASE_USERNAME") ?? "sampleapp",
password: Environment.get("DATABASE_PASSWORD") ?? "sampleapp_password", password: Environment.get("DATABASE_PASSWORD") ?? "sampleapp_password",
database: Environment.get("DATABASE_NAME") ?? "sampleapp", database: Environment.get("DATABASE_NAME") ?? "sampleapp",
tls: .prefer(try .init(configuration: .clientDefault))), tls: .prefer(try .init(configuration: .clientDefault)))
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
sqlLogLevel: sqlLogLevel
), as: .psql) ), as: .psql)
_ = try Environment.baseURL _ = try Environment.baseURL

View File

@ -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()
} }

View File

@ -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)
} }

View File

@ -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