Compare commits

...

10 Commits

12 changed files with 156 additions and 88 deletions

View File

@ -1,4 +1,6 @@
Copyright 2022 James Eversole (james@eversole.co) ISC LICENSE
Copyright James Eversole (james@eversole.co)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

View File

@ -1,7 +1,7 @@
cabal-version: 1.12 cabal-version: 1.12
name: Purr name: Purr
version: 0.3.0 version: 0.5.0
description: https://git.eversole.co/Purr description: https://git.eversole.co/Purr
author: James Eversole author: James Eversole
maintainer: james@eversole.co maintainer: james@eversole.co
@ -16,8 +16,7 @@ extra-source-files:
executable Purr executable Purr
main-is: Main.hs main-is: Main.hs
hs-source-dirs: hs-source-dirs:
app src
, src
default-extensions: default-extensions:
ConstraintKinds ConstraintKinds
DeriveGeneric DeriveGeneric
@ -64,5 +63,4 @@ executable Purr
Feature.Sharing.HTTP Feature.Sharing.HTTP
Feature.Sharing.SQLite Feature.Sharing.SQLite
Feature.Sharing.Templates Feature.Sharing.Templates
Lib
default-language: Haskell2010 default-language: Haskell2010

51
README
View File

@ -1,51 +0,0 @@
purr
-----
https://purr.eversole.co
a work-in-progress web application offering customizable password generation
and time-limited sharing of secrets.
TECH STACK
- Haskell and Scotty backend
- HTMX frontend
- SQLite database
GOALS
- Generate sufficiently memorable but secure passwords for use with accounts
that don't offer better authentication methods.
- Share text secrets with others without disclosing the secret in the
message itself.
- Provide a minimal and clean interface for generating and sharing passwords.
- Maintain a clean and organized codebase that can be extended to include more
utilities than originally anticipated.
- Be really cute compared to the competition.
WHY TRUST YOU?
You shouldn't. This is free and open-source software which you can run on your
own hardware.
DEPLOYMENT
Only Nix build instructions targeting containers are provided below,
but this project can be built and run without containers or Nix using Cabal.
- Clone this repository
- Build the container image (with flakes enabled): `nix build .#purr-container`
- Load the container image
- podman load -i result
- Use the provided docker stack example to deploy the container if desired
- docker stack deploy -c docker-stack.yml purr
DEVELOPMENT & SUPPORT
Per the permissive ISC license, you are free to do what you wish with this software. I hold
no liability for any defects and no guarantees are made to its usability.
Copyright James Eversole (james@eversole.co)

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# purr
[purr.eversole.co](https://purr.eversole.co)
a simple web application offering customizable password generation
and time-limited sharing of secrets.
## TECH STACK
- [Haskell](https://www.haskell.org)
- [Scotty](https://hackage.haskell.org/package/scotty)
- [HTMX](https://htmx.org)
- [SQLite](https://www.sqlite.org)
## GOALS
- Provide a minimal and clean interface for generating and sharing passwords.
- Generate sufficiently memorable but secure passwords for use with accounts
that don't offer better authentication methods.
- Share text secrets with others without disclosing the secret in the
message itself.
- Maintain a clean and organized codebase that can be extended to include more
utilities than originally anticipated.
## WHY TRUST PURR.EVERSOLE.CO?
You shouldn't. This is free and open-source software which you can run on your
own hardware!
## DEPLOYMENT
Only Nix build instructions are provided below.
### No Containers
1) Clone this repository
2) Build the application (with flakes enabled): `nix build '.#'`
3) Set the environment variables
- File: `cp examples/.env.example ./.env; $EDITOR ./.env`
- If you want to set them in a different way, you already know how.
4) Run the application: `./result/bin/Purr`
### Containers
1) Clone this repository
2) Build the container image (with flakes enabled): `nix build .#purr-container`
3) Load the container image: `podman load -i result`
4) Pick option 5, 6, or use your favorite process to manage containers and ENV
5) NixOS configuration:
```
virtualisation.oci-containers.containers.purr = {
image = "purr";
ports = [ "${PURR_EXTERNAL_PORT}:3000" ];
volumes = [
"/PATH/TO/PURR/data:/app/data"
];
environment = {
PURRNOFILE = "true";
ENVIRONMENT = "production";
APPLICATIONHOST = "localhost";
APPLICATIONPORT = "3000";
DATADIR = "/app/";
LINKLENGTH = "24";
ADMINEMAIL = "${YOUR_EMAIL}";
};
};
```
6) Docker Stack
1) Copy the docker-stack.yml example and edit as needed.
1) `cp examples/docker-stack.yml ./; $EDITOR ./docker-stack.yml`
2) Set environment variables:
- `cp examples/.env.example ./.env; $EDITOR ./.env`
- Or set them in the docker-stack.yml environment declaration.
3) Deploy: `docker stack deploy -c docker-stack.yml purr`
## DEVELOPMENT & SUPPORT
Per the permissive ISC license, you are free to do what you wish with this
software. No guarantees are made to its usability, security, or functionality.
Copyright James Eversole (james@eversole.co)

