better castle

This commit is contained in:
metacryst
2025-09-22 13:05:06 -05:00
parent 04f4155a19
commit a58632d983
10 changed files with 3293 additions and 114 deletions

21
main.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"fmt"
"log"
"github.com/alexedwards/argon2id"
)
func main() {
password := "mmmmmmmm"
// Use default recommended parameters
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
log.Fatal(err)
}
fmt.Println("Argon2 Hash:")
fmt.Println(hash)
}

67
server/db/db.go Normal file
View File

@@ -0,0 +1,67 @@
package db
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"github.com/alexedwards/argon2id"
)
var DB map[string]interface{}
type GetService struct{}
var Get = GetService{}
func (g GetService) User(id string) (map[string]interface{}, error) {
raw, ok := DB[id]
if !ok {
return nil, errors.New("user not found")
}
userData, ok := raw.(map[string]interface{})
log.Println(userData)
if !ok {
return nil, errors.New("user data is not in expected format")
}
return userData, nil
}
func InitDB() error {
file, err := os.Open("../db/users.json")
if err != nil {
fmt.Println("Error opening file:", err)
return errors.New("Failed to read db")
}
defer file.Close()
var data interface{}
err = json.NewDecoder(file).Decode(&data)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return errors.New("Failed to decode db")
}
result, ok := data.(map[string]interface{})
if !ok {
fmt.Println("Data is not a JSON object (map)")
return errors.New("Db is in the wrong format")
}
DB = result
// Use default recommended parameters
hash, err := argon2id.CreateHash("hunter2", argon2id.DefaultParams)
if err != nil {
log.Fatal(err)
}
fmt.Println("Argon2 Hash:")
fmt.Println(hash)
return nil
}

View File

@@ -1,8 +1,8 @@
module hyperia
go 1.23.0
go 1.24.0
toolchain go1.24.2
toolchain go1.24.5
require (
github.com/alexedwards/argon2id v1.0.0
@@ -23,6 +23,11 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/tools v0.36.1-0.20250903222949-a5c0eb837c9f // indirect
golang.org/x/tools/cmd/godoc v0.1.0-deprecated // indirect
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
)

View File

@@ -46,12 +46,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -88,6 +92,12 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.36.1-0.20250903222949-a5c0eb837c9f h1:jDEaVlf+r7N8Re8Es5pGylGkfnqcx9dfUCsd1T+biTs=
golang.org/x/tools v0.36.1-0.20250903222949-a5c0eb837c9f/go.mod h1:n+8pplxVZfXnmHBxWsfPnQRJ5vWroQDk+U2MFpjwtFY=
golang.org/x/tools/cmd/godoc v0.1.0-deprecated h1:sEGTwp9aZNTHsdf/2BGaRqE4ZLndRVH17rbQ2OVun9Q=
golang.org/x/tools/cmd/godoc v0.1.0-deprecated/go.mod h1:J6VY4iFch6TIm456U3fnw1EJZaIqcYlhHu6GpHQ9HJk=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=

View File

