diff --git a/README.md b/README.md index 7d59f11..399464e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,24 @@ -# Purr - Password Generation and Sharing +# Purr - Password Generation and Secret Sharing ![Big Purr and Sploot](https://eversole.co/Purr-Small.png "Purr!") -Purr is a work-in-progress web application offering customizable password generation and time-limited link sharing features. +Purr is a work-in-progress web application offering customizable password generation and time-limited sharing of secrets. ## What problems does Purr solve? 1. Generating sufficiently memorable but secure passwords for use with accounts that don't offer better authentication methods. -2. Sharing passwords or other text secrets with others via email/chat/etc., without disclosing the password itself. As passwords expire after a predefined period, the email/chat history where the information was shared don't become a purr-manent (sorry, **permanent**) vulnerability. +2. Sharing text secrets with others without disclosing the secret in the message itself. As secrets expire after a predefined period, the email/chat history where the information was shared don't become a purr-manent (sorry, **permanent**) vulnerability. 3. Being really cute compared to the competition. +## Why should I trust you with my secrets? +You shouldn't! This is [free and open-source software](https://git.eversole.co/James/Purr/src/branch/main/LICENSE) which you can run on your own hardware. Instructions for deployment are coming soon. + ## Tech Stack? 1. [Haskell](https://www.haskell.org) and [Scotty](https://github.com/scotty-web/scotty) backend. -2. [HTMX](https://github.com/bigskysoftware/htmx) and [Alpine.js](https://github.com/alpinejs/alpine) for the frontend. +2. [HTMX](https://github.com/bigskysoftware/htmx) for the frontend. 3. [MongoDB](https://github.com/mongodb/mongo) database. -## Why should I trust you with my passwords? -You shouldn't! This is [free and open-source software](https://git.eversole.co/James/Purr/src/branch/main/LICENSE) which you can run on your own hardware. Instructions for deployment are coming soon. +## Project Goals +1. Provide a minimal and clean interface for generating and sharing passwords. +2. Maintain a clean and organized codebase that can be extended to include more utilities than originally anticipated. +3. Aim for graceful degradation when JavaScript isn't enabled. ## Development & Support Please send me an [email](mailto:james@eversole.co) or join the [Support Chat](openpgp4fpr://FEB27223219E8DB3203225350462EA0901FE08F7#a=james%40eversole.co&g=Purr%20Support&x=RbVs8iQCVnf&i=-FuzUDK_RM1&s=KgeGtFFJtkq) in [DeltaChat](https://delta.chat)! diff --git a/src/Core/Configuration.hs b/src/Core/Configuration.hs index 6f88f68..fc5b26e 100644 --- a/src/Core/Configuration.hs +++ b/src/Core/Configuration.hs @@ -1,6 +1,6 @@ module Core.Configuration ( main ) where -import Core.Types +import Core.Types import Dhall diff --git a/src/Core/HTTP.hs b/src/Core/HTTP.hs index 1b1a103..d058653 100644 --- a/src/Core/HTTP.hs +++ b/src/Core/HTTP.hs @@ -2,11 +2,12 @@ module Core.HTTP ( app ) where import Core.Types -import Core.Templates (renderIndex) +import Core.Templates (renderIndex, renderStyle) import Feature.Sharing.HTTP as Sharing +import Data.Maybe (Maybe (Nothing)) import Network.Wai.Middleware.RequestLogger (logStdoutDev) -import Web.Scotty.Trans +import Web.Scotty.Trans app :: PurrApp () app = do @@ -15,7 +16,11 @@ app = do -- Core Routes get "/" $ do - html $ renderIndex + html $ renderIndex "/" Nothing + + get "/style.css" $ do + setHeader "Content-Type" "text/css" + text renderStyle -- Feature Routes Sharing.routes diff --git a/src/Core/Templates.hs b/src/Core/Templates.hs index d150c55..993468c 100644 --- a/src/Core/Templates.hs +++ b/src/Core/Templates.hs @@ -1,16 +1,19 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} -module Core.Templates ( renderIndex ) where - -import qualified Data.Text.Lazy as LT +module Core.Templates ( renderIndex, renderStyle ) where +import Data.Text.Lazy (Text) import Database.MongoDB (Document) import Text.Blaze.Html.Renderer.Text (renderHtml) import Text.Blaze.Html +import Text.Cassius (cassiusFile, renderCss) import Text.Hamlet (shamletFile) import Prelude -renderIndex :: LT.Text -renderIndex = renderHtml ( $(shamletFile "./views/index.hamlet") ) +renderIndex :: String -> Maybe String -> Text +renderIndex link password = renderHtml ( $(shamletFile "./views/index.hamlet") ) + +renderStyle :: Text +renderStyle = renderCss ( $(cassiusFile "./views/cassius/style.cassius") "/style.css" ) diff --git a/src/Feature/Sharing/HTTP.hs b/src/Feature/Sharing/HTTP.hs index ee34e72..6456795 100644 --- a/src/Feature/Sharing/HTTP.hs +++ b/src/Feature/Sharing/HTTP.hs @@ -1,9 +1,10 @@ module Feature.Sharing.HTTP ( routes ) where -import Core.Types +import Core.Types +import Core.Templates (renderIndex) -import Feature.Sharing.Templates -import qualified Feature.Sharing.Mongo as DB +import Feature.Sharing.Templates (renderPw) +import qualified Feature.Sharing.Mongo as DB import qualified Data.Text as T import qualified Data.Text.Lazy as LT @@ -12,21 +13,21 @@ import Control.Monad.Reader (ask, lift) import Data.AesonBson (aesonify) import Data.Bson (Document, Field (..), Value (..), lookup) import Web.Scotty.Trans -import Prelude hiding (id, lookup) +import Prelude hiding (lookup) routes :: PurrApp () routes = do get "/pw/:id" $ do - id <- param "id" - doc <- DB.findByLink id - html $ renderPw id (pwLookup doc) + reqId <- param "id" + doc <- DB.findByLink reqId + html $ renderIndex reqId (pwLookup doc) post "/pw" $ do - id <- param "userLink" - doc <- DB.findByLink id - html $ renderPw id (pwLookup doc) + reqId <- param "userLink" + doc <- DB.findByLink reqId + html $ renderPw reqId (pwLookup doc) pwLookup :: Maybe Document -> Maybe String -pwLookup (Just x) = (lookup "password" x) -pwLookup _ = Nothing +pwLookup (Just x) = lookup "password" x +pwLookup _ = Nothing diff --git a/src/Lib.hs b/src/Lib.hs index bd25ebd..a4412a4 100644 --- a/src/Lib.hs +++ b/src/Lib.hs @@ -1,23 +1,23 @@ module Lib ( main ) where -import Core.Types import qualified Core.Configuration as Configuration import qualified Core.HTTP as HTTP import qualified Core.Mongo as DB +import Core.Types import Control.Monad.Reader (liftIO, runReaderT) import Database.MongoDB (MongoContext) import GHC.Natural (popCountNatural) -import Web.Scotty.Trans (scottyT) import Prelude hiding (id) +import Web.Scotty.Trans (scottyT) main :: IO () main = do dhallConf <- liftIO Configuration.main dataDB <- liftIO $ DB.mongoSetup dhallConf let config = AppConfig { res = dhallConf - , dbconn = dataDB + , dbconn = dataDB } scottyT (port dhallConf) (flip runApp config) HTTP.app where runApp :: ConfigM a -> AppConfig -> IO a - runApp m c = runReaderT (runConfigM m) c + runApp m = runReaderT (runConfigM m) diff --git a/stack.yaml b/stack.yaml index eba5820..2a8eac2 100644 --- a/stack.yaml +++ b/stack.yaml @@ -39,6 +39,9 @@ extra-deps: - AesonBson-0.4.1@sha256:30a4ecb39e8da94dc1e1e8945eb0d4e33a833ae4342841b3c87c56b5918a90a1,1398 - bson-generic-0.0.9@sha256:ea6685daa618b2bbe6e189c33e195e812501baf42f53183eedc16f011690895a,817 +ghc-options: + '$everything': -haddock + # Override default flag values for local packages and extra-deps # flags: {} diff --git a/views/cassius/style.cassius b/views/cassius/style.cassius new file mode 100644 index 0000000..28d3d3a --- /dev/null +++ b/views/cassius/style.cassius @@ -0,0 +1,106 @@ +@colorOne: #222323 +@colorTwo: #f0f6f0 +@colorThree: #6D92AD +@colorFour: #435F5D + +html + background-color: #{colorTwo} + color: #{colorOne} + +body + font-family: Courier + font-size: 20px + text-align: left + +p + margin: 0.4em 0 0.4em 0 + +::placeholder + color: #{colorOne} + opacity: 1 + +.asciiCat + margin: 2% 3% 0 0 + font-size: 1.2vw + color: #{colorFour} + text-align: center + +.content + margin: 0 15% 0 15% + +.emptyReq + height: 1% + +.mainButton + padding: 0.75em 1.75em + background-color: #{colorThree} + color: #{colorTwo} + border-style: none + text-decoration: none + +.mainInput + font-weight: 400 + font-size: 1em + width: 50% + outline: none + color: #{colorOne} + background: #{colorTwo} + margin: 1em 0 + border-style: none none none none + padding: 0.4em 0 + box-sizing: border-box + -webkit-box-sizing: border-box + -moz-box-sizing: border-box + -webkit-transition: all 0.1s ease-in-out + -moz-transition: all 0.1s ease-in-out + -ms-transition: all 0.1s ease-in-out + -o-transition: all 0.1s ease-in-out + transition: all 0.1s ease-in-out + +.mainInput:focus + border-bottom: 0.2em solid #{colorThree}; + +.mainInput:focus::placeholder + color: #{colorThree} + opacity: 0.5 + +.pwResult + font-size: 1.5em + color: #{colorFour} + +.pwUtils + width: 75% + +.shareNew + margin-bottom: 2em + +.title + font-size: 2em + color: #{colorOne} + text-decoration: none + +.titleLink + all: unset + cursor: pointer + +.title h1 + margin: 0.1em 0 0.3em 0 + +@media only screen and (max-width : 768px) + body + text-align: center + font-size: 16px + + .asciiCat + display: none + + .mainInput + width: 95% + text-align: center + + .title + margin: 8% auto + font-size: 3em + + .pwUtils + width: 100% diff --git a/views/index.hamlet b/views/index.hamlet index 5085f4c..341c9d1 100644 --- a/views/index.hamlet +++ b/views/index.hamlet @@ -1,14 +1,58 @@ +$doctype 5 Purr + <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://unpkg.com/htmx.org@1.7.0" integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" crossorigin="anonymous"> + <link rel="stylesheet" href="/style.css"> + <body> - <h1>Welcome to Purr! - <p #requestedPw>No password currently requested. - <p>Ask for the <input name="userLink" type="text"/> password - <button hx-post="/pw" - hx-target="#requestedPw" - hx-swap="outerHTML" - hx-include="[name='userLink']" - /> - Get Password! + <pre #asciiCat .asciiCat> + _._ _,-'""`-._ + \ (,-.`._,'( |\`-/| + \ `-.-' \ )-`( , o o) + \ `- \`_`"'- + <div #content .content> + <div #title .title> + <h1> + <a #titleLink .titleLink href="/">Purr + <div #pwUtils .pwUtils> + <div #requestedPw .requestedPw> + $maybe pw <- password + <p>Here's the secret for /pw/#{link}: + <h2 .pwResult>#{pw} + $nothing + $if (link == "/") + <p .emptyReq> + $else + <p>No secret available at /pw/#{link} + + <div #shareNew .shareNew> + <p> + <input .mainInput + name="newPw" + type="text" + placeholder="Enter a Secret to Share" + /> + <button .mainButton + hx-post="/newpw" + hx-target="#requestedPw" + hx-swap="outerHTML" + hx-include="[name='newPw']" + /> + Share Secret + + <div #requestNew .requestNew> + <p> + <input .mainInput + name="userLink" + type="text" + placeholder="Enter a Link to View Secret" + /> + <button .mainButton + hx-post="/pw" + hx-target="#requestedPw" + hx-swap="outerHTML" + hx-include="[name='userLink']" + /> + Get Secret diff --git a/views/pw.hamlet b/views/pw.hamlet index 79ccd2f..b9d27cb 100644 --- a/views/pw.hamlet +++ b/views/pw.hamlet @@ -1,5 +1,6 @@ -<div #requestedPw> +<div #requestedPw .requestedPw> $maybe pw <- password - <p>Here's the password for https://purr.eversole.co/pw/#{link}: #{pw} + <p>Here's the secret for /pw/#{link}: + <h2 .pwResult>#{pw} $nothing - <p>No password available at https://purr.eversole.co/pw/#{link} + <p>No secret available at /pw/#{link}