1
0

feat: support login

- migrate old API into typescript but not finished (only webLogin works now)
- seperate the logger of backend due to the shitty behavior of Flask (change logging level)
This commit is contained in:
2026-05-14 21:13:13 +08:00
parent 6337ae432d
commit 2a280dcba0
15 changed files with 1013 additions and 157 deletions

75
frontend/src/api/admin.ts Normal file
View File

@@ -0,0 +1,75 @@
import { apiWrapper, boolApiWrapper } from './index'
// User interface
interface User {
username: string
isAdmin: boolean
// Add other user-related fields as needed
}
/**
* Get all users (admin only)
* @param token - Authentication token
* @returns Array of users or null if operation failed
*/
export async function get(token: string): Promise<User[] | undefined> {
return apiWrapper('/api/admin/get', { token });
}
/**
* Add new user (admin only)
* @param token - Authentication token
* @param username - Username
* @returns Created user or null if operation failed
*/
export async function add(token: string, username: string): Promise<User | undefined> {
return apiWrapper('/api/admin/add', { token, username });
}
/**
* Update user (admin only)
* @param token - Authentication token
* @param username - Username
* @param params - Update parameters (partial)
* @returns true if update successful, false otherwise
*/
export async function update(
token: string,
username: string,
params: {
password?: string
isAdmin?: boolean
}
): Promise<boolean> {
const data: any = {
token,
username
};
let count = 0;
if (typeof params.password !== 'undefined') {
data.password = params.password;
count++;
}
if (typeof params.isAdmin !== 'undefined') {
data.isAdmin = params.isAdmin;
count++;
}
// If no update parameters provided, return true
if (count === 0) {
return true;
}
return boolApiWrapper('/api/admin/update', data);
}
/**
* Delete user (admin only)
* @param token - Authentication token
* @param username - Username
* @returns true if deletion successful, false otherwise
*/
export async function deleteItem(token: string, username: string): Promise<boolean> {
return boolApiWrapper('/api/admin/delete', { token, username });
}

View File

@@ -0,0 +1,141 @@
import { apiWrapper, boolApiWrapper } from './index'
// Calendar event interface
interface CalendarEvent {
uuid: string
belongTo: string
title: string
description: string
eventDateTimeStart: string
eventDateTimeEnd: string
loopRules: string
timezoneOffset: number
lastChange: string
}
// Description object interface
interface DescriptionObject {
description: string
color: string
}
/**
* Serialize calendar description
* @param description - Description text
* @param color - Color value
* @returns JSON string
*/
export function serializeDescription(description: string, color: string): string {
const sobj: DescriptionObject = {
description,
color
}
return JSON.stringify(sobj)
}
/**
* Deserialize calendar description
* @param str - JSON string
* @returns Description object
*/
export function deserializeDescription(str: string): DescriptionObject {
try {
return JSON.parse(str) as DescriptionObject
} catch (err) {
return {
description: "",
color: "#000000" // DefaultColor
}
}
}
/**
* Get full calendar events within date range
* @param token - Authentication token
* @param startDateTime - Start datetime
* @param endDateTime - End datetime
* @returns Array of calendar events or null if operation failed
*/
export async function getFull(token: string, startDateTime: string, endDateTime: string): Promise<CalendarEvent[] | undefined> {
return apiWrapper('/api/calendar/getFull', { token, startDateTime, endDateTime });
}
/**
* Get calendar event detail by UUID
* @param token - Authentication token
* @param uuid - Event UUID
* @returns Calendar event or null if operation failed
*/
export async function getDetail(token: string, uuid: string): Promise<CalendarEvent | undefined> {
return apiWrapper('/api/calendar/getDetail', { token, uuid });
}
/**
* Update calendar event
* @param token - Authentication token
* @param uuid - Event UUID
* @param params - Update parameters (partial)
* @returns Updated calendar event or null if operation failed
*/
export async function update(
token: string,
uuid: string,
params: {
belongTo?: string
title?: string
description?: string
eventDateTimeStart?: string
eventDateTimeEnd?: string
loopRules?: string
timezoneOffset?: number
lastChange: string
}
): Promise<CalendarEvent | undefined> {
const data: any = {
token,
uuid,
lastChange: params.lastChange
};
if (params.belongTo !== undefined) data.belongTo = params.belongTo;
if (params.title !== undefined) data.title = params.title;
if (params.description !== undefined) data.description = params.description;
if (params.eventDateTimeStart !== undefined) data.eventDateTimeStart = params.eventDateTimeStart;
if (params.eventDateTimeEnd !== undefined) data.eventDateTimeEnd = params.eventDateTimeEnd;
if (params.loopRules !== undefined) data.loopRules = params.loopRules;
if (params.timezoneOffset !== undefined) data.timezoneOffset = params.timezoneOffset;
return apiWrapper('/api/calendar/update', data);
}
/**
* Add new calendar event
* @param token - Authentication token
* @param params - Event parameters
* @returns Created calendar event or null if operation failed
*/
export async function add(
token: string,
params: {
belongTo: string
title: string
description: string
eventDateTimeStart: string
eventDateTimeEnd: string
loopRules: string
timezoneOffset: number
}
): Promise<CalendarEvent | undefined> {
return apiWrapper('/api/calendar/add', { token, ...params });
}
/**
* Delete calendar event
* @param token - Authentication token
* @param uuid - Event UUID
* @param lastChange - Last change timestamp
* @returns true if deletion successful, false otherwise
*/
export async function deleteEvent(token: string, uuid: string, lastChange: string): Promise<boolean> {
return boolApiWrapper('/api/calendar/delete', { token, uuid, lastChange });
}

