This commit is contained in:
metacryst
2025-09-30 17:44:39 -05:00
parent 7c8fd24b49
commit faf2041b7f
12 changed files with 421 additions and 270 deletions

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@
package-lock.json
_/build
content
db/*
db/*
server/db/users.json

View File

@@ -1,3 +1,3 @@
BASE_URL=localhost:3003
BASE_URL=http://localhost:3003
JWT_SECRET=950b15c8c1c8a27dd716bba3ab51d96ce49afa85cae72884cf22e936e1bc0cb9
ENV=development

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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
View 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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View 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>