WIP: Begins work on login / signup, adds user database models, authentication needs implemented.
This commit is contained in:
@@ -6051,6 +6051,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.mt-8 {
|
.mt-8 {
|
||||||
margin-top: calc(var(--spacing) * 8);
|
margin-top: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
@@ -6133,6 +6136,12 @@
|
|||||||
.mb-2 {
|
.mb-2 {
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.mb-8 {
|
.mb-8 {
|
||||||
margin-bottom: calc(var(--spacing) * 8);
|
margin-bottom: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
@@ -6956,6 +6965,9 @@
|
|||||||
.h-36 {
|
.h-36 {
|
||||||
height: calc(var(--spacing) * 36);
|
height: calc(var(--spacing) * 36);
|
||||||
}
|
}
|
||||||
|
.h-\[1em\] {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
.h-\[var\(--radix-select-trigger-height\)\] {
|
.h-\[var\(--radix-select-trigger-height\)\] {
|
||||||
height: var(--radix-select-trigger-height);
|
height: var(--radix-select-trigger-height);
|
||||||
}
|
}
|
||||||
@@ -7135,6 +7147,12 @@
|
|||||||
.w-\[100px\] {
|
.w-\[100px\] {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
.w-\[400px\] {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.w-\[600px\] {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
.w-auto {
|
.w-auto {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
@@ -7147,6 +7165,12 @@
|
|||||||
.w-screen {
|
.w-screen {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
.w-xl {
|
||||||
|
width: var(--container-xl);
|
||||||
|
}
|
||||||
|
.w-xs {
|
||||||
|
width: var(--container-xs);
|
||||||
|
}
|
||||||
.max-w-2xl {
|
.max-w-2xl {
|
||||||
max-width: var(--container-2xl);
|
max-width: var(--container-2xl);
|
||||||
}
|
}
|
||||||
@@ -7872,6 +7896,9 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.overflow-x-auto {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
.overflow-x-hidden {
|
.overflow-x-hidden {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
@@ -8223,6 +8250,10 @@
|
|||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
}
|
}
|
||||||
|
.border-1 {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
.border-2 {
|
.border-2 {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
@@ -8399,6 +8430,18 @@
|
|||||||
.border-\[\#fbf0df\] {
|
.border-\[\#fbf0df\] {
|
||||||
border-color: #fbf0df;
|
border-color: #fbf0df;
|
||||||
}
|
}
|
||||||
|
.border-base-300 {
|
||||||
|
border-color: var(--color-base-300);
|
||||||
|
}
|
||||||
|
.border-base-content\/5 {
|
||||||
|
border-color: var(--color-base-content);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.border-gray-100 {
|
||||||
|
border-color: var(--color-gray-100);
|
||||||
|
}
|
||||||
.border-gray-200 {
|
.border-gray-200 {
|
||||||
border-color: var(--color-gray-200);
|
border-color: var(--color-gray-200);
|
||||||
}
|
}
|
||||||
@@ -8565,6 +8608,12 @@
|
|||||||
.bg-\[\#fbf0df\] {
|
.bg-\[\#fbf0df\] {
|
||||||
background-color: #fbf0df;
|
background-color: #fbf0df;
|
||||||
}
|
}
|
||||||
|
.bg-base-100 {
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
}
|
||||||
|
.bg-base-200 {
|
||||||
|
background-color: var(--color-base-200);
|
||||||
|
}
|
||||||
.bg-blue-400 {
|
.bg-blue-400 {
|
||||||
background-color: var(--color-blue-400);
|
background-color: var(--color-blue-400);
|
||||||
}
|
}
|
||||||
@@ -10168,6 +10217,9 @@
|
|||||||
.text-blue-400 {
|
.text-blue-400 {
|
||||||
color: var(--color-blue-400);
|
color: var(--color-blue-400);
|
||||||
}
|
}
|
||||||
|
.text-error {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
.text-gray-400 {
|
.text-gray-400 {
|
||||||
color: var(--color-gray-400);
|
color: var(--color-gray-400);
|
||||||
}
|
}
|
||||||
@@ -10177,6 +10229,9 @@
|
|||||||
.text-green-400 {
|
.text-green-400 {
|
||||||
color: var(--color-green-400);
|
color: var(--color-green-400);
|
||||||
}
|
}
|
||||||
|
.text-info {
|
||||||
|
color: var(--color-info);
|
||||||
|
}
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
@@ -10186,6 +10241,9 @@
|
|||||||
.text-slate-900 {
|
.text-slate-900 {
|
||||||
color: var(--color-slate-900);
|
color: var(--color-slate-900);
|
||||||
}
|
}
|
||||||
|
.text-success {
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
.text-white {
|
.text-white {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ extension DatabaseClient.Migrations: DependencyKey {
|
|||||||
public static let liveValue = Self(
|
public static let liveValue = Self(
|
||||||
run: {
|
run: {
|
||||||
[
|
[
|
||||||
|
User.Migrate(),
|
||||||
|
User.Token.Migrate(),
|
||||||
Project.Migrate(),
|
Project.Migrate(),
|
||||||
ComponentPressureLoss.Migrate(),
|
ComponentPressureLoss.Migrate(),
|
||||||
EquipmentInfo.Migrate(),
|
EquipmentInfo.Migrate(),
|
||||||
|
|||||||
294
Sources/DatabaseClient/Users.swift
Normal file
294
Sources/DatabaseClient/Users.swift
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Fluent
|
||||||
|
import ManualDCore
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
extension DatabaseClient {
|
||||||
|
@DependencyClient
|
||||||
|
public struct Users: Sendable {
|
||||||
|
public var create: @Sendable (User.Create) async throws -> User
|
||||||
|
public var delete: @Sendable (User.ID) async throws -> Void
|
||||||
|
public var get: @Sendable (User.ID) async throws -> User?
|
||||||
|
public var login: @Sendable (User.Login) async throws -> User.Token
|
||||||
|
public var logout: @Sendable (User.Token.ID) async throws -> Void
|
||||||
|
// public var token: @Sendable (User.ID) async throws -> User.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DatabaseClient.Users: TestDependencyKey {
|
||||||
|
public static let testValue = Self()
|
||||||
|
|
||||||
|
public static func live(database: any Database) -> Self {
|
||||||
|
.init(
|
||||||
|
create: { request in
|
||||||
|
let model = try request.toModel()
|
||||||
|
try await model.save(on: database)
|
||||||
|
return try model.toDTO()
|
||||||
|
},
|
||||||
|
delete: { id in
|
||||||
|
guard let model = try await UserModel.find(id, on: database) else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
try await model.delete(on: database)
|
||||||
|
},
|
||||||
|
get: { id in
|
||||||
|
try await UserModel.find(id, on: database).map { try $0.toDTO() }
|
||||||
|
},
|
||||||
|
login: { request in
|
||||||
|
guard
|
||||||
|
let user = try await UserModel.query(on: database)
|
||||||
|
.with(\.$token)
|
||||||
|
.filter(\UserModel.$email == request.email)
|
||||||
|
.first()
|
||||||
|
else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
|
||||||
|
let token: User.Token
|
||||||
|
|
||||||
|
// Check if there's a user token
|
||||||
|
if let userToken = user.token {
|
||||||
|
token = try userToken.toDTO()
|
||||||
|
} else {
|
||||||
|
// generate a new token
|
||||||
|
let tokenModel = try user.generateToken()
|
||||||
|
try await tokenModel.save(on: database)
|
||||||
|
token = try tokenModel.toDTO()
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
},
|
||||||
|
logout: { tokenID in
|
||||||
|
guard let token = try await UserTokenModel.find(tokenID, on: database) else { return }
|
||||||
|
try await token.delete(on: database)
|
||||||
|
}
|
||||||
|
// ,
|
||||||
|
// token: { id in
|
||||||
|
// }
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension User {
|
||||||
|
struct Migrate: AsyncMigration {
|
||||||
|
let name = "CreateUser"
|
||||||
|
|
||||||
|
func prepare(on database: any Database) async throws {
|
||||||
|
try await database.schema(UserModel.schema)
|
||||||
|
.id()
|
||||||
|
.field("username", .string, .required)
|
||||||
|
.field("email", .string, .required)
|
||||||
|
.field("password_hash", .string, .required)
|
||||||
|
.field("created_at", .datetime)
|
||||||
|
.field("updated_at", .datetime)
|
||||||
|
.unique(on: "email", "username")
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any Database) async throws {
|
||||||
|
try await database.schema(UserModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension User.Token {
|
||||||
|
struct Migrate: AsyncMigration {
|
||||||
|
let name = "CreateUserToken"
|
||||||
|
|
||||||
|
func prepare(on database: any Database) async throws {
|
||||||
|
try await database.schema(UserTokenModel.schema)
|
||||||
|
.id()
|
||||||
|
.field("value", .string, .required)
|
||||||
|
.field("user_id", .uuid, .required, .references(UserModel.schema, "id"))
|
||||||
|
.unique(on: "value")
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any Database) async throws {
|
||||||
|
try await database.schema(UserTokenModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension User {
|
||||||
|
|
||||||
|
static func hashPassword(_ password: String) throws -> String {
|
||||||
|
try Bcrypt.hash(password, cost: 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension User.Create {
|
||||||
|
|
||||||
|
func toModel() throws -> UserModel {
|
||||||
|
try validate()
|
||||||
|
return try .init(username: username, email: email, passwordHash: User.hashPassword(password))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate() throws {
|
||||||
|
guard !username.isEmpty else {
|
||||||
|
throw ValidationError("Username should not be empty.")
|
||||||
|
}
|
||||||
|
guard !email.isEmpty else {
|
||||||
|
throw ValidationError("Email should not be empty")
|
||||||
|
}
|
||||||
|
guard password.count > 8 else {
|
||||||
|
throw ValidationError("Password should be more than 8 characters long.")
|
||||||
|
}
|
||||||
|
guard password == confirmPassword else {
|
||||||
|
throw ValidationError("Passwords do not match.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UserModel: Model, @unchecked Sendable {
|
||||||
|
|
||||||
|
static let schema = "user"
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: "username")
|
||||||
|
var username: String
|
||||||
|
|
||||||
|
@Field(key: "email")
|
||||||
|
var email: String
|
||||||
|
|
||||||
|
@Field(key: "password_hash")
|
||||||
|
var passwordHash: String
|
||||||
|
|
||||||
|
@Timestamp(key: "createdAt", on: .create, format: .iso8601)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: "updatedAt", on: .update, format: .iso8601)
|
||||||
|
var updatedAt: Date?
|
||||||
|
|
||||||
|
@OptionalChild(for: \.$user)
|
||||||
|
var token: UserTokenModel?
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: UUID? = nil,
|
||||||
|
username: String,
|
||||||
|
email: String,
|
||||||
|
passwordHash: String
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.passwordHash = passwordHash
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDTO() throws -> User {
|
||||||
|
try .init(
|
||||||
|
id: requireID(),
|
||||||
|
email: email,
|
||||||
|
username: username,
|
||||||
|
createdAt: createdAt!,
|
||||||
|
updatedAt: updatedAt!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateToken() throws -> UserTokenModel {
|
||||||
|
try .init(
|
||||||
|
value: [UInt8].random(count: 16).base64,
|
||||||
|
userID: requireID()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyPassword(_ password: String) throws -> Bool {
|
||||||
|
try Bcrypt.verify(password, created: passwordHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UserTokenModel: Model, Codable, @unchecked Sendable {
|
||||||
|
|
||||||
|
static let schema = "user_token"
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: "value")
|
||||||
|
var value: String
|
||||||
|
|
||||||
|
@Parent(key: "user_id")
|
||||||
|
var user: UserModel
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(id: UUID? = nil, value: String, userID: UserModel.IDValue) {
|
||||||
|
self.id = id
|
||||||
|
self.value = value
|
||||||
|
$user.id = userID
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDTO() throws -> User.Token {
|
||||||
|
try .init(id: requireID(), userID: $user.id, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Authentication
|
||||||
|
|
||||||
|
extension User: Authenticatable {}
|
||||||
|
extension User: SessionAuthenticatable {
|
||||||
|
public var sessionID: String { username }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct UserPasswordAuthenticator: AsyncBasicAuthenticator {
|
||||||
|
public typealias User = ManualDCore.User
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public func authenticate(basic: BasicAuthorization, for request: Request) async throws {
|
||||||
|
guard
|
||||||
|
let user = try await UserModel.query(on: request.db)
|
||||||
|
.filter(\UserModel.$username == basic.username)
|
||||||
|
.first(),
|
||||||
|
try user.verifyPassword(basic.password)
|
||||||
|
else {
|
||||||
|
throw Abort(.unauthorized)
|
||||||
|
}
|
||||||
|
try request.auth.login(user.toDTO())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct UserTokenAuthenticator: AsyncBearerAuthenticator {
|
||||||
|
public typealias User = ManualDCore.User
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public func authenticate(bearer: BearerAuthorization, for request: Request) async throws {
|
||||||
|
guard
|
||||||
|
let token = try await UserTokenModel.query(on: request.db)
|
||||||
|
.filter(\UserTokenModel.$value == bearer.token)
|
||||||
|
.with(\UserTokenModel.$user)
|
||||||
|
.first()
|
||||||
|
else {
|
||||||
|
throw Abort(.unauthorized)
|
||||||
|
}
|
||||||
|
try request.auth.login(token.user.toDTO())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct UserSessionAuthenticator: AsyncSessionAuthenticator {
|
||||||
|
public typealias User = ManualDCore.User
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public func authenticate(sessionID: User.SessionID, for request: Request) async throws {
|
||||||
|
guard
|
||||||
|
let user = try await UserModel.query(on: request.db)
|
||||||
|
.filter(\UserModel.$username == sessionID)
|
||||||
|
.first()
|
||||||
|
else {
|
||||||
|
throw Abort(.unauthorized)
|
||||||
|
}
|
||||||
|
try request.auth.login(user.toDTO())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ extension SiteRoute {
|
|||||||
case room(RoomRoute)
|
case room(RoomRoute)
|
||||||
case frictionRate(FrictionRateRoute)
|
case frictionRate(FrictionRateRoute)
|
||||||
case effectiveLength(EffectiveLengthRoute)
|
case effectiveLength(EffectiveLengthRoute)
|
||||||
|
case user(UserRoute)
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
Route(.case(Self.project)) {
|
Route(.case(Self.project)) {
|
||||||
@@ -25,6 +26,9 @@ extension SiteRoute {
|
|||||||
Route(.case(Self.effectiveLength)) {
|
Route(.case(Self.effectiveLength)) {
|
||||||
SiteRoute.View.EffectiveLengthRoute.router
|
SiteRoute.View.EffectiveLengthRoute.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.user)) {
|
||||||
|
SiteRoute.View.UserRoute.router
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,3 +177,74 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
|||||||
case group
|
case group
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SiteRoute.View {
|
||||||
|
public enum UserRoute: Equatable, Sendable {
|
||||||
|
case login(Login)
|
||||||
|
case signup(Signup)
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.login)) {
|
||||||
|
SiteRoute.View.UserRoute.Login.router
|
||||||
|
}
|
||||||
|
Route(.case(Self.signup)) {
|
||||||
|
SiteRoute.View.UserRoute.Signup.router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SiteRoute.View.UserRoute {
|
||||||
|
|
||||||
|
public enum Login: Equatable, Sendable {
|
||||||
|
case index
|
||||||
|
case submit(User.Login)
|
||||||
|
|
||||||
|
static let rootPath = "login"
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.index)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.get
|
||||||
|
}
|
||||||
|
Route(.case(Self.submit)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.post
|
||||||
|
Body {
|
||||||
|
FormData {
|
||||||
|
Field("email", .string)
|
||||||
|
Field("password", .string)
|
||||||
|
}
|
||||||
|
.map(.memberwise(User.Login.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Signup: Equatable, Sendable {
|
||||||
|
case index
|
||||||
|
case submit(User.Create)
|
||||||
|
|
||||||
|
static let rootPath = "signup"
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.index)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.get
|
||||||
|
}
|
||||||
|
Route(.case(Self.submit)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.post
|
||||||
|
Body {
|
||||||
|
FormData {
|
||||||
|
Field("username", .string)
|
||||||
|
Field("email", .string)
|
||||||
|
Field("password", .string)
|
||||||
|
Field("confirmPassword", .string)
|
||||||
|
}
|
||||||
|
.map(.memberwise(User.Create.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
70
Sources/ManualDCore/User.swift
Normal file
70
Sources/ManualDCore/User.swift
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct User: Codable, Equatable, Identifiable, Sendable {
|
||||||
|
|
||||||
|
public let id: UUID
|
||||||
|
public let email: String
|
||||||
|
public let username: String
|
||||||
|
public let createdAt: Date
|
||||||
|
public let updatedAt: Date
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: UUID,
|
||||||
|
email: String,
|
||||||
|
username: String,
|
||||||
|
createdAt: Date,
|
||||||
|
updatedAt: Date
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.updatedAt = updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension User {
|
||||||
|
public struct Create: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let username: String
|
||||||
|
public let email: String
|
||||||
|
public let password: String
|
||||||
|
public let confirmPassword: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
username: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
confirmPassword: String
|
||||||
|
) {
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.password = password
|
||||||
|
self.confirmPassword = confirmPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Login: Codable, Equatable, Sendable {
|
||||||
|
public let email: String
|
||||||
|
public let password: String
|
||||||
|
|
||||||
|
public init(email: String, password: String) {
|
||||||
|
self.email = email
|
||||||
|
self.password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Token: Codable, Equatable, Identifiable, Sendable {
|
||||||
|
|
||||||
|
public let id: UUID
|
||||||
|
public let userID: User.ID
|
||||||
|
public let value: String
|
||||||
|
|
||||||
|
public init(id: UUID, userID: User.ID, value: String) {
|
||||||
|
self.id = id
|
||||||
|
self.userID = userID
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public struct Input: HTML, Sendable {
|
|||||||
.id(id ?? ""), .name(_name), .placeholder(placeholder),
|
.id(id ?? ""), .name(_name), .placeholder(placeholder),
|
||||||
.class(
|
.class(
|
||||||
"""
|
"""
|
||||||
w-full rounded-md bg-white px-3 py-1.5 text-slate-900 outline-1
|
input w-full rounded-md bg-white px-3 py-1.5 text-slate-900 outline-1
|
||||||
-outline-offset-1 outline-slate-300 focus:outline focus:-outline-offset-2
|
-outline-offset-1 outline-slate-300 focus:outline focus:-outline-offset-2
|
||||||
focus:outline-indigo-600 invalid:border-red-500 out-of-range:border-red-500
|
focus:outline-indigo-600 invalid:border-red-500 out-of-range:border-red-500
|
||||||
"""
|
"""
|
||||||
@@ -67,4 +67,30 @@ extension HTMLAttribute where Tag == HTMLTag.input {
|
|||||||
public static func step(_ value: String) -> Self {
|
public static func step(_ value: String) -> Self {
|
||||||
.init(name: "step", value: value)
|
.init(name: "step", value: value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func minlength(_ value: String) -> Self {
|
||||||
|
.init(name: "minlength", value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func pattern(value: String) -> Self {
|
||||||
|
.init(name: "pattern", value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func pattern(_ type: PatternType) -> Self {
|
||||||
|
pattern(value: type.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PatternType: Sendable {
|
||||||
|
case password
|
||||||
|
case username
|
||||||
|
|
||||||
|
var value: String {
|
||||||
|
switch self {
|
||||||
|
case .password:
|
||||||
|
return "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
|
||||||
|
case .username:
|
||||||
|
return "[A-Za-z][A-Za-z0-9\\-]*"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ extension SVG {
|
|||||||
public enum Key: Sendable {
|
public enum Key: Sendable {
|
||||||
case circlePlus
|
case circlePlus
|
||||||
case close
|
case close
|
||||||
|
case email
|
||||||
|
case key
|
||||||
case squarePen
|
case squarePen
|
||||||
|
case user
|
||||||
|
|
||||||
var svg: String {
|
var svg: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -29,10 +32,57 @@ extension SVG {
|
|||||||
return """
|
return """
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||||
"""
|
"""
|
||||||
|
case .email:
|
||||||
|
return """
|
||||||
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="2.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
|
||||||
|
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
case .key:
|
||||||
|
return """
|
||||||
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="2.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"
|
||||||
|
></path>
|
||||||
|
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor"></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
case .squarePen:
|
case .squarePen:
|
||||||
return """
|
return """
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||||
"""
|
"""
|
||||||
|
case .user:
|
||||||
|
return """
|
||||||
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="2.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="12" cy="7" r="4"></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ extension ViewController.Request {
|
|||||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||||
case .effectiveLength(let route):
|
case .effectiveLength(let route):
|
||||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||||
|
case .user(let route):
|
||||||
|
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||||
default:
|
default:
|
||||||
// FIX: FIX
|
// FIX: FIX
|
||||||
return mainPage
|
return mainPage
|
||||||
@@ -101,6 +103,24 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SiteRoute.View.UserRoute {
|
||||||
|
|
||||||
|
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
|
switch self {
|
||||||
|
case .login(.index):
|
||||||
|
return MainPage(active: .projects, showSidebar: false) {
|
||||||
|
LoginForm()
|
||||||
|
}
|
||||||
|
case .signup(.index):
|
||||||
|
return MainPage(active: .projects, showSidebar: false) {
|
||||||
|
LoginForm(style: .signup)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return div { "Fix Me!" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let mainPage: AnySendableHTML = {
|
private let mainPage: AnySendableHTML = {
|
||||||
MainPage(active: .projects) {
|
MainPage(active: .projects) {
|
||||||
div {
|
div {
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
|||||||
public var lang: String { "en" }
|
public var lang: String { "en" }
|
||||||
let inner: Inner
|
let inner: Inner
|
||||||
let activeTab: Sidebar.ActiveTab
|
let activeTab: Sidebar.ActiveTab
|
||||||
|
let showSidebar: Bool
|
||||||
|
|
||||||
init(active activeTab: Sidebar.ActiveTab, _ inner: () -> Inner) {
|
init(
|
||||||
|
active activeTab: Sidebar.ActiveTab,
|
||||||
|
showSidebar: Bool = true,
|
||||||
|
_ inner: () -> Inner
|
||||||
|
) {
|
||||||
self.activeTab = activeTab
|
self.activeTab = activeTab
|
||||||
|
self.showSidebar = showSidebar
|
||||||
self.inner = inner()
|
self.inner = inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +30,9 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
|||||||
// div(.class("bg-white dark:bg-gray-800 dark:text-white")) {
|
// div(.class("bg-white dark:bg-gray-800 dark:text-white")) {
|
||||||
div {
|
div {
|
||||||
div(.class("flex flex-row")) {
|
div(.class("flex flex-row")) {
|
||||||
|
if showSidebar {
|
||||||
Sidebar(active: activeTab)
|
Sidebar(active: activeTab)
|
||||||
|
}
|
||||||
main(.class("flex flex-col h-screen w-full px-6 py-10")) {
|
main(.class("flex flex-col h-screen w-full px-6 py-10")) {
|
||||||
inner
|
inner
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ struct RoomsView: HTML, Sendable {
|
|||||||
let rooms: [Room]
|
let rooms: [Room]
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
div(.class("m-10")) {
|
div {
|
||||||
Row {
|
Row {
|
||||||
h1(.class("text-3xl font-bold pb-6")) { "Room Loads" }
|
h1(.class("text-2xl font-bold")) { "Room Loads" }
|
||||||
div(
|
div(
|
||||||
.class("tooltip tooltip-left"),
|
.class("tooltip tooltip-left"),
|
||||||
.data("tip", value: "Add room")
|
.data("tip", value: "Add room")
|
||||||
@@ -20,94 +20,67 @@ struct RoomsView: HTML, Sendable {
|
|||||||
.hx.get(route: .room(.form(dismiss: false))),
|
.hx.get(route: .room(.form(dismiss: false))),
|
||||||
.hx.target("#roomForm"),
|
.hx.target("#roomForm"),
|
||||||
.hx.swap(.outerHTML),
|
.hx.swap(.outerHTML),
|
||||||
.class("btn btn-primary w-[40px]")
|
.class("btn btn-primary w-[40px] text-2xl")
|
||||||
) {
|
) {
|
||||||
"+"
|
"+"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.attributes(.class("pb-6"))
|
||||||
|
|
||||||
div(
|
div(.class("overflow-x-auto rounded-box border")) {
|
||||||
.id("roomTable"),
|
table(.class("table table-zebra"), .id("roomsTable")) {
|
||||||
.class(
|
thead {
|
||||||
"""
|
tr {
|
||||||
border border-gray-200 rounded-lg shadow-lg
|
th { Label("Name") }
|
||||||
grid grid-cols-5 p-4
|
th { Label("Heating Load") }
|
||||||
"""
|
th { Label("Cooling Total") }
|
||||||
)
|
th { Label("Cooling Sensible") }
|
||||||
) {
|
th { Label("Register Count") }
|
||||||
// Header
|
|
||||||
Label("Name")
|
|
||||||
// Pushes items to right
|
|
||||||
Row {
|
|
||||||
div {}
|
|
||||||
Label("Heating Load")
|
|
||||||
}
|
}
|
||||||
Row {
|
|
||||||
div {}
|
|
||||||
Label("Cooling Total")
|
|
||||||
}
|
}
|
||||||
Row {
|
tbody {
|
||||||
div {}
|
for room in rooms {
|
||||||
Label("Cooling Sensible")
|
tr {
|
||||||
|
td { room.name }
|
||||||
|
td {
|
||||||
|
Number(room.heatingLoad)
|
||||||
|
.attributes(.class("text-error"))
|
||||||
}
|
}
|
||||||
Row {
|
td {
|
||||||
div {}
|
Number(room.coolingLoad.total)
|
||||||
Label("Register Count")
|
.attributes(.class("text-success"))
|
||||||
}
|
}
|
||||||
|
td {
|
||||||
// Divider
|
Number(room.coolingLoad.sensible)
|
||||||
div(.class("border-b border-gray-200 col-span-5 mb-2")) {}
|
.attributes(.class("text-info"))
|
||||||
|
|
||||||
// Rows
|
|
||||||
for row in rooms {
|
|
||||||
span { row.name }
|
|
||||||
// Pushes items to right
|
|
||||||
Row {
|
|
||||||
div {}
|
|
||||||
Number(row.heatingLoad)
|
|
||||||
.attributes(.class("text-red-500"))
|
|
||||||
}
|
}
|
||||||
Row {
|
td {
|
||||||
div {}
|
Number(room.registerCount)
|
||||||
Number(row.coolingLoad.total)
|
|
||||||
.attributes(.class("text-green-400"))
|
|
||||||
}
|
}
|
||||||
Row {
|
|
||||||
div {}
|
|
||||||
Number(row.coolingLoad.sensible)
|
|
||||||
.attributes(.class("text-blue-400"))
|
|
||||||
}
|
}
|
||||||
Row {
|
|
||||||
div {}
|
|
||||||
Number(row.registerCount)
|
|
||||||
}
|
}
|
||||||
|
// TOTALS
|
||||||
// Divider
|
tr(.class("font-bold text-xl")) {
|
||||||
div(.class("border-b border-gray-200 col-span-5 mb-2")) {}
|
td { Label("Total") }
|
||||||
}
|
td {
|
||||||
|
|
||||||
// Totals
|
|
||||||
Label("Total")
|
|
||||||
Row {
|
|
||||||
div {}
|
|
||||||
Number(rooms.heatingTotal)
|
Number(rooms.heatingTotal)
|
||||||
.attributes(.class("badge badge-outline badge-error badge-xl text-xl font-bold"))
|
.attributes(.class("badge badge-outline badge-error badge-xl"))
|
||||||
}
|
}
|
||||||
Row {
|
td {
|
||||||
div {}
|
|
||||||
Number(rooms.coolingTotal)
|
Number(rooms.coolingTotal)
|
||||||
.attributes(.class("badge badge-outline badge-success badge-xl text-xl font-bold"))
|
.attributes(
|
||||||
|
.class("badge badge-outline badge-success badge-xl"))
|
||||||
}
|
}
|
||||||
Row {
|
td {
|
||||||
div {}
|
|
||||||
Number(rooms.coolingSensibleTotal)
|
Number(rooms.coolingSensibleTotal)
|
||||||
.attributes(.class("badge badge-outline badge-info badge-xl text-xl font-bold"))
|
.attributes(.class("badge badge-outline badge-info badge-xl"))
|
||||||
|
}
|
||||||
|
td {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Empty register count column
|
|
||||||
div {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomForm(dismiss: true)
|
RoomForm(dismiss: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Elementary
|
|||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
// TODO: Need to add active to sidebar links.
|
// TODO: Update to use DaisyUI drawer.
|
||||||
struct Sidebar: HTML {
|
struct Sidebar: HTML {
|
||||||
|
|
||||||
let active: ActiveTab
|
let active: ActiveTab
|
||||||
@@ -23,7 +23,7 @@ struct Sidebar: HTML {
|
|||||||
Label("Theme")
|
Label("Theme")
|
||||||
input(.type(.checkbox), .class("toggle theme-controller"), .value("light"))
|
input(.type(.checkbox), .class("toggle theme-controller"), .value("light"))
|
||||||
}
|
}
|
||||||
.attributes(.class("py-4"))
|
.attributes(.class("p-4"))
|
||||||
|
|
||||||
row(title: "Project", icon: .mapPin, route: .project(.index))
|
row(title: "Project", icon: .mapPin, route: .project(.index))
|
||||||
.attributes(.data("active", value: active == .projects ? "true" : "false"))
|
.attributes(.data("active", value: active == .projects ? "true" : "false"))
|
||||||
|
|||||||
158
Sources/ViewController/Views/User/LoginForm.swift
Normal file
158
Sources/ViewController/Views/User/LoginForm.swift
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
struct LoginForm: HTML, Sendable {
|
||||||
|
|
||||||
|
let style: Style
|
||||||
|
|
||||||
|
init(style: Style = .login) {
|
||||||
|
self.style = style
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some HTML {
|
||||||
|
form {
|
||||||
|
fieldset(.class("fieldset bg-base-200 border-base-300 rounded-box w-xl border p-4")) {
|
||||||
|
legend(.class("fieldset-legend")) { style.title }
|
||||||
|
|
||||||
|
if style == .signup {
|
||||||
|
label(.class("input validator w-full")) {
|
||||||
|
SVG(.user)
|
||||||
|
input(
|
||||||
|
.type(.text), .required, .placeholder("Username"),
|
||||||
|
.minlength("3"), .pattern(.username)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
div(.class("validator-hint hidden")) {
|
||||||
|
"Enter valid username"
|
||||||
|
br()
|
||||||
|
"Must be at least 3 characters"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label(.class("input validator w-full")) {
|
||||||
|
SVG(.email)
|
||||||
|
input(
|
||||||
|
.type(.email), .placeholder("Email"), .required
|
||||||
|
)
|
||||||
|
}
|
||||||
|
div(.class("validator-hint hidden")) { "Enter valid email address." }
|
||||||
|
|
||||||
|
label(.class("input validator w-full")) {
|
||||||
|
SVG(.key)
|
||||||
|
input(
|
||||||
|
.type(.password), .placeholder("Password"), .required,
|
||||||
|
.pattern(.password), .minlength("8")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if style == .signup {
|
||||||
|
label(.class("input validator w-full")) {
|
||||||
|
SVG(.key)
|
||||||
|
input(
|
||||||
|
.type(.password), .placeholder("Confirm Password"), .required,
|
||||||
|
.pattern(.password), .minlength("8")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div(.class("validator-hint hidden")) {
|
||||||
|
p {
|
||||||
|
"Must be more than 8 characters, including"
|
||||||
|
br()
|
||||||
|
"At least one number"
|
||||||
|
br()
|
||||||
|
"At least one lowercase letter"
|
||||||
|
br()
|
||||||
|
"At least one uppercase letter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button(.class("btn btn-neutral mt-4")) { style.title }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// div(.class("flex items-center justify-center")) {
|
||||||
|
// div(.class("w-full mx-auto")) {
|
||||||
|
// h1(.class("text-2xl font-bold")) { style.title }
|
||||||
|
// form(
|
||||||
|
// .class("w-full h-screen")
|
||||||
|
// ) {
|
||||||
|
// fieldset(.class("fieldset w-full")) {
|
||||||
|
// legend(.class("fieldset-legend")) { "Email" }
|
||||||
|
// label(.class("input validator")) {
|
||||||
|
// SVG(.email)
|
||||||
|
// input(
|
||||||
|
// .type(.email), .placeholder("mail@site.com"), .required,
|
||||||
|
// .autofocus
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// div(.class("validator-hint hidden")) { "Enter valid email address." }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if style == .signup {
|
||||||
|
// fieldset(.class("fieldset")) {
|
||||||
|
// legend(.class("fieldset-legend")) { "Name" }
|
||||||
|
// label(.class("input validator")) {
|
||||||
|
// input(
|
||||||
|
// .type(.text), .placeholder("Username"), .required,
|
||||||
|
// .init(name: "pattern", value: "[A-Za-z][A-Za-z0-9\\-]*"),
|
||||||
|
// .init(name: "minlength", value: "3")
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// div(.class("validator-hint hidden")) { "Enter valid email address." }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fieldset(.class("fieldset")) {
|
||||||
|
// legend(.class("fieldset-legend")) { "Password" }
|
||||||
|
// label(.class("input validator")) {
|
||||||
|
// SVG(.key)
|
||||||
|
// input(
|
||||||
|
// .type(.password), .placeholder("Password"), .required,
|
||||||
|
// .init(name: "pattern", value: "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"),
|
||||||
|
// .init(name: "minlength", value: "8")
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// if style == .signup {
|
||||||
|
// label(.class("input validator")) {
|
||||||
|
// SVG(.key)
|
||||||
|
// input(
|
||||||
|
// .type(.password), .placeholder("Confirm Password"), .required,
|
||||||
|
// .init(name: "pattern", value: "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"),
|
||||||
|
// .init(name: "minlength", value: "8")
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// div(.class("validator-hint hidden")) {
|
||||||
|
// p {
|
||||||
|
// "Must be more than 8 characters, including"
|
||||||
|
// br()
|
||||||
|
// "At least one number"
|
||||||
|
// br()
|
||||||
|
// "At least one lowercase letter"
|
||||||
|
// br()
|
||||||
|
// "At least one uppercase letter"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// SubmitButton(title: style.title)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LoginForm {
|
||||||
|
enum Style: Equatable, Sendable {
|
||||||
|
case login
|
||||||
|
case signup
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .login: return "Login"
|
||||||
|
case .signup: return "Sign Up"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
Sources/ViewController/Views/User/SignupForm.swift
Normal file
0
Sources/ViewController/Views/User/SignupForm.swift
Normal file
Reference in New Issue
Block a user