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

219
script.js
View File

@@ -1,4 +1,30 @@
document.addEventListener('DOMContentLoaded', () => { 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) --- // --- INDEXED DB HELPERS (Multi-Media Support) ---
const DB_NAME = 'SocialAppDB'; const DB_NAME = 'SocialAppDB';
const DB_VERSION = 1; const DB_VERSION = 1;
@@ -106,24 +132,44 @@ document.addEventListener('DOMContentLoaded', () => {
// Lightbox Elements // Lightbox Elements
const lightboxView = document.getElementById('lightbox-view'); const lightboxView = document.getElementById('lightbox-view');
const lightboxClose = document.getElementById('lightbox-close'); const lightboxClose = document.getElementById('lightbox-close');
const lightboxCarousel = document.querySelector('.lightbox-carousel'); const lightboxCarousel = lightboxView?.querySelector('.lightbox-carousel');
// State
let isLoginMode = true; let isLoginMode = true;
let pendingUser = null; let pendingUser = null;
let generatedOTP = null; let generatedOTP = null;
let newAvatarBase64 = null; let newAvatarBase64 = null;
// --- CHECK SESSION & LOGOUT --- // --- CHECK SESSION & LOGOUT ---
function checkSession() { async function checkSession() {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}'); const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
if (currentUser.email) { if (currentUser.email) {
if (loginView) { try {
loginView.classList.add('hidden'); // Verify user actually exists in DB (fixes "Ghost User" after DB wipe)
loginView.style.display = 'none'; const users = await API.getUsers();
} const validUser = users.find(u => u.email === currentUser.email);
if (feedView) {
feedView.classList.remove('hidden'); if (!validUser) {
initFeed(); 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 --- // --- ONBOARDING LOGIC ---
// Avatar Upload Handler
if (avatarTrigger && avatarInput) { if (avatarTrigger && avatarInput) {
avatarTrigger.addEventListener('click', () => avatarInput.click()); avatarTrigger.addEventListener('click', () => avatarInput.click());
avatarInput.addEventListener('change', (e) => { avatarInput.addEventListener('change', (e) => {
@@ -193,11 +238,9 @@ document.addEventListener('DOMContentLoaded', () => {
avatar: newAvatarBase64 || 'https://i.pravatar.cc/150?img=12' avatar: newAvatarBase64 || 'https://i.pravatar.cc/150?img=12'
}; };
// Save to DB // --- ASYNC SAVE ---
const existingUsers = JSON.parse(localStorage.getItem('socialAppUsers') || '[]'); await API.addUser(finalUser);
existingUsers.push(finalUser); localStorage.setItem('currentUser', JSON.stringify(finalUser)); // Session persistence
localStorage.setItem('socialAppUsers', JSON.stringify(existingUsers));
localStorage.setItem('currentUser', JSON.stringify(finalUser)); // Log them in
// Transition // Transition
onboardingView.classList.add('hidden'); onboardingView.classList.add('hidden');
@@ -206,7 +249,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFeed(); initFeed();
} catch (err) { } catch (err) {
console.error('Setup Error:', 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) { if (loginForm) {
loginForm.addEventListener('submit', (e) => { loginForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const email = emailInput.value.trim(); const email = emailInput.value.trim().toLowerCase();
const password = passwordInput.value.trim(); const password = passwordInput.value.trim();
const confirmPassword = confirmPasswordInput.value.trim(); const confirmPassword = confirmPasswordInput.value.trim();
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const existingUsers = JSON.parse(localStorage.getItem('socialAppUsers') || '[]');
if (isLoginMode) { if (!email || !password) { showFeedback(false, 'Missing fields'); return; }
const user = existingUsers.find(u => u.email === email && u.password === password);
if (user) { try {
localStorage.setItem('currentUser', JSON.stringify(user)); const users = await API.getUsers();
showFeedback(true, 'Success!');
setTimeout(navigateToFeed, 1000); 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 { } else {
showFeedback(false, 'Incorrect details'); if (users.find(u => u.email === email)) {
} showFeedback(false, 'User exists');
} else { return;
if (existingUsers.some(u => u.email === email)) { }
showFeedback(false, 'User exists'); if (password !== confirmPassword) {
return; showFeedback(false, 'Passwords mismatch');
} return;
if (password !== confirmPassword) { }
showFeedback(false, 'Passwords do not match');
return;
}
// Start Verification Flow // Start Verification Flow
pendingUser = { email, password, joinedAt: timestamp }; pendingUser = { email, password, joinedAt: timestamp };
generatedOTP = Math.floor(100000 + Math.random() * 900000).toString(); 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');
loginView.classList.add('hidden'); verificationView.classList.add('fade-in');
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 savePostState(state) { localStorage.setItem('socialAppPostState', JSON.stringify(state)); }
function isPostDeleted(id) { const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]'); return deletedPosts.includes(id); } function isPostDeleted(id) { const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]'); return deletedPosts.includes(id); }
// --- ASYNC LOAD FEED ---
// --- ASYNC LOAD FEED --- // --- ASYNC LOAD FEED ---
async function initFeed() { async function initFeed() {
const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); const container = document.querySelector('.posts-container');
const postsToRender = [...savedUserPosts].reverse(); if (!container) return;
container.innerHTML = '';
for (const post of postsToRender) { try {
if (!isPostDeleted(post.id) && !document.querySelector(`[data-post-id="${post.id}"]`)) { const posts = await API.getPosts();
let mediaItems = post.media; if (posts.length === 0) {
if (!mediaItems && post.imageSrc) { container.innerHTML = '<div style="text-align:center; padding:40px; color:#666;">No posts yet. Be the first!</div>';
mediaItems = [{ type: 'image', src: post.imageSrc }]; return;
}
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);
}
} }
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 --- // --- RENDER POST ---
function renderPost(post, prepend = false) { function renderPost(post, prepend = false) {
if (document.querySelector(`[data-post-id="${post.id}"]`)) return; 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...")