├── .anvil_editor.yaml
├── .gitignore
├── README.md
├── __init__.py
├── anvil.yaml
├── client_code
├── Home
│ ├── Header
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── RowTemplate1
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── __init__.py
│ └── form_template.yaml
├── Machines
│ ├── MachineDetail
│ │ ├── MachineRouteRow
│ │ │ ├── __init__.py
│ │ │ └── form_template.yaml
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── MachineRow
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── __init__.py
│ └── form_template.yaml
├── Routes
│ ├── RouteRow
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── __init__.py
│ └── form_template.yaml
├── Settings
│ ├── AddUser
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── AppUsers
│ │ ├── AppUsersTemplate
│ │ │ ├── __init__.py
│ │ │ └── form_template.yaml
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── __init__.py
│ └── form_template.yaml
├── Startup.py
└── Users
│ ├── PreAuth
│ ├── GenerateKey
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── PreAuthKeyRow
│ │ ├── __init__.py
│ │ └── form_template.yaml
│ ├── __init__.py
│ └── form_template.yaml
│ ├── UserRow
│ ├── __init__.py
│ └── form_template.yaml
│ ├── __init__.py
│ └── form_template.yaml
├── server_code
├── API_Interface.py
├── Hostess.py
└── RecordRecorder.py
└── theme
├── assets
├── milliner_logo.png
├── milliner_spinner.png
├── standard-page.html
└── theme.css
├── parameters.yaml
└── templates.yaml
/.anvil_editor.yaml:
--------------------------------------------------------------------------------
1 | unique_ids:
2 | forms:
3 | Users.PreAuth: '1680917206140202361516214.53912'
4 | Machines: '1680787304124770418668583.75'
5 | Routes: '1680792592718148150989093.66742'
6 | Home.RowTemplate1: '1680742075787691035143944.1995'
7 | Settings.AppUsers: '1680818557115792849019454.5806'
8 | Settings.AddUser: '1680814292012935083795394.9249'
9 | Users.PreAuth.GenerateKey: '1680931175859716587359885.5071'
10 | Machines.MachineDetail: '1680876986352129103239699.90977'
11 | Settings: '1680802990379829050247630.9993'
12 | Users: '1680789066429724812072809.3873'
13 | Routes.RouteRow: '1680792604662563304920925.5035'
14 | Users.PreAuth.PreAuthKeyRow: '1680917272692747028879605.7814'
15 | Users.UserRow: '1680828924011617711773402.4417'
16 | Machines.MachineRow: '1680787330601762866562982.6748'
17 | Settings.AppUsers.AppUsersTemplate: '1680808528309286578763962.20306'
18 | Machines.MachineDetail.MachineRouteRow: '1680879268230914845955877.3297'
19 | Home: HX4XYKOBKDJLASEPJRK5E47RME4D2VT3
20 | Home.Header: '168097609810740373230911.51868'
21 | modules:
22 | Startup: '1681078834820313478862549.32135'
23 | server_modules:
24 | API_Interface: '1680742140322703776832328.0709'
25 | Hostess: '1680787671616907323215116.5988'
26 | RecordRecorder: '1680784797723764777233107.3958'
27 | assets:
28 | milliner_logo.png: '1680796733581114653423428.42722'
29 | milliner_spinner.png: '1680870959214572130263182.6436'
30 | standard-page.html: VM5NH54ERXEK273777JO4IMYNUOIM4BZ
31 | theme.css: QDIZM2OVOKIZBAXMIRPNEDTR27VECD23
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.pyo
3 | __pycache__
4 | .anvil-data
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About This [Anvil](https://anvil.works/?utm_source=github:app_README) App
2 |
3 | ### Build web apps with nothing but Python.
4 |
5 | The app in this repository is built with [Anvil](https://anvil.works?utm_source=github:app_README), the framework for building web apps with nothing but Python. You can clone this app into your own Anvil account to use and modify.
6 |
7 | Below, you will find:
8 | - [How to open this app](#opening-this-app-in-anvil-and-getting-it-online) in Anvil and deploy it online
9 | - Information [about Anvil](#about-anvil)
10 | - And links to some handy [documentation and tutorials](#tutorials-and-documentation)
11 |
12 | ## Opening this app in Anvil and getting it online
13 |
14 | ### Cloning the app
15 |
16 | Go to the [Anvil Editor](https://anvil.works/build?utm_source=github:app_README) (you might need to sign up for a free account) and click on “Clone from GitHub” (underneath the “Blank App” option):
17 |
18 |
19 |
20 | Enter the URL of this GitHub repository. If you're not yet logged in, choose "GitHub credentials" as the authentication method and click "Connect to GitHub".
21 |
22 |
23 |
24 | Finally, click "Clone App".
25 |
26 | This app will then be in your Anvil account, ready for you to run it or start editing it! **Any changes you make will be automatically pushed back to this repository, if you have permission!** You might want to [make a new branch](https://anvil.works/docs/version-control-new-ide?utm_source=github:app_README).
27 |
28 | ### Running the app yourself:
29 |
30 | Find the **Run** button at the top-right of the Anvil editor:
31 |
32 |
33 |
34 |
35 | ### Publishing the app on your own URL
36 |
37 | Now you've cloned the app, you can [deploy it on the internet with two clicks](https://anvil.works/docs/deployment/quickstart?utm_source=github:app_README)! Find the **Publish** button at the top-right of the editor:
38 |
39 |
40 |
41 | When you click it, you will see the Publish dialog:
42 |
43 |
44 |
45 | Click **Publish This App**, and you will see that your app has been deployed at a new, public URL:
46 |
47 |
48 |
49 | That's it - **your app is now online**. Click the link and try it!
50 |
51 | ## About Anvil
52 |
53 | If you’re new to Anvil, welcome! Anvil is a platform for building full-stack web apps with nothing but Python. No need to wrestle with JS, HTML, CSS, Python, SQL and all their frameworks – just build it all in Python.
54 |
55 |
Learn About Anvil In 80 Seconds👇
62 |
63 |
65 |
66 | [](https://anvil.works?utm_source=github:app_README)
67 |
68 | To learn more about Anvil, visit [https://anvil.works](https://anvil.works?utm_source=github:app_README).
69 |
70 | ## Tutorials and documentation
71 |
72 | ### Tutorials
73 |
74 | If you are just starting out with Anvil, why not **[try the 10-minute Feedback Form tutorial](https://anvil.works/learn/tutorials/feedback-form?utm_source=github:app_README)**? It features step-by-step tutorials that will introduce you to the most important parts of Anvil.
75 |
76 | Anvil has tutorials on:
77 | - [Building Dashboards](https://anvil.works/learn/tutorials/data-science#dashboarding?utm_source=github:app_README)
78 | - [Multi-User Applications](https://anvil.works/learn/tutorials/multi-user-apps?utm_source=github:app_README)
79 | - [Building Web Apps with an External Database](https://anvil.works/learn/tutorials/external-database?utm_source=github:app_README)
80 | - [Deploying Machine Learning Models](https://anvil.works/learn/tutorials/deploy-machine-learning-model?utm_source=github:app_README)
81 | - [Taking Payments with Stripe](https://anvil.works/learn/tutorials/stripe?utm_source=github:app_README)
82 | - And [much more....](https://anvil.works/learn/tutorials?utm_source=github:app_README)
83 |
84 | ### Reference Documentation
85 |
86 | The Anvil reference documentation provides comprehensive information on how to use Anvil to build web applications. You can find the documentation [here](https://anvil.works/docs/overview?utm_source=github:app_README).
87 |
88 | If you want to get to the basics as quickly as possible, each section of this documentation features a [Quick-Start Guide](https://anvil.works/docs/overview/quickstarts?utm_source=github:app_README).
89 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # This repository is an Anvil app. Learn more at https://anvil.works/
3 | # To run the server-side code on your own machine, run:
4 | # pip install anvil-uplink
5 | # python -m anvil.run_app_via_uplink YourAppPackageName
6 |
7 | __path__ = [__path__[0] + "/server_code", __path__[0] + "/client_code"]
8 |
--------------------------------------------------------------------------------
/anvil.yaml:
--------------------------------------------------------------------------------
1 | scheduled_tasks:
2 | - task_name: record_machines
3 | time_spec:
4 | n: 5
5 | every: minute
6 | at: {}
7 | job_id: JMAKXPDN
8 | - task_name: record_users
9 | time_spec:
10 | n: 5
11 | every: minute
12 | at: {}
13 | job_id: BJVVVHAS
14 | - task_name: record_routes
15 | time_spec:
16 | n: 5
17 | every: minute
18 | at: {}
19 | job_id: FAGDAFJR
20 | services:
21 | - source: /runtime/services/tables.yml
22 | client_config: {enable_v2: true}
23 | server_config: {auto_create_missing_columns: false}
24 | - source: /runtime/services/anvil/users.yml
25 | client_config: {allow_signup: false, enable_automatically: true, use_email: true,
26 | confirm_email: false, allow_remember_me: true, remember_me_days: 30}
27 | server_config: {user_table: users}
28 | startup: {type: module, module: Startup}
29 | package_name: Milliner
30 | allow_embedding: false
31 | name: Milliner
32 | runtime_options:
33 | version: 2
34 | client_version: '3'
35 | server_version: python3-full
36 | server_spec: null
37 | server_spec_disabled: {base: python310-standard, requirements: protobuf}
38 | metadata: {title: Milliner, description: A simple UI to control a Headscale implementation.,
39 | logo_img: 'asset:milliner_logo.png'}
40 | startup_form: null
41 | native_deps: {head_html: ''}
43 | db_schema:
44 | machines:
45 | title: Machines
46 | client: none
47 | server: full
48 | columns:
49 | - name: name
50 | admin_ui: {}
51 | type: string
52 | - name: ipAddr
53 | admin_ui: {}
54 | type: simpleObject
55 | - name: discoKey
56 | admin_ui: {}
57 | type: string
58 | - name: expiry
59 | admin_ui: {}
60 | type: string
61 | - name: registerMethod
62 | admin_ui: {}
63 | type: string
64 | - name: machineKey
65 | admin_ui: {}
66 | type: string
67 | - name: createdAt
68 | admin_ui: {}
69 | type: string
70 | - name: lastSuccessfulUpdate
71 | admin_ui: {}
72 | type: string
73 | - name: nodeKey
74 | admin_ui: {}
75 | type: string
76 | - name: lastSeen
77 | admin_ui: {}
78 | type: string
79 | - name: user
80 | admin_ui: {}
81 | type: simpleObject
82 | - name: id
83 | admin_ui: {}
84 | type: string
85 | - name: givenName
86 | admin_ui: {}
87 | type: string
88 | - name: online
89 | admin_ui: {}
90 | type: bool
91 | - name: invalidTags
92 | admin_ui: {order: 17, width: 200}
93 | type: simpleObject
94 | - name: validTags
95 | admin_ui: {order: 17, width: 200}
96 | type: simpleObject
97 | - name: forcedTags
98 | admin_ui: {order: 17, width: 200}
99 | type: simpleObject
100 | - name: preAuthKey
101 | admin_ui: {order: 17, width: 200}
102 | type: simpleObject
103 | users:
104 | title: Users
105 | client: none
106 | server: full
107 | columns:
108 | - name: email
109 | admin_ui: {width: 200}
110 | type: string
111 | - name: enabled
112 | admin_ui: {width: 200}
113 | type: bool
114 | - name: last_login
115 | admin_ui: {width: 200}
116 | type: datetime
117 | - name: password_hash
118 | admin_ui: {width: 200}
119 | type: string
120 | - name: n_password_failures
121 | admin_ui: {width: 141}
122 | type: number
123 | - name: confirmed_email
124 | admin_ui: {width: 200}
125 | type: bool
126 | - name: signed_up
127 | admin_ui: {width: 200}
128 | type: datetime
129 | - name: remembered_logins
130 | admin_ui: {width: 200}
131 | type: simpleObject
132 | settings:
133 | title: Settings
134 | client: full
135 | server: full
136 | columns:
137 | - name: url
138 | admin_ui: {width: 200}
139 | type: string
140 | - name: api_key
141 | admin_ui: {width: 200}
142 | type: string
143 | - name: last_hs_sync
144 | admin_ui: {width: 200}
145 | type: string
146 | - name: api_key_expiration
147 | admin_ui: {width: 200}
148 | type: string
149 | - name: api_key_creation
150 | admin_ui: {width: 200}
151 | type: string
152 | - name: is_fresh_install
153 | admin_ui: {order: 5, width: 200}
154 | type: bool
155 | hs_users:
156 | title: hs_users
157 | client: none
158 | server: full
159 | columns:
160 | - name: id
161 | admin_ui: {width: 200}
162 | type: string
163 | - name: name
164 | admin_ui: {}
165 | type: string
166 | - name: createdAt
167 | admin_ui: {}
168 | type: string
169 | routes:
170 | title: Routes
171 | client: none
172 | server: full
173 | columns:
174 | - {name: givenName, admin_ui: null, type: string}
175 | - name: id
176 | admin_ui: {width: 200}
177 | type: number
178 | - name: prefix
179 | admin_ui: {}
180 | type: string
181 | - name: machineName
182 | admin_ui: {}
183 | type: string
184 | - name: machineIPs
185 | admin_ui: {}
186 | type: simpleObject
187 | - name: enabled
188 | admin_ui: {}
189 | type: bool
190 |
--------------------------------------------------------------------------------
/client_code/Home/Header/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import HeaderTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from ... import Startup
9 |
10 | class Header(HeaderTemplate):
11 | def __init__(self, **properties):
12 | # Set Form properties and Data Bindings.
13 | self.user = Startup.user
14 | if self.user:
15 | self.link_user.text = self.user['email']
16 | else:
17 | self.link_user.text = 'None'
18 | self.init_components(**properties)
19 |
20 | # Any code you write here will run before the form opens.
21 |
22 | def link_user_click(self, **event_args):
23 | """This method is called when the link is clicked"""
24 | anvil.users.configure_account_with_form()
25 |
26 | def button_sign_out_click(self, **event_args):
27 | """This method is called when the button is clicked"""
28 | get_open_form().column_panel_home.clear()
29 | anvil.users.logout()
30 | anvil.users.login_with_form()
31 | open_form('Home')
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/client_code/Home/Header/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: GridPanel
6 | properties: {spacing_above: small, spacing_below: small, background: '', foreground: '',
7 | border: '', visible: true, role: null, tooltip: ''}
8 | name: grid_panel_header
9 | layout_properties: {grid_position: 'YPTHJP,ELKNLH', full_width_row: true}
10 | components:
11 | - type: ColumnPanel
12 | properties: {col_widths: '{"WEFXQI":25,"XTTDOG":35}'}
13 | name: column_panel_1
14 | layout_properties: {row: VTIJMM, width_xs: 5, col_xs: 7}
15 | components:
16 | - type: Link
17 | properties: {role: null, url: '', align: right, tooltip: '', border: '', foreground: '',
18 | visible: true, text: '', font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
19 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small,
20 | italic: false, background: '', bold: false, underline: false, icon: 'fa:user'}
21 | name: link_user
22 | layout_properties: {row: VTIJMM, width_xs: 3, col_xs: 8, width: 136.65625, grid_position: 'ZJLUUA,XTTDOG'}
23 | data_bindings: []
24 | event_bindings: {click: link_user_click}
25 | components: []
26 | - type: Button
27 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true,
28 | foreground: '', visible: true, text: Sign Out, font_size: null, font: '',
29 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
30 | background: '', bold: false, underline: false, icon: 'fa:sign-out'}
31 | name: button_sign_out
32 | layout_properties: {row: VTIJMM, width_xs: 3, col_xs: 8, width: 159.328125,
33 | grid_position: 'ZJLUUA,WEFXQI'}
34 | event_bindings: {click: button_sign_out_click}
35 | is_package: true
36 | custom_component: true
37 |
--------------------------------------------------------------------------------
/client_code/Home/RowTemplate1/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import RowTemplate1Template
2 | from anvil import *
3 | import anvil.users
4 | import anvil.server
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 |
9 | class RowTemplate1(RowTemplate1Template):
10 | def __init__(self, **properties):
11 | # Set Form properties and Data Bindings.
12 | self.init_components(**properties)
13 |
14 | # Any code you write here will run before the form opens.
15 |
--------------------------------------------------------------------------------
/client_code/Home/RowTemplate1/form_template.yaml:
--------------------------------------------------------------------------------
1 | container: {type: DataRowPanel}
2 | components: []
3 | is_package: true
4 |
--------------------------------------------------------------------------------
/client_code/Home/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import HomeTemplate
2 | from anvil import *
3 | import anvil.users
4 | import anvil.server
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from ..Machines import Machines
9 | from ..Users import Users
10 | from ..Routes import Routes
11 | from ..Settings import Settings
12 | from .. import Startup
13 |
14 | class Home(HomeTemplate):
15 | def __init__(self, **properties):
16 | if app_tables.settings.get():
17 | self.label_sync_time.text = [r['last_hs_sync'] for r in app_tables.settings.search()][0]
18 | else:
19 | self.label_sync_time.text = 'Never Synced'
20 | self.item = anvil.server.call('get_machine_table').search(online=True)
21 | self.version = Startup.version
22 | self.url = Startup.url
23 | self.api_key = Startup.api_key
24 | # Set Form properties and Data Bindings.
25 | self.init_components(**properties)
26 |
27 |
28 | # Any code you write here will run before the form opens.
29 |
30 | def form_show(self, **event_args):
31 | """This method is called when the HTML panel is shown on the screen"""
32 | if self.url == "":
33 | alert('No URL has been set. The application will not function until one is set in Settings.', title='No URL!')
34 | if self.api_key == "":
35 | alert('No API Key has been set. The application will not function until one is set in Settings.', title='No API Key!')
36 |
37 | def link_machines_click(self, **event_args):
38 | """This method is called when the link is clicked"""
39 | self.column_panel_home.clear()
40 | self.column_panel_home.add_component(Machines(), full_width_row=True)
41 |
42 | def link_users_click(self, **event_args):
43 | """This method is called when the link is clicked"""
44 | self.column_panel_home.clear()
45 | self.column_panel_home.add_component(Users())
46 |
47 | def link_user_click(self, **event_args):
48 | """This method is called when the link is clicked"""
49 | anvil.users.configure_account_with_form()
50 |
51 | def image_logo_mouse_down(self, x, y, button, **event_args):
52 | """This method is called when a mouse button is pressed on this component"""
53 | open_form('Home')
54 |
55 | def link_home_click(self, **event_args):
56 | """This method is called when the link is clicked"""
57 | open_form('Home')
58 |
59 | def link_routes_click(self, **event_args):
60 | """This method is called when the link is clicked"""
61 | self.column_panel_home.clear()
62 | self.column_panel_home.add_component(Routes())
63 |
64 | def link_settings_click(self, **event_args):
65 | """This method is called when the link is clicked"""
66 | self.column_panel_home.clear()
67 | self.column_panel_home.add_component(Settings())
68 |
69 | def button_refresh_data_click(self, **event_args):
70 | """This method is called when the button is clicked"""
71 | with Notification('Refreshing Data from Headscale'):
72 | anvil.server.call('record_users')
73 | anvil.server.call('record_machines')
74 | anvil.server.call('record_routes')
75 | self.item = anvil.server.call('get_machine_table').search(online=True)
76 | self.label_sync_time.text = [r['last_hs_sync'] for r in app_tables.settings.search()][0]
77 |
78 | def button_test_function_click(self, **event_args):
79 | """This method is called when the button is clicked"""
80 | alert(anvil.server.call('get_api_key_info', self.url, self.api_key))
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/client_code/Home/form_template.yaml:
--------------------------------------------------------------------------------
1 | is_package: true
2 | container:
3 | type: HtmlTemplate
4 | properties: {html: '@theme:standard-page.html'}
5 | event_bindings: {show: form_show}
6 | components:
7 | - type: ColumnPanel
8 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
9 | wrap_on: mobile, col_spacing: medium, spacing_above: small, col_widths: '{}',
10 | spacing_below: small, background: ''}
11 | name: column_panel_home
12 | layout_properties: {slot: default}
13 | components:
14 | - type: form:Home.Header
15 | properties: {}
16 | name: header_1
17 | layout_properties: {grid_position: 'TMCTOK,AUCFBT', full_width_row: false}
18 | data_bindings: []
19 | - type: Label
20 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '',
21 | visible: true, text: Online Machines, font_size: null, font: '', spacing_above: none,
22 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false,
23 | underline: true, icon: ''}
24 | name: label_1
25 | layout_properties: {row: YDPWFW, width_xs: 2, col_xs: 5, grid_position: 'HWODQZ,PSOSUQ'}
26 | - type: DataGrid
27 | properties:
28 | role: null
29 | columns:
30 | - {id: JFGZMI, title: Id, data_key: id, $$hashKey: 'object:2411', width: '50'}
31 | - {id: EWYRCD, title: Name, data_key: givenName, $$hashKey: 'object:2397', width: '175',
32 | expand: true}
33 | - {id: CWEWNV, title: IpAddr, data_key: ipAddr, $$hashKey: 'object:2398', width: '175',
34 | expand: true}
35 | - {id: VDHOSS, title: LastSeen, data_key: lastSeen, $$hashKey: 'object:2408',
36 | width: '175', expand: true}
37 | - {id: YXSWKC, title: User, data_key: user, $$hashKey: 'object:2410', width: '125'}
38 | auto_header: true
39 | tooltip: ''
40 | border: ''
41 | foreground: ''
42 | rows_per_page: 20
43 | visible: true
44 | wrap_on: never
45 | show_page_controls: true
46 | spacing_above: small
47 | spacing_below: small
48 | background: ''
49 | name: data_grid_1
50 | layout_properties: {grid_position: 'UDSNGJ,MEHURZ'}
51 | components:
52 | - type: RepeatingPanel
53 | properties: {spacing_above: none, spacing_below: none, item_template: Home.RowTemplate1}
54 | name: repeating_panel_1
55 | layout_properties: {}
56 | data_bindings:
57 | - {property: items, code: self.item}
58 | - type: GridPanel
59 | properties: {spacing_above: none, spacing_below: none, background: '', foreground: '',
60 | border: '', visible: true, role: null, tooltip: ''}
61 | name: grid_panel_footer
62 | layout_properties: {slot: footer}
63 | components:
64 | - type: Button
65 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true,
66 | foreground: '', visible: true, text: Refresh Data, font_size: null, font: '',
67 | spacing_above: small, icon_align: left, spacing_below: none, italic: false,
68 | background: '', bold: false, underline: false, icon: 'fa:refresh'}
69 | name: button_refresh_data
70 | layout_properties: {row: WXHJII, width_xs: 3, col_xs: 0, width: 180.1875}
71 | event_bindings: {click: button_refresh_data_click}
72 | - type: ColumnPanel
73 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
74 | wrap_on: mobile, col_spacing: medium, spacing_above: none, col_widths: '{"HILOMR":15,"WWHTIK":45}',
75 | spacing_below: none, background: ''}
76 | name: column_panel_2
77 | layout_properties: {row: WXHJII, width_xs: 5, col_xs: 3, width: 320.3125}
78 | components:
79 | - type: Label
80 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
81 | visible: true, text: 'Last Sync:', font_size: null, font: '', spacing_above: small,
82 | icon_align: left, spacing_below: none, italic: false, background: '',
83 | bold: false, underline: false, icon: ''}
84 | name: label_sync
85 | layout_properties: {grid_position: 'RIIETN,HILOMR'}
86 | - type: Label
87 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
88 | visible: true, text: '', font_size: null, font: '', spacing_above: small,
89 | icon_align: left, spacing_below: none, italic: false, background: '',
90 | bold: false, underline: false, icon: ''}
91 | name: label_sync_time
92 | layout_properties: {grid_position: 'RIIETN,WWHTIK'}
93 | - type: Image
94 | properties: {role: null, vertical_align: center, height: 93, tooltip: '', border: '',
95 | foreground: '', visible: true, display_mode: shrink_to_fit, spacing_above: small,
96 | source: _/theme/milliner_logo.png, spacing_below: small, background: '', horizontal_align: center}
97 | name: image_logo
98 | layout_properties: {slot: logo}
99 | event_bindings: {mouse_down: image_logo_mouse_down}
100 | - type: LinearPanel
101 | properties: {}
102 | name: linear_panel_1
103 | layout_properties: {grid_position: 'YWEVWW,DNOXFE', slot: left-nav}
104 | components:
105 | - type: Link
106 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white,
107 | visible: true, text: Home, font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
108 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small,
109 | italic: false, background: '', bold: false, underline: false, icon: 'fa:home'}
110 | name: link_home
111 | layout_properties: {}
112 | event_bindings: {click: link_home_click}
113 | components: []
114 | - type: Link
115 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white,
116 | visible: true, text: Users, font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
117 | spacing_above: small, icon_align: left, col_widths: '', spacing_below: small,
118 | italic: false, background: '', bold: false, underline: false, icon: 'fa:users'}
119 | name: link_users
120 | layout_properties: {}
121 | event_bindings: {click: link_users_click}
122 | - type: Link
123 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white,
124 | visible: true, text: Machines, font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
125 | spacing_above: small, icon_align: left, col_widths: '', spacing_below: small,
126 | italic: false, background: '', bold: false, underline: false, icon: 'fa:desktop'}
127 | name: link_machines
128 | layout_properties: {}
129 | event_bindings: {click: link_machines_click}
130 | - type: Link
131 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white,
132 | visible: true, text: Routes, font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
133 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small,
134 | italic: false, background: '', bold: false, underline: false, icon: 'fa:map-signs'}
135 | name: link_routes
136 | layout_properties: {}
137 | event_bindings: {click: link_routes_click}
138 | components: []
139 | - type: Link
140 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white,
141 | visible: true, text: Settings, font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
142 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small,
143 | italic: false, background: '', bold: false, underline: false, icon: 'fa:sliders'}
144 | name: link_settings
145 | layout_properties: {}
146 | components:
147 | - type: ColumnPanel
148 | properties: {col_widths: '{}'}
149 | name: column_panel_1
150 | layout_properties: {grid_position: 'RXAHIM,YGPBUR'}
151 | components:
152 | - type: Spacer
153 | properties: {height: 250}
154 | name: spacer_1
155 | layout_properties: {grid_position: 'ZEUGAL,EUIWPS'}
156 | - type: Label
157 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
158 | visible: true, text: '', font_size: 9, font: '', spacing_above: small, icon_align: left,
159 | spacing_below: small, italic: true, background: '', bold: false, underline: true,
160 | icon: ''}
161 | name: label_version
162 | layout_properties: {grid_position: 'ZYXVJI,VDCHIU'}
163 | data_bindings: []
164 | event_bindings: {click: link_settings_click}
165 |
--------------------------------------------------------------------------------
/client_code/Machines/MachineDetail/MachineRouteRow/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import MachineRouteRowTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 |
9 | class MachineRouteRow(MachineRouteRowTemplate):
10 | def __init__(self, **properties):
11 | # Set Form properties and Data Bindings.
12 | self.init_components(**properties)
13 | self.label_online.text = self.item['machine']['online']
14 | # Any code you write here will run before the form opens.
15 |
--------------------------------------------------------------------------------
/client_code/Machines/MachineDetail/MachineRouteRow/form_template.yaml:
--------------------------------------------------------------------------------
1 | container: {type: DataRowPanel}
2 | components:
3 | - type: Label
4 | properties: {}
5 | name: label_online
6 | layout_properties: {column: PGODDC}
7 | is_package: true
8 |
--------------------------------------------------------------------------------
/client_code/Machines/MachineDetail/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import MachineDetailTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from ... import Startup
9 |
10 | class MachineDetail(MachineDetailTemplate):
11 | def __init__(self, id, machine_row, **properties):
12 | # Set Form properties and Data Bindings.
13 | self.url = Startup.url
14 | self.api_key = Startup.api_key
15 | self.item = anvil.server.call('get_machine_routes', self.url, self.api_key, id)
16 | self.machine = machine_row
17 | self.init_components(**properties)
18 | if self.item == {'routes': []}:
19 | self.column_panel_routes_container.clear()
20 | self.column_panel_routes_container.add_component(Label(text='This machine does not have any routes registered in Headscale.'))
21 | self.drop_down_users.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()]
22 | self.drop_down_users.selected_value = self.machine['user']
23 | else:
24 | self.repeating_panel_machine_routes.items = self.item['routes']
25 | self.drop_down_users.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()]
26 | self.drop_down_users.selected_value = self.machine['user']
27 | # Any code you write here will run before the form opens.
28 |
29 | def button_update_machine_settings_click(self, **event_args):
30 | """This method is called when the button is clicked"""
31 | self.raise_event("x-close-alert", value=True)
32 | if self.text_box_machine_name.tag == True:
33 | anvil.server.call('rename_machine', self.url, self.api_key, self.machine['id'], self.text_box_machine_name.text)
34 | anvil.server.call('move_user', self.url, self. api_key, self.machine['id'], self.drop_down_users.selected_value)
35 | else:
36 | anvil.server.call('move_user', self.url, self. api_key, self.machine['id'], self.drop_down_users.selected_value)
37 |
38 | def text_box_machine_name_change(self, **event_args):
39 | """This method is called when the text in this text box is edited"""
40 | self.text_box_machine_name.tag = True
41 |
42 | def button_delete_machine_click(self, **event_args):
43 | """This method is called when the button is clicked"""
44 | answer = confirm('Are you sure you want to delete ' + self.machine['givenName'])
45 | if answer:
46 | anvil.server.call('delete_machine', self.url, self.api_key, self.machine['id'])
47 | self.machine.delete()
48 | self.raise_event("x-close-alert", value=True)
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/client_code/Machines/MachineDetail/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: GridPanel
6 | properties: {}
7 | name: grid_panel_container
8 | layout_properties: {grid_position: 'ONHNCD,BZYSAX'}
9 | components:
10 | - type: Label
11 | properties: {role: title, align: left, tooltip: '', border: '', foreground: '',
12 | visible: true, text: Routes, font_size: null, font: '', spacing_above: small,
13 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
14 | underline: true, icon: 'fa:map-signs'}
15 | name: label_route
16 | layout_properties: {row: NSJBFV, width_xs: 10, col_xs: 1, width: 803.328125}
17 | - type: ColumnPanel
18 | properties: {col_widths: '{}'}
19 | name: column_panel_routes_container
20 | layout_properties: {row: TISYJI, width_xs: 10, col_xs: 1}
21 | components:
22 | - type: DataGrid
23 | properties:
24 | role: null
25 | columns:
26 | - {id: YDQOGZ, title: Prefixes, data_key: prefix, $$hashKey: 'object:69729'}
27 | - {id: JPGVYI, title: Enabled, data_key: enabled, $$hashKey: 'object:69995'}
28 | - {id: PGODDC, title: Online, data_key: '', $$hashKey: 'object:69997'}
29 | - {id: IHGIYW, title: Advertised, data_key: advertised, $$hashKey: 'object:69999'}
30 | auto_header: true
31 | tooltip: ''
32 | border: ''
33 | foreground: ''
34 | rows_per_page: 20
35 | visible: true
36 | wrap_on: never
37 | show_page_controls: true
38 | spacing_above: small
39 | spacing_below: small
40 | background: ''
41 | name: data_grid_machine_routes
42 | layout_properties: {row: XUMFTA, width_xs: 10, col_xs: 1, width: 803.328125,
43 | grid_position: 'EKACBV,AAEKYR'}
44 | components:
45 | - type: RepeatingPanel
46 | properties: {spacing_above: none, spacing_below: none, item_template: Machines.MachineDetail.MachineRouteRow}
47 | name: repeating_panel_machine_routes
48 | layout_properties: {}
49 | data_bindings: []
50 | - type: ColumnPanel
51 | properties: {col_widths: '{"YRYFIM":15,"VJHIWB":35,"DARJVK":15,"GMHCMT":35,"MPDJXC":30,"KNLIJN":30}'}
52 | name: column_panel_machine_settings
53 | layout_properties: {row: IDCJYU, width_xs: 7, col_xs: 1}
54 | components:
55 | - type: Label
56 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
57 | visible: true, text: 'Machine Name:', font_size: null, font: '', spacing_above: small,
58 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false,
59 | underline: false, icon: ''}
60 | name: label_machine_name
61 | layout_properties: {grid_position: 'NBSRYO,YRYFIM'}
62 | - type: TextBox
63 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: Type a new Machine Name,
64 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null,
65 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false,
66 | background: '', bold: false, underline: false}
67 | name: text_box_machine_name
68 | layout_properties: {grid_position: 'NBSRYO,VJHIWB'}
69 | event_bindings: {change: text_box_machine_name_change}
70 | - type: Button
71 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true,
72 | foreground: '', visible: true, text: Update, font_size: null, font: '', spacing_above: small,
73 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
74 | underline: false, icon: 'fa:floppy-o'}
75 | name: button_update_machine_settings
76 | layout_properties: {grid_position: 'NBSRYO,MPDJXC'}
77 | event_bindings: {click: button_update_machine_settings_click}
78 | - type: Label
79 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
80 | visible: true, text: 'Assigned User:', font_size: null, font: '', spacing_above: small,
81 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false,
82 | underline: false, icon: ''}
83 | name: label_machine_user
84 | layout_properties: {grid_position: 'MRMLUU,DARJVK'}
85 | - type: DropDown
86 | properties: {}
87 | name: drop_down_users
88 | layout_properties: {grid_position: 'MRMLUU,GMHCMT'}
89 | - type: Button
90 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true,
91 | foreground: '', visible: true, text: Delete, font_size: null, font: '', spacing_above: small,
92 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
93 | underline: false, icon: 'fa:trash'}
94 | name: button_delete_machine
95 | layout_properties:
96 | col_widths: {}
97 | grid_position: MRMLUU,KNLIJN
98 | event_bindings: {click: button_delete_machine_click}
99 | is_package: true
100 |
--------------------------------------------------------------------------------
/client_code/Machines/MachineRow/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import MachineRowTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from ..MachineDetail import MachineDetail
9 |
10 | class MachineRow(MachineRowTemplate):
11 | def __init__(self, **properties):
12 | # Set Form properties and Data Bindings.
13 | self.init_components(**properties)
14 |
15 | # Any code you write here will run before the form opens.
16 |
17 | def link_machine_id_click(self, **event_args):
18 | """This method is called when the link is clicked"""
19 | update = alert(MachineDetail(self.item['id'], self.item), large=True, title=self.item['name'], buttons=[])
20 | if update:
21 | self.parent.raise_event('x-refresh')
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client_code/Machines/MachineRow/form_template.yaml:
--------------------------------------------------------------------------------
1 | container: {type: DataRowPanel}
2 | components:
3 | - type: Link
4 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: '',
5 | visible: true, text: '', font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
6 | spacing_above: small, icon_align: left, col_widths: '', spacing_below: small,
7 | italic: false, background: '', bold: false, underline: false, icon: 'fa:desktop'}
8 | name: link_machine_id
9 | layout_properties: {column: RWBSWP}
10 | data_bindings:
11 | - {property: text, code: 'self.item[''id'']'}
12 | event_bindings: {click: link_machine_id_click}
13 | is_package: true
14 |
--------------------------------------------------------------------------------
/client_code/Machines/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import MachinesTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from .. import Startup
9 |
10 | class Machines(MachinesTemplate):
11 | def __init__(self, **properties):
12 | self.url = Startup.url
13 | self.api_key = Startup.api_key
14 | self.item = anvil.server.call('get_machine_table')
15 | self.repeating_panel_machines.set_event_handler('x-refresh', self.refresh_data)
16 | self.init_components(**properties)
17 | self.repeating_panel_machines.items = self.item.search()
18 | self.drop_down_user.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()]
19 | # Any code you write here will run before the form opens.
20 |
21 | def refresh_data(self, **event_args):
22 | anvil.server.call('record_machines')
23 | self.item = anvil.server.call('get_machine_table')
24 | self.repeating_panel_machines.items = self.item.search()
25 |
26 | def button_add_machine_click(self, **event_args):
27 | """This method is called when the button is clicked"""
28 | self.column_panel_add_machine.visible = True
29 |
30 | def button_save_new_machine_click(self, **event_args):
31 | """This method is called when the button is clicked"""
32 | alert(anvil.server.call('register_machine', self.url, self.api_key, self.text_box_nodekey.text, self.drop_down_user.selected_value))
33 | self.refresh_data_bindings()
34 |
35 | def button_cancel_click(self, **event_args):
36 | """This method is called when the button is clicked"""
37 | self.column_panel_add_machine.visible = False
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/client_code/Machines/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
4 | wrap_on: mobile, col_spacing: tiny, spacing_above: small, col_widths: '{}', spacing_below: small,
5 | background: ''}
6 | components:
7 | - type: form:Home.Header
8 | properties: {}
9 | name: header_1
10 | layout_properties: {grid_position: 'SQMKIS,LVQUDV', full_width_row: true}
11 | - type: Label
12 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '',
13 | visible: true, text: Machines, font_size: null, font: '', spacing_above: none,
14 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false,
15 | underline: true, icon: 'fa:desktop'}
16 | name: label_title
17 | layout_properties: {grid_position: 'KJKRCR,AFPXXY'}
18 | - type: DataGrid
19 | properties:
20 | role: null
21 | columns:
22 | - {id: RWBSWP, title: Id, data_key: id, $$hashKey: 'object:686', width: '60',
23 | expand: false}
24 | - {id: IYECDI, title: Name, data_key: givenName, $$hashKey: 'object:672', width: '100',
25 | expand: true}
26 | - {id: UAIMLQ, title: IpAddr, data_key: ipAddr, $$hashKey: 'object:673', expand: false,
27 | width: '150'}
28 | - {id: HLQAWD, title: User, data_key: user, $$hashKey: 'object:685', expand: false,
29 | width: '90'}
30 | - {id: KMRUBB, title: Expiry, data_key: expiry, $$hashKey: 'object:675', expand: true,
31 | width: '175'}
32 | - {id: ATNRNG, title: CreatedAt, data_key: createdAt, $$hashKey: 'object:678',
33 | expand: true, width: '175'}
34 | - {id: WCOZKP, title: Online, data_key: online, $$hashKey: 'object:688', width: '75',
35 | expand: false}
36 | - {id: MHXUUT, title: LastUpdate, data_key: lastSuccessfulUpdate, $$hashKey: 'object:680',
37 | expand: true, width: '175'}
38 | - {id: WWESIH, title: LastSeen, data_key: lastSeen, $$hashKey: 'object:683', expand: true,
39 | width: '175'}
40 | auto_header: true
41 | tooltip: ''
42 | border: ''
43 | foreground: ''
44 | rows_per_page: 20
45 | visible: true
46 | wrap_on: never
47 | show_page_controls: true
48 | spacing_above: small
49 | spacing_below: small
50 | background: ''
51 | name: data_grid_machines
52 | layout_properties: {grid_position: 'NDGYIH,QPSALL', full_width_row: true}
53 | components:
54 | - type: RepeatingPanel
55 | properties: {role: null, tooltip: '', border: '', foreground: '', items: null,
56 | visible: true, spacing_above: none, spacing_below: none, item_template: Machines.MachineRow,
57 | background: ''}
58 | name: repeating_panel_machines
59 | layout_properties: {}
60 | data_bindings: []
61 | - type: GridPanel
62 | properties: {spacing_above: small, spacing_below: none, background: '', foreground: '',
63 | border: '', visible: true, role: null, tooltip: ''}
64 | name: grid_panel_1
65 | layout_properties: {slot: footer}
66 | components:
67 | - type: Button
68 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
69 | foreground: '', visible: true, text: Add Machine, font_size: null, font: '',
70 | spacing_above: small, icon_align: left, spacing_below: none, italic: false,
71 | background: '', bold: false, underline: false, icon: 'fa:desktop'}
72 | name: button_add_machine
73 | layout_properties: {row: UCOPAM, width_xs: 3, col_xs: 0, width: 216.84375}
74 | event_bindings: {click: button_add_machine_click}
75 | - type: ColumnPanel
76 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: false,
77 | wrap_on: mobile, col_spacing: medium, spacing_above: small, col_widths: '{"RANRWU":10,"ZZSNKO":70,"GEFRIH":5,"WGLSKD":20,"NTPOIQ":10,"XEBUHN":10}',
78 | spacing_below: small, background: ''}
79 | name: column_panel_add_machine
80 | layout_properties: {row: ETSIAM, width_xs: 12, col_xs: 0, width: 933.40625}
81 | components:
82 | - type: Label
83 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
84 | visible: true, text: 'NodeKey:', font_size: null, font: '', spacing_above: none,
85 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false,
86 | underline: false, icon: ''}
87 | name: label_1
88 | layout_properties: {grid_position: 'CXZNMP,RANRWU'}
89 | - type: TextBox
90 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: '',
91 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null,
92 | font: '', spacing_above: none, type: text, spacing_below: none, italic: false,
93 | background: '', bold: false, underline: false}
94 | name: text_box_nodekey
95 | layout_properties: {grid_position: 'CXZNMP,ZZSNKO'}
96 | event_bindings: {}
97 | - type: Spacer
98 | properties: {height: 32}
99 | name: spacer_1
100 | layout_properties: {grid_position: 'CXZNMP,XEBUHN'}
101 | - type: Label
102 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
103 | visible: true, text: 'User:', font_size: null, font: '', spacing_above: none,
104 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false,
105 | underline: false, icon: ''}
106 | name: label_user
107 | layout_properties: {grid_position: 'IVFPJK,GEFRIH'}
108 | - type: DropDown
109 | properties:
110 | role: null
111 | align: full
112 | tooltip: ''
113 | placeholder: ''
114 | border: ''
115 | enabled: true
116 | foreground: ''
117 | items: []
118 | visible: true
119 | font_size: null
120 | font: ''
121 | spacing_above: none
122 | spacing_below: none
123 | italic: false
124 | background: ''
125 | bold: false
126 | underline: false
127 | include_placeholder: false
128 | name: drop_down_user
129 | layout_properties: {grid_position: 'IVFPJK,ZCFRPW'}
130 | event_bindings: {}
131 | - type: Button
132 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true,
133 | foreground: '', visible: true, text: Cancel, font_size: null, font: '',
134 | spacing_above: none, icon_align: left, spacing_below: none, italic: false,
135 | background: '', bold: false, underline: false, icon: 'fa:times-circle'}
136 | name: button_cancel
137 | layout_properties: {row: UCOPAM, width_xs: 2, col_xs: 3, width: 137.734375,
138 | grid_position: 'IVFPJK,WGLSKD'}
139 | event_bindings: {click: button_cancel_click}
140 | - type: Button
141 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
142 | foreground: '', visible: true, text: Save, font_size: null, font: '', spacing_above: none,
143 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
144 | underline: false, icon: 'fa:floppy-o'}
145 | name: button_save_new_machine
146 | layout_properties: {grid_position: 'IVFPJK,NTPOIQ'}
147 | event_bindings: {click: button_save_new_machine_click}
148 | is_package: true
149 |
--------------------------------------------------------------------------------
/client_code/Routes/RouteRow/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import RouteRowTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from ... import Startup
9 |
10 | class RouteRow(RouteRowTemplate):
11 | def __init__(self, **properties):
12 | # Set Form properties and Data Bindings.
13 | self.init_components(**properties)
14 |
15 | # Any code you write here will run before the form opens.
16 |
17 | def check_box_enabled_change(self, **event_args):
18 | """This method is called when this checkbox is checked or unchecked"""
19 | self.url = Startup.url
20 | self.api_key = Startup.api_key
21 | anvil.server.call('update_route', self.url, self.api_key, self.item['id'], self.check_box_enabled.checked)
22 | anvil.server.call('record_routes')
23 | self.parent.raise_event('x-refresh')
24 |
25 |
--------------------------------------------------------------------------------
/client_code/Routes/RouteRow/form_template.yaml:
--------------------------------------------------------------------------------
1 | container: {type: DataRowPanel}
2 | components:
3 | - type: CheckBox
4 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, foreground: '',
5 | allow_indeterminate: false, visible: true, text: '', font_size: null, font: '',
6 | spacing_above: small, spacing_below: small, italic: false, background: '', bold: false,
7 | checked: false, underline: false}
8 | name: check_box_enabled
9 | layout_properties: {column: CHTJAW}
10 | data_bindings:
11 | - {property: checked, code: 'self.item[''enabled'']', writeback: false}
12 | event_bindings: {change: check_box_enabled_change}
13 | is_package: true
14 |
--------------------------------------------------------------------------------
/client_code/Routes/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import RoutesTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 |
9 | class Routes(RoutesTemplate):
10 | def __init__(self, **properties):
11 | # Set Form properties and Data Bindings.
12 | self.item = anvil.server.call('get_routes_tables')
13 | self.init_components(**properties)
14 | self.repeating_panel_routes.add_event_handler('x-refresh', self.refresh_data)
15 | self.repeating_panel_routes.items = self.item.search()
16 | # Any code you write here will run before the form opens.
17 |
18 | def refresh_data(self, **event_args):
19 | self.refresh_data_bindings()
20 | self.repeating_panel_routes.items = self.item.search()
21 |
--------------------------------------------------------------------------------
/client_code/Routes/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: form:Home.Header
6 | properties: {}
7 | name: header_1
8 | layout_properties: {grid_position: 'DPISDB,BZBANN', full_width_row: true}
9 | - type: Label
10 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '',
11 | visible: true, text: Routes, font_size: null, font: '', spacing_above: none, icon_align: left,
12 | spacing_below: none, italic: false, background: '', bold: false, underline: true,
13 | icon: 'fa:map-signs'}
14 | name: label_1
15 | layout_properties: {grid_position: 'QZOCQY,ASMRMC'}
16 | - type: DataGrid
17 | properties:
18 | role: null
19 | columns:
20 | - {id: OJSHAX, title: Id, data_key: id, $$hashKey: 'object:1718', width: '50'}
21 | - {id: WJJKDO, title: Prefix, data_key: prefix, $$hashKey: 'object:1719', width: '175',
22 | expand: false}
23 | - {id: RQPBKG, title: Machine Name, data_key: givenName, $$hashKey: 'object:1720',
24 | width: '125', expand: true}
25 | - {id: DEYOGZ, title: MachineIPs, data_key: machineIPs, $$hashKey: 'object:1721',
26 | width: '175', expand: true}
27 | - {id: CHTJAW, title: Enabled, data_key: enabled, $$hashKey: 'object:1722', width: '90'}
28 | auto_header: true
29 | tooltip: ''
30 | border: ''
31 | foreground: ''
32 | rows_per_page: 20
33 | visible: true
34 | wrap_on: never
35 | show_page_controls: true
36 | spacing_above: small
37 | spacing_below: small
38 | background: ''
39 | name: data_grid_routes
40 | layout_properties: {grid_position: 'YDXYZE,QBBMJZ'}
41 | components:
42 | - type: RepeatingPanel
43 | properties: {spacing_above: none, spacing_below: none, item_template: Routes.RouteRow}
44 | name: repeating_panel_routes
45 | layout_properties: {}
46 | is_package: true
47 |
--------------------------------------------------------------------------------
/client_code/Settings/AddUser/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import AddUserTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 |
9 | class AddUser(AddUserTemplate):
10 | def __init__(self, **properties):
11 | # Set Form properties and Data Bindings.
12 | self.item['email'] = ''
13 | self.item['password'] = ''
14 | self.item['enabled'] = ''
15 | self.init_components(**properties)
16 | self.check_box_enabled.checked = True
17 | self.item['enabled'] = True
18 |
19 |
20 | # Any code you write here will run before the form opens.
21 |
22 |
23 |
--------------------------------------------------------------------------------
/client_code/Settings/AddUser/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: GridPanel
6 | properties: {}
7 | name: grid_panel_container
8 | layout_properties: {grid_position: 'TRDWYQ,GUIINP'}
9 | components:
10 | - type: ColumnPanel
11 | properties: {role: card, col_widths: '{"XIGHDZ":15,"HNDHCK":45,"TBHJJZ":15,"VFIWJQ":45}'}
12 | name: card_1
13 | layout_properties: {row: DDPLGN, width_xs: 6, col_xs: 3}
14 | components:
15 | - type: Label
16 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '',
17 | visible: true, text: Add a User, font_size: null, font: '', spacing_above: small,
18 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
19 | underline: true, icon: 'fa:users'}
20 | name: label_1
21 | layout_properties: {grid_position: 'TWHDCI,KOUNMP'}
22 | - type: Label
23 | properties: {role: null, align: center, tooltip: '', border: '', foreground: '',
24 | visible: true, text: 'Email:', font_size: null, font: '', spacing_above: small,
25 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false,
26 | underline: false, icon: ''}
27 | name: label_email
28 | layout_properties: {grid_position: 'VQZNRK,XIGHDZ'}
29 | - type: TextBox
30 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: '',
31 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null,
32 | font: '', spacing_above: small, type: email, spacing_below: small, italic: false,
33 | background: '', bold: false, underline: false}
34 | name: text_box_email
35 | layout_properties: {grid_position: 'VQZNRK,HNDHCK'}
36 | data_bindings:
37 | - {property: text, code: 'self.item[''email'']', writeback: true}
38 | - type: Label
39 | properties: {role: null, align: center, tooltip: '', border: '', foreground: '',
40 | visible: true, text: 'Password:', font_size: null, font: '', spacing_above: small,
41 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false,
42 | underline: false, icon: ''}
43 | name: label_2
44 | layout_properties: {grid_position: 'AFBOQG,TBHJJZ'}
45 | - type: TextBox
46 | properties: {role: null, align: left, hide_text: true, tooltip: '', placeholder: '',
47 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null,
48 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false,
49 | background: '', bold: false, underline: false}
50 | name: text_box_password
51 | layout_properties: {grid_position: 'AFBOQG,VFIWJQ'}
52 | data_bindings:
53 | - {property: text, code: 'self.item[''password'']', writeback: true}
54 | - type: CheckBox
55 | properties: {role: null, align: center, tooltip: '', border: '', enabled: true,
56 | foreground: '', allow_indeterminate: false, visible: true, text: Enable user,
57 | font_size: null, font: '', spacing_above: small, spacing_below: small, italic: false,
58 | background: '', bold: false, checked: true, underline: false}
59 | name: check_box_enabled
60 | layout_properties: {grid_position: 'ERSFCE,BREHIQ'}
61 | data_bindings:
62 | - {property: checked, code: 'self.item[''enabled'']', writeback: true}
63 | event_bindings: {}
64 | is_package: true
65 |
--------------------------------------------------------------------------------
/client_code/Settings/AppUsers/AppUsersTemplate/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import AppUsersTemplateTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 |
9 | class AppUsersTemplate(AppUsersTemplateTemplate):
10 | def __init__(self, **properties):
11 | # Set Form properties and Data Bindings.
12 | self.set_event_handler('x-refresh-child', self.refresh_data)
13 | self.init_components(**properties)
14 |
15 | # Any code you write here will run before the form opens.
16 | def refresh_data(self, **event_args):
17 | self.data_row_panel_view.item = self.item
18 |
19 | def button_edit_click(self, **event_args):
20 | """This method is called when the button is clicked"""
21 | self.data_row_panel_view.visible = False
22 | self.data_row_panel_edit.visible = True
23 |
24 | def button_save_user_edit_click(self, **event_args):
25 | """This method is called when the button is clicked"""
26 | if self.text_box_password.text == '':
27 | anvil.server.call('update_app_user', self.text_box_email.text, self.item, False, None)
28 | else:
29 | anvil.server.call('update_app_user', self.text_box_email.text, self.item, True, self.text_box_password.text)
30 | self.parent.raise_event('x-refresh')
31 | self.data_row_panel_view.visible = True
32 | self.data_row_panel_edit.visible = False
33 |
34 | def button_delete_user_click(self, **event_args):
35 | """This method is called when the button is clicked"""
36 | delete_user = alert('Are you SURE you want to DELETE ' + self.item['email'], buttons=[('Yes', True), ('No', False)])
37 | if delete_user:
38 | with Notification('Deleted User ' + self.item['email'], style='info'):
39 | self.item.delete()
40 | self.parent.raise_event('x-refresh')
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/client_code/Settings/AppUsers/AppUsersTemplate/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: DataRowPanel
3 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: false,
4 | visible: true, font_size: null, font: '', spacing_above: none, spacing_below: none,
5 | italic: false, background: '', bold: false, underline: false}
6 | data_bindings: []
7 | components:
8 | - type: DataRowPanel
9 | properties: {}
10 | name: data_row_panel_view
11 | layout_properties: {column: null}
12 | data_bindings:
13 | - {property: item, code: self.item}
14 | components:
15 | - type: Label
16 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
17 | visible: true, text: '************************', font_size: null, font: '',
18 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
19 | background: '', bold: false, underline: false, icon: ''}
20 | name: label_password
21 | layout_properties: {column: NJORLQ}
22 | - type: CheckBox
23 | properties: {role: null, align: left, tooltip: '', border: '', enabled: false,
24 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null,
25 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '',
26 | bold: false, checked: false, underline: false}
27 | name: check_box_view_enabled
28 | layout_properties: {column: XLHPBD}
29 | data_bindings:
30 | - {property: checked, code: 'self.item[''enabled'']', writeback: false}
31 | - type: CheckBox
32 | properties: {role: null, align: left, tooltip: '', border: '', enabled: false,
33 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null,
34 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '',
35 | bold: false, checked: false, underline: false}
36 | name: check_box_view_conf_email
37 | layout_properties: {column: PZYHXE}
38 | data_bindings:
39 | - {property: checked, code: 'self.item[''confirmed_email'']', writeback: false}
40 | - type: Button
41 | properties: {role: null, align: center, tooltip: '', border: '', enabled: true,
42 | foreground: '', visible: true, text: '', font_size: null, font: '', spacing_above: small,
43 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
44 | underline: false, icon: 'fa:pencil'}
45 | name: button_edit
46 | layout_properties: {column: QFPEZE}
47 | event_bindings: {click: button_edit_click}
48 | - type: DataRowPanel
49 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: true,
50 | visible: false, item: null, font_size: null, font: '', spacing_above: none, spacing_below: none,
51 | italic: false, background: '', bold: false, underline: false}
52 | name: data_row_panel_edit
53 | layout_properties: {column: null}
54 | data_bindings:
55 | - {property: item, code: self.item}
56 | components:
57 | - type: Button
58 | properties: {role: null, align: center, tooltip: '', border: '', enabled: true,
59 | foreground: '', visible: true, text: '', font_size: null, font: '', spacing_above: small,
60 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
61 | underline: false, icon: 'fa:floppy-o'}
62 | name: button_save_user_edit
63 | layout_properties: {column: QFPEZE}
64 | event_bindings: {click: button_save_user_edit_click}
65 | - type: TextBox
66 | properties: {role: null, align: left, hide_text: true, tooltip: '', placeholder: '********************',
67 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null,
68 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false,
69 | background: '', bold: false, underline: false}
70 | name: text_box_password
71 | layout_properties: {column: NJORLQ}
72 | event_bindings: {}
73 | data_bindings: []
74 | - type: CheckBox
75 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true,
76 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null,
77 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '',
78 | bold: false, checked: false, underline: false}
79 | name: check_box_edit_enabled
80 | layout_properties: {column: XLHPBD}
81 | data_bindings:
82 | - {property: checked, code: 'self.item[''enabled'']', writeback: true}
83 | - type: CheckBox
84 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true,
85 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null,
86 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '',
87 | bold: false, checked: false, underline: false}
88 | name: check_box_edit_confirmed_email
89 | layout_properties: {column: PZYHXE}
90 | data_bindings:
91 | - {property: checked, code: 'self.item[''confirmed_email'']', writeback: true}
92 | - type: TextBox
93 | properties: {}
94 | name: text_box_email
95 | layout_properties: {column: EWLQOA}
96 | event_bindings: {}
97 | data_bindings:
98 | - {property: text, code: 'self.item[''email'']', writeback: false}
99 | - type: Button
100 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
101 | foreground: '', visible: true, text: Delete Milliner User, font_size: null,
102 | font: '', spacing_above: small, icon_align: left, spacing_below: small, italic: false,
103 | background: '', bold: false, underline: false, icon: 'fa:trash'}
104 | name: button_delete_user
105 | layout_properties: {column: null}
106 | event_bindings: {click: button_delete_user_click}
107 | is_package: true
108 |
--------------------------------------------------------------------------------
/client_code/Settings/AppUsers/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import AppUsersTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from ..AddUser import AddUser
8 | from anvil.tables import app_tables
9 |
10 | class AppUsers(AppUsersTemplate):
11 | def __init__(self, **properties):
12 | # Set Form properties and Data Bindings.
13 | self.repeating_panel_appusers.set_event_handler('x-refresh', self.refresh_data)
14 | self.users = {}
15 | self.item = anvil.server.call('get_app_users_table').search()
16 | self.init_components(**properties)
17 |
18 | # Any code you write here will run before the form opens.
19 |
20 | def refresh_data(self, **event_args):
21 | self.item = anvil.server.call('get_app_users_table').search()
22 |
23 | def button_add_user_click(self, **event_args):
24 | """This method is called when the button is clicked"""
25 | save = alert(AddUser(item=self.users), buttons=[('Save', True), ('Cancel', False)], large=True)
26 | if save:
27 | with Notification('Adding User ' + self.users['email'], timeout=3, style='success'):
28 | anvil.server.call('add_appuser', self.users['email'], self.users['enabled'], True, self.users['password'])
29 | self.refresh_data()
30 |
31 |
--------------------------------------------------------------------------------
/client_code/Settings/AppUsers/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: form:Home.Header
6 | properties: {}
7 | name: header_1
8 | layout_properties: {grid_position: 'JDDFWY,VKEPGO'}
9 | - type: DataGrid
10 | properties:
11 | role: null
12 | columns:
13 | - {id: XLHPBD, title: Enabled, data_key: enabled, $$hashKey: 'object:224149',
14 | width: '80'}
15 | - {id: EWLQOA, title: Email, data_key: email, $$hashKey: 'object:224148', width: '175',
16 | expand: true}
17 | - {id: NJORLQ, title: Password, data_key: password, $$hashKey: 'object:224164',
18 | width: '175', expand: true}
19 | - {id: BJTNIH, title: Last Login, data_key: last_login, $$hashKey: 'object:224150',
20 | width: '175', expand: true}
21 | - {id: ADBALN, title: Password Failures, data_key: n_password_failures, $$hashKey: 'object:224152',
22 | width: 100, expand: false}
23 | - {id: PZYHXE, title: Confirmed Email, data_key: confirmed_email, $$hashKey: 'object:224153',
24 | width: 90, expand: false}
25 | - {id: QFPEZE, title: '', data_key: '', $$hashKey: 'object:230262', width: '75',
26 | expand: false}
27 | auto_header: true
28 | tooltip: ''
29 | border: ''
30 | foreground: ''
31 | rows_per_page: 20
32 | visible: true
33 | wrap_on: never
34 | show_page_controls: true
35 | spacing_above: small
36 | spacing_below: small
37 | background: ''
38 | name: data_grid_appusers
39 | layout_properties: {grid_position: 'GDBZFO,EBQUII'}
40 | components:
41 | - type: RepeatingPanel
42 | properties: {role: null, tooltip: '', border: '', foreground: '', items: null,
43 | visible: true, spacing_above: none, spacing_below: none, item_template: Settings.AppUsers.AppUsersTemplate,
44 | background: ''}
45 | name: repeating_panel_appusers
46 | layout_properties: {}
47 | data_bindings:
48 | - {property: items, code: self.item}
49 | - type: GridPanel
50 | properties: {}
51 | name: grid_panel_footer
52 | layout_properties: {slot: footer}
53 | components:
54 | - type: Button
55 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
56 | foreground: '', visible: true, text: Add Milliner User, font_size: null, font: '',
57 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
58 | background: '', bold: false, underline: false, icon: 'fa:users'}
59 | name: button_add_user
60 | layout_properties: {row: FFDUTA, width_xs: 4, col_xs: 0, width: 184.34375}
61 | event_bindings: {click: button_add_user_click}
62 | is_package: true
63 |
--------------------------------------------------------------------------------
/client_code/Settings/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import SettingsTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from .AppUsers import AppUsers
9 | from .. import Startup
10 |
11 | class Settings(SettingsTemplate):
12 | def __init__(self, **properties):
13 | self.users = anvil.server.call('get_app_users_table').search()
14 | self.url = Startup.url
15 | self.api_key = Startup.api_key
16 | # Set Form properties and Data Bindings.
17 | self.init_components(**properties)
18 |
19 | # Any code you write here will run before the form opens.
20 |
21 | def button_save_click(self, **event_args):
22 | """This method is called when the button is clicked"""
23 | if app_tables.settings.get() == None:
24 | app_tables.settings.add_row(url=self.url, api_key=self.api_key)
25 | self.refresh_data_bindings()
26 | else:
27 | settings_row = app_tables.settings.get()
28 | settings_row.update(url=self.text_box_url.text, api_key=self.text_box_api_key.text)
29 | self.refresh_data_bindings()
30 |
31 | def button_test_api_click(self, **event_args):
32 | """This method is called when the button is clicked"""
33 | try:
34 | status_returned = anvil.server.call('test_api_key', url=self.url, api_key=self.api_key)
35 | except:
36 | raise Exception('No URL has been saved in Settings')
37 | if status_returned == 200:
38 | status = f'{status_returned} - Connection Succesful'
39 | else:
40 | status = f'Unable to connect - {status_returned}. Please check your API key, URL, and CORS headers'
41 | alert(status, title="API Test Results")
42 |
43 | def button_app_users_click(self, **event_args):
44 | """This method is called when the button is clicked"""
45 | self.clear()
46 | self.add_component(AppUsers())
47 |
48 | def button_renew_api_key_click(self, **event_args):
49 | """This method is called when the button is clicked"""
50 | result, response, new_api_key = anvil.server.call('renew_api_key', url=self.url, api_key=self.api_key)
51 | if result == True and response == 'Key updated and validated':
52 | anvil.server.call('update_api_key_settings', new_api_key)
53 | self.api_key = [r['api_key'] for r in app_tables.settings.search()][0]
54 | Notification(response, timeout=3).show()
55 | elif result == True:
56 | Notification(response, timeout=3).show()
57 | elif result == False:
58 | Notification(response, timeout=3).show()
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/client_code/Settings/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: form:Home.Header
6 | properties: {}
7 | name: header_1
8 | layout_properties: {grid_position: 'MNOOSN,PVTMSV', full_width_row: true}
9 | - type: ColumnPanel
10 | properties: {col_widths: '{}'}
11 | name: column_panel_settings
12 | layout_properties: {grid_position: 'NGXVPF,MZNVVM'}
13 | components:
14 | - type: GridPanel
15 | properties: {spacing_above: small, spacing_below: small, background: '', foreground: '',
16 | border: '', visible: true, role: null, tooltip: ''}
17 | name: grid_panel_1
18 | layout_properties: {grid_position: 'EILTKL,NMJNVE'}
19 | components:
20 | - type: Label
21 | properties: {role: title, align: left, tooltip: '', border: '', foreground: '',
22 | visible: true, text: 'Headscale Server Settings:', font_size: null, font: '',
23 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
24 | background: '', bold: false, underline: true, icon: ''}
25 | name: label_header1
26 | layout_properties: {row: SBYKCF, width_xs: 12, col_xs: 0, width: 750}
27 | - type: Label
28 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
29 | visible: true, text: 'Headscale Server URL:', font_size: null, font: '', spacing_above: small,
30 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false,
31 | underline: false, icon: ''}
32 | name: label_url
33 | layout_properties: {row: QYHVSU, width_xs: 2, col_xs: 0, width: 136.65625}
34 | - type: TextBox
35 | properties: {}
36 | name: text_box_url
37 | layout_properties: {row: QYHVSU, width_xs: 9, col_xs: 2}
38 | data_bindings:
39 | - {property: text, code: self.url, writeback: true}
40 | - type: Label
41 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '',
42 | visible: true, text: 'Headscale API Key:', font_size: null, font: '', spacing_above: small,
43 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false,
44 | underline: false, icon: ''}
45 | name: label_api_key
46 | layout_properties: {row: FGOVGD, width_xs: 2, col_xs: 0, width: 136.65625}
47 | - type: TextBox
48 | properties: {}
49 | name: text_box_api_key
50 | layout_properties: {row: FGOVGD, width_xs: 9, col_xs: 2}
51 | data_bindings:
52 | - {property: text, code: self.api_key, writeback: true}
53 | - type: Button
54 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
55 | foreground: '', visible: true, text: Renew API Key, font_size: null, font: '',
56 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
57 | background: '', bold: false, underline: false, icon: 'fa:refresh'}
58 | name: button_renew_api_key
59 | layout_properties: {row: GILCGU, width_xs: 3, col_xs: 0, width: 229.988}
60 | event_bindings: {click: button_renew_api_key_click}
61 | - type: Button
62 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
63 | foreground: '', visible: true, text: Test API, font_size: null, font: '',
64 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
65 | background: '', bold: false, underline: false, icon: 'fa:cog'}
66 | name: button_test_api
67 | layout_properties: {row: GILCGU, width_xs: 2, col_xs: 3, width: 99.9875}
68 | event_bindings: {click: button_test_api_click}
69 | - type: Button
70 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true,
71 | foreground: '', visible: true, text: Save, font_size: null, font: '', spacing_above: small,
72 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
73 | underline: false, icon: 'fa:floppy-o'}
74 | name: button_save
75 | layout_properties: {row: GILCGU, width_xs: 2, col_xs: 9, width: 136.65625}
76 | event_bindings: {click: button_save_click}
77 | - type: Label
78 | properties: {role: title, align: left, tooltip: '', border: '', foreground: '',
79 | visible: true, text: 'Milliner Settings:', font_size: null, font: '', spacing_above: small,
80 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
81 | underline: true, icon: ''}
82 | name: label_app_users
83 | layout_properties: {row: FMZJBQ, width_xs: 12, col_xs: 0, width: 750}
84 | - type: Button
85 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
86 | foreground: '', visible: true, text: Milliner Users, font_size: null, font: '',
87 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
88 | background: '', bold: false, underline: false, icon: 'fa:users'}
89 | name: button_app_users
90 | layout_properties: {row: YCCUCA, width_xs: 3, col_xs: 0, width: 165}
91 | event_bindings: {click: button_app_users_click}
92 | is_package: true
93 |
--------------------------------------------------------------------------------
/client_code/Startup.py:
--------------------------------------------------------------------------------
1 | import anvil.server
2 | import anvil.users
3 | import anvil.tables as tables
4 | import anvil.tables.query as q
5 | from anvil.tables import app_tables
6 | from anvil import *
7 |
8 | def fresh_install():
9 | fresh_install = anvil.server.call('check_fresh_install')
10 | if not fresh_install['settings_exists']:
11 | url = ''
12 | api_key = ''
13 | return url, api_key
14 | elif fresh_install['settings_exists']:
15 | url = [r['url'] for r in app_tables.settings.search()][0]
16 | api_key = [r['api_key'] for r in app_tables.settings.search()][0]
17 | return url, api_key
18 |
19 | def error_handler(err):
20 | alert(str(err), title="An error has occurred", large=True, dismissible=False)
21 | # 0.1.4 - Headscale API updated machines to nodes, basic API re-write completed.
22 | version = 'v.0.1.4'
23 | url, api_key = fresh_install()
24 | user = anvil.users.get_user()
25 | set_default_error_handling(error_handler)
26 |
27 | def startup():
28 | user = anvil.users.login_with_form()
29 | open_form('Home')
30 |
31 | if __name__ == "__main__":
32 | startup()
33 |
34 |
--------------------------------------------------------------------------------
/client_code/Users/PreAuth/GenerateKey/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import GenerateKeyTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 |
9 | class GenerateKey(GenerateKeyTemplate):
10 | def __init__(self, **properties):
11 | # Set Form properties and Data Bindings
12 | self.init_components(**properties)
13 | self.drop_down_users.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()]
14 |
15 | # Any code you write here will run before the form opens.
16 |
17 | def check_box_ephmeral_change(self, **event_args):
18 | """This method is called when this checkbox is checked or unchecked"""
19 | if self.check_box_ephmeral.checked:
20 | self.item['is_ephemeral'] = 'True'
21 | else:
22 | self.item['is_ephemeral'] = 'False'
23 |
24 | #def check_box_reusable_change(self, **event_args):
25 | # """This method is called when this checkbox is checked or unchecked"""
26 | #if self.check_box_reusable.checked:
27 | #self.item['is_reusable'] = 'True'
28 | #else:
29 | #self.item['is_reusable'] = 'False'
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/client_code/Users/PreAuth/GenerateKey/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | data_bindings: []
5 | components:
6 | - type: GridPanel
7 | properties: {}
8 | name: grid_panel_1
9 | layout_properties: {grid_position: 'SNKWKX,YNQQYM'}
10 | components:
11 | - type: DropDown
12 | properties:
13 | role: null
14 | align: full
15 | tooltip: ''
16 | placeholder: Select User
17 | border: ''
18 | enabled: true
19 | foreground: ''
20 | items: []
21 | visible: true
22 | font_size: null
23 | font: ''
24 | spacing_above: small
25 | spacing_below: small
26 | italic: false
27 | background: ''
28 | bold: false
29 | underline: false
30 | include_placeholder: true
31 | name: drop_down_users
32 | layout_properties: {grid_position: 'FAOQXD,ZRWIGG', row: GQYFHH, width_xs: 12,
33 | col_xs: 0}
34 | data_bindings:
35 | - {property: selected_value, code: 'self.item[''selected_user'']', writeback: true}
36 | event_bindings: {}
37 | - type: DropDown
38 | properties:
39 | role: null
40 | align: full
41 | tooltip: ''
42 | placeholder: ''
43 | border: ''
44 | enabled: true
45 | foreground: ''
46 | items: ['7', '15', '30', '45', '90']
47 | visible: true
48 | font_size: null
49 | font: ''
50 | spacing_above: small
51 | spacing_below: small
52 | italic: false
53 | background: ''
54 | bold: false
55 | underline: false
56 | include_placeholder: false
57 | name: drop_down_timeout
58 | layout_properties: {row: UXZNJQ, width_xs: 12, col_xs: 0, width: 970}
59 | data_bindings:
60 | - {property: selected_value, code: 'self.item[''expiration_date'']', writeback: true}
61 | - type: CheckBox
62 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true,
63 | foreground: '', allow_indeterminate: false, visible: true, text: Ephemeral,
64 | font_size: null, font: '', spacing_above: small, spacing_below: small, italic: false,
65 | background: '', bold: false, checked: false, underline: false}
66 | name: check_box_ephemeral
67 | layout_properties: {row: RKWCJY, width_xs: 1, col_xs: 1, width: 136.667}
68 | data_bindings:
69 | - {property: checked, code: 'self.item[''is_ephemeral'']', writeback: true}
70 | event_bindings: {}
71 | - type: CheckBox
72 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true,
73 | foreground: '', allow_indeterminate: false, visible: true, text: Reusable, font_size: null,
74 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '',
75 | bold: false, checked: false, underline: false}
76 | name: check_box_reusable
77 | layout_properties: {row: RKWCJY, width_xs: 1, col_xs: 5, width: 136.667}
78 | data_bindings:
79 | - {property: checked, code: 'self.item[''is_reusable'']', writeback: true}
80 | event_bindings: {}
81 | is_package: true
82 |
--------------------------------------------------------------------------------
/client_code/Users/PreAuth/PreAuthKeyRow/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import PreAuthKeyRowTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 |
9 | class PreAuthKeyRow(PreAuthKeyRowTemplate):
10 | def __init__(self, **properties):
11 | # Set Form properties and Data Bindings.
12 | self.init_components(**properties)
13 |
14 | # Any code you write here will run before the form opens.
15 |
--------------------------------------------------------------------------------
/client_code/Users/PreAuth/PreAuthKeyRow/form_template.yaml:
--------------------------------------------------------------------------------
1 | container: {type: DataRowPanel}
2 | components:
3 | - type: CheckBox
4 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, foreground: '',
5 | allow_indeterminate: false, visible: true, text: '', font_size: null, font: '',
6 | spacing_above: small, spacing_below: small, italic: false, background: '', bold: false,
7 | checked: false, underline: false}
8 | name: check_box_key
9 | layout_properties: {column: FZRFDK}
10 | is_package: true
11 |
--------------------------------------------------------------------------------
/client_code/Users/PreAuth/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import PreAuthTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from .GenerateKey import GenerateKey
9 | import json
10 | import datetime
11 | from ... import Startup
12 |
13 | class PreAuth(PreAuthTemplate):
14 | def __init__(self, user_name, **properties):
15 | # Set Form properties and Data Bindings.
16 | self.item['selected_user'] = None
17 | self.item['is_reusable'] = False
18 | self.item['is_ephemeral'] = False
19 | self.item['expiration_date'] = '7'
20 | self.user_name = user_name
21 | self.url = Startup.url
22 | self.api_key = Startup.api_key
23 | self.response = anvil.server.call('get_preauth_keys', self.url, self.api_key, user_name)
24 | self.repeating_panel_preauth_keys.items = self.response['preAuthKeys']
25 | self.init_components(**properties)
26 |
27 | # Any code you write here will run before the form opens.
28 |
29 | def get_selected_rows(self):
30 | selected = []
31 | for row_template in self.repeating_panel_preauth_keys.get_components():
32 | if row_template.check_box_key.checked:
33 | selected.append(row_template.item)
34 | return selected
35 |
36 | def button_generate_key_click(self, **event_args):
37 | """This method is called when the button is clicked"""
38 | generate_decision = alert(GenerateKey(item=self.item), title='Select a User to generate a new PreAuth key', buttons=[('Ok', True),('Cancel', False)])
39 | if generate_decision:
40 | # get the current date and time in UTC format
41 | now = datetime.datetime.utcnow()
42 | # add the input number of days to the current date and time
43 | future_date = now + datetime.timedelta(days=int(self.item['expiration_date']))
44 | # convert the future date to ISO 8601 format with UTC timezone
45 | iso_date = future_date.replace(microsecond=0).isoformat() + 'Z'
46 | key_dict = {'user': self.item['selected_user'], 'reusable': self.item['is_reusable'], 'ephemeral': self.item['is_ephemeral'], 'expiration': iso_date}
47 | json_string = json.dumps(key_dict)
48 | return_response = anvil.server.call('add_preauth_key', self.url, self.api_key, json_string)
49 | if return_response['status'] == 'True':
50 | notification_body = return_response['body']['preAuthKey']['key'] + ' generated for user ' + self.user_name + ' and will expire on ' + return_response['body']['preAuthKey']['expiration']
51 | notification_title = 'PreAuth Key Generated Successfully'
52 | else:
53 | notification_body = 'Unable to Generate a PreAuth key for user ' + self.user_name
54 | notification_title = 'Error Occurred'
55 | Notification(notification_body, timeout=6, title=notification_title).show()
56 | self.response = anvil.server.call('get_preauth_keys', self.url, self.api_key, self.user_name)
57 | self.repeating_panel_preauth_keys.items = self.response['preAuthKeys']
58 |
59 | def button_1_click(self, **event_args):
60 | """This method is called when the button is clicked"""
61 | selected = self.get_selected_rows()
62 | for key_rows in selected:
63 | keys = key_rows.get('key')
64 | users = key_rows.get('user')
65 | json_string = '{"user": "'+users+'", "key": "'+keys+'"}'
66 | anvil.server.call('expire_preauth_key', self.url, self.api_key, json_string)
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/client_code/Users/PreAuth/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {col_widths: '{}'}
4 | components:
5 | - type: form:Home.Header
6 | properties: {}
7 | name: header_1
8 | layout_properties: {grid_position: 'WCMTGB,JYNGSM', full_width_row: true}
9 | - type: GridPanel
10 | properties: {spacing_above: small, spacing_below: small, background: '', foreground: '',
11 | border: '', visible: true, role: null, tooltip: ''}
12 | name: grid_panel_container
13 | layout_properties: {grid_position: 'MDEBHZ,ZPUWEO', full_width_row: true}
14 | components:
15 | - type: DataGrid
16 | properties:
17 | role: null
18 | columns:
19 | - {id: PJIVNW, title: Key, data_key: key, $$hashKey: 'object:60009', width: '225',
20 | expand: true}
21 | - {id: WWADRM, title: Expiration, data_key: expiration, $$hashKey: 'object:60010',
22 | expand: true, width: ''}
23 | - {id: OLEACZ, title: Created At, data_key: createdAt, $$hashKey: 'object:60044',
24 | expand: true}
25 | - {id: YKFNKO, title: Ephemeral, data_key: ephemeral, $$hashKey: 'object:60008',
26 | width: '90', expand: false}
27 | - {id: EKKGHH, title: Reusable, data_key: reusable, $$hashKey: 'object:60046',
28 | width: '80'}
29 | - {id: KQKAWX, title: Used, data_key: used, $$hashKey: 'object:60048', width: '70'}
30 | - {id: FZRFDK, title: '', data_key: '', $$hashKey: 'object:62888', width: '40'}
31 | auto_header: true
32 | tooltip: ''
33 | border: ''
34 | foreground: ''
35 | rows_per_page: 20
36 | visible: true
37 | wrap_on: never
38 | show_page_controls: true
39 | spacing_above: small
40 | spacing_below: small
41 | background: ''
42 | name: data_grid_preauth_keys
43 | layout_properties: {grid_position: 'LVOVJJ,TIMEFJ', row: LCKOZS, width_xs: 12,
44 | col_xs: 0, width: 1106}
45 | components:
46 | - type: RepeatingPanel
47 | properties: {spacing_above: none, spacing_below: none, item_template: Users.PreAuth.PreAuthKeyRow}
48 | name: repeating_panel_preauth_keys
49 | layout_properties: {}
50 | - type: GridPanel
51 | properties: {spacing_above: small, spacing_below: none, background: '', foreground: '',
52 | border: '', visible: true, role: null, tooltip: ''}
53 | name: grid_panel_1
54 | layout_properties: {slot: footer}
55 | components:
56 | - type: Button
57 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true,
58 | foreground: '', visible: true, text: Generate Preauth Key, font_size: null,
59 | font: '', spacing_above: none, icon_align: left, spacing_below: none, italic: false,
60 | background: '', bold: false, underline: false, icon: 'fa:refresh'}
61 | name: button_generate_key
62 | layout_properties: {row: YYWVFW, width_xs: 4, col_xs: 0, width: 265.194}
63 | event_bindings: {click: button_generate_key_click}
64 | - type: Button
65 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
66 | foreground: '', visible: true, text: Expire Preauth Key, font_size: null,
67 | font: '', spacing_above: none, icon_align: left, spacing_below: none, italic: false,
68 | background: '', bold: false, underline: false, icon: 'fa:times-circle'}
69 | name: button_1
70 | layout_properties: {row: YYWVFW, width_xs: 4, col_xs: 4, width: 265.194}
71 | event_bindings: {click: button_1_click}
72 | is_package: true
73 |
--------------------------------------------------------------------------------
/client_code/Users/UserRow/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import UserRowTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from ..PreAuth import PreAuth
9 | from ... import Startup
10 |
11 | class UserRow(UserRowTemplate):
12 | def __init__(self, **properties):
13 | self.url = Startup.url
14 | self.api_key = Startup.api_key
15 | # Set Form properties and Data Bindings.
16 | self.init_components(**properties)
17 |
18 | # Any code you write here will run before the form opens.
19 |
20 | def button_cancel_click(self, **event_args):
21 | """This method is called when the button is clicked"""
22 | self.data_row_panel_edit.visible = False
23 | self.data_row_panel_view.visible = True
24 |
25 | def button_update_click(self, **event_args):
26 | """This method is called when the button is clicked"""
27 | pass
28 |
29 | def button_delete_user_click(self, **event_args):
30 | """This method is called when the button is clicked"""
31 | anvil.server.call('delete_user', self.url, self.api_key, self.item['name'])
32 | self.item.delete()
33 | #anvil.server.call('delete_hs_table_row', self.item)
34 | self.parent.raise_event('x-refresh')
35 |
36 | def link_id_click(self, **event_args):
37 | """This method is called when the link is clicked"""
38 | self.data_row_panel_view.visible = False
39 | self.data_row_panel_edit.visible = True
40 |
41 | def button_preauth_keys_click(self, **event_args):
42 | """This method is called when the button is clicked"""
43 | form = get_open_form()
44 | form.column_panel_home.clear()
45 | form.column_panel_home.add_component(PreAuth(self.label_name.text))
46 | #alert(PreAuth(self.label_name.text), large=True)
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/client_code/Users/UserRow/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: DataRowPanel
3 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: false,
4 | visible: true, font_size: null, font: '', spacing_above: none, spacing_below: none,
5 | italic: false, background: '', bold: false, underline: false}
6 | components:
7 | - type: DataRowPanel
8 | properties: {role: null, align: left, tooltip: '', border: none, foreground: '',
9 | auto_display_data: true, visible: true, item: null, font_size: null, font: '',
10 | spacing_above: none, spacing_below: none, italic: false, background: '', bold: false,
11 | underline: false}
12 | name: data_row_panel_view
13 | layout_properties: {column: null}
14 | components:
15 | - type: Link
16 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: '',
17 | visible: true, text: '', font_size: null, wrap_on: mobile, font: '', col_spacing: medium,
18 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small,
19 | italic: false, background: '', bold: false, underline: false, icon: 'fa:user'}
20 | name: link_id
21 | layout_properties: {column: GQMYHE}
22 | data_bindings:
23 | - {property: text, code: 'self.item[''id'']'}
24 | components: []
25 | event_bindings: {click: link_id_click}
26 | - type: Label
27 | properties: {}
28 | name: label_name
29 | layout_properties: {column: VHVAOD}
30 | data_bindings:
31 | - {property: text, code: 'self.item[''name'']'}
32 | data_bindings:
33 | - {property: item, code: self.item}
34 | - type: DataRowPanel
35 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: true,
36 | visible: false, item: null, font_size: null, font: '', spacing_above: none, spacing_below: none,
37 | italic: false, background: '', bold: false, underline: false}
38 | name: data_row_panel_edit
39 | layout_properties: {column: null}
40 | data_bindings:
41 | - {property: item, code: self.item}
42 | components:
43 | - type: TextBox
44 | properties: {}
45 | name: text_box_username
46 | layout_properties: {column: VHVAOD}
47 | data_bindings:
48 | - {property: text, code: 'self.item[''name'']', writeback: false}
49 | - type: GridPanel
50 | properties: {spacing_above: none, spacing_below: none, background: '', foreground: '',
51 | border: '', visible: true, role: null, tooltip: ''}
52 | name: grid_panel_1
53 | layout_properties: {column: null}
54 | components:
55 | - type: ColumnPanel
56 | properties: {col_widths: '{}'}
57 | name: column_panel_1
58 | layout_properties: {row: JGAIMY, width_xs: 5, col_xs: 0}
59 | components:
60 | - type: Button
61 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true,
62 | foreground: '', visible: true, text: Delete User, font_size: null, font: '',
63 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
64 | background: '', bold: false, underline: false, icon: 'fa:trash'}
65 | name: button_delete_user
66 | layout_properties: {row: JGAIMY, width_xs: 3, col_xs: 0, width: 158.5, grid_position: 'BICXUX,RLVHZW'}
67 | event_bindings: {click: button_delete_user_click}
68 | - type: Button
69 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
70 | foreground: '', visible: true, text: Pre-Auth Keys, font_size: null, font: '',
71 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
72 | background: '', bold: false, underline: false, icon: 'fa:key'}
73 | name: button_preauth_keys
74 | layout_properties: {row: JGAIMY, width_xs: 3, col_xs: 3, width: 158.5, grid_position: 'BICXUX,JHSEFB'}
75 | event_bindings: {click: button_preauth_keys_click}
76 | - type: Button
77 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true,
78 | foreground: '', visible: true, text: Cancel, font_size: null, font: '', spacing_above: small,
79 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
80 | underline: false, icon: 'fa:times-circle'}
81 | name: button_cancel
82 | layout_properties: {grid_position: 'GWPDGT,YBCQHC', row: JGAIMY, width_xs: 2,
83 | col_xs: 8, width: 132.328125}
84 | event_bindings: {click: button_cancel_click}
85 | - type: Button
86 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
87 | foreground: '', visible: true, text: Update, font_size: null, font: '', spacing_above: small,
88 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false,
89 | underline: false, icon: 'fa:floppy-o'}
90 | name: button_update
91 | layout_properties: {grid_position: 'GWPDGT,UWURVQ', row: JGAIMY, width_xs: 2,
92 | col_xs: 10, width: 132.328125}
93 | event_bindings: {click: button_update_click}
94 | is_package: true
95 |
--------------------------------------------------------------------------------
/client_code/Users/__init__.py:
--------------------------------------------------------------------------------
1 | from ._anvil_designer import UsersTemplate
2 | from anvil import *
3 | import anvil.server
4 | import anvil.users
5 | import anvil.tables as tables
6 | import anvil.tables.query as q
7 | from anvil.tables import app_tables
8 | from .. import Startup
9 | class Users(UsersTemplate):
10 | def __init__(self, **properties):
11 | self.url = Startup.url
12 | self.api_key = Startup.api_key
13 | self.item = anvil.server.call('get_hs_users_table').search()
14 | self.init_components(**properties)
15 | self.repeating_panel_users.set_event_handler('x-refresh', self.refresh_data)
16 | #self.repeating_panel_users.items = self.item.search()
17 | # Any code you write here will run before the form opens.
18 |
19 | def refresh_data(self, **event_args):
20 | self.item = anvil.server.call('get_hs_users_table').search()
21 |
22 | def button_save_new_user_click(self, **event_args):
23 | """This method is called when the button is clicked"""
24 | user_name = self.text_box_username.text
25 | json_string = '{"name": "'+user_name+'"}'
26 | status = anvil.server.call('add_user', self.url, self.api_key, json_string)
27 | if status == True:
28 | Notification(f'Username {self.text_box_username.text} added succesfully').show()
29 | anvil.server.call('record_users')
30 | self.item = anvil.server.call('get_hs_users_table').search()
31 | self.column_panel_new_user.visible = False
32 | self.button_add_user.visible = True
33 | else:
34 | Notification('Unable to add user to Headscale').show()
35 | self.column_panel_new_user.visible = False
36 | self.button_add_user.visible = True
37 |
38 | def button_add_user_click(self, **event_args):
39 | """This method is called when the button is clicked"""
40 | self.button_add_user.visible = False
41 | self.column_panel_new_user.visible = True
42 |
43 | def button_cancel_click(self, **event_args):
44 | """This method is called when the button is clicked"""
45 | self.column_panel_new_user.visible = False
46 | self.button_add_user.visible = True
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/client_code/Users/form_template.yaml:
--------------------------------------------------------------------------------
1 | container:
2 | type: ColumnPanel
3 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true,
4 | wrap_on: mobile, col_spacing: tiny, spacing_above: small, col_widths: '{}', spacing_below: small,
5 | background: ''}
6 | data_bindings: []
7 | components:
8 | - type: form:Home.Header
9 | properties: {}
10 | name: header_1
11 | layout_properties: {grid_position: 'ZGVNOS,YOROMF', full_width_row: true}
12 | - type: Label
13 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '',
14 | visible: true, text: Headscale Users, font_size: null, font: '', spacing_above: none,
15 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false,
16 | underline: true, icon: 'fa:users'}
17 | name: label_title
18 | layout_properties: {grid_position: 'KJKRCR,AFPXXY'}
19 | - type: DataGrid
20 | properties:
21 | role: null
22 | columns:
23 | - {id: GQMYHE, title: Id, data_key: id, $$hashKey: 'object:11412', width: '60'}
24 | - {id: VHVAOD, title: Name, data_key: name, $$hashKey: 'object:11413'}
25 | - {id: JFSQGB, title: CreatedAt, data_key: createdAt, $$hashKey: 'object:11414'}
26 | auto_header: true
27 | tooltip: ''
28 | border: ''
29 | foreground: ''
30 | rows_per_page: 20
31 | visible: true
32 | wrap_on: never
33 | show_page_controls: true
34 | spacing_above: small
35 | spacing_below: small
36 | background: ''
37 | name: data_grid_1
38 | layout_properties: {grid_position: 'AMHIAO,EPUTYC'}
39 | components:
40 | - type: RepeatingPanel
41 | properties: {spacing_above: none, spacing_below: none, item_template: Users.UserRow}
42 | name: repeating_panel_users
43 | layout_properties: {}
44 | data_bindings:
45 | - {property: items, code: self.item}
46 | - type: GridPanel
47 | properties: {spacing_above: small, spacing_below: none, background: '', foreground: '',
48 | border: '', visible: true, role: null, tooltip: ''}
49 | name: grid_panel_footer
50 | layout_properties: {slot: footer, grid_position: 'KNMQKZ,ZTRYOJ'}
51 | components:
52 | - type: Button
53 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
54 | foreground: '', visible: true, text: Add User, font_size: null, font: '',
55 | spacing_above: small, icon_align: left, spacing_below: none, italic: false,
56 | background: '', bold: false, underline: false, icon: 'fa:user'}
57 | name: button_add_user
58 | layout_properties: {row: LJSSCW, width_xs: 3, col_xs: 0, width: 184.34375}
59 | event_bindings: {click: button_add_user_click}
60 | - type: ColumnPanel
61 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: false,
62 | wrap_on: mobile, col_spacing: medium, spacing_above: none, col_widths: '{"TAWCJI":10,"VYGRLX":35,"WCRMFS":13,"ITNSKN":15}',
63 | spacing_below: none, background: ''}
64 | name: column_panel_new_user
65 | layout_properties: {column: VHVAOD, slot: footer, row: LJSSCW, width_xs: 9,
66 | col_xs: 0, width: 613.046875}
67 | components:
68 | - type: Label
69 | properties: {role: null, align: center, tooltip: '', border: '', foreground: '',
70 | visible: true, text: 'Username:', font_size: null, font: '', spacing_above: small,
71 | icon_align: left, spacing_below: small, italic: false, background: '', bold: true,
72 | underline: true, icon: ''}
73 | name: label_username
74 | layout_properties: {grid_position: 'YXLDXX,TAWCJI'}
75 | - type: TextBox
76 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: Type a new Username,
77 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null,
78 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false,
79 | background: '', bold: false, underline: false}
80 | name: text_box_username
81 | layout_properties: {grid_position: 'YXLDXX,VYGRLX'}
82 | - type: Button
83 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true,
84 | foreground: '', visible: true, text: Cancel, font_size: null, font: '',
85 | spacing_above: small, icon_align: left, spacing_below: small, italic: false,
86 | background: '', bold: false, underline: false, icon: 'fa:times-circle'}
87 | name: button_cancel
88 | layout_properties: {grid_position: 'YXLDXX,ITNSKN'}
89 | event_bindings: {click: button_cancel_click}
90 | - type: Button
91 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true,
92 | foreground: '', visible: true, text: Save, font_size: null, font: '', spacing_above: small,
93 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false,
94 | underline: false, icon: 'fa:floppy-o'}
95 | name: button_save_new_user
96 | layout_properties: {row: MVIIHF, width_xs: 5, col_xs: 0, width: 122.484375,
97 | grid_position: 'YXLDXX,WCRMFS'}
98 | event_bindings: {click: button_save_new_user_click}
99 | is_package: true
100 |
--------------------------------------------------------------------------------
/server_code/API_Interface.py:
--------------------------------------------------------------------------------
1 | import anvil.tables as tables
2 | from anvil.tables import app_tables
3 | import anvil.server
4 | import requests
5 | from datetime import timedelta, date, datetime
6 | from dateutil import parser
7 |
8 | ##################################################################
9 | # Functions related to HEADSCALE and API KEYS
10 | ##################################################################
11 | # Tests the API key
12 | @anvil.server.callable
13 | def test_api_key(url, api_key):
14 | response = requests.get(
15 | str(url)+"/api/v1/apikey",
16 | headers={
17 | 'Accept': 'application/json',
18 | 'Authorization': 'Bearer '+str(api_key)
19 | }
20 | )
21 | return response.status_code
22 |
23 | # Expires an API key
24 | @anvil.server.callable
25 | def expire_key(url, api_key):
26 | payload = {'prefix':str(api_key[0:10])}
27 | json_payload=json.dumps(payload)
28 | print("Sending the payload '"+str(json_payload)+"' to the headscale server")
29 |
30 | response = requests.post(
31 | str(url)+"/api/v1/apikey/expire",
32 | data=json_payload,
33 | headers={
34 | 'Accept': 'application/json',
35 | 'Content-Type': 'application/json',
36 | 'Authorization': 'Bearer '+str(api_key)
37 | }
38 | )
39 | return response.status_code
40 |
41 | # Checks if the key needs to be renewed
42 | # If it does, renews the key, then expires the old key
43 | @anvil.server.callable
44 | def renew_api_key(url, api_key):
45 | # 0 = Key has been updated or key is not in need of an update
46 | # 1 = Key has failed validity check or has failed to write the API key
47 | # Check when the key expires and compare it to todays date:
48 | key_info = get_api_key_info(url, api_key)
49 | expiration_time = key_info["expiration"]
50 | today_date = date.today()
51 | expire = parser.parse(expiration_time)
52 | expire_fmt = str(expire.year) + "-" + str(expire.month).zfill(2) + "-" + str(expire.day).zfill(2)
53 | expire_date = date.fromisoformat(expire_fmt)
54 | delta = expire_date - today_date
55 | tmp = today_date + timedelta(days=90)
56 | new_expiration_date = str(tmp)+"T00:00:00.000000Z"
57 |
58 | # If the delta is less than 5 days, renew the key:
59 | if delta < timedelta(days=5):
60 | print("Key is about to expire. Delta is "+str(delta))
61 | payload = {'expiration':str(new_expiration_date)}
62 | json_payload=json.dumps(payload)
63 | print("Sending the payload '"+str(json_payload)+"' to the headscale server")
64 |
65 | response = requests.post(
66 | str(url)+"/api/v1/apikey",
67 | data=json_payload,
68 | headers={
69 | 'Accept': 'application/json',
70 | 'Content-Type': 'application/json',
71 | 'Authorization': 'Bearer '+str(api_key)
72 | }
73 | )
74 | new_key = response.json()
75 | print("JSON: "+json.dumps(new_key))
76 | print("New Key is: "+new_key["apiKey"])
77 | api_key_test = test_api_key(url, new_key["apiKey"])
78 | print("Testing the key: "+str(api_key_test))
79 | # Test if the new key works:
80 | if api_key_test == 200:
81 | print("The new key is valid and we are writing it to the file")
82 | if not set_api_key(new_key["apiKey"]):
83 | print("We failed writing the new key!")
84 | return False, 'Key write failed', ''
85 | print("Key validated and written. Moving to expire the key.")
86 | expire_key(url, api_key)
87 | return True, 'Key updated and validated', new_key['apiKey']
88 | else:
89 | print("Testing the API key failed.")
90 | return False, 'The API Key test failed', ''
91 | else: return True, 'No Update Required', '' # No work is required
92 |
93 | # Gets information about the current API key
94 | @anvil.server.callable
95 | def get_api_key_info(url, api_key):
96 | print("Getting API key information")
97 | response = requests.get(
98 | str(url)+"/api/v1/apikey",
99 | headers={
100 | 'Accept': 'application/json',
101 | 'Authorization': 'Bearer '+str(api_key)
102 | }
103 | )
104 | json_response = response.json()
105 | # Find the current key in the array:
106 | key_prefix = str(api_key[0:10])
107 | print("Looking for valid API Key...")
108 | for key in json_response["apiKeys"]:
109 | if key_prefix == key["prefix"]:
110 | print("Key found.")
111 | return key
112 | print("Could not find a valid key in Headscale. Need a new API key.")
113 | return "Key not found"
114 |
115 | ##################################################################
116 | # Functions related to MACHINES
117 | ##################################################################
118 |
119 | # register a new machine
120 | @anvil.server.callable
121 | def register_machine(url, api_key, machine_key, user):
122 | print("Registering machine %s to user %s", str(machine_key), str(user))
123 | response = requests.post(
124 | str(url)+"/api/v1/node/register?user="+str(user)+"&key="+str(machine_key),
125 | headers={
126 | 'Accept': 'application/json',
127 | 'Authorization': 'Bearer '+str(api_key)
128 | }
129 | )
130 | return response.json()
131 |
132 |
133 | # Sets the machines tags
134 | @anvil.server.callable
135 | def set_machine_tags(url, api_key, machine_id, tags_list):
136 | print("Setting machine_id %s tag %s", str(machine_id), str(tags_list))
137 | response = requests.post(
138 | str(url)+"/api/v1/node/"+str(machine_id)+"/tags",
139 | data=tags_list,
140 | headers={
141 | 'Accept': 'application/json',
142 | 'Content-Type': 'application/json',
143 | 'Authorization': 'Bearer '+str(api_key)
144 | }
145 | )
146 | return response.json()
147 |
148 | # Moves machine_id to user "new_user"
149 | @anvil.server.callable
150 | def move_user(url, api_key, machine_id, new_user):
151 | print("Moving machine_id %s to user %s", str(machine_id), str(new_user))
152 | response = requests.post(
153 | str(url)+"/api/v1/node/"+str(machine_id)+"/user?user="+str(new_user),
154 | headers={
155 | 'Accept': 'application/json',
156 | 'Authorization': 'Bearer '+str(api_key)
157 | }
158 | )
159 | return response.json()
160 | @anvil.server.callable
161 | def update_route(url, api_key, route_id, current_state):
162 | if current_state == False:
163 | action = "disable"
164 | elif current_state == True:
165 | action = "enable"
166 | print(f"Updating Route {route_id}: Action: {action}")
167 |
168 | # Debug
169 | print("URL: "+str(url))
170 | print("Route ID: "+str(route_id))
171 | print("Current State: "+str(current_state))
172 | print("Action to take: "+str(action))
173 |
174 | response = requests.post(
175 | str(url)+"/api/v1/routes/"+str(route_id)+"/"+str(action),
176 | headers={
177 | 'Accept': 'application/json',
178 | 'Authorization': 'Bearer '+str(api_key)
179 | }
180 | )
181 | return response.json()
182 |
183 | @anvil.server.callable
184 | # Get all machines on the Headscale network
185 | def get_machines(url, api_key):
186 | response = requests.get(
187 | str(url)+"/api/v1/node",
188 | headers={
189 | 'Accept': 'application/json',
190 | 'Authorization': 'Bearer '+str(api_key)
191 | }
192 | )
193 | return response.json()
194 |
195 | @anvil.server.callable
196 | # Get machine with "machine_id" on the Headscale network
197 | def get_machine_info(url, api_key, machine_id):
198 | print("Getting information for machine ID %s", str(machine_id))
199 | response = requests.get(
200 | str(url)+"/api/v1/node/"+str(machine_id),
201 | headers={
202 | 'Accept': 'application/json',
203 | 'Authorization': 'Bearer '+str(api_key)
204 | }
205 | )
206 | return response.json()
207 |
208 | @anvil.server.callable
209 | # Delete a machine from Headscale
210 | def delete_machine(url, api_key, machine_id):
211 | print("Deleting machine %s", str(machine_id))
212 | response = requests.delete(
213 | str(url)+"/api/v1/node/"+str(machine_id),
214 | headers={
215 | 'Accept': 'application/json',
216 | 'Authorization': 'Bearer '+str(api_key)
217 | }
218 | )
219 | status = "True" if response.status_code == 200 else "False"
220 | if response.status_code == 200:
221 | print("Machine deleted.")
222 | else:
223 | print("Deleting machine failed! %s", str(response.json()))
224 | return {"status": status, "body": response.json()}
225 |
226 | @anvil.server.callable
227 | # Rename "machine_id" with name "new_name"
228 | def rename_machine(url, api_key, machine_id, new_name):
229 | print("Renaming machine %s", str(machine_id))
230 | response = requests.post(
231 | str(url)+"/api/v1/node/"+str(machine_id)+"/rename/"+str(new_name),
232 | headers={
233 | 'Accept': 'application/json',
234 | 'Authorization': 'Bearer '+str(api_key)
235 | }
236 | )
237 | status = "True" if response.status_code == 200 else "False"
238 | if response.status_code == 200:
239 | print("Machine renamed")
240 | else:
241 | print("Machine rename failed! %s", str(response.json()))
242 | return {"status": status, "body": response.json()}
243 |
244 | @anvil.server.callable
245 | # Gets routes for the passed machine_id
246 | def get_machine_routes(url, api_key, machine_id):
247 | print("Getting routes for machine %s", str(machine_id))
248 | response = requests.get(
249 | str(url)+"/api/v1/node/"+str(machine_id)+"/routes",
250 | headers={
251 | 'Accept': 'application/json',
252 | 'Authorization': 'Bearer '+str(api_key)
253 | }
254 | )
255 | if response.status_code == 200:
256 | print("Routes obtained")
257 | else:
258 | print("Failed to get routes: %s", str(response.json()))
259 | return response.json()
260 |
261 | @anvil.server.callable
262 | # Gets routes for the entire tailnet
263 | def get_routes(url, api_key):
264 | print("Getting routes")
265 | response = requests.get(
266 | str(url)+"/api/v1/routes",
267 | headers={
268 | 'Accept': 'application/json',
269 | 'Authorization': 'Bearer '+str(api_key)
270 | }
271 | )
272 | return response.json()
273 | ##################################################################
274 | # Functions related to USERS
275 | ##################################################################
276 |
277 | @anvil.server.callable
278 | # Get all users in use
279 | def get_users(url, api_key):
280 | print("Getting Users")
281 | response = requests.get(
282 | str(url)+"/api/v1/user",
283 | headers={
284 | 'Accept': 'application/json',
285 | 'Authorization': 'Bearer '+str(api_key)
286 | }
287 | )
288 | return response.json()
289 |
290 | @anvil.server.callable
291 | # Rename "old_name" with name "new_name"
292 | def rename_user(url, api_key, old_name, new_name):
293 | print("Renaming user %s to %s.", str(old_name), str(new_name))
294 | response = requests.post(
295 | str(url)+"/api/v1/user/"+str(old_name)+"/rename/"+str(new_name),
296 | headers={
297 | 'Accept': 'application/json',
298 | 'Authorization': 'Bearer '+str(api_key)
299 | }
300 | )
301 | status = "True" if response.status_code == 200 else "False"
302 | if response.status_code == 200:
303 | print("User renamed.")
304 | else:
305 | print("Renaming User failed!")
306 | return {"status": status, "body": response.json()}
307 |
308 | @anvil.server.callable
309 | # Delete a user from Headscale
310 | def delete_user(url, api_key, user_name):
311 | print("Deleting a User: %s", str(user_name))
312 | response = requests.delete(
313 | str(url)+"/api/v1/user/"+str(user_name),
314 | headers={
315 | 'Accept': 'application/json',
316 | 'Authorization': 'Bearer '+str(api_key)
317 | }
318 | )
319 | status = "True" if response.status_code == 200 else "False"
320 | if response.status_code == 200:
321 | print("User deleted.")
322 | else:
323 | print("Deleting User failed!")
324 | return {"status": status, "body": response.json()}
325 |
326 | @anvil.server.callable
327 | # Add a user from Headscale
328 | def add_user(url, api_key, data):
329 | print("Adding user: %s", str(data))
330 | response = requests.post(
331 | str(url)+"/api/v1/user",
332 | data=data,
333 | headers={
334 | 'Accept': 'application/json',
335 | 'Content-Type': 'application/json',
336 | 'Authorization': 'Bearer '+str(api_key)
337 | }
338 | )
339 | status = True if response.status_code == 200 else False
340 | if response.status_code == 200:
341 | print("User added.")
342 | else:
343 | print("Adding User failed!")
344 | return status
345 |
346 | ##################################################################
347 | # Functions related to PREAUTH KEYS in USERS
348 | ##################################################################
349 |
350 | @anvil.server.callable
351 | # Get all PreAuth keys associated with a user "user_name"
352 | def get_preauth_keys(url, api_key, user_name):
353 | print("Getting PreAuth Keys in User %s", str(user_name))
354 | response = requests.get(
355 | str(url)+"/api/v1/preauthkey?user="+str(user_name),
356 | headers={
357 | 'Accept': 'application/json',
358 | 'Authorization': 'Bearer '+str(api_key)
359 | }
360 | )
361 | return response.json()
362 |
363 | @anvil.server.callable
364 | # Add a preauth key to the user "user_name" given the booleans "ephemeral"
365 | # and "reusable" with the expiration date "date" contained in the JSON payload "data"
366 | def add_preauth_key(url, api_key, data):
367 | print("Adding PreAuth Key: %s", str(data))
368 | response = requests.post(
369 | str(url)+"/api/v1/preauthkey",
370 | data=data,
371 | headers={
372 | 'Accept': 'application/json',
373 | 'Content-Type': 'application/json',
374 | 'Authorization': 'Bearer '+str(api_key)
375 | }
376 | )
377 | status = "True" if response.status_code == 200 else "False"
378 | if response.status_code == 200:
379 | print("PreAuth Key added.")
380 | else:
381 | print("Adding PreAuth Key failed!")
382 | print(response)
383 | return {"status": status, "body": response.json()}
384 |
385 | @anvil.server.callable
386 | # Expire a pre-auth key. data is {"user": "string", "key": "string"}
387 | def expire_preauth_key(url, api_key, data):
388 | print("Expiring PreAuth Key...")
389 | response = requests.post(
390 | str(url)+"/api/v1/preauthkey/expire",
391 | data=data,
392 | headers={
393 | 'Accept': 'application/json',
394 | 'Content-Type': 'application/json',
395 | 'Authorization': 'Bearer '+str(api_key)
396 | }
397 | )
398 | status = "True" if response.status_code == 200 else "False"
399 | print("expire_preauth_key - Return: "+str(response.json()))
400 | print("expire_preauth_key - Status: "+str(status))
401 | return {"status": status, "body": response.json()}
402 |
--------------------------------------------------------------------------------
/server_code/Hostess.py:
--------------------------------------------------------------------------------
1 | import anvil.tables as tables
2 | from anvil.tables import app_tables
3 | import anvil.server
4 |
5 | @anvil.server.callable
6 | def get_machine_table():
7 | return app_tables.machines.client_writable_cascade()
8 |
9 | @anvil.server.callable
10 | def get_hs_users_table():
11 | return app_tables.hs_users.client_writable_cascade()
12 |
13 | @anvil.server.callable
14 | def get_routes_tables():
15 | return app_tables.routes.client_writable_cascade()
16 |
17 | @anvil.server.callable
18 | @tables.in_transaction
19 | def check_fresh_install():
20 | if len(app_tables.users.search()) == 0:
21 | app_tables.users.add_row(confirmed_email=True, email='admin@milliner.login', enabled=True, password_hash='$2y$10$QoAnY4j687bSu/DI/C4n7.Wm2kLIT8fIyCid8406DvqYuBJayaFUK')
22 | created_user = True
23 | else:
24 | created_user = False
25 | if len(app_tables.settings.search()) == 0:
26 | settings_exists = False
27 | else:
28 | settings_exists = True
29 | return {'created_user': created_user, 'settings_exists': settings_exists}
30 |
31 | @anvil.server.callable
32 | def get_app_users_table():
33 | return app_tables.users.client_writable_cascade()
34 |
35 | @anvil.server.callable
36 | def update_app_user(email, item, pw_update, password):
37 | appuser_row = app_tables.users.get_by_id(item.get_id())
38 | if pw_update == False:
39 | appuser_row.update(email=email)
40 | elif pw_update == True:
41 | import bcrypt
42 | password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
43 | appuser_row.update(email=email, password_hash=password_hash.decode('utf-8'))
44 |
45 | @anvil.server.callable
46 | def add_appuser(email, enabled, confirmed_email, password):
47 | import bcrypt
48 | password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
49 | app_tables.users.add_row(email=email, enabled=enabled, confirmed_email=confirmed_email, password_hash=password_hash.decode('utf-8'))
50 |
51 | @anvil.server.callable
52 | def delete_hs_table_row(item):
53 | item.delete()
54 |
55 | @anvil.server.callable
56 | def delete_machine_row(machine_id):
57 | machine_row = app_tables.machines.get(id=machine_id)
58 | machine_row.delete()
59 |
60 | @anvil.server.callable
61 | def update_api_key_settings(new_api_key):
62 | app_tables.settings.get().update(api_key=new_api_key)
63 |
64 |
--------------------------------------------------------------------------------
/server_code/RecordRecorder.py:
--------------------------------------------------------------------------------
1 | import anvil.tables as tables
2 | from anvil.tables import app_tables
3 | import anvil.server
4 | import json
5 |
6 | @anvil.server.callable
7 | @anvil.server.background_task
8 | def record_machines():
9 | fresh_install = anvil.server.call('check_fresh_install')
10 | if not fresh_install['settings_exists']:
11 | url = ''
12 | api_key = ''
13 | elif fresh_install['settings_exists']:
14 | url = [r['url'] for r in app_tables.settings.search()][0]
15 | api_key = [r['api_key'] for r in app_tables.settings.search()][0]
16 | import datetime
17 | now = datetime.datetime.now()
18 | date_string = now.strftime("%Y-%m-%d")
19 | time_string =now.strftime("%H:%M:%S")
20 | data=anvil.server.call('get_machines', url, api_key)
21 | for machine in data['nodes']:
22 | machine_row = app_tables.machines.get(id=machine['id'])
23 | if machine_row:
24 | machine_row.update(name=machine['name'], id=machine['id'], ipAddr=machine['ipAddresses'],
25 | online=machine['online'],
26 | user= machine['user']['name'],
27 | lastSeen= machine['lastSeen'],
28 | expiry= machine['expiry'],
29 | registerMethod= machine['registerMethod'],
30 | discoKey= machine['discoKey'],
31 | createdAt= machine['createdAt'],
32 | invalidTags= machine['invalidTags'],
33 | validTags= machine['validTags'],
34 | machineKey= machine['machineKey'],
35 | lastSuccessfulUpdate= machine['lastSuccessfulUpdate'],
36 | givenName= machine['givenName'],
37 | forcedTags = machine['forcedTags'],
38 | nodeKey = machine['nodeKey'],
39 | preAuthKey= machine['preAuthKey'])
40 | else:
41 | app_tables.machines.add_row(name=machine['name'], id=machine['id'], ipAddr=machine['ipAddresses'],
42 | online=machine['online'],
43 | user= machine['user']['name'],
44 | lastSeen= machine['lastSeen'],
45 | expiry= machine['expiry'],
46 | registerMethod= machine['registerMethod'],
47 | discoKey= machine['discoKey'],
48 | createdAt= machine['createdAt'],
49 | invalidTags= machine['invalidTags'],
50 | validTags= machine['validTags'],
51 | machineKey= machine['machineKey'],
52 | lastSuccessfulUpdate= machine['lastSuccessfulUpdate'],
53 | givenName= machine['givenName'],
54 | forcedTags = machine['forcedTags'],
55 | nodeKey = machine['nodeKey'],
56 | preAuthKey= machine['preAuthKey'])
57 | response = anvil.server.call('get_api_key_info', url, api_key)
58 | settings_row = app_tables.settings.get()
59 | settings_row.update(last_hs_sync=f'{date_string}-{time_string}', api_key_creation=response['createdAt'], api_key_expiration=response['expiration'])
60 |
61 | @anvil.server.callable
62 | @anvil.server.background_task
63 | def record_users():
64 | fresh_install = anvil.server.call('check_fresh_install')
65 | if not fresh_install['settings_exists']:
66 | url = ''
67 | api_key = ''
68 | elif fresh_install['settings_exists']:
69 | url = [r['url'] for r in app_tables.settings.search()][0]
70 | api_key = [r['api_key'] for r in app_tables.settings.search()][0]
71 | data = anvil.server.call('get_users', url, api_key)
72 | for user in data['users']:
73 | user_row = app_tables.hs_users.get(id=user['id'])
74 | if user_row:
75 | user_row.update(id=user['id'], name=user['name'], createdAt=user['createdAt'])
76 | else:
77 | app_tables.hs_users.add_row(id=user['id'], name=user['name'], createdAt=user['createdAt'])
78 |
79 | @anvil.server.callable
80 | @anvil.server.background_task
81 | def record_routes():
82 | fresh_install = anvil.server.call('check_fresh_install')
83 | if not fresh_install['settings_exists']:
84 | url = ''
85 | api_key = ''
86 | elif fresh_install['settings_exists']:
87 | url = [r['url'] for r in app_tables.settings.search()][0]
88 | api_key = [r['api_key'] for r in app_tables.settings.search()][0]
89 | data = anvil.server.call('get_routes', url, api_key)
90 | for route in data['routes']:
91 | route_row = app_tables.routes.get(id=int(route['id']))
92 | if route_row:
93 | route_row.update(id=int(route['id']), machineName=route['node']['name'], givenName=route['node']['givenName'], prefix=route['prefix'], enabled=route['enabled'], machineIPs=route['node']['ipAddresses'])
94 | else:
95 | app_tables.routes.add_row(id=int(route['id']), machineName=route['node']['name'], givenName=route['node']['givenName'], prefix=route['prefix'], enabled=route['enabled'], machineIPs=route['node']['ipAddresses'])
96 |
--------------------------------------------------------------------------------
/theme/assets/milliner_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonp92/Milliner/482612833ce7a26aa63f21045fea37e8184c472b/theme/assets/milliner_logo.png
--------------------------------------------------------------------------------
/theme/assets/milliner_spinner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonp92/Milliner/482612833ce7a26aa63f21045fea37e8184c472b/theme/assets/milliner_spinner.png
--------------------------------------------------------------------------------
/theme/assets/standard-page.html:
--------------------------------------------------------------------------------
1 |