View File

@ -1,7 +0,0 @@
module Main where
import Prelude
import qualified Lib
main :: IO ()
main = Lib.main

File diff suppressed because one or more lines are too long

View File

@ -1,21 +1,48 @@
module Core.Configuration where module Core.Configuration ( adminEmail, appPort
, confLinkLength, dataPath, dbPath
, encKey, getRuntimeEnvironment
, keyFileInit, init) where
import qualified Data.ByteString as B import qualified Data.ByteString as B
import Control.Monad (mapM)
import Configuration.Dotenv
import Core.Types import Core.Types
import Crypto.Saltine.Core.SecretBox (newKey) import Crypto.Saltine.Core.SecretBox (newKey)
import Crypto.Saltine.Class (encode) import Crypto.Saltine.Class (encode)
import Configuration.Dotenv import Prelude hiding (init)
import System.Directory (doesFileExist) import System.Directory (doesFileExist)
import System.Environment (getEnv, lookupEnv) import System.Environment (getEnv, lookupEnv)
-- Make the dotenv file configuration available if PURRNOFILE is not present -- Load environment variables from dotenv file if required
main :: IO () init :: IO ()
main = do init = do
envFile <- lookupEnv "PURRNOFILE" reqEnvLookup <- getRequiredEnv
case envFile of if (Nothing `elem` reqEnvLookup)
Nothing -> loadFile defaultConfig then checkEnvFile requiredEnvVars
_ -> putStrLn "Not using dotenv file" else pure ()
where
getRequiredEnv :: IO [Maybe String]
getRequiredEnv = mapM (\s -> lookupEnv s) requiredEnvVars
checkEnvFile :: [String] -> IO ()
checkEnvFile requiredEnv = do
dotEnvExists <- doesFileExist "./.env"
if dotEnvExists
then do
loadFile defaultConfig
fromEnvFile <- getRequiredEnv
if (Nothing `elem` fromEnvFile)
then error $ missingEnvMsg requiredEnv
else pure ()
else error $ "Cannot find .env file in application directory.\n"
++ missingEnvMsg requiredEnv
missingEnvMsg :: [String] -> String
missingEnvMsg required =
"Missing required environment variable(s).\n"
++ "All required environment variables:\n"
++ unlines required
-- Check if an encryption key exists on the filesystem and create one if not -- Check if an encryption key exists on the filesystem and create one if not
keyFileInit :: IO () keyFileInit :: IO ()
@ -39,6 +66,9 @@ encKey = do
adminEmail :: IO String adminEmail :: IO String
adminEmail = getEnv "ADMINEMAIL" adminEmail = getEnv "ADMINEMAIL"
getRuntimeEnvironment :: IO String
getRuntimeEnvironment = getEnv "ENVIRONMENT"
appPort :: IO String appPort :: IO String
appPort = getEnv "APPLICATIONPORT" appPort = getEnv "APPLICATIONPORT"
@ -50,3 +80,8 @@ dbPath = "data/Purr.sqlite"
confLinkLength :: IO String confLinkLength :: IO String
confLinkLength = getEnv "LINKLENGTH" confLinkLength = getEnv "LINKLENGTH"
requiredEnvVars :: [String]
requiredEnvVars = [ "ADMINEMAIL", "APPLICATIONHOST", "APPLICATIONPORT"
, "DATADIR", "ENVIRONMENT", "LINKLENGTH"
]

View File

