12 Commits

Author SHA1 Message Date
gawells 7d1be226b6 move some stuff to a new JS file 2026-06-14 08:13:30 -04:00
gawells 65ed53c5d1 grab picture when moving it around 2026-06-14 07:58:29 -04:00
gawells ff5d9724d6 simple cursor controller 2026-06-14 07:58:17 -04:00
gawells 8859449be5 add boundies for moving image around 2026-06-13 13:41:39 -04:00
gawells f0ace2b43f unblur background on error 2026-06-12 13:23:50 -04:00
gawells 07f78510ed display profile picture image 2026-06-12 13:16:14 -04:00
gawells 4870318148 add a cursor helper 2026-06-11 18:58:57 -04:00
gawells c9f9f3ef6c add in confirm change button 2026-06-11 18:38:37 -04:00
gawells d82d2bba20 redo in memory sesisons to have TTLs 2026-06-11 18:13:31 -04:00
gawells 22c4666fa7 update session stores to decide TTLs 2026-06-09 12:31:27 -04:00
gawells e41a81c487 get a cropped square to exist 2026-06-08 21:36:10 -04:00
gawells 600c3abf20 make new photo pop up in box before changing 2026-06-08 21:09:12 -04:00
10 changed files with 322 additions and 55 deletions
+55 -5
View File
@@ -10,6 +10,7 @@
<link rel="stylesheet" href="static/error/error.css" /> <link rel="stylesheet" href="static/error/error.css" />
<link rel="stylesheet" href="static/profile_page.css" /> <link rel="stylesheet" href="static/profile_page.css" />
<link rel="stylesheet" href="static/progress_bar.css" /> <link rel="stylesheet" href="static/progress_bar.css" />
<link rel="stylesheet" href="static/cursor.css" />
<div id="popup_background" class="blocked hidden"></div> <div id="popup_background" class="blocked hidden"></div>
@@ -101,6 +102,15 @@
<button id="final_change_password_button">Change Password</button> <button id="final_change_password_button">Change Password</button>
</div> </div>
<div id="resize_photo_dialouge" class="card static_center hidden">
<div id="image_container">
<img id="new_profile_image" alt="new-profile-image" />
<div id="darken_part"></div>
</div>
<div id="resize_photo_box"></div>
<button id="confirm_change">Confirm Change</button>
</div>
<img id="logo_image" alt="logo" src="/logo" /> <img id="logo_image" alt="logo" src="/logo" />
<div id="main_content"> <div id="main_content">
<div class="cards"> <div class="cards">
@@ -168,28 +178,68 @@
</div> </div>
</div> </div>
<script src="/static/javascript/cursor.js" type="text/javascript"></script>
<script
src="/static/javascript/resize_photo_dialouge.js"
type="text/javascript"
></script>
<script type="text/javascript"> <script type="text/javascript">
const button = document.getElementById("edit_picture_button"); const button = document.getElementById("edit_picture_button");
const fileInput = document.getElementById("file_input"); const fileInput = document.getElementById("file_input");
const confirm_change = document.getElementById("confirm_change");
const new_profile_image = document.getElementById("new_profile_image");
button.addEventListener("click", () => { button.addEventListener("click", () => {
fileInput.click(); // opens file picker fileInput.click();
}); });
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
var currentPreviewURL = null; var currentPreviewURL = null,
image = null,
file = null;
var x_start = 0,
y_start = 0;
var bitmap;
fileInput.addEventListener("change", async () => { fileInput.addEventListener("change", async () => {
document.getElementById("popup_background").classList.remove("hidden"); document.getElementById("popup_background").classList.remove("hidden");
const file = fileInput.files[0]; file = fileInput.files[0];
if (!file) return; if (!file) return;
if (file.type !== "image/jpeg") { if (file.type !== "image/jpeg") {
alert("Only JPEG images are allowed."); alert("Only JPEG images are allowed.");
fileInput.value = ""; fileInput.value = "";
document.getElementById("popup_background").classList.add("hidden");
return; return;
} }
document
.getElementById("resize_photo_dialouge")
.classList.remove("hidden");
image = URL.createObjectURL(file);
bitmap = await createImageBitmap(file);
new_profile_image.src = image;
resize_photo_box.style.setProperty(
"--image-width",
bitmap.width + "px",
);
new_profile_image.style.setProperty("--x-offset", "0px");
new_profile_image.style.setProperty("--y-offset", "0px");
});
confirm_change.addEventListener("click", async () => {
document
.getElementById("resize_photo_dialouge")
.classList.add("hidden");
const formData = new FormData(); const formData = new FormData();
formData.append("photo", file); formData.append("photo", file);
formData.append( formData.append(
@@ -211,8 +261,8 @@
URL.revokeObjectURL(currentPreviewURL); URL.revokeObjectURL(currentPreviewURL);
} }
img.src = URL.createObjectURL(file); img.src = image;
currentPreviewURL = img.src; currentPreviewURL = image;
}); });
</script> </script>
+1 -23
View File
@@ -7,7 +7,6 @@ import (
"astraltech.xyz/accountmanager/src/logging" "astraltech.xyz/accountmanager/src/logging"
"astraltech.xyz/accountmanager/src/store" "astraltech.xyz/accountmanager/src/store"
"astraltech.xyz/accountmanager/src/worker"
) )
const SessionCookieName = "session_token" const SessionCookieName = "session_token"
@@ -39,10 +38,6 @@ func (manager *SessionManager) SetStoreType(storeType StoreType, params ...any)
case InMemory: case InMemory:
{ {
manager.store = store.NewMemoryStore[*SessionData]() manager.store = store.NewMemoryStore[*SessionData]()
worker.CreateWorker(time.Minute*5, func() {
inMemStore, _ := manager.store.(*store.MemoryStore[*SessionData])
cleanupInMemoryStore(inMemStore)
})
break break
} }
case Redis: case Redis:
@@ -70,7 +65,7 @@ func (manager *SessionManager) CreateSession(userID string) (cookie *http.Cookie
CSRFToken: CSRFToken, CSRFToken: CSRFToken,
ExpiresAt: time.Now().Add(time.Hour), ExpiresAt: time.Now().Add(time.Hour),
} }
err = manager.store.Create(token, &newSessionData) err = manager.store.CreateExpiring(token, &newSessionData, time.Hour)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -110,23 +105,6 @@ func (manager *SessionManager) GetSession(r *http.Request) (*SessionData, error)
return data, nil return data, nil
} }
func cleanupInMemoryStore(m *store.MemoryStore[*SessionData]) {
logging.Debug("Cleaning up memory store sessions")
now := time.Now()
m.Lock.Lock()
defer m.Lock.Unlock()
deleted := 0
for id, session := range m.Sessions {
if now.After(session.ExpiresAt) {
delete(m.Sessions, id)
deleted = deleted + 1
}
}
logging.Infof("Cleaned up %d stale sessions", deleted)
}
func (manager *SessionManager) DeleteSession(sessionId string) error { func (manager *SessionManager) DeleteSession(sessionId string) error {
return manager.store.Delete(sessionId) return manager.store.Delete(sessionId)
} }
+5
View File
@@ -1,9 +1,14 @@
package store package store
import (
"time"
)
// A simple key value store that can either just be single instance in memory or a redis server (for now) // A simple key value store that can either just be single instance in memory or a redis server (for now)
type KeyValueStore[Value any] interface { type KeyValueStore[Value any] interface {
Create(key string, value Value) error Create(key string, value Value) error
CreateExpiring(key string, value Value, ttl time.Duration) error
Get(key string) (Value, error) Get(key string) (Value, error)
Update(key string, session Value) error Update(key string, session Value) error
Delete(key string) error Delete(key string) error
+109 -24
View File
@@ -2,72 +2,157 @@ package store
import ( import (
"sync" "sync"
"time"
"astraltech.xyz/accountmanager/src/logging" "astraltech.xyz/accountmanager/src/logging"
"astraltech.xyz/accountmanager/src/worker"
) )
type KeyValue[Value any] struct {
value Value
expireTime time.Time
}
type ExpireTime struct {
expiryTime time.Time
key string
}
type MemoryStore[Value any] struct { type MemoryStore[Value any] struct {
Sessions map[string]Value Keys map[string]KeyValue[Value]
Lock sync.RWMutex Lock sync.RWMutex
ExpireTimes []ExpireTime
} }
func NewMemoryStore[Value any]() *MemoryStore[Value] { func NewMemoryStore[Value any]() *MemoryStore[Value] {
logging.Debug("Creating new in memory session store") logging.Debug("Creating new in memory session store")
store := &MemoryStore[Value]{ store := &MemoryStore[Value]{
Sessions: make(map[string]Value), Keys: make(map[string]KeyValue[Value]),
Lock: sync.RWMutex{},
ExpireTimes: []ExpireTime{},
} }
worker.CreateWorker(time.Minute*5, store.CleanupExpired)
return store return store
} }
func (m *MemoryStore[Value]) Create(key string, session Value) (err error) { func (m *MemoryStore[Value]) CreateExpiring(key string, value Value, duration time.Duration) error {
hashedkey := HashKey(key) var expiry time.Time
if duration != 0 {
expiry = time.Now().Add(duration)
}
m.Lock.Lock() m.Lock.Lock()
defer m.Lock.Unlock() defer m.Lock.Unlock()
_, exist := m.Sessions[hashedkey]
hashedkey := HashKey(key)
_, exist := m.Keys[hashedkey]
if exist { if exist {
return ErrKeyAlreadyExists return ErrKeyAlreadyExists
} }
m.Sessions[hashedkey] = session m.Keys[hashedkey] = KeyValue[Value]{
value: value,
expireTime: expiry,
}
if duration != 0 {
low := 0
high := len(m.ExpireTimes)
for low < high {
mid := (low + high) / 2
if m.ExpireTimes[mid].expiryTime.Before(expiry) {
low = mid + 1
} else {
high = mid
}
}
insertIndex := low
m.ExpireTimes = append(m.ExpireTimes, ExpireTime{})
copy(m.ExpireTimes[insertIndex+1:], m.ExpireTimes[insertIndex:])
m.ExpireTimes[insertIndex] = ExpireTime{
key: key,
expiryTime: expiry,
}
}
return nil return nil
} }
func (m *MemoryStore[Value]) CleanupExpired() {
m.Lock.Lock()
defer m.Lock.Unlock()
now := time.Now()
for len(m.ExpireTimes) > 0 && !now.Before(m.ExpireTimes[0].expiryTime) {
key, exists := m.Keys[HashKey(m.ExpireTimes[0].key)]
if exists && key.expireTime.Equal(m.ExpireTimes[0].expiryTime) {
m.deleteWithoutLock(m.ExpireTimes[0].key)
}
m.ExpireTimes = m.ExpireTimes[1:]
}
}
func (m *MemoryStore[Value]) Create(key string, session Value) error {
return m.CreateExpiring(key, session, 0)
}
func (m *MemoryStore[Value]) Get(key string) (Value, error) { func (m *MemoryStore[Value]) Get(key string) (Value, error) {
var data Value var data KeyValue[Value]
var zeroValue Value
hashedkey := HashKey(key)
m.Lock.RLock() m.Lock.RLock()
hashedkey := HashKey(key) data, exists := m.Keys[hashedkey]
data, exists := m.Sessions[hashedkey]
m.Lock.RUnlock() m.Lock.RUnlock()
if exists == false {
return data, ErrKeyNotFound if !exists {
return zeroValue, ErrKeyNotFound
} }
return data, nil
if !data.expireTime.IsZero() && !time.Now().Before(data.expireTime) {
m.Lock.Lock()
if current, ok := m.Keys[hashedkey]; ok && current.expireTime.Equal(data.expireTime) {
delete(m.Keys, hashedkey)
}
m.Lock.Unlock()
return zeroValue, ErrKeyNotFound
}
return data.value, nil
} }
func (m *MemoryStore[Value]) Update(sessionID string, session Value) error { func (m *MemoryStore[Value]) Update(sessionID string, value Value) error {
hashedkey := HashKey(sessionID) hashedkey := HashKey(sessionID)
m.Lock.Lock() m.Lock.Lock()
defer m.Lock.Unlock() defer m.Lock.Unlock()
_, exist := m.Sessions[hashedkey] old_key, exist := m.Keys[hashedkey]
if !exist { if !exist {
return ErrKeyNotFound return ErrKeyNotFound
} }
m.Sessions[hashedkey] = session m.Keys[hashedkey] = KeyValue[Value]{
value: value,
expireTime: old_key.expireTime,
}
return nil return nil
} }
func (m *MemoryStore[Value]) Delete(sessionID string) error { func (m *MemoryStore[Value]) deleteWithoutLock(key string) error {
hashedkey := HashKey(sessionID) hashedkey := HashKey(key)
_, exist := m.Keys[hashedkey]
if !exist {
return ErrKeyNotFound
}
delete(m.Keys, hashedkey)
return nil
}
func (m *MemoryStore[Value]) Delete(key string) error {
m.Lock.Lock() m.Lock.Lock()
defer m.Lock.Unlock() defer m.Lock.Unlock()
_, exist := m.Sessions[hashedkey] return m.deleteWithoutLock(key)
if !exist {
return ErrKeyNotFound
}
delete(m.Sessions, hashedkey)
return nil
} }
+5 -3
View File
@@ -43,8 +43,7 @@ func NewRedisStore[Value any](redis_server string, prefix string) *RedisStore[Va
} }
return store return store
} }
func (m *RedisStore[Value]) CreateExpiring(key string, value Value, ttl time.Duration) error {
func (m *RedisStore[Value]) Create(key string, value Value) (err error) {
hashedSession := m.RedisHash(key) hashedSession := m.RedisHash(key)
data, err := json.Marshal(value) data, err := json.Marshal(value)
@@ -52,7 +51,7 @@ func (m *RedisStore[Value]) Create(key string, value Value) (err error) {
return ErrKeyBackend return ErrKeyBackend
} }
created, err := m.client.SetNX(m.ctx, hashedSession, data, time.Hour).Result() created, err := m.client.SetNX(m.ctx, hashedSession, data, ttl).Result()
if err != nil { if err != nil {
logging.Error(err.Error()) logging.Error(err.Error())
return ErrKeyBackend return ErrKeyBackend
@@ -63,6 +62,9 @@ func (m *RedisStore[Value]) Create(key string, value Value) (err error) {
} }
return nil return nil
} }
func (m *RedisStore[Value]) Create(key string, value Value) error {
return m.CreateExpiring(key, value, 0)
}
func (m *RedisStore[Value]) Get(sessionID string) (Value, error) { func (m *RedisStore[Value]) Get(sessionID string) (Value, error) {
hashed := m.RedisHash(sessionID) hashed := m.RedisHash(sessionID)
var session_data Value var session_data Value
+1
View File
@@ -9,6 +9,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
overflow: hidden;
} }
.static_center { .static_center {
+3
View File
@@ -0,0 +1,3 @@
.cursor_grabbing {
cursor: grabbing;
}
+20
View File
@@ -0,0 +1,20 @@
var currentX, currentY;
document.addEventListener("mousemove", (e) => {
const rect = document.body.getBoundingClientRect();
currentX = e.clientX;
currentY = e.clientY;
});
const CURSOR_NORMAL = "cursor_normal";
const CURSOR_GRABBING = "cursor_grabbing";
document.body.classList.remove(CURSOR_NORMAL);
let current_cursor_state = CURSOR_NORMAL;
function set_cursor_state(new_cursor_state) {
document.body.classList.remove(current_cursor_state);
document.body.classList.add(new_cursor_state);
current_cursor_state = new_cursor_state;
}
@@ -0,0 +1,52 @@
const resize_photo_box = document.getElementById("resize_photo_box");
// Drag handling
var startXOffset = 0,
startYOffset = 0;
function calculateOffsets() {
const yOffset = startYOffset + (currentY - y_start);
const xOffset = startXOffset + (currentX - x_start);
var top = bitmap.height / 2 - yOffset - 175;
if (top > 0) top = 0;
var bottom = -(bitmap.height / 2) - yOffset + 10 + 175;
if (bottom < 0) bottom = 0;
var left = bitmap.width / 2 - xOffset - 175;
if (left > 0) left = 0;
var right = -(bitmap.width / 2) - xOffset + 10 + 175;
if (right < 0) right = 0;
return { x: xOffset + left + right, y: yOffset + top + bottom };
}
var mouseClicked = false;
resize_photo_box.addEventListener("mousemove", () => {
if (!mouseClicked) return;
offsets = calculateOffsets();
y_top = new_profile_image.style.setProperty("--x-offset", offsets.x + "px");
new_profile_image.style.setProperty("--y-offset", offsets.y + "px");
});
resize_photo_box.addEventListener("mousedown", () => {
x_start = currentX;
y_start = currentY;
mouseClicked = true;
set_cursor_state(CURSOR_GRABBING);
});
resize_photo_box.addEventListener("mouseup", () => {
offsets = calculateOffsets();
startXOffset = offsets.x;
startYOffset = offsets.y;
});
document.body.addEventListener("mouseup", () => {
mouseClicked = false;
set_cursor_state(CURSOR_NORMAL);
});
+71
View File
@@ -116,6 +116,10 @@
position: fixed; position: fixed;
} }
#resize_photo_dialouge {
z-index: 10000;
}
#change_password_dialogue input { #change_password_dialogue input {
width: 350px; width: 350px;
} }
@@ -149,3 +153,70 @@
border-style: solid; border-style: solid;
color: var(--text-main); color: var(--text-main);
} }
#image_container {
position: absolute;
}
#darken_part {
/*background-color: black;*/
z-index: 3;
width: 100vw;
height: 100vh;
}
#confirm_change {
z-index: 2;
}
#new_profile_image {
z-index: 1;
transform: translateX(calc(-50% + var(--x-offset)))
translateY(calc(-50% + var(--y-offset)));
position: absolute;
left: calc(350px / 2);
top: calc(350px / 2);
}
#resize_photo_box {
background-color: rgba(0, 0, 0, 0);
width: 350px;
aspect-ratio: 1 / 1;
border-style: dashed;
border-color: black;
border-width: 5px;
z-index: 4;
}
#resize_photo_box:hover {
background-color: rgba(0, 0, 0, 0);
width: 350px;
aspect-ratio: 1 / 1;
border-style: dashed;
border-color: black;
border-width: 5px;
z-index: 4;
}
/*
#resize_photo_box:hover {
cursor: grab;
}
#new_profile_image {
height: 350px;
position: absolute;
transform: translateX(5px) translateY(5px);
object-fit: cover;
}
#image_container {
height: 350px;
width: fit-content;
position: absolute;
background-color: red;
}*/