Skip to content

Commit

Permalink
feat: authentication functions
Browse files Browse the repository at this point in the history
  • Loading branch information
RockChinQ committed Sep 19, 2023
1 parent 0fa670e commit 2c70b92
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 17 deletions.
9 changes: 9 additions & 0 deletions free_one_api/common/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from hashlib import md5

def md5_digest(s: str) -> str:
# 创建md5对象
new_md5 = md5()
# 这里必须用encode()函数对字符串进行编码,不然会报 TypeError: Unicode-objects must be encoded before hashing
new_md5.update(s.encode(encoding='utf-8'))
# 加密
return new_md5.hexdigest()
9 changes: 7 additions & 2 deletions free_one_api/impls/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import yaml

from ..common import crypto
from .router import mgr as routermgr
from .router import api as apigroup

Expand Down Expand Up @@ -51,10 +52,11 @@ def run(self):
"path": "free_one_api.db",
},
"router": {
"port": 3000
"port": 3000,
"token": "12345678",
},
"web": {
"frontend_path": "../web/dist/"
"frontend_path": "../web/dist/",
}
}

Expand Down Expand Up @@ -95,6 +97,9 @@ async def make_application(config_path: str) -> Application:
# make router manager
from .router import mgr as routermgr

# set router authentication token
routergroup.APIGroup.token = crypto.md5_digest(config['router']['token'])

# import all api groups
from .router import api as apigroup
from .router import web as webgroup
Expand Down
4 changes: 2 additions & 2 deletions free_one_api/impls/router/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def __init__(self, dbmgr: db.DatabaseInterface, chanmgr: channelmgr.AbsChannelMa
self.keymgr = keymgr
self.group_name = "/api"

@self.api("/channel/list", ["GET"])
@self.api("/channel/list", ["GET"], auth=True)
async def channel_list():
# load channels from db to memory
chan_list = await self.chanmgr.list_channels()
Expand Down Expand Up @@ -215,7 +215,7 @@ async def key_create():
key_name = data["name"]

if await self.keymgr.has_key_name(key_name):
raise Exception("key name already exists: "+key_name)
raise ValueError("key name already exists: "+key_name)

key = apikey.FreeOneAPIKey.make_new(key_name)

Expand Down
19 changes: 19 additions & 0 deletions free_one_api/impls/router/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import quart

from ...models.router import group as routergroup
from ...common import crypto


class WebPageGroup(routergroup.APIGroup):
Expand All @@ -14,6 +15,24 @@ def __init__(self, config: dict):
self.frontend_path = config["frontend_path"] if "frontend_path" in config else "./web/dist/"
self.group_name = ""

@self.api("/check_password", ["POST"])
async def check_password():
data = await quart.request.get_json()
if "password" not in data:
return quart.jsonify({
"code": 1,
"message": "No password provided."
})
if data["password"] != routergroup.APIGroup.token:
return quart.jsonify({
"code": 2,
"message": "Wrong token."
})
return quart.jsonify({
"code": 0,
"message": "ok"
})

@self.api("/ping", ["GET"])
async def ping():
return "pong"
Expand Down
37 changes: 34 additions & 3 deletions free_one_api/models/router/group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import abc

import quart

from ..database import db


Expand All @@ -11,11 +13,14 @@ class APIGroup(metaclass=abc.ABCMeta):
e.g. /api
"""

token: str
"""Static token for all groups."""

apis: list[tuple[str, list[str], callable, dict]]

dbmgr: db.DatabaseInterface

def api(self, path: str, methods: list[str], **kwargs):
def api(self, path: str, methods: list[str], auth: bool=False, **kwargs):
"""Register an API.
Args:
Expand All @@ -26,8 +31,34 @@ def api(self, path: str, methods: list[str], **kwargs):

def decorator(handler):
"""Decorator."""
self.apis.append((self.group_name+path, methods, handler, kwargs))
return handler

async def authenticated_handler(*args, **kwargs):
"""Authenticated handler."""
auth = quart.request.headers.get("Authorization")
if auth is None:
return quart.jsonify({
"code": 1,
"message": "No authorization provided."
})
if not auth.startswith("Bearer "):
return quart.jsonify({
"code": 2,
"message": "Wrong authorization type."
})
token = auth[7:]
if token != APIGroup.token:
return quart.jsonify({
"code": 3,
"message": "Wrong token, please re-login."
})
return await handler(*args, **kwargs)

new_handler = handler
if auth:
new_handler = authenticated_handler

self.apis.append((self.group_name+path, methods, new_handler, kwargs))
return new_handler

return decorator

Expand Down
2 changes: 1 addition & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ def main():
application.run()

if __name__ == "__main__":
main()
main()
15 changes: 15 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"dependencies": {
"axios": "^1.5.0",
"element-plus": "^2.3.14",
"js-cookie": "^3.0.5",
"js-md5": "^0.7.3",
"mockjs": "^1.1.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
Expand Down
48 changes: 45 additions & 3 deletions web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,46 @@
import Home from './components/Home.vue';
import Channel from './components/Channel.vue';
import APIKey from './components/APIKey.vue';
import { setPassword, getPassword, clearPassword, checkPassword } from './common/account';
import { ElMessageBox, ElMessage } from 'element-plus';
import { ref } from 'vue';
const currentTab = ref('home');
function showLoginDialog() {
ElMessageBox.prompt('Please enter your token:', 'Enter token', {
confirmButtonText: 'OK',
inputErrorMessage: 'Invalid Format',
inputType: 'password',
inputPattern: /\S+/,
})
.then(({ value }) => {
// login
checkPassword(value)
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Input canceled',
})
})
}
function switchTab(target){
if (getPassword() == ""){
showLoginDialog()
return
}
currentTab.value = target
}
function logout(){
clearPassword()
currentTab.value = 'home'
}
</script>

