Compare commits
10 Commits
4909bb9c96
...
3d09446bed
Author | SHA1 | Date | |
---|---|---|---|
3d09446bed | |||
70b883cfe0 | |||
2514d5befd | |||
3863ddf42e | |||
19c6801d12 | |||
66cf9d4600 | |||
5072fb4df4 | |||
d713f9b4e6 | |||
a1edace600 | |||
5354c950fa |
4
LICENSE
4
LICENSE
@ -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.
|
||||||
|
|
||||||
|
@ -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
51
README
@ -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
79
README.md
Normal 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)
|
@ -1,7 +0,0 @@
|
|||||||
module Main where
|
|
||||||
|
|
||||||
import Prelude
|
|
||||||
import qualified Lib
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = Lib.main
|
|
1
data/assets/public/htmx.js
Normal file
1
data/assets/public/htmx.js
Normal file
File diff suppressed because one or more lines are too long
@ -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"
|
||||||
|
]
|
||||||
|
@ -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
|
||||||
|
@ -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\
|
||||||
|
@ -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)
|
@ -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%
|
||||||
|
|
||||||
|
@ -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 ♥
|
made with ♥
|
||||||
|
|
|
|
||||||
<a href="mailto:#{email}">contact
|
<a href="mailto:#{email}">contact
|
||||||
|
Loading…
x
Reference in New Issue
Block a user