diff --git a/src/snek/app.py b/src/snek/app.py
index 584b321..f26fc06 100644
--- a/src/snek/app.py
+++ b/src/snek/app.py
@@ -21,6 +21,7 @@ from snek.view.docs import DocsHTMLView, DocsMDView
from snek.view.index import IndexView
from snek.view.login import LoginView
from snek.view.login_form import LoginFormView
+from snek.view.logout import LogoutView
from snek.view.register import RegisterView
from snek.view.register_form import RegisterFormView
from snek.view.status import StatusView
@@ -68,6 +69,8 @@ class Application(BaseApplication):
)
self.router.add_view("/about.html", AboutHTMLView)
self.router.add_view("/about.md", AboutMDView)
+ self.router.add_view("/logout.json", LogoutView)
+ self.router.add_view("/logout.html", LogoutView)
self.router.add_view("/docs.html", DocsHTMLView)
self.router.add_view("/docs.md", DocsMDView)
self.router.add_view("/status.json", StatusView)
diff --git a/src/snek/form/login.py b/src/snek/form/login.py
index 3d6d9a7..2966053 100644
--- a/src/snek/form/login.py
+++ b/src/snek/form/login.py
@@ -1,11 +1,24 @@
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
+class AuthField(FormInputElement):
+
+ @property
+ async def errors(self):
+ result = await super().errors
+ if self.model.password.value and self.model.username.value:
+ if not await self.app.services.user.validate_login(
+ self.model.username.value, self.model.password.value
+ ):
+ return ["Invalid username or password"]
+ return result
+
+
class LoginForm(Form):
title = HTMLElement(tag="h1", text="Login")
- username = FormInputElement(
+ username = AuthField(
name="username",
required=True,
min_length=2,
@@ -14,7 +27,7 @@ class LoginForm(Form):
place_holder="Username",
type="text",
)
- password = FormInputElement(
+ password = AuthField(
name="password",
required=True,
regex=r"^[a-zA-Z0-9_.+-]{6,}",
@@ -25,3 +38,14 @@ class LoginForm(Form):
action = FormButtonElement(
name="action", value="submit", text="Login", type="button"
)
+
+ @property
+ async def is_valid(self):
+ return all(
+ [
+ self["username"],
+ self["password"],
+ not await self.username.errors,
+ not await self.password.errors,
+ ]
+ )
diff --git a/src/snek/service/user.py b/src/snek/service/user.py
index 5124640..cfcd6b8 100644
--- a/src/snek/service/user.py
+++ b/src/snek/service/user.py
@@ -5,13 +5,23 @@ from snek.system.service import BaseService
class UserService(BaseService):
mapper_name = "user"
+ async def validate_login(self, username, password):
+ model = await self.get(username=username)
+ print("FOUND USER!", model, flush=True)
+ if not model:
+ return False
+ print("AU", password, model.password.value, flush=True)
+ if not await security.verify(password, model["password"]):
+ return False
+ return True
+
async def register(self, email, username, password):
if await self.exists(username=username):
raise Exception("User already exists.")
model = await self.new()
- model.email = email
- model.username = username
- model.password = await security.hash(password)
+ model.email.value = email
+ model.username.value = username
+ model.password.value = await security.hash(password)
if await self.save(model):
return model
raise Exception(f"Failed to create user: {model.errors}.")
diff --git a/src/snek/static/app.js b/src/snek/static/app.js
index d8d3a8f..133cbcd 100644
--- a/src/snek/static/app.js
+++ b/src/snek/static/app.js
@@ -113,9 +113,98 @@ class RESTClient {
return result
}
}
-
const rest = new RESTClient()
+class EventHandler {
+
+ constructor(){
+ this.subscribers = {}
+ }
+ addEventListener(type,handler){
+ if(!this.subscribers[type])
+ this.subscribers[type] = []
+ this.subscribers[type].push(handler)
+ }
+ emit(type,...data){
+ if(this.subscribers[type])
+ this.subscribers[type].forEach(handler=>handler(...data))
+ }
+
+}
+
+class Chat extends EventHandler {
+
+ constructor() {
+ super()
+ this._url = window.location.hostname == 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname +'/chat.ws'
+ this._socket = null
+ this._wait_connect = null
+ this._promises = {}
+ }
+ connect(){
+ if(this._wait_connect)
+ return this._wait_connect
+
+ const me = this
+ return new Promise(async (resolve,reject)=>{
+ me._wait_connect = resolve
+ me._socket = new WebSocket(me._url)
+ console.debug("Connecting..")
+
+ me._socket.onconnect = ()=>{
+ me._connected()
+ me._wait_socket(me)
+ }
+ })
+
+ }
+ generateUniqueId() {
+ return 'id-' + Math.random().toString(36).substr(2, 9); // Example: id-k5f9zq7
+ }
+ call(method,...args){
+ const me = this
+ return new Promise(async (resolve,reject)=>{
+ try{
+ const command = {method:method,args:args,message_id:me.generateUniqueId()}
+ me._promises[command.message_id] = resolve
+ await me._socket.send(JSON.stringify(command))
+
+ }catch(e){
+ reject(e)
+ }
+ })
+ }
+ _connected() {
+ const me = this
+ this._socket.onmessage = (event) => {
+ const message = JSON.parse(event.data)
+ if(message.message_id && me._promises[message.message_id]){
+ me._promises[message.message_id](message)
+ delete me._promises[message.message_id]
+ }else{
+ me.emit("message",me, message)
+ }
+ //const room = this.rooms.find(room=>room.name == message.room)
+ //if(!room){
+ // this.rooms.push(new Room(message.room))
+ }
+ this._socket.onclose = (event) => {
+ me._wait_socket = null
+ me._socket = null
+ me.emit('close',me)
+ }
+ }
+
+ async privmsg(room, text) {
+ await rest.post("/api/privmsg",{
+ room:room,
+ text:text
+ })
+ }
+
+}
+
+
class App {
rooms = []
constructor() {
diff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py
index f6c6200..489ff90 100644
--- a/src/snek/system/mapper.py
+++ b/src/snek/system/mapper.py
@@ -29,8 +29,14 @@ class BaseMapper:
async def get(self, uid: str = None, **kwargs) -> BaseModel:
if uid:
kwargs["uid"] = uid
- self.new()
record = self.table.find_one(**kwargs)
+ if not record:
+ return None
+ record = dict(record)
+ model = await self.new()
+ for key, value in record.items():
+ model[key] = value
+ return model
return await self.model_class.from_record(mapper=self, record=record)
async def exists(self, **kwargs):
@@ -40,10 +46,9 @@ class BaseMapper:
return self.table.count(**kwargs)
async def save(self, model: BaseModel) -> bool:
- record = await model.record
- if not record.get("uid"):
- raise Exception(f"Attempt to save without uid: {record}.")
- return self.table.upsert(record, ["uid"])
+ if not model.record.get("uid"):
+ raise Exception(f"Attempt to save without uid: {model.record}.")
+ return self.table.upsert(model.record, ["uid"])
async def find(self, **kwargs) -> typing.AsyncGenerator:
if not kwargs.get("_limit"):
diff --git a/src/snek/system/model.py b/src/snek/system/model.py
index 7efde64..ba3fc45 100644
--- a/src/snek/system/model.py
+++ b/src/snek/system/model.py
@@ -243,7 +243,7 @@ class BaseModel:
@classmethod
async def from_record(cls, record, mapper):
- model = cls.__new__()
+ model = cls()
model.mapper = mapper
model.record = record
return model
@@ -258,15 +258,15 @@ class BaseModel:
@property
def record(self):
- return {field.name: field.value for field in self.fields}
+ return {key: field.value for key, field in self.fields.items()}
@record.setter
- def record(self, value):
- for key, value in self._record.items():
+ def record(self, val):
+ for key, value in val.items():
field = self.fields.get(key)
if not field:
continue
- field.value = value
+ self[key] = value
return self
def __init__(self, *args, **kwargs):
@@ -321,7 +321,7 @@ class BaseModel:
self.__dict__[key] = value
@property
- async def record(self):
+ async def recordz(self):
obj = await self.to_json()
record = {}
for key, value in obj.items():
diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html
index 5a636f0..ae0bb06 100644
--- a/src/snek/templates/web.html
+++ b/src/snek/templates/web.html
@@ -11,10 +11,10 @@
diff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py
index 576ddc6..f0efce9 100644
--- a/src/snek/view/login_form.py
+++ b/src/snek/view/login_form.py
@@ -4,3 +4,11 @@ from snek.system.view import BaseFormView
class LoginFormView(BaseFormView):
form = LoginForm
+
+ async def submit(self, form):
+ if await form.is_valid:
+ self.session["logged_in"] = True
+ self.session["username"] = form.username.value
+ self.session["uid"] = form.uid.value
+ return {"redirect_url": "/web.html"}
+ return {"is_valid": False}
diff --git a/src/snek/view/logout.py b/src/snek/view/logout.py
new file mode 100644
index 0000000..eb5c1ae
--- /dev/null
+++ b/src/snek/view/logout.py
@@ -0,0 +1,27 @@
+from aiohttp import web
+
+from snek.system.view import BaseView
+
+
+class LogoutView(BaseView):
+
+ redirect_url = "/"
+ login_required = True
+
+ async def get(self):
+ try:
+ del self.session["logged_in"]
+ del self.session["uid"]
+ del self.session["username"]
+ except KeyError:
+ pass
+ return web.HTTPFound(self.redirect_url)
+
+ async def post(self):
+ try:
+ del self.session["logged_in"]
+ del self.session["uid"]
+ del self.session["username"]
+ except KeyError:
+ pass
+ return await self.json_response({"redirect_url": self.redirect_url})