View File

@@ -0,0 +1,126 @@
import { apiWrapper, boolApiWrapper } from './index'
type Uuid = string;
// Collection item interface
interface CollectionItem {
uuid: Uuid
name: string
lastChange: string
}
// Sharing info interface
interface SharingInfo {
target: string
// Add other sharing-related fields as needed
}
/**
* Get all owned collections
* @param token - Authentication token
* @returns Array of collection items or null if operation failed
*/
export async function getFullOwn(token: string): Promise<CollectionItem[] | undefined> {
return apiWrapper('/api/collection/getFullOwn', { token });
}
/**
* Get owned collection detail by UUID
* @param token - Authentication token
* @param uuid - Collection UUID
* @returns Collection item or null if operation failed
*/
export async function getDetailOwn(token: string, uuid: Uuid): Promise<CollectionItem | undefined> {
return apiWrapper('/api/collection/getDetailOwn', { token, uuid });
}
/**
* Add new owned collection
* @param token - Authentication token
* @param name - Collection name
* @returns Created collection item or null if operation failed
*/
export async function addOwn(token: string, name: string): Promise<Uuid | undefined> {
return apiWrapper('/api/collection/addOwn', { token, name });
}
/**
* Update owned collection
* @param token - Authentication token
* @param uuid - Collection UUID
* @param name - New name
* @param lastChange - Last change timestamp
* @returns Updated collection item or null if operation failed
*/
export async function updateOwn(
token: string,
uuid: Uuid,
name: string,
lastChange: string
): Promise<CollectionItem | undefined> {
return apiWrapper('/api/collection/updateOwn', { token, uuid, name, lastChange });
}
/**
* Delete owned collection
* @param token - Authentication token
* @param uuid - Collection UUID
* @param lastChange - Last change timestamp
* @returns true if deletion successful, false otherwise
*/
export async function deleteOwn(token: string, uuid: Uuid, lastChange: string): Promise<boolean> {
return boolApiWrapper('/api/collection/deleteOwn', { token, uuid, lastChange });
}
/**
* Get sharing information for a collection
* @param token - Authentication token
* @param uuid - Collection UUID
* @returns Array of sharing info or null if operation failed
*/
export async function getSharing(token: string, uuid: Uuid): Promise<SharingInfo[] | undefined> {
return apiWrapper('/api/collection/getSharing', { token, uuid });
}
/**
* Delete sharing for a collection
* @param token - Authentication token
* @param uuid - Collection UUID
* @param target - Target user
* @param lastChange - Last change timestamp
* @returns Result data or null if operation failed
*/
export async function deleteSharing(
token: string,
uuid: Uuid,
target: string,
lastChange: string
): Promise<any | undefined> {
return apiWrapper('/api/collection/deleteSharing', { token, uuid, target, lastChange });
}
/**
* Add sharing for a collection
* @param token - Authentication token
* @param uuid - Collection UUID
* @param target - Target user
* @param lastChange - Last change timestamp
* @returns Result data or null if operation failed
*/
export async function addSharing(
token: string,
uuid: Uuid,
target: string,
lastChange: string
): Promise<any | undefined> {
return apiWrapper('/api/collection/addSharing', { token, uuid, target, lastChange });
}
/**
* Get all shared collections
* @param token - Authentication token
* @returns Array of collection items or null if operation failed
*/
export async function getShared(token: string): Promise<CollectionItem[] | undefined> {
return apiWrapper('/api/collection/getShared', { token });
}