@ -1,22 +1,28 @@
module Core.HTTP ( app ) where module Core.HTTP ( app ) where
import Core.Configuration (adminEmail, confLinkLength) import Core.Configuration ( adminEmail
, confLinkLength
, getRuntimeEnvironment)
import Core.Types import Core.Types
import Core.Templates (renderIndex, renderStyle) import Core.Templates (renderIndex, renderStyle)
import Feature.Generation.HTTP as Generation import Feature.Generation.HTTP as Generation
import Feature.Sharing.HTTP as Sharing import Feature.Sharing.HTTP as Sharing
import Control.Monad (void)
import Control.Monad.Trans (liftIO) import Control.Monad.Trans (liftIO)
import Data.Maybe (Maybe (Nothing)) import Data.Maybe (Maybe (Nothing))
import Network.Wai.Middleware.RequestLogger (logStdoutDev) import Network.Wai.Middleware.RequestLogger (logStdout, logStdoutDev)
import Network.Wai.Middleware.Static import Network.Wai.Middleware.Static
import Web.Scotty import Web.Scotty
app :: PurrApp () app :: String -> PurrApp ()
app = do app env = do
-- Middleware that are processed on every request -- Middleware that are processed on every request
middleware logStdoutDev case env of
"production" -> middleware logStdout
"prod" -> middleware logStdout
_ -> middleware logStdoutDev
middleware $ staticPolicy (noDots >-> addBase "data/assets/public") middleware $ staticPolicy (noDots >-> addBase "data/assets/public")
-- Core Routes -- Core Routes

View File

@ -6,12 +6,13 @@ import Core.Types
import Data.ByteString as B import Data.ByteString as B
import Database.SQLite.Simple import Database.SQLite.Simple
import Database.SQLite.Simple.FromRow import Database.SQLite.Simple.FromRow
import Prelude hiding (init)
import qualified Data.Text as T import qualified Data.Text as T
-- Set up SQLite database table when Purr starts if it doesn't already exist -- Set up SQLite database table when Purr starts if it doesn't already exist
main :: IO () init :: IO ()
main = do init = do
conn <- open dbPath conn <- open dbPath
execute_ conn execute_ conn
"CREATE TABLE IF NOT EXISTS pws\ "CREATE TABLE IF NOT EXISTS pws\

View File

@ -1,4 +1,4 @@
module Lib ( main ) where module Main ( main ) where
import qualified Core.Configuration as Configuration import qualified Core.Configuration as Configuration
import qualified Core.HTTP as HTTP import qualified Core.HTTP as HTTP
@ -16,16 +16,16 @@ main = do
-- Initialize the RNG used for sodium encryption (Saltine library) -- Initialize the RNG used for sodium encryption (Saltine library)
sodiumInit sodiumInit
{- Initialize our dotenv configuration which reads from a .env configuration {- Initialize our dotenv configuration which reads from a .env configuration
file unless the PURRNOFILE env var exists already. -} if any required variables are missing from the provided environment -}
Configuration.main Configuration.init
{- Initialize the encryption key file if it doesn't {- Initialize the encryption key file if it doesn't
exist yet or use the existing key -} exist yet or use the existing key -}
Configuration.keyFileInit Configuration.keyFileInit
{- Initialize our database by ensuring the SQLite file exists {- Initialize our database by ensuring the SQLite file exists
and has tables setup as the application expects -} and has tables setup as the application expects -}
DB.main DB.init
{- Get the configured port to run on and start the Scotty webserver app {- Get the configured port to run on and start the Scotty webserver app
defined in HTTP.app -} defined in HTTP.app -}
appPortStr <- Configuration.appPort port <- Configuration.appPort
let appPort = read appPortStr :: Int env <- Configuration.getRuntimeEnvironment
scotty appPort HTTP.app scotty (read port) (HTTP.app env)

View File

@ -70,6 +70,7 @@ input[type=number]
padding: 1em 0.5em 0.5em 1.5em padding: 1em 0.5em 0.5em 1.5em
background-color: #{colorTwo} background-color: #{colorTwo}
box-shadow: 8px 8px 12px #ccc box-shadow: 8px 8px 12px #ccc
overflow-x: scroll
.generators .numberInput .generators .numberInput
background-color: #{colorTwo} background-color: #{colorTwo}
@ -276,6 +277,9 @@ input[type=number]
.mainButton .mainButton
width: 80% width: 80%
.generators
font-size: 12px
.genButton .genButton
width: 80% width: 80%

View File

@ -4,14 +4,14 @@ $doctype 5
<head> <head>
<title>purr <title>purr
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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"> <script src="/htmx.js" integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw">
<script src="/copyButtons.js" integrity="sha384-eNQZr7QWPQmi/EWi4lVVFOavm+Eibmh7iDvDptgE0j5fI3xycLssbDBZbKphi8pk"> <script src="/copyButtons.js" integrity="sha384-eNQZr7QWPQmi/EWi4lVVFOavm+Eibmh7iDvDptgE0j5fI3xycLssbDBZbKphi8pk">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
<body> <body>
<header> <header>
<a href="https://git.eversole.co/purr"> <a href="https://git.eversole.co">
made with &#9829; made with &#9829;
| |
<a href="mailto:#{email}">contact <a href="mailto:#{email}">contact