<template>
Expand All @@ -16,9 +51,16 @@ const currentTab = ref('home');
<img id="logo" src="./assets/logo.png" alt="logo">
<text id="project_name">Free One API</text></a>
</div>
<div class="tab_btn flex_container" @click="currentTab = 'home'">Home</div>
<div class="tab_btn flex_container" @click="currentTab = 'channel'">Channels</div>
<div class="tab_btn flex_container" @click="currentTab = 'apikey'">API Keys</div>
<div class="tab_btn flex_container" @click="switchTab('home')">Home</div>
<div class="tab_btn flex_container" @click="switchTab('channel')">Channels</div>
<div class="tab_btn flex_container" @click="switchTab('apikey')">API Keys</div>
<div id="login_info">
<el-button :type="getPassword()==''?'success':'danger'"
@click="getPassword()==''?showLoginDialog():logout()"
>
{{ getPassword()==''?'Login':'Logout' }}
</el-button>
</div>
</div>

<div id="content">
Expand Down
55 changes: 55 additions & 0 deletions web/src/common/account.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import cookies from 'js-cookie'
import axios from 'axios'
import md5 from 'js-md5'
import { ElMessage } from 'element-plus'

let password = "";

function setPassword(pwd) {
password = pwd;
cookies.set('password', pwd, { expires: 365 })
}

function getPassword() {
if (password === "") {
password = cookies.get('password')
}

if (password === undefined) {
password = "";
}

return password;
}

function clearPassword() {
password = "";
cookies.remove('password')
window.location.reload();
}

function checkPassword(pwd) {
axios.post('/check_password', {
"password": md5(pwd)
})
.then(function (response) {
if (response.data.code === 0) {
setPassword(pwd);
window.location.reload();
} else {
ElMessage.error(response.data.message);
}
})
.catch(function (error) {
console.log(error);
ElMessage.error("Network error");
}
);
}

export {
setPassword,
getPassword,
clearPassword,
checkPassword
}
20 changes: 14 additions & 6 deletions web/src/components/Channel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ function refreshChannelList() {
axios.get('/api/channel/list')
.then(res => {
console.log(res);
channelList.value = res.data.data;
ElNotification({
message: 'Successfully refreshed channel list.',
type: 'success',
duration: 1800
})
if (res.data.code != 0) {
ElNotification({
message: 'Failed: ' + res.data.message,
type: 'error'
})
return;
}else{
channelList.value = res.data.data;
ElNotification({
message: 'Successfully refreshed channel list.',
type: 'success',
duration: 1800
})
}
})
.catch(err => {
console.log(err);
Expand Down
10 changes: 10 additions & 0 deletions web/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import 'element-plus/dist/index.css'

import VueClipboard from 'vue3-clipboard'

import axios from 'axios'
import { getPassword } from './common/account.js'
import md5 from 'js-md5'

axios.interceptors.request.use(function (config) {
config.headers['Authorization'] = "Bearer " + md5(getPassword());
console.log(config);
return config;
})

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
Expand Down
5 changes: 5 additions & 0 deletions web/src/mock/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ Mock.mock(/\/api\/key\/raw\/(\d+)/, "get", key_raw)
import { create_result } from './data/key/create.js'

Mock.mock("/api/key/create", "post", create_result)

Mock.mock("/check_password", "post", {
"code": 0,
"message": "ok",
})

0 comments on commit 2c70b92

Please sign in to comment.