initial commit. Cloned timetracker repository
This commit is contained in:
66
bo/src/composable/useCookie.ts
Normal file
66
bo/src/composable/useCookie.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export const useCookie = () => {
|
||||
function getCookie(name: string): string | null {
|
||||
const cookies = document.cookie ? document.cookie.split('; ') : []
|
||||
|
||||
for (const cookie of cookies) {
|
||||
const [key, ...rest] = cookie.split('=')
|
||||
if (key === name) {
|
||||
return decodeURIComponent(rest.join('='))
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function setCookie(
|
||||
name: string,
|
||||
value: string,
|
||||
options?: {
|
||||
days?: number
|
||||
path?: string
|
||||
domain?: string
|
||||
secure?: boolean
|
||||
sameSite?: 'Lax' | 'Strict' | 'None'
|
||||
},
|
||||
) {
|
||||
let cookie = `${name}=${encodeURIComponent(value)}`
|
||||
|
||||
if (options?.days) {
|
||||
const date = new Date()
|
||||
date.setTime(date.getTime() + options.days * 24 * 60 * 60 * 1000)
|
||||
cookie += `; expires=${date.toUTCString()}`
|
||||
}
|
||||
|
||||
cookie += `; path=${options?.path ?? '/'}`
|
||||
|
||||
if (options?.domain) {
|
||||
cookie += `; domain=${options.domain}`
|
||||
}
|
||||
|
||||
if (options?.secure) {
|
||||
cookie += `; Secure`
|
||||
}
|
||||
|
||||
if (options?.sameSite) {
|
||||
cookie += `; SameSite=${options.sameSite}`
|
||||
}
|
||||
|
||||
document.cookie = cookie
|
||||
}
|
||||
|
||||
function deleteCookie(name: string, path: string = '/', domain?: string) {
|
||||
let cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}`
|
||||
|
||||
if (domain) {
|
||||
cookie += `; domain=${domain}`
|
||||
}
|
||||
|
||||
document.cookie = cookie
|
||||
}
|
||||
|
||||
return {
|
||||
getCookie,
|
||||
setCookie,
|
||||
deleteCookie,
|
||||
}
|
||||
}
|
||||
77
bo/src/composable/useFetchJson.ts
Normal file
77
bo/src/composable/useFetchJson.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { Resp } from '@/types'
|
||||
|
||||
export async function useFetchJson<T = unknown>(url: string, opt?: RequestInit): Promise<Resp<T>> {
|
||||
const prefix = import.meta.env.VITE_API_URL ?? ''
|
||||
const urlFull = join(prefix, url)
|
||||
|
||||
const headers = new Headers(opt?.headers)
|
||||
if (!headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
...opt,
|
||||
headers,
|
||||
// Always include cookies so the backend can read the HTTPOnly access_token
|
||||
credentials: 'same-origin',
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(urlFull, fetchOptions)
|
||||
|
||||
const contentType = res.headers.get('content-type') ?? ''
|
||||
|
||||
if (!contentType.includes('application/json')) {
|
||||
throw { message: 'this is not proper json format' } as Resp<any>
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
// Handle 401 — access token expired; try to refresh via the HTTPOnly refresh_token cookie
|
||||
if (res.status === 401) {
|
||||
const { useAuthStore } = await import('@/stores/auth')
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const refreshed = await authStore.refreshAccessToken()
|
||||
|
||||
if (refreshed) {
|
||||
// Retry the original request — cookies are updated by the refresh endpoint
|
||||
const retryRes = await fetch(urlFull, fetchOptions)
|
||||
|
||||
const retryContentType = retryRes.headers.get('content-type') ?? ''
|
||||
if (!retryContentType.includes('application/json')) {
|
||||
throw { message: 'this is not proper json format' } as Resp<any>
|
||||
}
|
||||
|
||||
const retryData = await retryRes.json()
|
||||
|
||||
if (!retryRes.ok) {
|
||||
throw retryData as Resp<any>
|
||||
}
|
||||
|
||||
return retryData as Resp<T>
|
||||
}
|
||||
|
||||
// Refresh failed — logout and propagate the error
|
||||
authStore.logout()
|
||||
throw data as Resp<any>
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw data as Resp<any>
|
||||
}
|
||||
|
||||
return data as Resp<T>
|
||||
} catch (error) {
|
||||
throw error as Resp<any>
|
||||
}
|
||||
}
|
||||
|
||||
export function join(...parts: string[]): string {
|
||||
const path = parts
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
.replace(/\/{2,}/g, '/')
|
||||
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
87
bo/src/composable/useRepoApi.ts
Normal file
87
bo/src/composable/useRepoApi.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useFetchJson } from './useFetchJson'
|
||||
import type { Resp } from '@/types/response'
|
||||
|
||||
const API_PREFIX = '/api/v1/repo'
|
||||
|
||||
export interface QuarterData {
|
||||
quarter: string
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface IssueTimeSummary {
|
||||
IssueID: number
|
||||
IssueName: string
|
||||
UserId: number
|
||||
Initials: string
|
||||
CreatedDate: string
|
||||
TotalHoursSpent: number
|
||||
}
|
||||
|
||||
export interface IssueResponse {
|
||||
items: IssueTimeSummary[]
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface PagingParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export async function getRepos(): Promise<any> {
|
||||
const result = await useFetchJson<number[]>(`${API_PREFIX}/get-repos`)
|
||||
return result
|
||||
}
|
||||
|
||||
// export async function getYears(repoID: number): Promise<any> {
|
||||
// return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`)
|
||||
// }
|
||||
// console.log(getYears(), 'leraaaaaa')
|
||||
|
||||
export async function getYears(repoID: number): Promise<any> {
|
||||
return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`);
|
||||
}
|
||||
|
||||
// Correct way to log the data
|
||||
|
||||
export async function getQuarters(repoID: number, year: number): Promise<any> {
|
||||
return useFetchJson<QuarterData[]>(`${API_PREFIX}/get-quarters?repoID=${repoID}&year=${year}`)
|
||||
}
|
||||
|
||||
// export async function getIssues(
|
||||
// repoID: number,
|
||||
// year: number,
|
||||
// quarter: number,
|
||||
// page: number = 1,
|
||||
// pageSize: number = 10
|
||||
// ): Promise<any> {
|
||||
// // The get-issues endpoint uses GET with pagination in query params
|
||||
// return useFetchJson<IssueResponse>(
|
||||
// `${API_PREFIX}/get-issues?repoID=${repoID}&year=${year}&quarter=${quarter}&page_number=${page}&elements_per_page=${pageSize}`
|
||||
// )
|
||||
// }
|
||||
// async function logYears() {
|
||||
// const years = await getIssues(7); // pass a repoID
|
||||
// console.log(years, 'leraaaaaa');
|
||||
// }
|
||||
export async function getIssues(
|
||||
repoID: number,
|
||||
year: number,
|
||||
quarter: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 10
|
||||
): Promise<any> {
|
||||
return useFetchJson<IssueResponse>(
|
||||
`${API_PREFIX}/get-issues?repoID=${repoID}&year=${year}&quarter=${quarter}&page_number=${page}&elements_per_page=${pageSize}`
|
||||
);
|
||||
}
|
||||
|
||||
// Correct logging function
|
||||
async function logIssues() {
|
||||
const repoID = 7;
|
||||
const year = 2026; // example year
|
||||
const quarter = 1; // example quarter
|
||||
const issues = await getIssues(repoID, year, quarter);
|
||||
console.log(issues, 'leraaaaaa');
|
||||
}
|
||||
|
||||
logIssues();
|
||||
98
bo/src/composable/useValidation.ts
Normal file
98
bo/src/composable/useValidation.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
import { settings } from '@/router/settings'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
|
||||
export const useValidation = () => {
|
||||
const errors = [] as FormError[]
|
||||
|
||||
function reset() {
|
||||
errors.length = 0
|
||||
}
|
||||
|
||||
function validateFirstName(first_name_ref: Ref<string>, name: string, message: string) {
|
||||
if (!first_name_ref.value || !/^[A-Za-z]{2,}$/.test(first_name_ref.value)) {
|
||||
errors.push({ name: name, message: message })
|
||||
}
|
||||
}
|
||||
|
||||
function validateLastName(last_name_ref: Ref<string>, name: string, message: string) {
|
||||
if (!last_name_ref.value || !/^[A-Za-z]{2,}$/.test(last_name_ref.value)) {
|
||||
errors.push({ name: name, message: message })
|
||||
}
|
||||
}
|
||||
|
||||
function validateEmail(email_ref: Ref<string>, name: string, message: string) {
|
||||
if (!email_ref.value || !/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(email_ref.value)) {
|
||||
errors.push({ name: name, message: message })
|
||||
}
|
||||
}
|
||||
|
||||
// function validatePasswords(
|
||||
// password_ref: Ref<string>,
|
||||
// password_name: string,
|
||||
// confirm_password_ref: Ref<string>,
|
||||
// confirm_name: string,
|
||||
// message_password: string,
|
||||
// message_confirm_password: string,
|
||||
// ) {
|
||||
// const regex = new RegExp(settings.app?.password_regex ?? '^.{8,}$')
|
||||
|
||||
// if (!password_ref.value) {
|
||||
// errors.push({ name: password_name, message: message_password })
|
||||
// } else if (!regex.test(password_ref.value)) {
|
||||
// errors.push({
|
||||
// name: password_name,
|
||||
// message: 'general.registration_validation_password_requirements'
|
||||
// })
|
||||
// }
|
||||
|
||||
// if (!confirm_password_ref.value) {
|
||||
// errors.push({ name: confirm_name, message: message_confirm_password })
|
||||
// } else if (password_ref.value !== confirm_password_ref.value) {
|
||||
// errors.push({
|
||||
// name: confirm_name,
|
||||
// message: 'registration_validation_password_not_same'
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
function validatePasswords(
|
||||
password_ref: Ref<string>,
|
||||
password_name: string,
|
||||
confirm_password_ref: Ref<string>,
|
||||
confirm_name: string,
|
||||
message_confirm_password: string,
|
||||
) {
|
||||
const regexPass = new RegExp(
|
||||
'^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=[\\]{};:\'",.<>/?]).{8,}$'
|
||||
)
|
||||
|
||||
if (!password_ref.value) {
|
||||
errors.push({ name: password_name, message: i18n.t('validate_error.password_required') })
|
||||
} else if (!regexPass.test(password_ref.value)) {
|
||||
errors.push({
|
||||
name: password_name,
|
||||
message: i18n.t('validate_error.registration_validation_password_requirements')
|
||||
})
|
||||
}
|
||||
|
||||
if (!confirm_password_ref.value) {
|
||||
errors.push({ name: confirm_name, message: message_confirm_password })
|
||||
} else if (password_ref.value !== confirm_password_ref.value) {
|
||||
errors.push({
|
||||
name: confirm_name,
|
||||
message: i18n.t('validate_error.registration_validation_password_not_same')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reset,
|
||||
validateFirstName,
|
||||
validateLastName,
|
||||
validateEmail,
|
||||
validatePasswords,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user