signins
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
||||
package-lock.json
|
||||
_/build
|
||||
content
|
||||
db/*
|
||||
db/*
|
||||
server/db/users.json
|
||||
@@ -1,3 +1,3 @@
|
||||
BASE_URL=localhost:3003
|
||||
BASE_URL=http://localhost:3003
|
||||
JWT_SECRET=950b15c8c1c8a27dd716bba3ab51d96ce49afa85cae72884cf22e936e1bc0cb9
|
||||
ENV=development
|
||||
@@ -10,24 +10,31 @@ import (
|
||||
"github.com/alexedwards/argon2id"
|
||||
)
|
||||
|
||||
var DB map[string]interface{}
|
||||
type User struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
// Other fields as needed
|
||||
}
|
||||
|
||||
var DB map[string]User
|
||||
|
||||
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")
|
||||
}
|
||||
func (g GetService) UserByEmail(email string) (map[string]interface{}, error) {
|
||||
for key, value := range DB {
|
||||
if value.Email == email {
|
||||
log.Println("found")
|
||||
return map[string]interface{}{
|
||||
"key": key,
|
||||
"email": value.Email,
|
||||
"password": value.Password,
|
||||
}, nil
|
||||
}
|
||||
fmt.Printf("Key: %s, Value: %v\n", key, value)
|
||||
}
|
||||
|
||||
return userData, nil
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
func InitDB() error {
|
||||
@@ -38,19 +45,12 @@ func InitDB() error {
|
||||
}
|
||||
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")
|
||||
}
|
||||
var result map[string]User
|
||||
err = json.NewDecoder(file).Decode(&result)
|
||||
if err != nil {
|
||||
fmt.Println("Error decoding JSON:", err)
|
||||
return errors.New("failed to decode db")
|
||||
}
|
||||
|
||||
DB = result
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
// "strings"
|
||||
"os"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
"hyperia/db"
|
||||
|
||||
@@ -29,28 +30,57 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var creds loginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Unable to parse form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
email := r.FormValue("email")
|
||||
password := r.FormValue("password")
|
||||
creds.Email = email
|
||||
creds.Password = password
|
||||
|
||||
user, err := getUserByCredentials(creds)
|
||||
if err != nil {
|
||||
if err != nil || user == 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)
|
||||
keyInt, err := strconv.Atoi(user["key"].(string))
|
||||
if err != nil {
|
||||
// This means the string couldn't be parsed as an int — handle it
|
||||
log.Println("user['key'] is not a valid int:", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
jwtToken, err := GenerateJWT(keyInt)
|
||||
if err != nil {
|
||||
log.Println("JWT generation error:", err)
|
||||
http.Error(w, "Failed to generate auth token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: jwtToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Domain: "." + os.Getenv("BASE_URL"), // or ".localhost" — this allows subdomains
|
||||
Secure: true, // default to true (production)
|
||||
MaxAge: 2 * 60 * 60,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func getUserByCredentials(loginCreds loginRequest) (map[string]interface{}, error) {
|
||||
|
||||
// email := strings.TrimSpace(strings.ToLower(loginCreds.Email))
|
||||
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)
|
||||
user, err := db.Get.UserByEmail(email)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
@@ -14,20 +14,14 @@ func HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Domain: "." + os.Getenv("BASE_URL"), // must match what you set when logging in
|
||||
HttpOnly: true,
|
||||
Domain: "." + os.Getenv("BASE_URL"), // must match what you set when logging in
|
||||
Secure: true,
|
||||
Expires: time.Unix(0, 0), // way in the past
|
||||
MaxAge: -1, // tells browser to delete immediately
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
if config.ENV == "development" {
|
||||
cookie.Secure = false
|
||||
cookie.Domain = ".hyperia.local"
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
http.Redirect(w, r, config.BASE_URL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
100
server/handlers/signup.go
Normal file
100
server/handlers/signup.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Struct for incoming JSON request
|
||||
type SignupRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// Struct for JSON response
|
||||
type SignupResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func HandleSignup(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Failed to parse form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(r.FormValue("email"))
|
||||
if email == "" {
|
||||
http.Error(w, "Missing email", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Optional: basic email format check
|
||||
if !strings.Contains(email, "@") {
|
||||
http.Error(w, "Invalid email format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Received signup from email: %s", email)
|
||||
err := AddUserToFile(email, "db/users.json")
|
||||
if err != nil {
|
||||
log.Printf("Error saving user: %v", err)
|
||||
http.Error(w, "Failed to save user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with success
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(SignupResponse{
|
||||
Message: fmt.Sprintf("Signup received for %s", email),
|
||||
})
|
||||
}
|
||||
|
||||
func AddUserToFile(email string, filepath string) error {
|
||||
// Read the current users (if file exists)
|
||||
users := make(map[string]User)
|
||||
|
||||
if existingData, err := os.ReadFile(filepath); err == nil && len(existingData) > 0 {
|
||||
if err := json.Unmarshal(existingData, &users); err != nil {
|
||||
return fmt.Errorf("invalid users.json format: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Find the next numeric key
|
||||
maxID := 0
|
||||
for key := range users {
|
||||
id, err := strconv.Atoi(key)
|
||||
if err == nil && id > maxID {
|
||||
maxID = id
|
||||
}
|
||||
}
|
||||
newID := strconv.Itoa(maxID + 1)
|
||||
|
||||
// Add new user
|
||||
users[newID] = User{Email: email}
|
||||
|
||||
// Marshal updated data
|
||||
updated, err := json.MarshalIndent(users, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal updated users: %v", err)
|
||||
}
|
||||
|
||||
// Write updated data back to file
|
||||
if err := os.WriteFile(filepath, updated, 0644); err != nil {
|
||||
return fmt.Errorf("could not write to users file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
// "os"
|
||||
"time"
|
||||
|
||||
"hyperia/config"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func GenerateJWT(applicantId int) (string, error) {
|
||||
func GenerateJWT(userId int) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"applicant_id": applicantId,
|
||||
"applicant_id": userId,
|
||||
"exp": time.Now().Add(2 * time.Hour).Unix(), // expires in 2 hours
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
@@ -24,59 +21,4 @@ func GenerateJWT(applicantId int) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
func HandleVerify(w http.ResponseWriter, r *http.Request) {
|
||||
// token := r.URL.Query().Get("token")
|
||||
// if token == "" {
|
||||
// http.Error(w, "Missing token", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// v, err := GetApplicantVerificationByToken(token)
|
||||
// if err != nil {
|
||||
// log.Println("Invalid token: ", token)
|
||||
// http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if time.Since(v.CreatedOn) > 30*time.Minute || v.Expired {
|
||||
// log.Println("Token expired: ", token)
|
||||
// http.Error(w, "Token expired", http.StatusUnauthorized)
|
||||
// return
|
||||
// }
|
||||
|
||||
// _, err = DB.Exec(`
|
||||
// UPDATE ApplicantVerifications SET Expired = 1 WHERE ApplicantId = $1
|
||||
// `, v.ApplicantId)
|
||||
// if err != nil {
|
||||
// http.Error(w, "Failed to update verification", http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
|
||||
// jwtToken, err := GenerateJWT(v.ApplicantId)
|
||||
// if err != nil {
|
||||
// log.Println("JWT generation error:", err)
|
||||
// http.Error(w, "Failed to generate auth token", http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
|
||||
// cookie := &http.Cookie{
|
||||
// Name: "auth_token",
|
||||
// Value: jwtToken,
|
||||
// Path: "/",
|
||||
// HttpOnly: true,
|
||||
// Domain: "." + os.Getenv("BASE_URL"), // or ".localhost" — this allows subdomains
|
||||
// Secure: true, // default to true (production)
|
||||
// MaxAge: 2 * 60 * 60,
|
||||
// SameSite: http.SameSiteLaxMode,
|
||||
// }
|
||||
// if config.ENV == "development" {
|
||||
// cookie.Secure = false
|
||||
// cookie.Domain = ".hyperia.local"
|
||||
// }
|
||||
|
||||
// http.SetCookie(w, cookie)
|
||||
log.Println("Verification success.")
|
||||
http.Redirect(w, r, config.BASE_URL, http.StatusSeeOther)
|
||||
}
|
||||
159
server/main.go
159
server/main.go
@@ -8,7 +8,7 @@ import (
|
||||
"hyperia/db"
|
||||
"hyperia/handlers"
|
||||
"hyperia/logger"
|
||||
"runtime/debug"
|
||||
// "runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -27,28 +27,19 @@ func main() {
|
||||
}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error().
|
||||
Interface("panic_reason", r).
|
||||
Bytes("stack_trace", debug.Stack()).
|
||||
Msg("panic in http goroutine")
|
||||
}
|
||||
}()
|
||||
// Keeps server from crashing if a request fails
|
||||
// defer func() {
|
||||
// if r := recover(); r != nil {
|
||||
// log.Error().
|
||||
// Interface("panic_reason", r).
|
||||
// Bytes("stack_trace", debug.Stack()).
|
||||
// Msg("panic in http goroutine")
|
||||
// }
|
||||
// }()
|
||||
|
||||
subdomain := ""
|
||||
host := strings.Split(r.Host, ":")[0] // remove port
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) > 2 || (len(parts) > 1 && parts[1] == "localhost") {
|
||||
subdomain = parts[0]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/_") {
|
||||
handleAsset(w, r)
|
||||
} else if subdomain == "apply" {
|
||||
authMiddleware(handleApply)(w, r)
|
||||
} else if subdomain == "pma" {
|
||||
authMiddleware(handlePMA)(w, r)
|
||||
if(loggedIn(w, r)) {
|
||||
log.Info().Msg("logged")
|
||||
handleSite(w, r)
|
||||
} else {
|
||||
handlePublic(w, r)
|
||||
}
|
||||
@@ -59,37 +50,32 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("failed to start server: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handlePublic(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/signup" {
|
||||
handlers.HandleSignup(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api/login" {
|
||||
handlers.HandleLogin(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api/join" {
|
||||
handlers.HandleJoin(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/verify" {
|
||||
handlers.HandleVerify(w, r)
|
||||
if strings.HasPrefix(r.URL.Path, "/_") {
|
||||
handleAsset(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
servePublicFile(w, r)
|
||||
}
|
||||
|
||||
func handleAsset(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
filePath := filepath.Join("../ui", path)
|
||||
log.Debug().Msgf("serving asset: %s", filePath)
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
|
||||
func servePublicFile(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
if path == "/" {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
path = "/index.html"
|
||||
} else if !strings.Contains(path, ".") {
|
||||
path = filepath.Join("/pages", path) + ".html"
|
||||
@@ -100,77 +86,62 @@ func servePublicFile(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
|
||||
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("auth_token")
|
||||
if err != nil {
|
||||
log.Warn().Msg("Unauthorized - missing auth token")
|
||||
http.Error(w, "Unauthorized - missing auth token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
jwtToken := cookie.Value
|
||||
|
||||
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(config.JWT_SECRET), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error authenticating jwt")
|
||||
}
|
||||
if err != nil || !token.Valid {
|
||||
http.Error(w, "Unauthorized - invalid auth token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
func handleSite(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/signout" {
|
||||
handlers.HandleLogout(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/_") {
|
||||
handleAsset(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
serveSiteFiles(w, r)
|
||||
}
|
||||
|
||||
func handleApply(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// if r.URL.Path == "/api/application-save" {
|
||||
// handlers.HandleApplicationSubmit(w, r)
|
||||
// return
|
||||
// }
|
||||
// if r.URL.Path == "/api/get-application" {
|
||||
// handlers.HandleGetApplication(w, r)
|
||||
// return
|
||||
// }
|
||||
// if r.URL.Path == "/logout" {
|
||||
// handlers.HandleLogout(w, r)
|
||||
// return
|
||||
// }
|
||||
// if r.URL.Path == "/" {
|
||||
// handlers.CheckApplicationCompleteMiddleware(w, r)
|
||||
// }
|
||||
// if r.URL.Path == "/complete" {
|
||||
// handlers.ApplicationSubmitMiddleware(w, r)
|
||||
// }
|
||||
|
||||
func serveSiteFiles(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
if path == "/" {
|
||||
path = "/index.html"
|
||||
} else if !strings.Contains(path, ".") {
|
||||
path = filepath.Join("/pages", path) + ".html"
|
||||
}
|
||||
|
||||
filePath := filepath.Join("../ui/apply", path)
|
||||
log.Debug().Msgf("Serving apply subdomain: %s", filePath)
|
||||
filePath := filepath.Join("../ui/site", path)
|
||||
log.Debug().Msgf("serving: %s", filePath)
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
|
||||
func handlePMA(w http.ResponseWriter, r *http.Request) {
|
||||
func handleAsset(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if path == "/" {
|
||||
path = "/index.html"
|
||||
} else if !strings.Contains(path, ".") {
|
||||
path = filepath.Join("/pages", path) + ".html"
|
||||
filePath := filepath.Join("../ui", path)
|
||||
log.Debug().Msgf("serving asset: %s", filePath)
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
|
||||
func loggedIn(w http.ResponseWriter, r *http.Request) bool {
|
||||
cookie, err := r.Cookie("auth_token")
|
||||
if err != nil {
|
||||
log.Warn().Msg("Unauthorized - missing auth token")
|
||||
return false
|
||||
}
|
||||
jwtToken := cookie.Value
|
||||
|
||||
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(config.JWT_SECRET), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error authenticating jwt")
|
||||
return false
|
||||
}
|
||||
if err != nil || !token.Valid {
|
||||
return false
|
||||
}
|
||||
|
||||
filePath := filepath.Join("../ui/pma", path)
|
||||
log.Debug().Msgf("serving pma subdomain: %s", filePath)
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -34,6 +34,14 @@
|
||||
import NavBar from "./components/NavBar.js"
|
||||
import SideBar from "./components/SideBar.js"
|
||||
</script>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
if (window.location.search) {
|
||||
// Replace the URL with the clean path (e.g., '/')
|
||||
window.history.replaceState(null, '', '/');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<span id="title" class="link" onclick='window.location.href="/"'>hyperia
|
||||
@@ -45,12 +53,61 @@
|
||||
<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: 3.5rem; margin-left: auto">A Classical Christian Community</h1>
|
||||
<p>Hyperia is a social network for people who are a part of the Christian church and the European tradition.</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>
|
||||
<div style="position: absolute; top: 110vh; left: 50vw; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center;">
|
||||
|
||||
<!-- INTRO SECTION -->
|
||||
<div style="display: flex; flex-direction: column; align-items: flex-start; max-width: 50vw;">
|
||||
<h1 style="font-family: Canterbury; font-size: 3.5rem; margin-left: auto;">A Classical Christian Community</h1>
|
||||
|
||||
<p>Hyperia is a private society for people who are a part of the Christian church and the European tradition.</p>
|
||||
<p>Inspired by the Classical Christian schooling movement that began in the 1990s, Hyperia aims to create a similar space for adults.</p>
|
||||
<p>These are five of our main goals:</p>
|
||||
|
||||
<div style="height: 5vh;"></div>
|
||||
</div>
|
||||
|
||||
<!-- GOALS SECTION -->
|
||||
<div style="display: flex; flex-direction: column; align-items: flex-start; width: 65vw; margin-top: 2vh;">
|
||||
<ol style="font-size: 1.1rem; line-height: 1.7;">
|
||||
<li style="margin-bottom: 1.5rem;">
|
||||
<strong>Reintroduce Heroism and Romance into Western Life</strong><br>
|
||||
When men are disconnected from ancestry, God, and land, we no longer have anything to fight for. Therefore, we will not fight.<br><br>
|
||||
When men do not fight, women are unprotected. They hate the men for being weak, and the men hate them too.<br><br>
|
||||
This is the cycle we are in, and this is the cycle we must break.<br><br>
|
||||
Hyperia Security allows men to be protectors of God and tradition. It allows them to fight for a society which is directly linked to our past and our future. We believe that, given this opportunity, men will rise to the occasion.<br><br>
|
||||
<em>Single women may join Hyperia for free.</em>
|
||||
</li>
|
||||
|
||||
<li style="margin-bottom: 1.5rem;">
|
||||
<strong>Reunite the Mind and Body of the West</strong><br>
|
||||
Since 1945, Western intellectuals and their people have been polarized against each other. This became obvious in 2016, with the divide between urban and rural voters in the election of Trump.<br><br>
|
||||
This polarization has terrible consequences, such as the classism of the opioid epidemic and the outsourcing of American jobs overseas. Now, finally, there is a chance to reunite.<br><br>
|
||||
We seek a society in which both groups can live in the communities they desire, and in which both groups work together for the common good.
|
||||
</li>
|
||||
|
||||
<li style="margin-bottom: 1.5rem;">
|
||||
<strong>Create a Market of Our Own Goods</strong><br>
|
||||
Outsourcing has been a disaster for the West. America's economy is almost all service-based, and foreign schemes like H1-B and Chinese factories have taken vast amounts of jobs.<br><br>
|
||||
Hyperia will have a job board and marketplace that is exclusive to members. The marketplace will highlight goods which are made by other members, and also which are American-made.
|
||||
</li>
|
||||
|
||||
<li style="margin-bottom: 1.5rem;">
|
||||
<strong>Revitalize Classical Christian Culture</strong><br>
|
||||
Currently, America is laden with exposure to secular and anti-Western influences. Hyperia will provide spaces, which are digital and eventually physical, that abide by Christian rules and are in favor of Western culture.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Resettle America</strong><br>
|
||||
There are many towns and cities in America which have fallen prey to negligence and disrepair. Hyperia will focus on restoring these places, and not allow them to be overrun or abandoned.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div style="height: 10vh;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
<link rel="icon" href="_/icons/logo.svg">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<style>
|
||||
|
||||
#items {
|
||||
position: absolute;
|
||||
top: 45vh;
|
||||
top: 50vh;
|
||||
left: 50vw;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
@@ -19,28 +18,43 @@
|
||||
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;
|
||||
input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
display: inline-block; /* makes background and padding behave */
|
||||
padding: 0.2em 0.5em; /* adds breathing room */
|
||||
border-top: 2px solid var(--accent);
|
||||
border-bottom: 2px solid var(--accent);
|
||||
height: 2em;
|
||||
width: 15vw;
|
||||
padding: 0.2em;
|
||||
transition: border .2s, padding .2s;
|
||||
color: var(--accent)
|
||||
}
|
||||
input::placeholder {
|
||||
color: var(--accent);
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
padding: 0.4em;
|
||||
border-top: 2px solid var(--red);
|
||||
border-bottom: 2px solid var(--red);
|
||||
}
|
||||
input:focus::placeholder {
|
||||
color: var(--red)
|
||||
}
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:focus {
|
||||
transition: background-color 600000s 0s, color 600000s 0s;
|
||||
}
|
||||
input[data-autocompleted] {
|
||||
background-color: #c7a67b !important;
|
||||
}
|
||||
|
||||
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 */
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
input {
|
||||
width: 50vw
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="_/code/util.js"></script>
|
||||
<script type="module">
|
||||
@@ -50,17 +64,55 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<span id="title" onclick='window.location.href="/"'>hyperia
|
||||
</span>
|
||||
<span id="title" onclick='console.log("hey"); window.location.href="/"'>hyperia</span>
|
||||
<div class="links" style="z-index: 1; cursor: default; position: fixed; top: 5.5vh; right: 4.5vw">
|
||||
<a href="join">join</a>
|
||||
<span>|</span>
|
||||
<a href="signin">sign in</a>
|
||||
</div>
|
||||
|
||||
<p style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)">
|
||||
Hyperia is invite-only. You'll need a friend to invite you.
|
||||
</p>
|
||||
<div id="items">
|
||||
|
||||
<script>
|
||||
Array.from($("a*")).forEach((link) => {
|
||||
link.addEventListener("click", () => window.history.pushState("", "", link.innerHTML.toLowerCase()))
|
||||
})
|
||||
</script>
|
||||
<form id="signup-form">
|
||||
<input name="email" id="email" placeholder="email" required style="margin-bottom: 15px;">
|
||||
<br>
|
||||
<p id="applicant-message" style="color: green; font-size: 1em; margin: 0.5em 0;"></p>
|
||||
<button type="submit" style="background-color: rgb(193, 135, 29)">Sign Up</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('signup-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault(); // prevent page reload
|
||||
|
||||
const email = document.getElementById('email').value;
|
||||
const messageEl = document.getElementById('applicant-message');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({ email }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
messageEl.style.color = 'green';
|
||||
messageEl.textContent = data.message;
|
||||
} else {
|
||||
const error = await res.text();
|
||||
messageEl.style.color = 'red';
|
||||
messageEl.textContent = 'Error: ' + error;
|
||||
}
|
||||
} catch (err) {
|
||||
messageEl.style.color = 'red';
|
||||
messageEl.textContent = 'Error submitting form.';
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -65,44 +65,22 @@
|
||||
</head>
|
||||
<body>
|
||||
<span id="title" onclick='console.log("hey"); window.location.href="/"'>hyperia</span>
|
||||
<div class="links" style="z-index: 1; cursor: default; position: fixed; top: 5.5vh; right: 4.5vw">
|
||||
<a href="join">join</a>
|
||||
<span>|</span>
|
||||
<a href="signin">sign in</a>
|
||||
</div>
|
||||
|
||||
<div id="items">
|
||||
<input id="email" placeholder="email"></input>
|
||||
<form id="login-form" action="/api/login" method="POST">
|
||||
<input name="email" placeholder="email" style="margin-bottom: 15px;" required>
|
||||
<br>
|
||||
<input id="password" placeholder="password"></input>
|
||||
<input name="password" type="password" placeholder="password" required>
|
||||
<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>
|
||||
<button type="submit" style="background-color: rgb(193, 135, 29)">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
ui/site/index.html
Normal file
26
ui/site/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hyperia</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="_/icons/logo.svg">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<span id="title" class="link" onclick='window.location.href="/"'>hyperia
|
||||
</span>
|
||||
<img class="main-image">
|
||||
<div class="links" style="z-index: 1; cursor: default; position: fixed; top: 5.5vh; right: 4.5vw">
|
||||
<a href="#" onclick="logout(); return false;">sign out</a>
|
||||
<script>
|
||||
function logout() {
|
||||
fetch('/signout', { method: 'GET', credentials: 'include' })
|
||||
.then(() => {
|
||||
// Force a clean full-page reload of "/"
|
||||
window.location.replace('/?loggedout');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user