View File

@@ -0,0 +1,43 @@
import { apiWrapper, boolApiWrapper } from './index'
// /**
// * Login with salt
// * @param username - Username
// * @param password - Password
// * @returns Token if login successful, undefined otherwise
// */
// export async function login(username: string, password: string): Promise<string | undefined> {
// const salt: string | undefined = await apiWrapper('/api/common/salt', { username });
// if (typeof salt === 'undefined') return undefined;
// const computedPassword = computePasswordWithSalt(password, salt);
// return apiWrapper('/api/common/login', { username, password: computedPassword });
// }
/**
* Web login
* @param username - Username
* @param password - Password
* @returns Token if login successful, undefined otherwise
*/
export async function webLogin(username: string, password: string): Promise<string | undefined> {
return apiWrapper('/api/common/webLogin', { username, password });
}
/**
* Logout
* @param token - Authentication token
* @returns true if logout successful, false otherwise if logout failed
*/
export async function logout(token: string): Promise<boolean> {
return boolApiWrapper('/api/common/logout', { token });
}
/**
* Validate token
* @param token - Authentication token
* @returns true if token is valid, false otherwise
*/
export async function tokenValid(token: string): Promise<boolean> {
return boolApiWrapper('/api/common/tokenValid', { token });
}

57
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,57 @@
// Response interface
interface ApiResponse<T = any> {
success: boolean,
error: string,
data: T,
}
export async function apiWrapper<T>(url: string, data: Record<string, any>): Promise<T | undefined> {
try {
// 自动编码为 key=value&key2=value2 格式
const params = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
params.append(key, String(value));
});
// 发起请求
const response = await fetch(url, {
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "same-origin",
headers: {
// 明确指定内容类型
'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: "follow",
referrerPolicy: "no-referrer",
body: params.toString(),
});
// 检查 HTTP 状态码 (fetch 只有在网络故障时才会 rejectHTTP 404/500 不会)
if (!response.ok) {
console.error(`HTTP failed: ${response.status}`);
}
// 解析 JSON body
// 注意response.json() 返回的是一个 Promise所以需要 await
const payload = await response.json() as ApiResponse<T>;
// 检查API返回结果
if (payload.success) {
return payload.data;
} else {
console.error(`API failed: ${payload.error}`);
return undefined;
}
} catch (error) {
// 统一错误处理
console.error(`Fetch failed: ${error}`);
return undefined;
}
}
export async function boolApiWrapper<U>(url: string, data: Record<string, any>): Promise<boolean> {
const rv = await apiWrapper<null>(url, data);
return rv !== undefined;
}

View File

@@ -0,0 +1,45 @@
import { apiWrapper, boolApiWrapper } from './index'
// Token info interface
interface TokenInfo {
token: string
// Add other token-related fields as needed
}
/**
* Check if current user is admin
* @param token - Authentication token
* @returns true if user is admin, false otherwise
*/
export async function isAdmin(token: string): Promise<boolean> {
return boolApiWrapper('/api/profile/isAdmin', { token });
}
/**
* Change user password
* @param token - Authentication token
* @param password - New password
* @returns true if change successful, false otherwise
*/
export async function changePassword(token: string, password: string): Promise<boolean> {
return boolApiWrapper('/api/profile/changePassword', { token, password });
}
/**
* Get user tokens
* @param token - Authentication token
* @returns Array of token info or undefined if operation failed
*/
export async function getToken(token: string): Promise<TokenInfo[] | undefined> {
return apiWrapper('/api/profile/getToken', { token });
}
/**
* Delete a token
* @param token - Authentication token
* @param deleteToken - Token to delete
* @returns true if deletion successful, false otherwise
*/
export async function deleteToken(token: string, deleteToken: string): Promise<boolean> {
return boolApiWrapper('/api/profile/deleteToken', { token, deleteToken });
}

