diff --git a/.gitignore b/.gitignore index 80d73fb..17ded36 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.pyc db.sqlite3 **/migrations/** +/profiles/static/avatars/* +!/profiles/static/avatars/default.env diff --git a/accounts/tests/__init__.py b/accounts/tests/__init__.py index 4187043..80d6cf2 100644 --- a/accounts/tests/__init__.py +++ b/accounts/tests/__init__.py @@ -1,4 +1,5 @@ from .register import * from .login import * +from .logout import * from .edit import * from .delete import * \ No newline at end of file diff --git a/accounts/tests/delete.py b/accounts/tests/delete.py index f7cfc47..438d6d1 100644 --- a/accounts/tests/delete.py +++ b/accounts/tests/delete.py @@ -21,12 +21,17 @@ class DeleteTest(TestCase): def test_normal_delete(self): - response: HttpResponse = self.client.delete(self.url) + response: HttpResponse = self.client.delete(self.url, {"password": self.password}, content_type='application/json') response_text: str = response.content.decode("utf-8") self.assertEqual(response_text, '"user deleted"') + def test_wrong_pass(self): + response: HttpResponse = self.client.delete(self.url, {"password": "cacaman a frapper"}, content_type='application/json') + errors: dict = eval(response.content) + self.assertDictEqual(errors, {"password": ["Password wrong."]}) + def test_no_logged(self): self.client.logout() - response: HttpResponse = self.client.post(self.url) + response: HttpResponse = self.client.delete(self.url, {"password": self.password}, content_type='application/json') errors: dict = eval(response.content) self.assertDictEqual(errors, {"detail":"Authentication credentials were not provided."}) \ No newline at end of file diff --git a/accounts/views/delete.py b/accounts/views/delete.py index 0a3265c..30b4c7c 100644 --- a/accounts/views/delete.py +++ b/accounts/views/delete.py @@ -8,5 +8,12 @@ class DeleteView(APIView): permission_classes = (permissions.IsAuthenticated,) authentication_classes = (SessionAuthentication,) def delete(self, request: HttpRequest): + data: dict = request.data + + password: str = data["password"] + if (password is None): + return Response({"password": ["This field may not be blank."]}) + if (request.user.check_password(password) == False): + return Response({"password": ["Password wrong."]}) request.user.delete() return Response("user deleted", status=status.HTTP_200_OK) \ No newline at end of file diff --git a/accounts/views/edit.py b/accounts/views/edit.py index 8325b06..c62a201 100644 --- a/accounts/views/edit.py +++ b/accounts/views/edit.py @@ -12,6 +12,9 @@ class EditView(APIView): permission_classes = (permissions.IsAuthenticated,) authentication_classes = (SessionAuthentication,) + def get(self, request: HttpRequest): + return Response({"username": request.user.username, "id": request.user.pk}) + def patch(self, request: HttpRequest): data: dict = request.data diff --git a/accounts/views/logged.py b/accounts/views/logged.py index 3956cd2..8e2fecb 100644 --- a/accounts/views/logged.py +++ b/accounts/views/logged.py @@ -13,4 +13,6 @@ class LoggedView(APIView): authentication_classes = (SessionAuthentication,) def get(self, request: HttpRequest): - return Response(str(request.user.is_authenticated), status=status.HTTP_200_OK) \ No newline at end of file + if (request.user.is_authenticated): + return Response({'id': request.user.pk}, status=status.HTTP_200_OK) + return Response('false', status=status.HTTP_200_OK) \ No newline at end of file diff --git a/accounts/views/login.py b/accounts/views/login.py index 1ee192f..490f79b 100644 --- a/accounts/views/login.py +++ b/accounts/views/login.py @@ -20,4 +20,4 @@ class LoginView(APIView): if user is None: return Response({'user': ['Username or password wrong.']}, status.HTTP_200_OK) login(request, user) - return Response('user connected', status=status.HTTP_200_OK) \ No newline at end of file + return Response({'id': user.pk}, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/accounts/views/register.py b/accounts/views/register.py index 5bdacc2..ee5724a 100644 --- a/accounts/views/register.py +++ b/accounts/views/register.py @@ -3,6 +3,7 @@ from ..serializers.register import RegisterSerialiser from rest_framework.views import APIView from rest_framework.response import Response from django.http import HttpRequest +from django.contrib.auth import login class RegisterView(APIView): permission_classes = (permissions.AllowAny,) @@ -12,5 +13,6 @@ class RegisterView(APIView): if serializer.is_valid(raise_exception=True): user = serializer.create(data) if user: + login(request, user) return Response("user created", status=status.HTTP_201_CREATED) return Response(status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/frontend/static/css/profiles/profile.css b/frontend/static/css/profiles/profile.css new file mode 100644 index 0000000..559ea88 --- /dev/null +++ b/frontend/static/css/profiles/profile.css @@ -0,0 +1,11 @@ +#app #avatar +{ + height: 100px; + width: 100px; +} + +#app #username +{ + height: 100px; + width: 100px; +} \ No newline at end of file diff --git a/frontend/static/css/profiles/profiles.css b/frontend/static/css/profiles/profiles.css new file mode 100644 index 0000000..7002c0f --- /dev/null +++ b/frontend/static/css/profiles/profiles.css @@ -0,0 +1,11 @@ +#app .item img +{ + height: 100px; + width: 100px; +} + +#app .item a +{ + height: 100px; + width: 100px; +} \ No newline at end of file diff --git a/frontend/static/js/api/account.js b/frontend/static/js/api/account.js new file mode 100644 index 0000000..1004c1c --- /dev/null +++ b/frontend/static/js/api/account.js @@ -0,0 +1,65 @@ +class Account +{ + constructor (client) + { + this.client = client; + } + + async create(username, password) + { + let response = await this.client._post("/api/accounts/register", {username: username, password: password}); + let response_data = await response.json() + + if (response_data == "user created") + { + this._logged = true; + return null; + } + return response_data + } + + async delete(password) + { + let response = await this.client._delete("/api/accounts/delete", {password: password}); + let response_data = await response.json(); + + if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'})) + { + this.client._logged = false; + return null; + } + console.log(response_data) + if (response_data == "user deleted") + this.client._logged = false; + return response_data; + } + + async get() + { + let response = await this.client._get("/api/accounts/edit"); + let response_data = await response.json(); + + if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'})) + { + this.client._logged = false; + return null; + } + return response_data; + } + + async update(data, password) + { + data.current_password = password; + let response = await this.client._patch_json("/api/accounts/edit", data); + let response_data = await response.json(); + + if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'})) + { + this.client._logged = false; + return null; + } + return response_data; + } +} + +export { Account } \ No newline at end of file diff --git a/frontend/static/js/api/accounts.js b/frontend/static/js/api/accounts.js deleted file mode 100644 index 154a563..0000000 --- a/frontend/static/js/api/accounts.js +++ /dev/null @@ -1,15 +0,0 @@ -class Accounts -{ - constructor (client) - { - this.client = client; - } - - async create(username, password) - { - let response = await this.client._post("/api/accounts/register", {username: username, password: password}); - return response - } -} - -export { Accounts } \ No newline at end of file diff --git a/frontend/static/js/api/client.js b/frontend/static/js/api/client.js index 37c82d6..90fc88e 100644 --- a/frontend/static/js/api/client.js +++ b/frontend/static/js/api/client.js @@ -1,11 +1,24 @@ -import { Accounts } from "./accounts.js"; +import { Account } from "./account.js"; +import { Profile } from "./profile.js"; +import { Profiles } from "./profiles.js"; + +function getCookie(name) +{ + let cookie = {}; + document.cookie.split(';').forEach(function(el) { + let split = el.split('='); + cookie[split[0].trim()] = split.slice(1).join("="); + }) + return cookie[name]; +} class Client { constructor(url) { this._url = url; - this.accounts = new Accounts(this); + this.account = new Account(this); + this.profiles = new Profiles(this); this._logged = undefined; } @@ -30,18 +43,59 @@ class Client method: "POST", headers: { "Content-Type": "application/json", + "X-CSRFToken": getCookie("csrftoken"), }, body: JSON.stringify(data), }); return response; } + async _delete(uri, data) + { + let response = await fetch(this._url + uri, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCookie("csrftoken"), + }, + body: JSON.stringify(data), + }); + return response; + } + + async _patch_json(uri, data) + { + let response = await fetch(this._url + uri, { + method: "PATCH", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + return response; + } + + async _patch_file(uri, file) + { + let response = await fetch(this._url + uri, { + method: "PATCH", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + }, + body: file, + }); + return response; + } + async login(username, password) { let response = await this._post("/api/accounts/login", {username: username, password: password}) let data = await response.json(); - if (data == "user connected") + if (data.id != undefined) { + this.me = new Profile(this) + await this.me.init(data.id) this.logged = true; return null; } @@ -58,7 +112,13 @@ class Client { let response = await this._get("/api/accounts/logged"); let data = await response.json(); - return data === "True"; + + if (data.id !== undefined) + { + this.me = new Profile(this) + await this.me.init(data.id) + } + return data.id !== undefined; } } diff --git a/frontend/static/js/api/profile.js b/frontend/static/js/api/profile.js new file mode 100644 index 0000000..4ecdab9 --- /dev/null +++ b/frontend/static/js/api/profile.js @@ -0,0 +1,35 @@ +class Profile +{ + constructor (client, username = undefined, avatar_url = undefined, user_id = undefined) + { + this.client = client; + this.username = username; + this.avatar_url = avatar_url + this.user_id = user_id + } + + async init(id) + { + let response = await this.client._get(`/api/profiles/${id}`); + let response_data = await response.json(); + + this.id = id; + this.username = response_data.username; + this.avatar_url = response_data.avatar_url; + } + + async change_avatar(form_data) + { + let response = await this.client._patch_file(`/api/profiles/${this.id}`, form_data); + let response_data = await response.json() + + return response_data; + } + + async setData (data) + { + + } +} + +export {Profile} \ No newline at end of file diff --git a/frontend/static/js/api/profiles.js b/frontend/static/js/api/profiles.js new file mode 100644 index 0000000..c439abc --- /dev/null +++ b/frontend/static/js/api/profiles.js @@ -0,0 +1,30 @@ +import { Profile } from "./profile.js"; + +class Profiles +{ + constructor (client) + { + this.client = client + } + + async all() + { + let response = await this.client._get("/api/profiles/"); + let response_data = await response.json(); + + let profiles = [] + response_data.forEach((profile) => { + profiles.push(new Profile(this.client, profile.username, profile.avatar_url, profile.user_id)) + }); + return profiles; + } + + async getProfile(user_id) + { + let profile = new Profile(this.client); + await profile.init(user_id); + return profile; + } +} + +export {Profiles} \ No newline at end of file diff --git a/frontend/static/js/index.js b/frontend/static/js/index.js index d5bef31..e3cf98c 100644 --- a/frontend/static/js/index.js +++ b/frontend/static/js/index.js @@ -1,7 +1,5 @@ import LoginView from "./views/accounts/LoginView.js"; import Dashboard from "./views/Dashboard.js"; -import Posts from "./views/Posts.js"; -import PostView from "./views/PostView.js"; import Settings from "./views/Settings.js"; import Search from "./views/Search.js"; import Chat from "./views/Chat.js"; @@ -11,6 +9,9 @@ import LogoutView from "./views/accounts/LogoutView.js"; import { Client } from "./api/client.js"; import AbstractRedirectView from "./views/AbstractRedirectView.js"; +import MeView from "./views/MeView.js"; +import ProfilePageView from "./views/profiles/ProfilePageView.js"; +import ProfilesView from "./views/profiles/ProfilesView.js"; let client = new Client(location.protocol + "//" + location.host) @@ -35,8 +36,8 @@ const navigateTo = async (uri) => { const router = async (uri = "") => { const routes = [ { path: "/", view: Dashboard }, - { path: "/posts", view: Posts }, - { path: "/posts/:id", view: PostView }, + { path: "/profiles", view: ProfilesView}, + { path: "/profiles/:id", view: ProfilePageView }, { path: "/settings", view: Settings }, { path: "/login", view: LoginView }, { path: "/logout", view: LogoutView }, @@ -44,6 +45,7 @@ const router = async (uri = "") => { { path: "/search", view: Search }, { path: "/chat", view: Chat }, { path: "/home", view: HomeView }, + { path: "/me", view: MeView }, ]; // Test each route for potential match diff --git a/frontend/static/js/views/HomeView.js b/frontend/static/js/views/HomeView.js index 5f9f3fb..d9fd194 100644 --- a/frontend/static/js/views/HomeView.js +++ b/frontend/static/js/views/HomeView.js @@ -9,6 +9,7 @@ export default class extends AbstractAuthentificateView { async getHtml() { return `

HOME

+ Me Logout `; } diff --git a/frontend/static/js/views/MeView.js b/frontend/static/js/views/MeView.js new file mode 100644 index 0000000..8ae6378 --- /dev/null +++ b/frontend/static/js/views/MeView.js @@ -0,0 +1,126 @@ +import { client, navigateTo } from "../index.js"; +import AbstractAuthentificateView from "./AbstractAuthentifiedView.js"; + +export default class extends AbstractAuthentificateView +{ + constructor(params) + { + super(params, "Me"); + } + + async postInit() + { + if (this.fill() === null) + return; + document.getElementById("save-button").onclick = this.save; + document.getElementById("delete-button").onclick = this.delete_accounts; + } + + async fill() + { + let data = await client.account.get(); + + if (data === null) + { + navigateTo("/login") + return; + } + document.getElementById("username").value = data.username; + } + + async delete_accounts() + { + let current_password = document.getElementById("current_password").value; + + let response_data = await client.account.delete(current_password); + + if (response_data === null) + { + navigateTo("/login"); + return; + } + + ["delete", "current_password"].forEach(error_field => { + let error_display = document.getElementById(`error_${error_field}`); + if (error_display != null) + error_display.innerHTML = ""; + }); + + if (response_data === "user deleted") + { + document.getElementById(`error_delete`).innerHTML = "OK"; + navigateTo("/login") + return; + } + + document.getElementById("error_current_password").innerHTML = response_data["password"] + } + + async save() + { + let username = document.getElementById("username").value; + let new_password = document.getElementById("new_password").value; + let current_password = document.getElementById("current_password").value; + + let data = {}; + + data.username = username; + if (new_password.length != 0) + data.new_password = new_password; + let response_data = await client.account.update(data, current_password); + + if (response_data === null) + { + navigateTo("/login"); + return; + } + else if (response_data === "data has been alterate") + { + navigateTo("/me"); + return; + } + + ["username", "new_password", "current_password"].forEach(error_field => { + let error_display = document.getElementById(`error_${error_field}`); + if (error_display != null) + error_display.innerHTML = ""; + }); + + Object.keys(response_data).forEach(error_field => { + let error_display = document.getElementById(`error_${error_field}`); + if (error_display != null) + error_display.innerHTML = response_data[error_field]; + }); + let avatar = document.getElementById("avatar"); + + if (avatar.files[0] !== undefined) + { + let form_data = new FormData(); + form_data.append("file", avatar.files[0]); + await client.me.change_avatar(form_data) + } + } + + async getHtml() + { + return ` +

ME

+
+ + + + + + +
+
+ +
+ + + + + Logout + `; + } +} \ No newline at end of file diff --git a/frontend/static/js/views/PostView.js b/frontend/static/js/views/PostView.js deleted file mode 100644 index d5f01bf..0000000 --- a/frontend/static/js/views/PostView.js +++ /dev/null @@ -1,15 +0,0 @@ -import AbstractView from "./AbstractView.js"; - -export default class extends AbstractView { - constructor(params) { - super(params, "Viewing Post"); - this.postId = params.id; - } - - async getHtml() { - return ` -

Post

-

You are viewing post #${this.postId}.

- `; - } -} diff --git a/frontend/static/js/views/Posts.js b/frontend/static/js/views/Posts.js deleted file mode 100644 index f000557..0000000 --- a/frontend/static/js/views/Posts.js +++ /dev/null @@ -1,14 +0,0 @@ -import AbstractView from "./AbstractView.js"; - -export default class extends AbstractView { - constructor(params) { - super(params, "Posts"); - } - - async getHtml() { - return ` -

Posts

-

You are viewing the posts!

- `; - } -} \ No newline at end of file diff --git a/frontend/static/js/views/accounts/RegisterView.js b/frontend/static/js/views/accounts/RegisterView.js index f6c23ec..210c93c 100644 --- a/frontend/static/js/views/accounts/RegisterView.js +++ b/frontend/static/js/views/accounts/RegisterView.js @@ -1,14 +1,19 @@ import { client, navigateTo } from "../../index.js"; -import AbstractAuthentifiedView from "../AbstractNonAuthentified.js"; +import AbstractNonAuthentifiedView from "../AbstractNonAuthentified.js"; async function register() { let username = document.getElementById("username").value; let password = document.getElementById("password").value; - let response = await client.accounts.create(username, password); - let response_data = await response.json(); - + let response_data = await client.account.create(username, password); + + if (response_data == null) + { + navigateTo("/home"); + return; + } + ["username", "user", "password"].forEach(error_field => { let error_display = document.getElementById(`error_${error_field}`); if (error_display != null) @@ -22,7 +27,7 @@ async function register() }); } -export default class extends AbstractAuthentifiedView { +export default class extends AbstractNonAuthentifiedView { constructor(params) { super(params, "Register", "/home"); } diff --git a/frontend/static/js/views/profiles/ProfilePageView.js b/frontend/static/js/views/profiles/ProfilePageView.js new file mode 100644 index 0000000..ffd9ee7 --- /dev/null +++ b/frontend/static/js/views/profiles/ProfilePageView.js @@ -0,0 +1,29 @@ +import AbstractView from "../AbstractView.js"; +import { client } from "../../index.js" + +export default class extends AbstractView { + constructor(params) { + super(params, "Profile "); + this.user_id = params.id; + } + + async postInit() + { + let profile = await client.profiles.getProfile(this.user_id); + + let username_element = document.getElementById("username"); + username_element.href = `/profiles/${this.user_id}`; + username_element.appendChild(document.createTextNode(profile.username)); + + let avatar_element = document.getElementById("avatar"); + avatar_element.src = profile.avatar_url; + } + + async getHtml() { + return ` + + + + `; + } +} \ No newline at end of file diff --git a/frontend/static/js/views/profiles/ProfilesView.js b/frontend/static/js/views/profiles/ProfilesView.js new file mode 100644 index 0000000..d07284e --- /dev/null +++ b/frontend/static/js/views/profiles/ProfilesView.js @@ -0,0 +1,40 @@ +import AbstractView from "../AbstractView.js"; +import { client } from "../../index.js" + +export default class extends AbstractView { + constructor(params) { + super(params, "Profiles"); + } + + async postInit() + { + let profiles = await client.profiles.all() + let profile_list_element = document.getElementById("profile-list") + + profiles.forEach((profile) => { + let profile_element = document.createElement("div"); + profile_element.className = "item"; + + let avatar = document.createElement("img"); + avatar.src = profile.avatar_url; + + let username = document.createElement("a"); + username.href = `/profiles/${profile.user_id}`; + username.appendChild(document.createTextNode(profile.username)); + + profile_element.appendChild(avatar); + profile_element.appendChild(username); + + profile_list_element.appendChild(profile_element) + }); + } + + async getHtml() { + return ` + +
+ +
+ `; + } +} \ No newline at end of file diff --git a/frontend/templates/index.html b/frontend/templates/index.html index e6b8e37..b6d4f58 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -10,8 +10,7 @@