mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 13:32:51 +00:00
Starting with mail functionality
This commit is contained in:
parent
2e3454c7a1
commit
f235f3a030
@ -23,6 +23,7 @@ trait BackendApp[F[_]] {
|
|||||||
def node: ONode[F]
|
def node: ONode[F]
|
||||||
def job: OJob[F]
|
def job: OJob[F]
|
||||||
def item: OItem[F]
|
def item: OItem[F]
|
||||||
|
def mail: OMail[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
object BackendApp {
|
object BackendApp {
|
||||||
@ -45,6 +46,7 @@ object BackendApp {
|
|||||||
nodeImpl <- ONode(store)
|
nodeImpl <- ONode(store)
|
||||||
jobImpl <- OJob(store, httpClientEc)
|
jobImpl <- OJob(store, httpClientEc)
|
||||||
itemImpl <- OItem(store)
|
itemImpl <- OItem(store)
|
||||||
|
mailImpl <- OMail(store)
|
||||||
} yield new BackendApp[F] {
|
} yield new BackendApp[F] {
|
||||||
val login: Login[F] = loginImpl
|
val login: Login[F] = loginImpl
|
||||||
val signup: OSignup[F] = signupImpl
|
val signup: OSignup[F] = signupImpl
|
||||||
@ -57,6 +59,7 @@ object BackendApp {
|
|||||||
val node = nodeImpl
|
val node = nodeImpl
|
||||||
val job = jobImpl
|
val job = jobImpl
|
||||||
val item = itemImpl
|
val item = itemImpl
|
||||||
|
val mail = mailImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package docspell.backend.ops
|
||||||
|
|
||||||
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
|
import cats.data.OptionT
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.store._
|
||||||
|
import docspell.store.records.RUserEmail
|
||||||
|
|
||||||
|
trait OMail[F[_]] {
|
||||||
|
|
||||||
|
def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]]
|
||||||
|
|
||||||
|
def createSettings(data: F[RUserEmail]): F[AddResult]
|
||||||
|
|
||||||
|
def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int]
|
||||||
|
}
|
||||||
|
|
||||||
|
object OMail {
|
||||||
|
|
||||||
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, OMail[F]] =
|
||||||
|
Resource.pure(new OMail[F] {
|
||||||
|
def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] =
|
||||||
|
store.transact(RUserEmail.findByAccount(accId, nameQ))
|
||||||
|
|
||||||
|
def createSettings(data: F[RUserEmail]): F[AddResult] =
|
||||||
|
for {
|
||||||
|
ru <- data
|
||||||
|
ins = RUserEmail.insert(ru)
|
||||||
|
exists = RUserEmail.exists(ru.uid, ru.name)
|
||||||
|
ar <- store.add(ins, exists)
|
||||||
|
} yield ar
|
||||||
|
|
||||||
|
def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int] = {
|
||||||
|
val op = for {
|
||||||
|
um <- OptionT(RUserEmail.getByName(accId, name))
|
||||||
|
n <- OptionT.liftF(RUserEmail.update(um.id, data))
|
||||||
|
} yield n
|
||||||
|
|
||||||
|
store.transact(op.value).map(_.getOrElse(0))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1222,7 +1222,10 @@ paths:
|
|||||||
sent e-mails.
|
sent e-mails.
|
||||||
|
|
||||||
Multiple e-mail settings can be specified, they are
|
Multiple e-mail settings can be specified, they are
|
||||||
distinguished by their `name`.
|
distinguished by their `name`. The query `q` parameter does a
|
||||||
|
simple substring search in the connection name.
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/q"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Ok
|
description: Ok
|
||||||
@ -1352,12 +1355,9 @@ components:
|
|||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- smtpHost
|
- smtpHost
|
||||||
- smtpPort
|
|
||||||
- smtpUser
|
|
||||||
- smtpPassword
|
|
||||||
- from
|
- from
|
||||||
- sslType
|
- sslType
|
||||||
- certificateCheck
|
- ignoreCertificates
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
@ -1374,9 +1374,11 @@ components:
|
|||||||
format: password
|
format: password
|
||||||
from:
|
from:
|
||||||
type: string
|
type: string
|
||||||
|
replyTo:
|
||||||
|
type: string
|
||||||
sslType:
|
sslType:
|
||||||
type: string
|
type: string
|
||||||
certificateCheck:
|
ignoreCertificates:
|
||||||
type: boolean
|
type: boolean
|
||||||
CheckFileResult:
|
CheckFileResult:
|
||||||
description: |
|
description: |
|
||||||
|
@ -57,19 +57,20 @@ object RestServer {
|
|||||||
token: AuthToken
|
token: AuthToken
|
||||||
): HttpRoutes[F] =
|
): HttpRoutes[F] =
|
||||||
Router(
|
Router(
|
||||||
"auth" -> LoginRoutes.session(restApp.backend.login, cfg),
|
"auth" -> LoginRoutes.session(restApp.backend.login, cfg),
|
||||||
"tag" -> TagRoutes(restApp.backend, token),
|
"tag" -> TagRoutes(restApp.backend, token),
|
||||||
"equipment" -> EquipmentRoutes(restApp.backend, token),
|
"equipment" -> EquipmentRoutes(restApp.backend, token),
|
||||||
"organization" -> OrganizationRoutes(restApp.backend, token),
|
"organization" -> OrganizationRoutes(restApp.backend, token),
|
||||||
"person" -> PersonRoutes(restApp.backend, token),
|
"person" -> PersonRoutes(restApp.backend, token),
|
||||||
"source" -> SourceRoutes(restApp.backend, token),
|
"source" -> SourceRoutes(restApp.backend, token),
|
||||||
"user" -> UserRoutes(restApp.backend, token),
|
"user" -> UserRoutes(restApp.backend, token),
|
||||||
"collective" -> CollectiveRoutes(restApp.backend, token),
|
"collective" -> CollectiveRoutes(restApp.backend, token),
|
||||||
"queue" -> JobQueueRoutes(restApp.backend, token),
|
"queue" -> JobQueueRoutes(restApp.backend, token),
|
||||||
"item" -> ItemRoutes(restApp.backend, token),
|
"item" -> ItemRoutes(restApp.backend, token),
|
||||||
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
||||||
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
||||||
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token)
|
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
|
||||||
|
"email/settings" -> MailSettingsRoutes(restApp.backend, token)
|
||||||
)
|
)
|
||||||
|
|
||||||
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package docspell.restserver.routes
|
||||||
|
|
||||||
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
|
import org.http4s._
|
||||||
|
import org.http4s.dsl.Http4sDsl
|
||||||
|
import org.http4s.circe.CirceEntityEncoder._
|
||||||
|
import org.http4s.circe.CirceEntityDecoder._
|
||||||
|
|
||||||
|
import docspell.backend.BackendApp
|
||||||
|
import docspell.backend.auth.AuthToken
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.restapi.model._
|
||||||
|
import docspell.store.records.RUserEmail
|
||||||
|
import docspell.store.EmilUtil
|
||||||
|
|
||||||
|
object MailSettingsRoutes {
|
||||||
|
|
||||||
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
|
val dsl = new Http4sDsl[F] {}
|
||||||
|
import dsl._
|
||||||
|
|
||||||
|
HttpRoutes.of {
|
||||||
|
case req @ GET -> Root =>
|
||||||
|
val q = req.params.get("q").map(_.trim).filter(_.nonEmpty)
|
||||||
|
for {
|
||||||
|
list <- backend.mail.getSettings(user.account, q)
|
||||||
|
res = list.map(convert)
|
||||||
|
resp <- Ok(EmailSettingsList(res.toList))
|
||||||
|
} yield resp
|
||||||
|
|
||||||
|
case req @ POST -> Root =>
|
||||||
|
for {
|
||||||
|
in <- req.as[EmailSettings]
|
||||||
|
resp <- Ok(BasicResult(false, "not implemented"))
|
||||||
|
} yield resp
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert(ru: RUserEmail): EmailSettings =
|
||||||
|
EmailSettings(
|
||||||
|
ru.name,
|
||||||
|
ru.smtpHost,
|
||||||
|
ru.smtpPort,
|
||||||
|
ru.smtpUser,
|
||||||
|
ru.smtpPassword,
|
||||||
|
EmilUtil.mailAddressString(ru.mailFrom),
|
||||||
|
ru.mailReplyTo.map(EmilUtil.mailAddressString _),
|
||||||
|
EmilUtil.sslTypeString(ru.smtpSsl),
|
||||||
|
!ru.smtpCertCheck
|
||||||
|
)
|
||||||
|
}
|
34
modules/store/src/main/scala/docspell/store/EmilUtil.scala
Normal file
34
modules/store/src/main/scala/docspell/store/EmilUtil.scala
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package docspell.store
|
||||||
|
|
||||||
|
import emil._
|
||||||
|
import emil.javamail.syntax._
|
||||||
|
|
||||||
|
object EmilUtil {
|
||||||
|
|
||||||
|
def readSSLType(str: String): Either[String, SSLType] =
|
||||||
|
str.toLowerCase match {
|
||||||
|
case "ssl" => Right(SSLType.SSL)
|
||||||
|
case "starttls" => Right(SSLType.StartTLS)
|
||||||
|
case "none" => Right(SSLType.NoEncryption)
|
||||||
|
case _ => Left(s"Invalid ssl-type: $str")
|
||||||
|
}
|
||||||
|
|
||||||
|
def unsafeReadSSLType(str: String): SSLType =
|
||||||
|
readSSLType(str).fold(sys.error, identity)
|
||||||
|
|
||||||
|
def sslTypeString(st: SSLType): String =
|
||||||
|
st match {
|
||||||
|
case SSLType.SSL => "ssl"
|
||||||
|
case SSLType.StartTLS => "starttls"
|
||||||
|
case SSLType.NoEncryption => "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
def readMailAddress(str: String): Either[String, MailAddress] =
|
||||||
|
MailAddress.parse(str)
|
||||||
|
|
||||||
|
def unsafeReadMailAddress(str: String): MailAddress =
|
||||||
|
readMailAddress(str).fold(sys.error, identity)
|
||||||
|
|
||||||
|
def mailAddressString(ma: MailAddress): String =
|
||||||
|
ma.asUnicodeString
|
||||||
|
}
|
@ -2,18 +2,15 @@ package docspell.store.impl
|
|||||||
|
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.{Instant, LocalDate}
|
import java.time.{Instant, LocalDate}
|
||||||
|
import io.circe.{Decoder, Encoder}
|
||||||
import docspell.common.Timestamp
|
|
||||||
|
|
||||||
import docspell.common._
|
|
||||||
import doobie._
|
import doobie._
|
||||||
//import doobie.implicits.javatime._
|
|
||||||
import doobie.implicits.legacy.instant._
|
import doobie.implicits.legacy.instant._
|
||||||
import doobie.util.log.Success
|
import doobie.util.log.Success
|
||||||
import io.circe.{Decoder, Encoder}
|
|
||||||
import docspell.common.syntax.all._
|
|
||||||
import emil.{MailAddress, SSLType}
|
import emil.{MailAddress, SSLType}
|
||||||
import emil.javamail.syntax._
|
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.common.syntax.all._
|
||||||
|
import docspell.store.EmilUtil
|
||||||
|
|
||||||
trait DoobieMeta {
|
trait DoobieMeta {
|
||||||
|
|
||||||
@ -92,29 +89,14 @@ trait DoobieMeta {
|
|||||||
Meta[String].imap(Language.unsafe)(_.iso3)
|
Meta[String].imap(Language.unsafe)(_.iso3)
|
||||||
|
|
||||||
implicit val sslType: Meta[SSLType] =
|
implicit val sslType: Meta[SSLType] =
|
||||||
Meta[String].imap(DoobieMeta.readSSLType)(DoobieMeta.sslTypeString)
|
Meta[String].imap(EmilUtil.unsafeReadSSLType)(EmilUtil.sslTypeString)
|
||||||
|
|
||||||
implicit val mailAddress: Meta[MailAddress] =
|
implicit val mailAddress: Meta[MailAddress] =
|
||||||
Meta[String].imap(str => MailAddress.parse(str).fold(sys.error, identity))(_.asUnicodeString)
|
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
|
||||||
}
|
}
|
||||||
|
|
||||||
object DoobieMeta extends DoobieMeta {
|
object DoobieMeta extends DoobieMeta {
|
||||||
import org.log4s._
|
import org.log4s._
|
||||||
private val logger = getLogger
|
private val logger = getLogger
|
||||||
|
|
||||||
private def readSSLType(str: String): SSLType =
|
|
||||||
str.toLowerCase match {
|
|
||||||
case "ssl" => SSLType.SSL
|
|
||||||
case "starttls" => SSLType.StartTLS
|
|
||||||
case "none" => SSLType.NoEncryption
|
|
||||||
case _ => sys.error(s"Invalid ssl-type: $str")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def sslTypeString(st: SSLType): String =
|
|
||||||
st match {
|
|
||||||
case SSLType.SSL => "ssl"
|
|
||||||
case SSLType.StartTLS => "starttls"
|
|
||||||
case SSLType.NoEncryption => "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@ package docspell.store.records
|
|||||||
|
|
||||||
import doobie._
|
import doobie._
|
||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
|
import cats.data.OptionT
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store.impl.Column
|
import docspell.store.impl.Column
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
@ -10,11 +13,11 @@ import emil.{MailAddress, SSLType}
|
|||||||
case class RUserEmail(
|
case class RUserEmail(
|
||||||
id: Ident,
|
id: Ident,
|
||||||
uid: Ident,
|
uid: Ident,
|
||||||
name: String,
|
name: Ident,
|
||||||
smtpHost: String,
|
smtpHost: String,
|
||||||
smtpPort: Int,
|
smtpPort: Option[Int],
|
||||||
smtpUser: String,
|
smtpUser: Option[String],
|
||||||
smtpPassword: Password,
|
smtpPassword: Option[Password],
|
||||||
smtpSsl: SSLType,
|
smtpSsl: SSLType,
|
||||||
smtpCertCheck: Boolean,
|
smtpCertCheck: Boolean,
|
||||||
mailFrom: MailAddress,
|
mailFrom: MailAddress,
|
||||||
@ -24,6 +27,67 @@ case class RUserEmail(
|
|||||||
|
|
||||||
object RUserEmail {
|
object RUserEmail {
|
||||||
|
|
||||||
|
def apply[F[_]: Sync](
|
||||||
|
uid: Ident,
|
||||||
|
name: Ident,
|
||||||
|
smtpHost: String,
|
||||||
|
smtpPort: Option[Int],
|
||||||
|
smtpUser: Option[String],
|
||||||
|
smtpPassword: Option[Password],
|
||||||
|
smtpSsl: SSLType,
|
||||||
|
smtpCertCheck: Boolean,
|
||||||
|
mailFrom: MailAddress,
|
||||||
|
mailReplyTo: Option[MailAddress]
|
||||||
|
): F[RUserEmail] =
|
||||||
|
for {
|
||||||
|
now <- Timestamp.current[F]
|
||||||
|
id <- Ident.randomId[F]
|
||||||
|
} yield RUserEmail(
|
||||||
|
id,
|
||||||
|
uid,
|
||||||
|
name,
|
||||||
|
smtpHost,
|
||||||
|
smtpPort,
|
||||||
|
smtpUser,
|
||||||
|
smtpPassword,
|
||||||
|
smtpSsl,
|
||||||
|
smtpCertCheck,
|
||||||
|
mailFrom,
|
||||||
|
mailReplyTo,
|
||||||
|
now
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply(
|
||||||
|
accId: AccountId,
|
||||||
|
name: Ident,
|
||||||
|
smtpHost: String,
|
||||||
|
smtpPort: Option[Int],
|
||||||
|
smtpUser: Option[String],
|
||||||
|
smtpPassword: Option[Password],
|
||||||
|
smtpSsl: SSLType,
|
||||||
|
smtpCertCheck: Boolean,
|
||||||
|
mailFrom: MailAddress,
|
||||||
|
mailReplyTo: Option[MailAddress]
|
||||||
|
): OptionT[ConnectionIO, RUserEmail] =
|
||||||
|
for {
|
||||||
|
now <- OptionT.liftF(Timestamp.current[ConnectionIO])
|
||||||
|
id <- OptionT.liftF(Ident.randomId[ConnectionIO])
|
||||||
|
user <- OptionT(RUser.findByAccount(accId))
|
||||||
|
} yield RUserEmail(
|
||||||
|
id,
|
||||||
|
user.uid,
|
||||||
|
name,
|
||||||
|
smtpHost,
|
||||||
|
smtpPort,
|
||||||
|
smtpUser,
|
||||||
|
smtpPassword,
|
||||||
|
smtpSsl,
|
||||||
|
smtpCertCheck,
|
||||||
|
mailFrom,
|
||||||
|
mailReplyTo,
|
||||||
|
now
|
||||||
|
)
|
||||||
|
|
||||||
val table = fr"useremail"
|
val table = fr"useremail"
|
||||||
|
|
||||||
object Columns {
|
object Columns {
|
||||||
@ -65,7 +129,54 @@ object RUserEmail {
|
|||||||
sql"${v.id},${v.uid},${v.name},${v.smtpHost},${v.smtpPort},${v.smtpUser},${v.smtpPassword},${v.smtpSsl},${v.smtpCertCheck},${v.mailFrom},${v.mailReplyTo},${v.created}"
|
sql"${v.id},${v.uid},${v.name},${v.smtpHost},${v.smtpPort},${v.smtpUser},${v.smtpPassword},${v.smtpSsl},${v.smtpCertCheck},${v.mailFrom},${v.mailReplyTo},${v.created}"
|
||||||
).update.run
|
).update.run
|
||||||
|
|
||||||
|
def update(eId: Ident, v: RUserEmail): ConnectionIO[Int] =
|
||||||
|
updateRow(table, id.is(eId), commas(
|
||||||
|
name.setTo(v.name),
|
||||||
|
smtpHost.setTo(v.smtpHost),
|
||||||
|
smtpPort.setTo(v.smtpPort),
|
||||||
|
smtpUser.setTo(v.smtpUser),
|
||||||
|
smtpPass.setTo(v.smtpPassword),
|
||||||
|
smtpSsl.setTo(v.smtpSsl),
|
||||||
|
smtpCertCheck.setTo(v.smtpCertCheck),
|
||||||
|
mailFrom.setTo(v.mailFrom),
|
||||||
|
mailReplyTo.setTo(v.mailReplyTo)
|
||||||
|
)).update.run
|
||||||
|
|
||||||
def findByUser(userId: Ident): ConnectionIO[Vector[RUserEmail]] =
|
def findByUser(userId: Ident): ConnectionIO[Vector[RUserEmail]] =
|
||||||
selectSimple(all, table, uid.is(userId)).query[RUserEmail].to[Vector]
|
selectSimple(all, table, uid.is(userId)).query[RUserEmail].to[Vector]
|
||||||
|
|
||||||
|
private def findByAccount0(
|
||||||
|
accId: AccountId,
|
||||||
|
nameQ: Option[String],
|
||||||
|
exact: Boolean
|
||||||
|
): Query0[RUserEmail] = {
|
||||||
|
val mUid = uid.prefix("m")
|
||||||
|
val mName = name.prefix("m")
|
||||||
|
val uId = RUser.Columns.uid.prefix("u")
|
||||||
|
val uColl = RUser.Columns.cid.prefix("u")
|
||||||
|
val uLogin = RUser.Columns.login.prefix("u")
|
||||||
|
val from = table ++ fr"m INNER JOIN" ++ RUser.table ++ fr"u ON" ++ mUid.is(uId)
|
||||||
|
val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user)) ++ (nameQ match {
|
||||||
|
case Some(str) if exact => Seq(mName.is(str))
|
||||||
|
case Some(str) if !exact => Seq(mName.lowerLike(s"%${str.toLowerCase}%"))
|
||||||
|
case None => Seq.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
selectSimple(all, from, and(cond)).query[RUserEmail]
|
||||||
|
}
|
||||||
|
|
||||||
|
def findByAccount(
|
||||||
|
accId: AccountId,
|
||||||
|
nameQ: Option[String]
|
||||||
|
): ConnectionIO[Vector[RUserEmail]] =
|
||||||
|
findByAccount0(accId, nameQ, false).to[Vector]
|
||||||
|
|
||||||
|
def getByName(accId: AccountId, name: Ident): ConnectionIO[Option[RUserEmail]] =
|
||||||
|
findByAccount0(accId, Some(name.id), true).option
|
||||||
|
|
||||||
|
def exists(accId: AccountId, name: Ident): ConnectionIO[Boolean] =
|
||||||
|
getByName(accId, name).map(_.isDefined)
|
||||||
|
|
||||||
|
def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] =
|
||||||
|
selectCount(id, table, and(uid.is(userId), name.is(connName))).query[Int].unique.map(_ > 0)
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,36 @@ module Comp.EmailSettingsForm exposing
|
|||||||
( Model
|
( Model
|
||||||
, Msg
|
, Msg
|
||||||
, emptyModel
|
, emptyModel
|
||||||
|
, getSettings
|
||||||
|
, init
|
||||||
, update
|
, update
|
||||||
, view
|
, view
|
||||||
)
|
)
|
||||||
|
|
||||||
import Api.Model.EmailSettings exposing (EmailSettings)
|
import Api.Model.EmailSettings exposing (EmailSettings)
|
||||||
|
import Comp.Dropdown
|
||||||
|
import Comp.IntField
|
||||||
|
import Comp.PasswordInput
|
||||||
|
import Data.SSLType exposing (SSLType)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (onInput)
|
import Html.Events exposing (onCheck, onInput)
|
||||||
|
import Util.Maybe
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ settings : EmailSettings
|
{ settings : EmailSettings
|
||||||
, name : String
|
, name : String
|
||||||
|
, host : String
|
||||||
|
, portField : Comp.IntField.Model
|
||||||
|
, portNum : Maybe Int
|
||||||
|
, user : Maybe String
|
||||||
|
, passField : Comp.PasswordInput.Model
|
||||||
|
, password : Maybe String
|
||||||
|
, from : String
|
||||||
|
, replyTo : Maybe String
|
||||||
|
, sslType : Comp.Dropdown.Model SSLType
|
||||||
|
, ignoreCertificates : Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -22,6 +39,22 @@ emptyModel : Model
|
|||||||
emptyModel =
|
emptyModel =
|
||||||
{ settings = Api.Model.EmailSettings.empty
|
{ settings = Api.Model.EmailSettings.empty
|
||||||
, name = ""
|
, name = ""
|
||||||
|
, host = ""
|
||||||
|
, portField = Comp.IntField.init (Just 0) Nothing "SMTP Port"
|
||||||
|
, portNum = Nothing
|
||||||
|
, user = Nothing
|
||||||
|
, passField = Comp.PasswordInput.init
|
||||||
|
, password = Nothing
|
||||||
|
, from = ""
|
||||||
|
, replyTo = Nothing
|
||||||
|
, sslType =
|
||||||
|
Comp.Dropdown.makeSingleList
|
||||||
|
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||||
|
, placeholder = ""
|
||||||
|
, options = Data.SSLType.all
|
||||||
|
, selected = Just Data.SSLType.None
|
||||||
|
}
|
||||||
|
, ignoreCertificates = False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -29,21 +62,103 @@ init : EmailSettings -> Model
|
|||||||
init ems =
|
init ems =
|
||||||
{ settings = ems
|
{ settings = ems
|
||||||
, name = ems.name
|
, name = ems.name
|
||||||
|
, host = ems.smtpHost
|
||||||
|
, portField = Comp.IntField.init (Just 0) Nothing "SMTP Port"
|
||||||
|
, portNum = ems.smtpPort
|
||||||
|
, user = ems.smtpUser
|
||||||
|
, passField = Comp.PasswordInput.init
|
||||||
|
, password = ems.smtpPassword
|
||||||
|
, from = ems.from
|
||||||
|
, replyTo = ems.replyTo
|
||||||
|
, sslType =
|
||||||
|
Comp.Dropdown.makeSingleList
|
||||||
|
{ makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
|
||||||
|
, placeholder = ""
|
||||||
|
, options = Data.SSLType.all
|
||||||
|
, selected = Data.SSLType.fromString ems.sslType
|
||||||
|
}
|
||||||
|
, ignoreCertificates = ems.ignoreCertificates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getSettings : Model -> ( Maybe String, EmailSettings )
|
||||||
|
getSettings model =
|
||||||
|
( Util.Maybe.fromString model.settings.name
|
||||||
|
, { name = model.name
|
||||||
|
, smtpHost = model.host
|
||||||
|
, smtpUser = model.user
|
||||||
|
, smtpPort = model.portNum
|
||||||
|
, smtpPassword = model.password
|
||||||
|
, from = model.from
|
||||||
|
, replyTo = model.replyTo
|
||||||
|
, sslType =
|
||||||
|
Comp.Dropdown.getSelected model.sslType
|
||||||
|
|> List.head
|
||||||
|
|> Maybe.withDefault Data.SSLType.None
|
||||||
|
|> Data.SSLType.toString
|
||||||
|
, ignoreCertificates = model.ignoreCertificates
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= SetName String
|
= SetName String
|
||||||
|
| SetHost String
|
||||||
|
| PortMsg Comp.IntField.Msg
|
||||||
|
| SetUser String
|
||||||
|
| PassMsg Comp.PasswordInput.Msg
|
||||||
|
| SSLTypeMsg (Comp.Dropdown.Msg SSLType)
|
||||||
|
| SetFrom String
|
||||||
|
| SetReplyTo String
|
||||||
|
| ToggleCheckCert
|
||||||
|
|
||||||
|
|
||||||
isValid : Model -> Bool
|
isValid : Model -> Bool
|
||||||
isValid model =
|
isValid model =
|
||||||
True
|
model.host /= "" && model.name /= ""
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
( model, Cmd.none )
|
case msg of
|
||||||
|
SetName str ->
|
||||||
|
( { model | name = str }, Cmd.none )
|
||||||
|
|
||||||
|
SetHost str ->
|
||||||
|
( { model | host = str }, Cmd.none )
|
||||||
|
|
||||||
|
PortMsg m ->
|
||||||
|
let
|
||||||
|
( pm, val ) =
|
||||||
|
Comp.IntField.update m model.portField
|
||||||
|
in
|
||||||
|
( { model | portField = pm, portNum = val }, Cmd.none )
|
||||||
|
|
||||||
|
SetUser str ->
|
||||||
|
( { model | user = Util.Maybe.fromString str }, Cmd.none )
|
||||||
|
|
||||||
|
PassMsg m ->
|
||||||
|
let
|
||||||
|
( pm, val ) =
|
||||||
|
Comp.PasswordInput.update m model.passField
|
||||||
|
in
|
||||||
|
( { model | passField = pm, password = val }, Cmd.none )
|
||||||
|
|
||||||
|
SSLTypeMsg m ->
|
||||||
|
let
|
||||||
|
( sm, sc ) =
|
||||||
|
Comp.Dropdown.update m model.sslType
|
||||||
|
in
|
||||||
|
( { model | sslType = sm }, Cmd.map SSLTypeMsg sc )
|
||||||
|
|
||||||
|
SetFrom str ->
|
||||||
|
( { model | from = str }, Cmd.none )
|
||||||
|
|
||||||
|
SetReplyTo str ->
|
||||||
|
( { model | replyTo = Util.Maybe.fromString str }, Cmd.none )
|
||||||
|
|
||||||
|
ToggleCheckCert ->
|
||||||
|
( { model | ignoreCertificates = not model.ignoreCertificates }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
view : Model -> Html Msg
|
||||||
@ -61,8 +176,81 @@ view model =
|
|||||||
[ type_ "text"
|
[ type_ "text"
|
||||||
, value model.name
|
, value model.name
|
||||||
, onInput SetName
|
, onInput SetName
|
||||||
, placeholder "Connection name"
|
, placeholder "Connection name, e.g. 'gmail.com'"
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
|
, div [ class "ui info message" ]
|
||||||
|
[ text "The connection name must not contain whitespace or special characters."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "fields" ]
|
||||||
|
[ div [ class "fifteen wide required field" ]
|
||||||
|
[ label [] [ text "SMTP Host" ]
|
||||||
|
, input
|
||||||
|
[ type_ "text"
|
||||||
|
, placeholder "SMTP host name, e.g. 'mail.gmail.com'"
|
||||||
|
, value model.host
|
||||||
|
, onInput SetHost
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
, Html.map PortMsg (Comp.IntField.view model.portNum model.portField)
|
||||||
|
]
|
||||||
|
, div [ class "two fields" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [] [ text "SMTP User" ]
|
||||||
|
, input
|
||||||
|
[ type_ "text"
|
||||||
|
, placeholder "SMTP Username, e.g. 'your.name@gmail.com'"
|
||||||
|
, Maybe.withDefault "" model.user |> value
|
||||||
|
, onInput SetUser
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ label [] [ text "SMTP Password" ]
|
||||||
|
, Html.map PassMsg (Comp.PasswordInput.view model.password model.passField)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "two fields" ]
|
||||||
|
[ div [ class "required field" ]
|
||||||
|
[ label [] [ text "From Address" ]
|
||||||
|
, input
|
||||||
|
[ type_ "text"
|
||||||
|
, placeholder "Sender E-Mail address"
|
||||||
|
, value model.from
|
||||||
|
, onInput SetFrom
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ label [] [ text "Reply-To" ]
|
||||||
|
, input
|
||||||
|
[ type_ "text"
|
||||||
|
, placeholder "Optional reply-to E-Mail address"
|
||||||
|
, Maybe.withDefault "" model.replyTo |> value
|
||||||
|
, onInput SetReplyTo
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "two fields" ]
|
||||||
|
[ div [ class "inline field" ]
|
||||||
|
[ div [ class "ui checkbox" ]
|
||||||
|
[ input
|
||||||
|
[ type_ "checkbox"
|
||||||
|
, checked model.ignoreCertificates
|
||||||
|
, onCheck (\_ -> ToggleCheckCert)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, label [] [ text "Ignore certificate check" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "two fields" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [] [ text "SSL" ]
|
||||||
|
, Html.map SSLTypeMsg (Comp.Dropdown.view model.sslType)
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -17,6 +17,7 @@ import Comp.YesNoDimmer
|
|||||||
import Data.Flags exposing (Flags)
|
import Data.Flags exposing (Flags)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (onClick, onInput)
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
@ -25,6 +26,7 @@ type alias Model =
|
|||||||
, viewMode : ViewMode
|
, viewMode : ViewMode
|
||||||
, formError : Maybe String
|
, formError : Maybe String
|
||||||
, loading : Bool
|
, loading : Bool
|
||||||
|
, query : String
|
||||||
, deleteConfirm : Comp.YesNoDimmer.Model
|
, deleteConfirm : Comp.YesNoDimmer.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,6 +38,7 @@ emptyModel =
|
|||||||
, viewMode = Table
|
, viewMode = Table
|
||||||
, formError = Nothing
|
, formError = Nothing
|
||||||
, loading = False
|
, loading = False
|
||||||
|
, query = ""
|
||||||
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,18 +56,139 @@ type ViewMode
|
|||||||
type Msg
|
type Msg
|
||||||
= TableMsg Comp.EmailSettingsTable.Msg
|
= TableMsg Comp.EmailSettingsTable.Msg
|
||||||
| FormMsg Comp.EmailSettingsForm.Msg
|
| FormMsg Comp.EmailSettingsForm.Msg
|
||||||
|
| SetQuery String
|
||||||
|
| InitNew
|
||||||
|
| YesNoMsg Comp.YesNoDimmer.Msg
|
||||||
|
| RequestDelete
|
||||||
|
| SetViewMode ViewMode
|
||||||
|
|
||||||
|
|
||||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update flags msg model =
|
update flags msg model =
|
||||||
( model, Cmd.none )
|
case msg of
|
||||||
|
InitNew ->
|
||||||
|
let
|
||||||
|
ems =
|
||||||
|
Api.Model.EmailSettings.empty
|
||||||
|
|
||||||
|
nm =
|
||||||
|
{ model
|
||||||
|
| viewMode = Form
|
||||||
|
, formError = Nothing
|
||||||
|
, formModel = Comp.EmailSettingsForm.init ems
|
||||||
|
}
|
||||||
|
in
|
||||||
|
( nm, Cmd.none )
|
||||||
|
|
||||||
|
TableMsg m ->
|
||||||
|
let
|
||||||
|
( tm, tc ) =
|
||||||
|
Comp.EmailSettingsTable.update m model.tableModel
|
||||||
|
in
|
||||||
|
( { model | tableModel = tm }, Cmd.map TableMsg tc )
|
||||||
|
|
||||||
|
FormMsg m ->
|
||||||
|
let
|
||||||
|
( fm, fc ) =
|
||||||
|
Comp.EmailSettingsForm.update m model.formModel
|
||||||
|
in
|
||||||
|
( { model | formModel = fm }, Cmd.map FormMsg fc )
|
||||||
|
|
||||||
|
SetQuery str ->
|
||||||
|
( { model | query = str }, Cmd.none )
|
||||||
|
|
||||||
|
YesNoMsg m ->
|
||||||
|
let
|
||||||
|
( dm, flag ) =
|
||||||
|
Comp.YesNoDimmer.update m model.deleteConfirm
|
||||||
|
in
|
||||||
|
( { model | deleteConfirm = dm }, Cmd.none )
|
||||||
|
|
||||||
|
RequestDelete ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
SetViewMode m ->
|
||||||
|
( { model | viewMode = m }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
view : Model -> Html Msg
|
||||||
view model =
|
view model =
|
||||||
case model.viewMode of
|
case model.viewMode of
|
||||||
Table ->
|
Table ->
|
||||||
Html.map TableMsg (Comp.EmailSettingsTable.view model.tableModel)
|
viewTable model
|
||||||
|
|
||||||
Form ->
|
Form ->
|
||||||
Html.map FormMsg (Comp.EmailSettingsForm.view model.formModel)
|
viewForm model
|
||||||
|
|
||||||
|
|
||||||
|
viewTable : Model -> Html Msg
|
||||||
|
viewTable model =
|
||||||
|
div []
|
||||||
|
[ div [ class "ui secondary menu container" ]
|
||||||
|
[ div [ class "ui container" ]
|
||||||
|
[ div [ class "fitted-item" ]
|
||||||
|
[ div [ class "ui icon input" ]
|
||||||
|
[ input
|
||||||
|
[ type_ "text"
|
||||||
|
, onInput SetQuery
|
||||||
|
, value model.query
|
||||||
|
, placeholder "Search…"
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, i [ class "ui search icon" ]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "right menu" ]
|
||||||
|
[ div [ class "fitted-item" ]
|
||||||
|
[ a
|
||||||
|
[ class "ui primary button"
|
||||||
|
, href "#"
|
||||||
|
, onClick InitNew
|
||||||
|
]
|
||||||
|
[ i [ class "plus icon" ] []
|
||||||
|
, text "New Settings"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, Html.map TableMsg (Comp.EmailSettingsTable.view model.tableModel)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewForm : Model -> Html Msg
|
||||||
|
viewForm model =
|
||||||
|
div [ class "ui segment" ]
|
||||||
|
[ Html.map YesNoMsg (Comp.YesNoDimmer.view model.deleteConfirm)
|
||||||
|
, Html.map FormMsg (Comp.EmailSettingsForm.view model.formModel)
|
||||||
|
, div
|
||||||
|
[ classList
|
||||||
|
[ ( "ui error message", True )
|
||||||
|
, ( "invisible", model.formError == Nothing )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ Maybe.withDefault "" model.formError |> text
|
||||||
|
]
|
||||||
|
, div [ class "ui divider" ] []
|
||||||
|
, button [ class "ui primary button" ]
|
||||||
|
[ text "Submit"
|
||||||
|
]
|
||||||
|
, a [ class "ui secondary button", onClick (SetViewMode Table), href "" ]
|
||||||
|
[ text "Cancel"
|
||||||
|
]
|
||||||
|
, if model.formModel.settings.name /= "" then
|
||||||
|
a [ class "ui right floated red button", href "", onClick RequestDelete ]
|
||||||
|
[ text "Delete" ]
|
||||||
|
|
||||||
|
else
|
||||||
|
span [] []
|
||||||
|
, div
|
||||||
|
[ classList
|
||||||
|
[ ( "ui dimmer", True )
|
||||||
|
, ( "active", model.loading )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ div [ class "ui loader" ] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
108
modules/webapp/src/main/elm/Comp/IntField.elm
Normal file
108
modules/webapp/src/main/elm/Comp/IntField.elm
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
module Comp.IntField exposing (Model, Msg, init, update, view)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (onInput)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ min : Maybe Int
|
||||||
|
, max : Maybe Int
|
||||||
|
, label : String
|
||||||
|
, error : Maybe String
|
||||||
|
, lastInput : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= SetValue String
|
||||||
|
|
||||||
|
|
||||||
|
init : Maybe Int -> Maybe Int -> String -> Model
|
||||||
|
init min max label =
|
||||||
|
{ min = min
|
||||||
|
, max = max
|
||||||
|
, label = label
|
||||||
|
, error = Nothing
|
||||||
|
, lastInput = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tooLow : Model -> Int -> Bool
|
||||||
|
tooLow model n =
|
||||||
|
Maybe.map ((<) n) model.min
|
||||||
|
|> Maybe.withDefault False
|
||||||
|
|
||||||
|
|
||||||
|
tooHigh : Model -> Int -> Bool
|
||||||
|
tooHigh model n =
|
||||||
|
Maybe.map ((>) n) model.max
|
||||||
|
|> Maybe.withDefault False
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Maybe Int )
|
||||||
|
update msg model =
|
||||||
|
let
|
||||||
|
tooHighError =
|
||||||
|
Maybe.withDefault 0 model.max
|
||||||
|
|> String.fromInt
|
||||||
|
|> (++) "Number must be <= "
|
||||||
|
|
||||||
|
tooLowError =
|
||||||
|
Maybe.withDefault 0 model.min
|
||||||
|
|> String.fromInt
|
||||||
|
|> (++) "Number must be >= "
|
||||||
|
in
|
||||||
|
case msg of
|
||||||
|
SetValue str ->
|
||||||
|
let
|
||||||
|
m =
|
||||||
|
{ model | lastInput = str }
|
||||||
|
in
|
||||||
|
case String.toInt str of
|
||||||
|
Just n ->
|
||||||
|
if tooLow model n then
|
||||||
|
( { m | error = Just tooLowError }
|
||||||
|
, Nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
else if tooHigh model n then
|
||||||
|
( { m | error = Just tooHighError }
|
||||||
|
, Nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
( { m | error = Nothing }, Just n )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( { m | error = Just ("'" ++ str ++ "' is not a valid number!") }
|
||||||
|
, Nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
view : Maybe Int -> Model -> Html Msg
|
||||||
|
view nval model =
|
||||||
|
div
|
||||||
|
[ classList
|
||||||
|
[ ( "field", True )
|
||||||
|
, ( "error", model.error /= Nothing )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ label [] [ text model.label ]
|
||||||
|
, input
|
||||||
|
[ type_ "text"
|
||||||
|
, Maybe.map String.fromInt nval
|
||||||
|
|> Maybe.withDefault model.lastInput
|
||||||
|
|> value
|
||||||
|
, onInput SetValue
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, div
|
||||||
|
[ classList
|
||||||
|
[ ( "ui pointing red basic label", True )
|
||||||
|
, ( "hidden", model.error == Nothing )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ Maybe.withDefault "" model.error |> text
|
||||||
|
]
|
||||||
|
]
|
74
modules/webapp/src/main/elm/Comp/PasswordInput.elm
Normal file
74
modules/webapp/src/main/elm/Comp/PasswordInput.elm
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
module Comp.PasswordInput exposing
|
||||||
|
( Model
|
||||||
|
, Msg
|
||||||
|
, init
|
||||||
|
, update
|
||||||
|
, view
|
||||||
|
)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (onClick, onInput)
|
||||||
|
import Util.Maybe
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ show : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init : Model
|
||||||
|
init =
|
||||||
|
{ show = False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= ToggleShow (Maybe String)
|
||||||
|
| SetPassword String
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Maybe String )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
ToggleShow pw ->
|
||||||
|
( { model | show = not model.show }
|
||||||
|
, pw
|
||||||
|
)
|
||||||
|
|
||||||
|
SetPassword str ->
|
||||||
|
let
|
||||||
|
pw =
|
||||||
|
Util.Maybe.fromString str
|
||||||
|
in
|
||||||
|
( model, pw )
|
||||||
|
|
||||||
|
|
||||||
|
view : Maybe String -> Model -> Html Msg
|
||||||
|
view pw model =
|
||||||
|
div [ class "ui left action input" ]
|
||||||
|
[ button
|
||||||
|
[ class "ui icon button"
|
||||||
|
, type_ "button"
|
||||||
|
, onClick (ToggleShow pw)
|
||||||
|
]
|
||||||
|
[ i
|
||||||
|
[ classList
|
||||||
|
[ ( "ui eye icon", True )
|
||||||
|
, ( "slash", model.show )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
, input
|
||||||
|
[ type_ <|
|
||||||
|
if model.show then
|
||||||
|
"text"
|
||||||
|
|
||||||
|
else
|
||||||
|
"password"
|
||||||
|
, onInput SetPassword
|
||||||
|
, Maybe.withDefault "" pw |> value
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
60
modules/webapp/src/main/elm/Data/SSLType.elm
Normal file
60
modules/webapp/src/main/elm/Data/SSLType.elm
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
module Data.SSLType exposing
|
||||||
|
( SSLType(..)
|
||||||
|
, all
|
||||||
|
, fromString
|
||||||
|
, label
|
||||||
|
, toString
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type SSLType
|
||||||
|
= None
|
||||||
|
| SSL
|
||||||
|
| StartTLS
|
||||||
|
|
||||||
|
|
||||||
|
all : List SSLType
|
||||||
|
all =
|
||||||
|
[ None, SSL, StartTLS ]
|
||||||
|
|
||||||
|
|
||||||
|
toString : SSLType -> String
|
||||||
|
toString st =
|
||||||
|
case st of
|
||||||
|
None ->
|
||||||
|
"none"
|
||||||
|
|
||||||
|
SSL ->
|
||||||
|
"ssl"
|
||||||
|
|
||||||
|
StartTLS ->
|
||||||
|
"starttls"
|
||||||
|
|
||||||
|
|
||||||
|
fromString : String -> Maybe SSLType
|
||||||
|
fromString str =
|
||||||
|
case String.toLower str of
|
||||||
|
"none" ->
|
||||||
|
Just None
|
||||||
|
|
||||||
|
"ssl" ->
|
||||||
|
Just SSL
|
||||||
|
|
||||||
|
"starttls" ->
|
||||||
|
Just StartTLS
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
|
||||||
|
label : SSLType -> String
|
||||||
|
label st =
|
||||||
|
case st of
|
||||||
|
None ->
|
||||||
|
"None"
|
||||||
|
|
||||||
|
SSL ->
|
||||||
|
"SSL/TLS"
|
||||||
|
|
||||||
|
StartTLS ->
|
||||||
|
"StartTLS"
|
@ -1,5 +1,6 @@
|
|||||||
module Util.Maybe exposing
|
module Util.Maybe exposing
|
||||||
( isEmpty
|
( fromString
|
||||||
|
, isEmpty
|
||||||
, nonEmpty
|
, nonEmpty
|
||||||
, or
|
, or
|
||||||
, withDefault
|
, withDefault
|
||||||
@ -38,3 +39,16 @@ or listma =
|
|||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
or els
|
or els
|
||||||
|
|
||||||
|
|
||||||
|
fromString : String -> Maybe String
|
||||||
|
fromString str =
|
||||||
|
let
|
||||||
|
s =
|
||||||
|
String.trim str
|
||||||
|
in
|
||||||
|
if s == "" then
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
else
|
||||||
|
Just str
|
||||||
|
Loading…
x
Reference in New Issue
Block a user