AccountsAndCrossPlatformLogin

This commit is contained in:
2026-01-30 20:52:12 +11:00
parent 39a7c392fb
commit 430eef5f23
4 changed files with 234 additions and 108 deletions

13
database.json Normal file
View File

@@ -0,0 +1,13 @@
{
"users": [
{
"email": "test@test",
"password": "test",
"joinedAt": "2026-01-30T09:47:19.314Z",
"username": "Test",
"bio": "test",
"avatar": "https://i.pravatar.cc/150?img=12"
}
],
"posts": []
}

View File

@@ -321,7 +321,7 @@
<div class="media-counter" style="bottom: 40px; top: auto;"></div> <!-- Re-use styling -->
</div>
<script src="script.js"></script>
<script src="script.js?v=2.0"></script>
</body>
</html>

219
script.js
View File

@@ -1,4 +1,30 @@
document.addEventListener('DOMContentLoaded', () => {
// --- API CONFIG ---
const API = {
async getUsers() {
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`Server Error: ${res.status}`);
return await res.json();
},
async addUser(user) {
await fetch('/api/users/add', { method: 'POST', body: JSON.stringify(user) });
},
async getPosts() {
const res = await fetch('/api/posts');
if (!res.ok) throw new Error(`Server Error: ${res.status}`);
return await res.json();
},
async addPost(post) {
await fetch('/api/posts/add', { method: 'POST', body: JSON.stringify(post) });
},
async updatePost(post) {
await fetch('/api/posts/update', { method: 'POST', body: JSON.stringify(post) });
},
async deletePost(id) {
await fetch('/api/posts/delete', { method: 'POST', body: JSON.stringify({ id }) });
}
};
// --- INDEXED DB HELPERS (Multi-Media Support) ---
const DB_NAME = 'SocialAppDB';
const DB_VERSION = 1;
@@ -106,24 +132,44 @@ document.addEventListener('DOMContentLoaded', () => {
// Lightbox Elements
const lightboxView = document.getElementById('lightbox-view');
const lightboxClose = document.getElementById('lightbox-close');
const lightboxCarousel = document.querySelector('.lightbox-carousel');
const lightboxCarousel = lightboxView?.querySelector('.lightbox-carousel');
// State
let isLoginMode = true;
let pendingUser = null;
let generatedOTP = null;
let newAvatarBase64 = null;
// --- CHECK SESSION & LOGOUT ---
function checkSession() {
async function checkSession() {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
if (currentUser.email) {
if (loginView) {
loginView.classList.add('hidden');
loginView.style.display = 'none';
}
if (feedView) {
feedView.classList.remove('hidden');
initFeed();
try {
// Verify user actually exists in DB (fixes "Ghost User" after DB wipe)
const users = await API.getUsers();
const validUser = users.find(u => u.email === currentUser.email);
if (!validUser) {
console.log('Session invalid: User not found on server.');
localStorage.removeItem('currentUser');
if (loginView && !loginView.classList.contains('hidden')) return; // Already on login
location.reload();
return;
}
if (loginView) {
loginView.classList.add('hidden');
loginView.style.display = 'none';
}
if (feedView) {
feedView.classList.remove('hidden');
initFeed();
}
} catch (err) {
console.warn('Session check failed:', err);
alert('Connection to Server Failed.\n\nReason: ' + err.message + '\n\nLogging out for security.');
localStorage.removeItem('currentUser');
location.reload();
}
}
}
@@ -140,7 +186,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
// --- ONBOARDING LOGIC ---
// Avatar Upload Handler
if (avatarTrigger && avatarInput) {
avatarTrigger.addEventListener('click', () => avatarInput.click());
avatarInput.addEventListener('change', (e) => {
@@ -193,11 +238,9 @@ document.addEventListener('DOMContentLoaded', () => {
avatar: newAvatarBase64 || 'https://i.pravatar.cc/150?img=12'
};
// Save to DB
const existingUsers = JSON.parse(localStorage.getItem('socialAppUsers') || '[]');
existingUsers.push(finalUser);
localStorage.setItem('socialAppUsers', JSON.stringify(existingUsers));
localStorage.setItem('currentUser', JSON.stringify(finalUser)); // Log them in
// --- ASYNC SAVE ---
await API.addUser(finalUser);
localStorage.setItem('currentUser', JSON.stringify(finalUser)); // Session persistence
// Transition
onboardingView.classList.add('hidden');
@@ -206,7 +249,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFeed();
} catch (err) {
console.error('Setup Error:', err);
alert('Something went wrong during setup: ' + err.message);
alert('Connection Error: ' + err.message);
}
});
}
@@ -334,42 +377,49 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (loginForm) {
loginForm.addEventListener('submit', (e) => {
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const email = emailInput.value.trim();
const email = emailInput.value.trim().toLowerCase();
const password = passwordInput.value.trim();
const confirmPassword = confirmPasswordInput.value.trim();
const timestamp = new Date().toISOString();
const existingUsers = JSON.parse(localStorage.getItem('socialAppUsers') || '[]');
if (isLoginMode) {
const user = existingUsers.find(u => u.email === email && u.password === password);
if (user) {
localStorage.setItem('currentUser', JSON.stringify(user));
showFeedback(true, 'Success!');
setTimeout(navigateToFeed, 1000);
if (!email || !password) { showFeedback(false, 'Missing fields'); return; }
try {
const users = await API.getUsers();
if (isLoginMode) {
const user = users.find(u => u.email === email && u.password === password);
if (user) {
localStorage.setItem('currentUser', JSON.stringify(user));
showFeedback(true, 'Success!');
setTimeout(navigateToFeed, 1000);
} else {
showFeedback(false, 'Incorrect details');
}
} else {
showFeedback(false, 'Incorrect details');
}
} else {
if (existingUsers.some(u => u.email === email)) {
showFeedback(false, 'User exists');
return;
}
if (password !== confirmPassword) {
showFeedback(false, 'Passwords do not match');
return;
}
if (users.find(u => u.email === email)) {
showFeedback(false, 'User exists');
return;
}
if (password !== confirmPassword) {
showFeedback(false, 'Passwords mismatch');
return;
}
// Start Verification Flow
pendingUser = { email, password, joinedAt: timestamp };
generatedOTP = Math.floor(100000 + Math.random() * 900000).toString();
// Start Verification Flow
pendingUser = { email, password, joinedAt: timestamp };
generatedOTP = Math.floor(100000 + Math.random() * 900000).toString();
alert(`[SERVER SYNC] Verification code: ${generatedOTP}`);
alert(`[MOCK EMAIL] Your verification code is: ${generatedOTP}`);
loginView.classList.add('hidden');
verificationView.classList.remove('hidden');
verificationView.classList.add('fade-in');
loginView.classList.add('hidden');
verificationView.classList.remove('hidden');
verificationView.classList.add('fade-in');
}
} catch (err) {
console.error(err);
showFeedback(false, 'Server Error');
}
});
}
@@ -379,76 +429,31 @@ document.addEventListener('DOMContentLoaded', () => {
function savePostState(state) { localStorage.setItem('socialAppPostState', JSON.stringify(state)); }
function isPostDeleted(id) { const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]'); return deletedPosts.includes(id); }
// --- ASYNC LOAD FEED ---
// --- ASYNC LOAD FEED ---
async function initFeed() {
const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]');
const postsToRender = [...savedUserPosts].reverse();
const container = document.querySelector('.posts-container');
if (!container) return;
container.innerHTML = '';
for (const post of postsToRender) {
if (!isPostDeleted(post.id) && !document.querySelector(`[data-post-id="${post.id}"]`)) {
let mediaItems = post.media;
if (!mediaItems && post.imageSrc) {
mediaItems = [{ type: 'image', src: post.imageSrc }];
}
if (!mediaItems || (mediaItems.length > 0 && mediaItems[0].src && mediaItems[0].src.length < 100)) {
const dbMedia = await getMediaFromDB(post.id);
if (dbMedia) mediaItems = dbMedia;
}
if (mediaItems && mediaItems.length > 0) {
const postWithMedia = { ...post, media: mediaItems };
renderPost(postWithMedia, true);
}
try {
const posts = await API.getPosts();
if (posts.length === 0) {
container.innerHTML = '<div style="text-align:center; padding:40px; color:#666;">No posts yet. Be the first!</div>';
return;
}
for (const post of posts) {
renderPost(post, false);
}
} catch (err) {
console.error('Feed Error:', err);
container.innerHTML = '<div style="text-align:center; color:red;">Connection Failed</div>';
}
// Restore State
const state = getPostState();
document.querySelectorAll('.post-card').forEach(card => {
const id = card.getAttribute('data-post-id');
if (id && isPostDeleted(id)) { card.remove(); return; }
if (id && state[id]) {
const data = state[id];
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
const userEmail = (currentUser.email || '').toLowerCase(); // Fix missing email check
const likeIcon = card.querySelector('.heart-icon');
const likesText = card.querySelector('.likes');
if (likeIcon && likesText) {
// Check if current user is in `likedBy` array
let likedBy = data.likedBy || [];
// Migration: if old `liked` boolean exists, legacy support (or reset)
if (data.liked && likedBy.length === 0) { likedBy = []; }
if (likedBy.includes(userEmail)) {
likeIcon.classList.add('liked');
likeIcon.style.fill = '#EF4444';
likeIcon.style.stroke = '#EF4444';
}
likesText.innerText = (data.likesCount || likedBy.length) + ' likes';
}
const commentsContainer = card.querySelector('.added-comments');
if (commentsContainer) {
commentsContainer.innerHTML = '';
if (data.comments) {
data.comments.forEach(comment => {
const el = document.createElement('div');
el.classList.add('comment-item');
// Handle both legacy strings and new objects
const author = (typeof comment === 'object') ? comment.username : 'someone';
const text = (typeof comment === 'object') ? comment.text : comment;
// Highlight own comments
const displayUser = ((typeof comment === 'object') && comment.email === userEmail) ? 'you' : author;
el.innerHTML = `<span class="comment-user">${displayUser}</span> ${text}`;
commentsContainer.appendChild(el);
});
}
}
}
});
}
// --- RENDER POST ---
function renderPost(post, prepend = false) {
if (document.querySelector(`[data-post-id="${post.id}"]`)) return;

108
server.py Normal file
View File

@@ -0,0 +1,108 @@
import http.server
import socketserver
import json
import os
import sys
from urllib.parse import urlparse, parse_qs
PORT = 8000
DB_FILE = 'database.json'
def load_db():
if not os.path.exists(DB_FILE):
return {"users": [], "posts": []}
try:
with open(DB_FILE, 'r') as f:
return json.load(f)
except:
return {"users": [], "posts": []}
def save_db(data):
with open(DB_FILE, 'w') as f:
json.dump(data, f, indent=2)
class CustomHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == '/api/users':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
self.end_headers()
db = load_db()
self.wfile.write(json.dumps(db.get("users", [])).encode())
return
elif parsed.path == '/api/posts':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
self.end_headers()
db = load_db()
self.wfile.write(json.dumps(db.get("posts", [])).encode())
return
# Default behavior (serve files)
super().do_GET()
def do_POST(self):
parsed = urlparse(self.path)
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode('utf-8')
try:
payload = json.loads(body)
except:
payload = None
if parsed.path == '/api/users/add':
db = load_db()
db["users"].append(payload)
save_db(db)
self.success(payload)
return
elif parsed.path == '/api/posts/add':
db = load_db()
# New posts go to top
db["posts"].insert(0, payload)
save_db(db)
self.success(payload)
return
elif parsed.path == '/api/posts/update':
# Expects full post object, finds by ID and replaces
db = load_db()
posts = db.get("posts", [])
for i, p in enumerate(posts):
if p["id"] == payload["id"]:
posts[i] = payload
break
db["posts"] = posts
save_db(db)
self.success(payload)
return
elif parsed.path == '/api/posts/delete':
# Expects { "id": "..." }
db = load_db()
posts = db.get("posts", [])
db["posts"] = [p for p in posts if p["id"] != payload["id"]]
save_db(db)
self.success({"deleted": True})
return
self.send_error(404, "API Endpoint not found")
def success(self, data):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
# Support binding to all interfaces
Handler = CustomHandler
with socketserver.TCPServer(("0.0.0.0", PORT), Handler) as httpd:
print(f"Serving at http://0.0.0.0:{PORT}")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nStopping server...")