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})