56
frontend/src/api/todo.ts Normal file
View File

@@ -0,0 +1,56 @@
import axios from 'axios'
import { apiWrapper, boolApiWrapper } from './index'
// Todo item interface
interface TodoItem {
uuid: string
data: any
lastChange: string
// Add other todo-related fields as needed
}
/**
* Get all todos
* @param token - Authentication token
* @returns Array of todo items or null if operation failed
*/
export async function getFull(token: string): Promise<TodoItem[] | undefined> {
return apiWrapper('/api/todo/getFull', { token });
}
/**
* Add new todo
* @param token - Authentication token
* @returns Created todo item or null if operation failed
*/
export async function add(token: string): Promise<TodoItem | undefined> {
return apiWrapper('/api/todo/add', { token });
}
/**
* Update todo
* @param token - Authentication token
* @param uuid - Todo UUID
* @param data - Todo data
* @param lastChange - Last change timestamp
* @returns Updated todo item or null if operation failed
*/
export async function update(
token: string,
uuid: string,
data: any,
lastChange: string
): Promise<TodoItem | undefined> {
return apiWrapper('/api/todo/update', { token, uuid, data, lastChange });
}
/**
* Delete todo
* @param token - Authentication token
* @param uuid - Todo UUID
* @param lastChange - Last change timestamp
* @returns true if deletion successful, false otherwise
*/
export async function deleteTodo(token: string, uuid: string, lastChange: string): Promise<boolean> {
return boolApiWrapper('/api/todo/delete', { token, uuid, lastChange });
}

View File

@@ -48,4 +48,8 @@ router.beforeEach((to, from) => {
}
})
export const goToHome = () => {
router.push({ name: 'Home' })
}
export default router

View File

@@ -1,11 +1,45 @@
<script setup lang="ts">
import MessageBox from '@/components/MessageBox.vue';
import { ref } from 'vue';
import MessageBox from '@/components/MessageBox.vue';
import { useTokenStore } from '@/stores/token';
import { webLogin as apiCommonWebLogin } from '@/api/common';
import { goToHome } from '@/router';
const isLoggingIn = ref<boolean>(false);
const username = ref<string>("");
const password = ref<string>("");
const messagebox = ref<InstanceType<typeof MessageBox> | null>(null);
const login = () => {
messagebox.value?.show("Fail to login. Please check your username or password.")
const login = async () => {
// disable UI first
isLoggingIn.value = true;
// // try get salt
// if (ccn_api_common_salt(username)) {
// // continue login
// if (ccn_api_common_login(username, password)) {
// // ok, logged
// // jump into home page again
// window.location.href = '/web/home';
// } else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-login"));
// } else ccn_messagebox_Show($.i18n.prop("ccn-i18n-js-fail-login"));
const token = await apiCommonWebLogin(username.value, password.value);
if (typeof token !== 'undefined') {
// OK. We have logged in.
// Update token storage
const tokenStore = useTokenStore();
tokenStore.login(token);
// Go to home page.
goToHome();
} else {
// Show login error.
messagebox.value?.show("Fail to login. Please check your username or password.");
}
// Enable all UI
isLoggingIn.value = false;
}
</script>
@@ -16,7 +50,7 @@ const login = () => {
<div class="field">
<label class="label">User Name</label>
<div class="control has-icons-left has-icons-right">
<input id="ccn-login-form-username" class="input" type="text">
<input v-model="username" :disabled="isLoggingIn" class="input" type="text">
<span class="icon is-small is-left">
<font-awesome-icon icon="fas fa-user"></font-awesome-icon>
</span>
@@ -25,7 +59,7 @@ const login = () => {
<div class="field">
<label class="label">Password</label>
<p class="control has-icons-left">
<input id="ccn-login-form-password" class="input" type="password">
<input v-model="password" :disabled="isLoggingIn" class="input" type="password">
<span class="icon is-small is-left">
<font-awesome-icon icon="fas fa-lock"></font-awesome-icon>
</span>
@@ -33,7 +67,7 @@ const login = () => {
</div>
<div class="control">
<button class="button is-primary" @click="login">Login</button>
<button class="button is-primary" :disabled="isLoggingIn" @click="login">Login</button>
</div>
</div>