@@ -2,23 +2,24 @@ package handlers
import (
"encoding/json"
// "errors"
"errors"
"log"
"net/http"
// "strings"
"hyperia/db"
"github.com/rs/zerolog/log"
// "github.com/alexedwards/argon2id"
"github.com/alexedwards/argon2id"
)
type loginRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
type user struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func HandleLogin(w http.ResponseWriter, r *http.Request) {
@@ -33,75 +34,38 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) {
return
}
// user, err := getUserByCredentials(creds.Name, creds.Password)
// if err != nil {
// http.Error(w, "Unauthorized: "+err.Error(), http.StatusUnauthorized)
// return
// }
user, err := getUserByCredentials(creds)
if err != nil {
http.Error(w, "Unauthorized: "+ err.Error(), http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
http.Error(w, "Not implemented", http.StatusMethodNotAllowed)
// json.NewEncoder(w).Encode(user)
http.Error(w, "Not implemented", http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(user)
}
// func getUserByCredentials(name string, password string) (*user, error) {
// var id int
// var dbName, dbHash string
func getUserByCredentials(loginCreds loginRequest) (map[string]interface{}, error) {
// name = strings.TrimSpace(strings.ToLower(name))
// err := DB.QueryRow("SELECT id, name, password FROM users WHERE LOWER(name) = LOWER($1)", name).Scan(&id, &dbName, &dbHash)
// if err != nil {
// return nil, errors.New("user not found")
// }
// match, err := argon2id.ComparePasswordAndHash(password, dbHash)
// if err != nil || !match {
// return nil, errors.New("invalid password")
// }
// return &user{
// ID: id,
// Name: dbName,
// }, nil
// }
func HandleApplicantLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
return
}
var creds loginRequest
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// exists, err := EmailExists(creds.Name)
// if err != nil {
// log.Err(err).Msg("error checking email")
// http.Error(w, "Internal server error", http.StatusInternalServerError)
// return
// }
// if !exists {
// http.Error(w, "Email does not exist.", http.StatusConflict)
// return
// }
token, err := generateVerificationToken(creds.Name)
// email := strings.TrimSpace(strings.ToLower(loginCreds.Email))
user, err := db.Get.User("1")
// err := DB.QueryRow("SELECT id, name, password FROM users WHERE LOWER(name) = LOWER($1)", name).Scan(&id, &dbName, &dbHash)
if err != nil {
log.Err(err).Msg("error generating verification token")
http.Error(w, "Error, please try again later.", http.StatusInternalServerError)
return
return nil, errors.New("user not found")
}
err = sendWelcomeEmail(creds.Name, token)
if err != nil {
log.Err(err).Msg("error sending welcome email")
http.Error(w, "Failed to send email", http.StatusInternalServerError)
return
dbPassword, ok := user["password"].(string)
if !ok {
return nil, errors.New("password format is invalid")
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
log.Println("pass: ", loginCreds, loginCreds.Password, dbPassword)
match, err := argon2id.ComparePasswordAndHash(loginCreds.Password, dbPassword)
if err != nil || !match {
return nil, errors.New("invalid password")
}
return user, nil
}

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"path/filepath"
"hyperia/config"
"hyperia/db"
"hyperia/handlers"
"hyperia/logger"
"runtime/debug"
@@ -16,15 +17,14 @@ import (
func main() {
config.SetConfiguration()
logger.ConfigureLogger()
// err := handlers.InitDB()
// if err != nil {
// log.Fatal().Msgf("failed to connect to database: %v", err)
// } else {
// log.Info().Msg("successfully connected to PostgreSQL")
// }
err := db.InitDB()
if err != nil {
log.Fatal().Msgf("failed to connect to database: %v", err)
} else {
log.Info().Msg("successfully connected to PostgreSQL")
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
defer func() {
@@ -55,7 +55,7 @@ func main() {
})
log.Info().Msgf("Server starting on http://localhost: %s", config.PORT)
err := http.ListenAndServe(":"+config.PORT, nil)
err = http.ListenAndServe(":"+config.PORT, nil)
if err != nil {
log.Fatal().Msgf("failed to start server: %v", err)
}
@@ -67,10 +67,6 @@ func handlePublic(w http.ResponseWriter, r *http.Request) {
handlers.HandleLogin(w, r)
return
}
if r.URL.Path == "/api/applicantlogin" {
handlers.HandleApplicantLogin(w, r)
return
}
if r.URL.Path == "/api/join" {
handlers.HandleJoin(w, r)
return

3094
ui/_/images/castle-dark3.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -20,12 +20,12 @@
}}
@media (prefers-color-scheme: dark) {
.main-image {
content:url("_/images/castle-dark2.svg");
height: 120vmin; bottom: -17vmin; left: 31vw;
content:url("_/images/castle-dark3.svg");
height: 1000px; bottom: -34vmin; left: 26vw;
}}
@media (prefers-color-scheme: dark) and (max-width: 600px) {
.main-image {
max-width: 195vw; height: 80vh; bottom: -17vmin; left: 0vw;
max-width: 195vw; height: 80vh; left: 0vw;
}}
</style>
<script src="_/code/util.js"></script>
@@ -44,6 +44,15 @@
<span>|</span>
<a href="signin">sign in</a>
</div>
<div style="display: flex; flex-direction: column; top: 110vh; left: 50vw; transform: translateX(-50%); align-items: left; position: absolute;">
<h1 style="font-family: Canterbury; font-size: 4rem; margin-left: auto">A Classical Christian Society</h1>
<p>Hyperia is a society for people who want to uphold European tradition and Christian values.</p>
<p>Inspired by the Classical Christian schooling movement that began in the 1990s, Hyperia aims to create a similar space for adults.</p>
<div style="height: 20vh"></div>
</div>
<style>
.link, a {
transition: background-color 0.3s ease, scale 0.3s;

View File

@@ -8,7 +8,7 @@
<style>
#items {
position: absolute;
top: 45vh;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
@@ -18,29 +18,6 @@
text-align: center; /* ensures text inside spans is centered */
}
a {
cursor: default;
text-decoration: underline;
text-underline-offset: 5px;
transition: background .02s, color .2s;
user-select: none;
padding: 4px;
border-radius: 5px;
display: inline-block; /* makes background and padding behave */
padding: 0.2em 0.5em; /* adds breathing room */
}
a:hover {
text-decoration: none;
background: var(--green);
color: var(--tan);
}
a:active {
background: var(--red); /* background color works now */
color: white; /* optional: change text color for contrast */
}
input {
background-color: transparent;
border: none;
@@ -78,8 +55,6 @@
width: 50vw
}
}
</style>
<script src="_/code/util.js"></script>
<script type="module">
@@ -91,9 +66,43 @@
<body>
<span id="title" onclick='console.log("hey"); window.location.href="/"'>hyperia</span>
<div id="items">
<input placeholder="email"></input>
<input id="email" placeholder="email"></input>
<br>
<input placeholder="password"></input>
<input id="password" placeholder="password"></input>
<br>
<p id="applicant-message" style="color: green; margin-left: 10px; display: inline-block; margin: 0px; margin-left: 20px; font-size: 1em"></p>
<br>
<div>
</div>
<button onclick="login()" style="background-color: rgb(193, 135, 29)">Sign In
<script>
async function login() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const messageEl = document.getElementById('applicant-message');
const res = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (res.ok) {
const data = await res.text();
messageEl.style.color = "green"
messageEl.textContent = "Check your email for a login link.";
} else {
const error = await res.text();
console.log(error)
messageEl.style.color = "red"
messageEl.textContent = "Error: " + error;
}
}
</script>
</button>
</div>
</body>
</html>

View File

@@ -1,4 +1,8 @@
See https://github.com/return-to-the-land/go-backend for instructions.
# Style Guidelines
- Font size is defined in the index.html. Only use rem as a unit.
- Font size is defined in the index.html. Only use rem as a unit.
# Documentation
```go install golang.org/x/tools/cmd/godoc@latest```
```$HOME/go/bin/godoc -http=:6060``` (to run on Mac)