diff --git a/apps/administration/AdministrationApp.ts b/apps/administration/AdministrationApp.ts
new file mode 100644
index 0000000..e17a651
--- /dev/null
+++ b/apps/administration/AdministrationApp.ts
@@ -0,0 +1,12 @@
+import { UserApp } from "../../browser/users/UserApp";
+import { UserSystem } from "../../browser/users/UserSystem";
+
+export class AdministrationApp extends UserApp
+{
+ static readonly ID = "administration";
+
+ constructor( userSystem:UserSystem )
+ {
+ super( userSystem, AdministrationApp.ID );
+ }
+}
\ No newline at end of file
diff --git a/apps/administration/ui/admin-button.html b/apps/administration/ui/admin-button.html
new file mode 100644
index 0000000..8cd1ce5
--- /dev/null
+++ b/apps/administration/ui/admin-button.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/apps/administration/ui/admin-ui.html b/apps/administration/ui/admin-ui.html
new file mode 100644
index 0000000..4d3d4c3
--- /dev/null
+++ b/apps/administration/ui/admin-ui.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Hello World!
+
+
\ No newline at end of file
diff --git a/apps/administration/ui/admin-ui.ts b/apps/administration/ui/admin-ui.ts
new file mode 100644
index 0000000..1af3d24
--- /dev/null
+++ b/apps/administration/ui/admin-ui.ts
@@ -0,0 +1,6 @@
+import { DOMElement } from "../../../browser/dom/DOMElement";
+
+export class AdminUI extends DOMElement
+{
+
+}
\ No newline at end of file
diff --git a/browser/dom/DOMElement.ts b/browser/dom/DOMElement.ts
new file mode 100644
index 0000000..4405750
--- /dev/null
+++ b/browser/dom/DOMElement.ts
@@ -0,0 +1,39 @@
+import { ElementType } from "./ElementType";
+import { MouseEventCallback, OnClick } from "./EventListeners";
+
+
+export class DomElementConstructorType
+{
+ type:ElementType;
+}
+
+export class DOMElement
+{
+ _element:T;
+ get element(){ return this._element; }
+
+ constructor( e:T )
+ {
+ this._element = e;
+ }
+
+ get( constructorType:DomElementConstructorType )
+ {
+ let element = constructorType.type.query( this._element );
+
+ return new ( constructorType as any )( element );
+ }
+
+ getAll( constructorType:DomElementConstructorType )
+ {
+ let elements = constructorType.type.queryAll( this._element );
+
+ return elements.map( e => new ( constructorType as any )( e ) );
+ }
+
+
+ onClick( action:MouseEventCallback, options?: any )
+ {
+ OnClick.add( this.element as any as HTMLElement, action, options );
+ }
+}
\ No newline at end of file
diff --git a/browser/users/UserApp.ts b/browser/users/UserApp.ts
new file mode 100644
index 0000000..260a89d
--- /dev/null
+++ b/browser/users/UserApp.ts
@@ -0,0 +1,13 @@
+import { UserSystem } from "./UserSystem";
+
+export class UserApp
+{
+ protected _userSystem:UserSystem;
+ protected _appID:string;
+
+ constructor( userSystem:UserSystem, appID:string )
+ {
+ this._userSystem = userSystem;
+ this._appID = appID;
+ }
+}
\ No newline at end of file
diff --git a/browser/users/UserSystem.ts b/browser/users/UserSystem.ts
new file mode 100644
index 0000000..647514f
--- /dev/null
+++ b/browser/users/UserSystem.ts
@@ -0,0 +1,286 @@
+import { Property } from "../events/Property";
+import { DataMessage } from "../messages/DataMessage";
+import { Message } from "../messages/Message";
+import { MessageTypes } from "../messages/MessageType";
+import { Request } from "../xhttp/Request";
+
+export class EmailData
+{
+ email:string;
+}
+
+export class EmailPasswordData extends EmailData
+{
+ password:string;
+}
+
+export class TokenData
+{
+ token:string;
+}
+
+export class PageTokenData
+{
+ pageToken:string;
+}
+
+export class LinkUserData extends PageTokenData
+{
+ userToken:string;
+ renewal:string;
+}
+
+export class LocationData
+{
+ country:string;
+ city:string;
+ km_range:number;
+}
+
+export class UserLoginDataInfo
+{
+ time:string;
+ location:LocationData;
+ app:string;
+ os:string;
+ deviceType:string;
+}
+
+export class UserData
+{
+ email:string;
+ id:string;
+ name:string;
+ lastLogins:UserLoginDataInfo[];
+}
+
+export class SessionData
+{
+ token:string;
+ renewal:string;
+}
+
+export class UserSystem
+{
+ static productionURL = "https://backend.rokojori.com:1712";
+ static localURL = "http://192.168.178.51:1712";
+
+ _isLocal = false;
+ _loggedIn = false;
+ _userData:UserData;
+
+ readonly userID = new Property( null );
+
+ _sessionData:SessionData;
+
+ get sessionToken(){ return this._sessionData?.token || null };
+
+ get isLoggedIn()
+ {
+ return this._loggedIn;
+ }
+
+ get userName()
+ {
+ return this._userData?.name || null;
+ }
+
+ get url()
+ {
+ return this._isLocal ? UserSystem.localURL : UserSystem.productionURL;
+ }
+
+
+ constructor( isLocal:boolean )
+ {
+ this._isLocal = isLocal;
+ }
+
+
+ async renew():Promise
+ {
+ if ( ! this._sessionData )
+ {
+ return Promise.resolve();
+ }
+
+ let functionURL = this.url + `/renew`;
+
+ let oldSessionData = this._sessionData;
+
+ try
+ {
+
+ let result = await Request.post>( functionURL, oldSessionData );
+
+ if ( MessageTypes.Error === result.type )
+ {
+ this._sessionData = null;
+ }
+ else
+ {
+ this._sessionData = result.data;
+ this._userData = await this.info();
+ }
+
+ }
+ catch ( e )
+ {
+ console.error( e );
+ }
+
+
+ return Promise.resolve();
+ }
+
+
+
+ async confirmSignup( id:string, token:string ):Promise
+ {
+ let functionURL = this.url + `/confirm-signup?id=${id}&token=${token}` ;
+
+ try
+ {
+ let result = await Request.get( functionURL );
+
+ return Promise.resolve( result.type !== MessageTypes.Error );
+ }
+ catch ( e )
+ {
+ console.error( e );
+ }
+
+
+ return Promise.resolve( false );
+ }
+
+ async signup( email:string, password:string ):Promise
+ {
+ let functionURL = this.url + "/signup";
+
+ let input:EmailPasswordData =
+ {
+ email: email,
+ password: password
+ };
+
+ try
+ {
+ let result = await Request.post( functionURL, input );
+
+ return Promise.resolve( result.type !== MessageTypes.Error );
+ }
+ catch ( e )
+ {
+ console.error( e );
+ }
+
+
+ return Promise.resolve( false );
+ }
+
+ async info():Promise
+ {
+ if ( this._sessionData === null )
+ {
+ return Promise.resolve( null );
+ }
+
+ let functionURL = this.url + "/info";
+
+ try
+ {
+ let result = await Request.post>( functionURL, { token: this.sessionToken } );
+ return Promise.resolve( result.data );
+ }
+ catch( e )
+ {
+ return Promise.resolve( null );
+ }
+ }
+
+
+ async login( email:string, password:string, debugOutput:string[] = undefined ):Promise
+ {
+ let functionURL = this.url + "/login";
+
+ let input:EmailPasswordData =
+ {
+ email: email,
+ password: password
+ };
+
+ try
+ {
+ let result = await Request.post>( functionURL, input );
+
+ if ( debugOutput )
+ {
+ debugOutput.push( JSON.stringify( result ) );
+ }
+
+
+ if ( result.type === MessageTypes.Error )
+ {
+ return Promise.resolve( false );
+ }
+
+ console.log( "Login Result:", result );
+
+
+ this._sessionData = result.data;
+
+ let userData = await this.info();
+
+ console.log( "userData Result:", userData );
+
+ this._loggedIn = true;
+ this._userData = userData;
+ this.userID.value = this._userData.id;
+
+ return Promise.resolve( true );
+ }
+ catch ( e )
+ {
+ console.error( e );
+
+ if ( debugOutput )
+ {
+ debugOutput.push( JSON.stringify( e.message ) );
+ }
+ }
+
+
+ return Promise.resolve( false );
+ }
+
+ async logout():Promise
+ {
+ if ( ! this._loggedIn )
+ {
+ return;
+ }
+
+ let functionURL = this.url + "/logout";
+
+ await Request.post>( functionURL, { token: this.sessionToken } );
+
+ this._loggedIn = false;
+ this._sessionData = null;
+ this._userData = null;
+
+ this.userID.value = null;
+
+ return Promise.resolve();
+
+ }
+
+ async requestPasswordChange( email:string ):Promise
+ {
+ let functionURL = this.url + "/request-password-change";
+
+ let result = await Request.post( functionURL, { email: email } );
+
+ return Promise.resolve( MessageTypes.Error !== result.type );
+ }
+}
\ No newline at end of file
diff --git a/browser/xhttp/Request.ts b/browser/xhttp/Request.ts
index 2eb17d9..0472efe 100644
--- a/browser/xhttp/Request.ts
+++ b/browser/xhttp/Request.ts
@@ -1,5 +1,45 @@
export class Request
{
+ static get( url:string ):Promise
+ {
+ let promise = new Promise
+ (
+ ( resolve, reject ) =>
+ {
+ let xhr = new XMLHttpRequest();
+
+ console.log( "get", url );
+ xhr.open( "GET", url, true );
+ xhr.responseType = "text";
+
+ xhr.onload=
+ () =>
+ {
+ console.log( xhr.responseURL, xhr.responseText );
+
+ if ( xhr.status !== 200 || xhr.responseText.startsWith( "ERROR:" ) )
+ {
+ reject( xhr.responseText )
+ }
+ else
+ {
+ resolve( JSON.parse( xhr.responseText ) as O );
+ }
+
+ };
+
+ xhr.onerror=(e)=>
+ {
+ reject( e );
+ }
+
+ xhr.send();
+ }
+ );
+
+ return promise;
+ }
+
static post( url:string, input:I ):Promise
{
let promise = new Promise