signins
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ package-lock.json
|
|||||||
_/build
|
_/build
|
||||||
content
|
content
|
||||||
db/*
|
db/*
|
||||||
|
server/db/users.json
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
BASE_URL=localhost:3003
|
BASE_URL=http://localhost:3003
|
||||||
JWT_SECRET=950b15c8c1c8a27dd716bba3ab51d96ce49afa85cae72884cf22e936e1bc0cb9
|
JWT_SECRET=950b15c8c1c8a27dd716bba3ab51d96ce49afa85cae72884cf22e936e1bc0cb9
|
||||||
ENV=development
|
ENV=development
|
||||||
@@ -10,26 +10,33 @@ import (
|
|||||||
"github.com/alexedwards/argon2id"
|
"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{}
|
type GetService struct{}
|
||||||
var Get = GetService{}
|
var Get = GetService{}
|
||||||
|
|
||||||
func (g GetService) User(id string) (map[string]interface{}, error) {
|
func (g GetService) UserByEmail(email string) (map[string]interface{}, error) {
|
||||||
raw, ok := DB[id]
|
for key, value := range DB {
|
||||||
if !ok {
|
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 nil, errors.New("user not found")
|
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 {
|
func InitDB() error {
|
||||||
file, err := os.Open("../db/users.json")
|
file, err := os.Open("../db/users.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,18 +45,11 @@ func InitDB() error {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
var data interface{}
|
var result map[string]User
|
||||||
|
err = json.NewDecoder(file).Decode(&result)
|
||||||
err = json.NewDecoder(file).Decode(&data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error decoding JSON:", err)
|
fmt.Println("Error decoding JSON:", err)
|
||||||
return errors.New("Failed to decode db")
|
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
|
DB = result
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
// "strings"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"hyperia/db"
|
"hyperia/db"
|
||||||
|
|
||||||
@@ -29,28 +30,57 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var creds loginRequest
|
var creds loginRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
http.Error(w, "Unable to parse form", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
email := r.FormValue("email")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
creds.Email = email
|
||||||
|
creds.Password = password
|
||||||
|
|
||||||
user, err := getUserByCredentials(creds)
|
user, err := getUserByCredentials(creds)
|
||||||
if err != nil {
|
if err != nil || user == nil {
|
||||||
http.Error(w, "Unauthorized: "+ err.Error(), http.StatusUnauthorized)
|
http.Error(w, "Unauthorized: "+ err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
keyInt, err := strconv.Atoi(user["key"].(string))
|
||||||
http.Error(w, "Not implemented", http.StatusMethodNotAllowed)
|
if err != nil {
|
||||||
json.NewEncoder(w).Encode(user)
|
// 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) {
|
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")
|
user, err := db.Get.UserByEmail(email)
|
||||||
// err := DB.QueryRow("SELECT id, name, password FROM users WHERE LOWER(name) = LOWER($1)", name).Scan(&id, &dbName, &dbHash)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("user not found")
|
return nil, errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,20 +14,14 @@ func HandleLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
Name: "auth_token",
|
Name: "auth_token",
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Domain: "." + os.Getenv("BASE_URL"), // must match what you set when logging in
|
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Domain: "." + os.Getenv("BASE_URL"), // must match what you set when logging in
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Expires: time.Unix(0, 0), // way in the past
|
Expires: time.Unix(0, 0), // way in the past
|
||||||
MaxAge: -1, // tells browser to delete immediately
|
MaxAge: -1, // tells browser to delete immediately
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.ENV == "development" {
|
|
||||||
cookie.Secure = false
|
|
||||||
cookie.Domain = ".hyperia.local"
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
http.Redirect(w, r, config.BASE_URL, http.StatusSeeOther)
|
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
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
// "os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hyperia/config"
|
"hyperia/config"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateJWT(applicantId int) (string, error) {
|
func GenerateJWT(userId int) (string, error) {
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"applicant_id": applicantId,
|
"applicant_id": userId,
|
||||||
"exp": time.Now().Add(2 * time.Hour).Unix(), // expires in 2 hours
|
"exp": time.Now().Add(2 * time.Hour).Unix(), // expires in 2 hours
|
||||||
"iat": time.Now().Unix(),
|
"iat": time.Now().Unix(),
|
||||||
}
|
}
|
||||||
@@ -25,58 +22,3 @@ func GenerateJWT(applicantId int) (string, error) {
|
|||||||
}
|
}
|
||||||
return signedToken, nil
|
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)
|
|
||||||
}
|
|
||||||
151
server/main.go
151
server/main.go
@@ -8,7 +8,7 @@ import (
|
|||||||
"hyperia/db"
|
"hyperia/db"
|
||||||
"hyperia/handlers"
|
"hyperia/handlers"
|
||||||
"hyperia/logger"
|
"hyperia/logger"
|
||||||
"runtime/debug"
|
// "runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -27,28 +27,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
// Keeps server from crashing if a request fails
|
||||||
if r := recover(); r != nil {
|
// defer func() {
|
||||||
log.Error().
|
// if r := recover(); r != nil {
|
||||||
Interface("panic_reason", r).
|
// log.Error().
|
||||||
Bytes("stack_trace", debug.Stack()).
|
// Interface("panic_reason", r).
|
||||||
Msg("panic in http goroutine")
|
// Bytes("stack_trace", debug.Stack()).
|
||||||
}
|
// Msg("panic in http goroutine")
|
||||||
}()
|
// }
|
||||||
|
// }()
|
||||||
|
|
||||||
subdomain := ""
|
if(loggedIn(w, r)) {
|
||||||
host := strings.Split(r.Host, ":")[0] // remove port
|
log.Info().Msg("logged")
|
||||||
parts := strings.Split(host, ".")
|
handleSite(w, r)
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
handlePublic(w, r)
|
handlePublic(w, r)
|
||||||
}
|
}
|
||||||
@@ -59,37 +50,32 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msgf("failed to start server: %v", err)
|
log.Fatal().Msgf("failed to start server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePublic(w http.ResponseWriter, r *http.Request) {
|
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" {
|
if r.URL.Path == "/api/login" {
|
||||||
handlers.HandleLogin(w, r)
|
handlers.HandleLogin(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.URL.Path == "/api/join" {
|
if strings.HasPrefix(r.URL.Path, "/_") {
|
||||||
handlers.HandleJoin(w, r)
|
handleAsset(w, r)
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.URL.Path == "/verify" {
|
|
||||||
handlers.HandleVerify(w, r)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
servePublicFile(w, r)
|
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) {
|
func servePublicFile(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
|
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("Pragma", "no-cache")
|
||||||
|
w.Header().Set("Expires", "0")
|
||||||
path = "/index.html"
|
path = "/index.html"
|
||||||
} else if !strings.Contains(path, ".") {
|
} else if !strings.Contains(path, ".") {
|
||||||
path = filepath.Join("/pages", path) + ".html"
|
path = filepath.Join("/pages", path) + ".html"
|
||||||
@@ -100,13 +86,45 @@ func servePublicFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.ServeFile(w, r, filePath)
|
http.ServeFile(w, r, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
func handleSite(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(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 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/site", path)
|
||||||
|
log.Debug().Msgf("serving: %s", filePath)
|
||||||
|
http.ServeFile(w, r, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 loggedIn(w http.ResponseWriter, r *http.Request) bool {
|
||||||
cookie, err := r.Cookie("auth_token")
|
cookie, err := r.Cookie("auth_token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Msg("Unauthorized - missing auth token")
|
log.Warn().Msg("Unauthorized - missing auth token")
|
||||||
http.Error(w, "Unauthorized - missing auth token", http.StatusUnauthorized)
|
return false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
jwtToken := cookie.Value
|
jwtToken := cookie.Value
|
||||||
|
|
||||||
@@ -119,58 +137,11 @@ func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("error authenticating jwt")
|
log.Err(err).Msg("error authenticating jwt")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
if err != nil || !token.Valid {
|
if err != nil || !token.Valid {
|
||||||
http.Error(w, "Unauthorized - invalid auth token", http.StatusUnauthorized)
|
return false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next(w, r)
|
return true
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
// }
|
|
||||||
|
|
||||||
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)
|
|
||||||
http.ServeFile(w, r, filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePMA(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/pma", path)
|
|
||||||
log.Debug().Msgf("serving pma subdomain: %s", filePath)
|
|
||||||
http.ServeFile(w, r, filePath)
|
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,14 @@
|
|||||||
import NavBar from "./components/NavBar.js"
|
import NavBar from "./components/NavBar.js"
|
||||||
import SideBar from "./components/SideBar.js"
|
import SideBar from "./components/SideBar.js"
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
if (window.location.search) {
|
||||||
|
// Replace the URL with the clean path (e.g., '/')
|
||||||
|
window.history.replaceState(null, '', '/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<span id="title" class="link" onclick='window.location.href="/"'>hyperia
|
<span id="title" class="link" onclick='window.location.href="/"'>hyperia
|
||||||
@@ -45,13 +53,62 @@
|
|||||||
<a href="signin">sign in</a>
|
<a href="signin">sign in</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; top: 110vh; left: 50vw; transform: translateX(-50%); align-items: left; position: absolute;">
|
<div style="position: absolute; top: 110vh; left: 50vw; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center;">
|
||||||
<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>
|
<!-- 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>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>
|
<p>These are five of our main goals:</p>
|
||||||
|
|
||||||
|
<div style="height: 5vh;"></div>
|
||||||
</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>
|
<style>
|
||||||
.link, a {
|
.link, a {
|
||||||
|
|||||||
@@ -6,10 +6,9 @@
|
|||||||
<link rel="icon" href="_/icons/logo.svg">
|
<link rel="icon" href="_/icons/logo.svg">
|
||||||
<link rel="stylesheet" href="index.css">
|
<link rel="stylesheet" href="index.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
#items {
|
#items {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 45vh;
|
top: 50vh;
|
||||||
left: 50vw;
|
left: 50vw;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
@@ -19,27 +18,42 @@
|
|||||||
text-align: center; /* ensures text inside spans is centered */
|
text-align: center; /* ensures text inside spans is centered */
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
input {
|
||||||
cursor: default;
|
background-color: transparent;
|
||||||
text-decoration: underline;
|
border: none;
|
||||||
text-underline-offset: 5px;
|
|
||||||
transition: background .02s, color .2s;
|
|
||||||
user-select: none;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
display: inline-block; /* makes background and padding behave */
|
border-top: 2px solid var(--accent);
|
||||||
padding: 0.2em 0.5em; /* adds breathing room */
|
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 {
|
@media (max-width: 600px) {
|
||||||
text-decoration: none;
|
input {
|
||||||
background: var(--green);
|
width: 50vw
|
||||||
color: var(--tan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:active {
|
|
||||||
background: var(--red); /* background color works now */
|
|
||||||
color: white; /* optional: change text color for contrast */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="_/code/util.js"></script>
|
<script src="_/code/util.js"></script>
|
||||||
@@ -50,17 +64,55 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<span id="title" onclick='window.location.href="/"'>hyperia
|
<span id="title" onclick='console.log("hey"); window.location.href="/"'>hyperia</span>
|
||||||
</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%)">
|
<div id="items">
|
||||||
Hyperia is invite-only. You'll need a friend to invite you.
|
|
||||||
</p>
|
<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>
|
<script>
|
||||||
Array.from($("a*")).forEach((link) => {
|
document.getElementById('signup-form').addEventListener('submit', async (e) => {
|
||||||
link.addEventListener("click", () => window.history.pushState("", "", link.innerHTML.toLowerCase()))
|
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>
|
</script>
|
||||||
|
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -65,44 +65,22 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<span id="title" onclick='console.log("hey"); 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>
|
||||||
|
|
||||||
<div id="items">
|
<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>
|
<br>
|
||||||
<input id="password" placeholder="password"></input>
|
<input name="password" type="password" placeholder="password" required>
|
||||||
<br>
|
<br>
|
||||||
<p id="applicant-message" style="color: green; margin-left: 10px; display: inline-block; margin: 0px; margin-left: 20px; font-size: 1em"></p>
|
<p id="applicant-message" style="color: green; margin-left: 10px; display: inline-block; margin: 0px; margin-left: 20px; font-size: 1em"></p>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<button type="submit" style="background-color: rgb(193, 135, 29)">Sign In</button>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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