Compare commits

...

3 Commits

Author SHA1 Message Date
12455ebd28 Did not push this yet 2025-06-03 11:42:45 +08:00
aa355e03fd Made validateUserToken avaible via get requests & updated the system so
that it now has a privacy policy & terms of service (TOS) And added a
add email & display current email logic.
2025-06-03 11:42:30 +08:00
45397675f5 Update settings page w/ deleting your account. and update translations. 2025-06-03 11:01:20 +08:00
7 changed files with 241 additions and 40 deletions

View File

@ -189,8 +189,8 @@ const openNews = (url: string, titleName: string) => {
emit("openArticles", url, titleName);
};
const openPublisher = (text: string) => {
emit("openNewsSourcePage", text);
const openPublisher = (slug: string, title: string) => {
emit("openNewsSourcePage", slug, title);
};
</script>
<template>
@ -243,7 +243,9 @@ const openPublisher = (text: string) => {
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<button @click="openPublisher(item.publisherId)">
<button
@click="openPublisher(item.publisherId, item.publisher)"
>
{{ item.publisher }}
</button>
</TooltipTrigger>

View File

@ -2,12 +2,19 @@
import { BadgeCheckIcon, OctagonAlertIcon } from "lucide-vue-next";
import { Input } from "~/components/ui/input";
const { t, locale } = useI18n();
const user = ref();
const user = ref("");
const enterFirstName = ref();
const useremail = ref();
const enteruseremail = ref();
onMounted(async () => {
const req = await fetch("/api/user/validateUserToken");
const res = await req.json();
user.value = res;
user.value = res.firstName;
useremail.value = res;
});
const setFirstName = async () => {
const staticFirstName = "";
};
const logoutAction = () => {};
const groqApiKeyRegex = /^gsk_[a-zA-Z0-9]{52}$/;
@ -45,12 +52,103 @@ const checkValidApiKey = () => {
}
isCorrect.value = groqApiKeyRegex.test(apiKey);
};
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
const showDeleteDialog = ref(false);
const showLogoutDialog = ref(false);
const confirmDelete = async () => {
await deleteAccount();
showDeleteDialog.value = false;
};
const confirmLogout = async () => {
showLogoutDialog.value = false;
};
const deleteAccount = async () => {
const req = await fetch("/api/user/action", {
method: "DELETE",
});
};
/**
*
* userAccount: fetchViaSQL[0].username,
requested_action: "CONTINUE",
email: fetchViaSQL[0].email,
avatarURL: fetchViaSQL[0].avatarurl,
firstName: fetchViaSQL[0].firstName,
*/
</script>
<template>
<div class="justify-center align-center text-center">
<div class="">Greetings, {{ user }}</div>
<div class="flex flex-row text-center align-center justify-center">
<span class="text-md p-1 text-nowrap">Your Groq API:&nbsp;</span>
<h1 class="text-3xl text-bold p-2">
{{ t("settings.greet") }}{{ user || t("settings.defaultname") }}
</h1>
<div class="flex flex-row text-center align-center justify-center p-1">
<span class="text-md p-1 text-nowrap">Change your name:&nbsp;</span>
<Input
type="text"
class="h-6 m-1 py-3 rounded"
v-model="enterFirstName"
placeholder="Ex: Howard ..."
/>
<!--If it is a valid api key or not.-->
<BadgeCheckIcon
v-if="enterFirstName"
class="w-8 h-8 p-1/2 mr-1 text-green-700"
/>
<OctagonAlertIcon
v-if="!enterFirstName"
class="w-8 h-8 p-1/2 mr-1 text-red-700"
/>
<button
class="p-1 text-sm bg-gray-400/60 rounded text-nowrap"
@click="setFirstName"
>
{{ t("settings.submit") }}
</button>
</div>
<div class="flex flex-row text-center align-center justify-center p-1">
<span class="text-md p-1 text-nowrap">Current email:&nbsp;</span>
<span>{{ useremail }}</span>
</div>
<div class="flex flex-row text-center align-center justify-center p-1">
<span class="text-md p-1 text-nowrap">Change your email:&nbsp;</span>
<Input
type="text"
class="h-6 m-1 py-3 rounded"
v-model="enterFirstName"
placeholder="Ex: example@gmail.com"
/>
<!--If it is a valid api key or not.-->
<BadgeCheckIcon
v-if="enterFirstName"
class="w-8 h-8 p-1/2 mr-1 text-green-700"
/>
<OctagonAlertIcon
v-if="!enterFirstName"
class="w-8 h-8 p-1/2 mr-1 text-red-700"
/>
<button
class="p-1 text-sm bg-gray-400/60 rounded text-nowrap"
@click="setFirstName"
>
{{ t("settings.submit") }}
</button>
</div>
<div class="flex flex-row text-center align-center justify-center p-1">
<span class="text-md p-1 text-nowrap"
>{{ t("settings.yourgroqapi") }}:&nbsp;</span
>
<Input
type="text"
class="h-6 m-1 py-3 rounded"
@ -70,16 +168,83 @@ const checkValidApiKey = () => {
class="w-8 h-8 p-1/2 mr-1 text-red-700"
/>
<button
class="p-1 text-sm bg-gray-400/60 rounded"
class="p-1 text-sm bg-gray-400/60 rounded text-nowrap"
@click="submitCustomApiKey"
>
Submit
{{ t("settings.submit") }}
</button>
</div>
<div class="bg-gray-200/70 p-2 m-2 w-full">
<button @click="logoutAction">Logout</button>
<Dialog v-model:open="showLogoutDialog">
<DialogTrigger asChild>
<Button variant="outline">{{ t("settings.logout") }}</Button>
</DialogTrigger>
<DialogContent class="!border-0 !bg-black !rounded">
<DialogHeader>
<DialogTitle>{{ t("settings.logout") }}</DialogTitle>
<DialogDescription>
{{ t("popuptext.logout") }}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button @click="showLogoutDialog = false" variant="outline">
{{ t("popup.cancel") }}
</Button>
<Button @click="confirmLogout" variant="destructive">
{{ t("popup.confirm") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div>
<div class="p-2 m-2 w-full rounded">
<h3 class="text-red-600">{{ t("settings.dangerzone") }}</h3>
<div class="pt-1 pb-0"></div>
<Dialog v-model:open="showDeleteDialog">
<DialogTrigger asChild>
<Button variant="destructive">
{{ t("settings.deleteaccount") }}
</Button>
</DialogTrigger>
<DialogContent class="!border-0 !bg-black !rounded">
<DialogHeader>
<DialogTitle>{{ t("settings.deleteaccount") }}</DialogTitle>
<DialogDescription>
{{ t("popuptext.delete") }}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button @click="showDeleteDialog = false" variant="outline">
{{ t("popup.cancel") }}
</Button>
<Button @click="confirmDelete" variant="destructive">
{{ t("settings.deleteaccount") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
<hr />
<div class="justiy-center align-center text-center">Settings v0.0.1</div>
<div
class="flex flex-row gap-2 m-1 p-2 justify-center align-center text-center"
>
<button
class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32"
>
Privacy Policy
</button>
<button
class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32"
>
TOS
</button>
</div>
<hr />
<div class="justiy-center align-center text-center">
{{ t("app.settings") }} v0.0.2
</div>
</div>
</template>

View File

@ -31,10 +31,10 @@
"mobileApp": "Is there a mobile app?"
},
"whydes": "Taiwan news mostly came from China-controlled / China-official media or from reporters who uses inappropriate content to farm clicks and engragement. Or just made up news by the reporters as the Taiwan's NCC doesn't give a shit about the fake news they are promoting as they think it doesn't matter, this platform is a wake up call to the NCC to start to moderate news media corporations that do fake news.",
"howdes": "We use web scraping to search for the latest news, and store it into a postgres database, for people to access, and the crawler is not like the OpenAI or any AI company, we just load the page once, and that's it nothing else on your page.",
"supportedLocationNews": "We currently only support news from Taiwan for now, but if you want more, you can contribute to the project by adding more news sources to the python scraping scripts.",
"howdes": "We use web scraping to search for the latest news, and store it into the system's cache for people to access. This saves server loading time.",
"supportedLocationNews": "We will only support Taiwan news, if your country has Line today, you can fork this project and change some of the scraping systems.",
"userData": "We handle your account and password in a secure way, with two encryption methods, one during transit to the server (and also SQL injecton), one saving to the database. For other user data, it is currently NOT end-to-end encrypted, like your AI chat history, your stars, and your name & email. In the near future, we will add E2E encryption (and I can also pratice).",
"mobileApp": "I will make a application using React Native in the future, but for now, mobile is not supported, but you can still kinda use it, it is just not pleasant."
"mobileApp": "I will make a application using React Native in the future, but for now, mobile is not supported."
},
"cards": {
"title": {
@ -65,7 +65,7 @@
"news": "News",
"sources": "Sources",
"about": "About this website",
"settings": "settings",
"settings": "Settings",
"leave": "Leave",
"login": "Login",
"license": "License",
@ -77,6 +77,23 @@
"newsview": "News View",
"areyousure": "Are you sure?"
},
"settings": {
"yourgroqapi": "Your Groq API",
"logout": "Logout",
"deleteaccount": "Delete your account",
"dangerzone": "DANGER ZONE",
"submit": "Submit",
"greet": "Greetings, ",
"defaultname": "User"
},
"popuptext": {
"logout": "Are you sure you want to logout?",
"delete": " This action cannot be undone. This will permanently delete your account and remove your data from our servers."
},
"popup": {
"cancel": "Cancel",
"confirm": "Confirm"
},
"tools": {
"title": "Tools",
"name": {

View File

@ -31,10 +31,10 @@
"mobileApp": "這個會做手機版嗎?"
},
"whydes": "台灣的新聞是要痲是來自中國控制/中國官方的媒體或是來自只想獲得點閱的記者。這個平台是要讓台灣的NCC知道發布假新聞就應該被調查而不是放著不管。",
"howdes": " 我們使用使用 Python 寫的網頁爬蟲來搜尋最新的新聞,並將其存入Postgres資料庫中。",
"supportedLocationNews": "We currently only support news from Taiwan for now, but if you want more, you can contribute to the project by adding more news sources to the python scraping scripts.",
"userData": "We handle your account and password in a secure way, with two encryption methods, one during transit to the server (and also SQL injecton), one saving to the database. For other user data, it is currently NOT end-to-end encrypted, like your AI chat history, your stars, and your name & email. In the near future, we will add E2E encryption (and I can also pratice).",
"mobileApp": "I will make a application using React Native in the future, but for now, mobile is not supported, but you can still kinda use it, it is just not pleasant."
"howdes": " 我們使用使用網頁爬蟲來搜尋最新的新聞,並將其存入系統的快取記憶體中。這樣可以省伺服器的運行時間",
"supportedLocationNews": "這個專案只會支援台灣新聞如果你要加其他有Line Today的新聞的話可以Fork這個專案並更改。",
"userData": "這個網站使用雙重加密第一次是使用SHA-512在使用者端加密第二次就使在伺服器使用 argon2 加密,並把使用者的帳號存入系統裡。",
"mobileApp": "可能會,但現在手機不支援。"
},
"cards": {
"title": {
@ -77,6 +77,23 @@
"newsview": "新聞",
"areyousure": "你確定?"
},
"settings": {
"yourgroqapi": "你的Groq API",
"logout": "登出",
"deleteaccount": "刪除你的帳號",
"dangerzone": "DANGER ZONE",
"submit": "送出",
"greet": "嗨, ",
"defaultname": "使用者"
},
"popup": {
"cancel": "取消",
"confirm": "確認"
},
"popuptext": {
"logout": "你確定要登出?",
"delete": "此操作無法撤銷。這將永久刪除您的帳戶並從我們的伺服器中刪除您的資料。"
},
"tools": {
"title": "工具",
"name": {

View File

@ -460,10 +460,10 @@ const openArticles = async (slug: string, titleName?: string) => {
}, 1000);
};
const openNewsSourcePage = async (slug: string) => {
const openNewsSourcePage = async (slug: string, title: string) => {
openingAppViaAnApp.value = true;
passedValues.value = slug;
const titleNameFinal = slug ? "關於" + slug : t("app.aboutNewsOrg");
const titleNameFinal = title ? "關於" + title : t("app.aboutNewsOrg");
findAndOpenWindow("aboutNewsOrg", titleNameFinal);
setTimeout(() => {

View File

@ -30,23 +30,24 @@ export default defineEventHandler(async (event) => {
"";
const bgImage = html("figure.keyVisual img").attr("srcset") || "";
const articles = [];
const regexArticleLinks = /[a-zA-Z0-9]{7}/g
const regexArticleLinks = /[a-zA-Z0-9]{7}/g;
const otherArticles = <any[]>[];
html("a.ltcp-link")
.each((i, element) => {
const articleLink = html(element).attr("href");
const articleTitle = html(element).find("h3.header").text();
const date = html(element).find("div._articleCard div.css-wqleh6 span").text();
if (articleLink && articleTitle) {
const articleSlug = articleLink.matchAll(regexArticleLinks);
otherArticles.push({
index: i,
title: articleTitle,
link: articleSlug,
date: date,
});
}
});
html("a.ltcp-link").each((i, element) => {
const articleLink = html(element).attr("href");
const articleTitle = html(element).find("h3.header").text();
const date = html(element)
.find("div._articleCard div.css-wqleh6 span")
.text();
if (articleLink && articleTitle) {
const articleSlug = articleLink.matchAll(regexArticleLinks);
otherArticles.push({
index: i,
title: articleTitle,
link: articleSlug,
date: date,
});
}
});
return {
name: newsOrgName,
description: description,

View File

@ -1,12 +1,11 @@
import sql from "~/server/components/postgres";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const token = getCookie(event, "token");
if (!token) {
return {
error: "INVALID_TOKEN",
requested_action: "LOGOUT_USER",
requested_action: "USE_DEFAULT_STATE",
};
}
const checkIsUUIDRegex =