materia-web-client: unify style with catppuccin

materia-server: fixing auth
This commit is contained in:
L-Nafaryus 2024-06-20 00:11:35 +05:00
parent 997f37d5ee
commit d8b19da646
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
31 changed files with 349 additions and 205 deletions

View File

@ -219,7 +219,9 @@
}; };
devShells.x86_64-linux.default = pkgs.mkShell { devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = with pkgs; [ postgresql redis ]; buildInputs = with pkgs; [ postgresql redis pdm nodejs ];
# greenlet requires libstdc++
LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
}; };
}; };
} }

View File

@ -46,8 +46,8 @@ materia-server = "materia_server.main:server"
[tool.pdm.scripts] [tool.pdm.scripts]
start-server.cmd = "python ./src/materia_server/main.py {args:start --app-mode development --log-level debug}" start-server.cmd = "python ./src/materia_server/main.py {args:start --app-mode development --log-level debug}"
db-upgrade.cmd = "alembic upgrade {args:head}" db-upgrade.cmd = "alembic -c ./src/materia_server/alembic.ini upgrade {args:head}"
db-downgrade.cmd = "alembic downgrade {args:base}" db-downgrade.shell = "alembic -c ./src/materia_server/alembic.ini downgrade {args:base}"
db-revision.cmd = "alembic revision {args:--autogenerate}" db-revision.cmd = "alembic revision {args:--autogenerate}"
remove-revisions.shell = "rm -v ./src/materia_server/models/migrations/versions/*.py" remove-revisions.shell = "rm -v ./src/materia_server/models/migrations/versions/*.py"

View File

@ -1 +1 @@
from materia_server.models.user.user import User, UserCredentials from materia_server.models.user.user import User, UserCredentials, UserIdentity

View File

@ -3,7 +3,7 @@ from typing import Optional
import time import time
import re import re
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr, ConfigDict
import pydantic import pydantic
from sqlalchemy import BigInteger, Enum from sqlalchemy import BigInteger, Enum
from sqlalchemy.orm import mapped_column, Mapped, relationship from sqlalchemy.orm import mapped_column, Mapped, relationship
@ -23,7 +23,7 @@ class User(Base):
id: Mapped[UUID] = mapped_column(primary_key = True, default = uuid4) id: Mapped[UUID] = mapped_column(primary_key = True, default = uuid4)
name: Mapped[str] = mapped_column(unique = True) name: Mapped[str] = mapped_column(unique = True)
lower_name: Mapped[str] = mapped_column(unique = True) lower_name: Mapped[str] = mapped_column(unique = True)
full_name: Mapped[str] full_name: Mapped[Optional[str]]
email: Mapped[str] email: Mapped[str]
is_email_private: Mapped[bool] = mapped_column(default = True) is_email_private: Mapped[bool] = mapped_column(default = True)
hashed_password: Mapped[str] hashed_password: Mapped[str]
@ -80,5 +80,25 @@ class UserCredentials(BaseModel):
password: str password: str
email: Optional[EmailStr] email: Optional[EmailStr]
class UserIdentity(BaseModel):
model_config = ConfigDict(from_attributes = True)
name: str
lower_name: str
full_name: Optional[str]
email: str
is_email_private: bool
must_change_password: bool
login_type: "LoginType"
created: int
updated: int
last_login: Optional[int]
is_active: bool
is_admin: bool
avatar: Optional[str]
from materia_server.models.repository.repository import Repository from materia_server.models.repository.repository import Repository

View File

@ -1 +1,2 @@
from materia_server.routers import api from materia_server.routers import api
from materia_server.routers import middleware

View File

@ -1,6 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from materia_server.routers.api import auth from materia_server.routers.api import auth
from materia_server.routers.api import user
router = APIRouter(prefix = "/api") router = APIRouter(prefix = "/api")
router.include_router(auth.router) router.include_router(auth.router)
router.include_router(user.router)

View File

@ -40,8 +40,9 @@ async def signup(body: user.UserCredentials, ctx: context.Context = Depends()):
@router.post("/auth/signin") @router.post("/auth/signin")
async def signin(body: user.UserCredentials, response: Response, ctx: context.Context = Depends()): async def signin(body: user.UserCredentials, response: Response, ctx: context.Context = Depends()):
if (current_user := await user.User.by_name(body.name, ctx.database) or await user.User.by_email(body.email, ctx.database) if body.email else None) is None: if (current_user := await user.User.by_name(body.name, ctx.database)) is None:
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid credentials") if (current_user := await user.User.by_email(str(body.email), ctx.database)) is None:
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid email")
if not security.validate_password(body.password, current_user.hashed_password, algo = ctx.config.security.password_hash_algo): if not security.validate_password(body.password, current_user.hashed_password, algo = ctx.config.security.password_hash_algo):
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid password") raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid password")

View File

@ -0,0 +1,5 @@
from fastapi import APIRouter
from materia_server.routers.api.user import user
router = APIRouter()
router.include_router(user.router)

View File

@ -0,0 +1,19 @@
from typing import Optional
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from materia_server import security
from materia_server.routers import context
from materia_server.models import user
from materia_server.models import auth
from materia_server.routers.middleware import JwtMiddleware
router = APIRouter(tags = ["user"])
@router.get("/user/identity", response_model = user.UserIdentity)
async def identity(request: Request, claims = Depends(JwtMiddleware()), ctx: context.Context = Depends()):
if not (current_user := await user.User.by_id(uuid.UUID(claims.sub), ctx.database)):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
return user.UserIdentity.model_validate(current_user)

View File

@ -1,6 +1,7 @@
from typing import Optional, Sequence from typing import Optional, Sequence
import uuid import uuid
from fastapi import HTTPException, Request, Response, status, Depends from fastapi import HTTPException, Request, Response, status, Depends, Cookie
from fastapi.security.base import SecurityBase
import jwt import jwt
from sqlalchemy import select from sqlalchemy import select
from pydantic import BaseModel from pydantic import BaseModel
@ -8,38 +9,71 @@ from enum import StrEnum
from http import HTTPMethod as HttpMethod from http import HTTPMethod as HttpMethod
from fastapi.security import HTTPBearer, OAuth2PasswordBearer, OAuth2PasswordRequestForm, APIKeyQuery, APIKeyCookie, APIKeyHeader from fastapi.security import HTTPBearer, OAuth2PasswordBearer, OAuth2PasswordRequestForm, APIKeyQuery, APIKeyCookie, APIKeyHeader
from materia.api.state import ConfigState, DatabaseState from materia_server import security
from materia.api.token import TokenClaims from materia_server.routers import context
from materia import db from materia_server.models import user
class JwtMiddleware(HTTPBearer): async def get_token_claims(token, ctx: context.Context = Depends()) -> security.TokenClaims:
def __init__(self, auto_error: bool = True):
super().__init__(auto_error = auto_error)
self.claims: Optional[TokenClaims] = None
async def __call__(self, request: Request, config: ConfigState = Depends(), database: DatabaseState = Depends()):
if token := request.cookies.get("token"):
pass
elif (credentials := await super().__call__(request)) and credentials.scheme == "Bearer":
token = credentials.credentials
if not token:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing token")
try: try:
self.claims = TokenClaims.verify(token, config.jwt.secret) secret = ctx.config.oauth2.jwt_secret if ctx.config.oauth2.jwt_signing_algo in ["HS256", "HS384", "HS512"] else ctx.config.oauth2.jwt_signing_key
user_id = uuid.UUID(self.claims.sub) # type: ignore claims = security.validate_token(token, secret)
user_id = uuid.UUID(claims.sub) # type: ignore
except jwt.PyJWKError as _: except jwt.PyJWKError as _:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token") raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
except ValueError as _: except ValueError as _:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token") raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token")
async with database.session() as session: if not (current_user := await user.User.by_id(user_id, ctx.database)):
if not (user := (await session.scalars(select(db.User).where(db.User.id == user_id))).first()):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user") raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
request.state.user = user return claims
class JwtBearer(HTTPBearer):
def __init__(self, **kwargs):
super().__init__(scheme_name = "Bearer", **kwargs)
self.claims = None
async def __call__(self, request: Request, ctx: context.Context = Depends()):
if credentials := await super().__call__(request):
token = credentials.credentials
else:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing token")
self.claims = await get_token_claims(token, ctx)
class JwtCookie(SecurityBase):
def __init(self, *, auto_error: bool = True):
self.auto_error = auto_error
self.claims = None
async def __call__(self, request: Request, response: Response, ctx: context.Context = Depends()):
if not (access_token := request.cookies.get(ctx.config.security.cookie_access_token_name)):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing token")
refresh_token = request.cookies.get(ctx.config.security.cookie_refresh_token_name)
if ctx.config.oauth2.jwt_signing_algo in ["HS256", "HS384", "HS512"]:
secret = ctx.config.oauth2.jwt_secret
else:
secret = ctx.config.oauth2.jwt_signing_key
try:
refresh_claims = security.validate_token(refresh_token, secret) if refresh_token else None
# TODO: check expiration
except jwt.PyJWTError:
refresh_claims = None
try:
access_claims = security.validate_token(access_token, secret)
# TODO: if exp then check refresh token and create new else raise
except jwt.PyJWTError as e:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Invalid token: {e}")
else:
# TODO: validate user
pass
self.claims = access_claims
WILDCARD = "*" WILDCARD = "*"
NULL = "null" NULL = "null"

View File

@ -8,6 +8,7 @@
"name": "materia-frontend", "name": "materia-frontend",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
"axios": "^1.6.8", "axios": "^1.6.8",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@ -484,6 +485,14 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@catppuccin/tailwindcss": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@catppuccin/tailwindcss/-/tailwindcss-0.1.6.tgz",
"integrity": "sha512-V+Y0AwZ5SSyvOVAcDl7Ng30xy+m82OKnEJ+9+kcZZ7lRyXuZrAb2GScdq9XR3v+ggt8qiZ/G4TvaC9cJ88AAXA==",
"peerDependencies": {
"tailwindcss": ">=3.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@ -1494,11 +1503,11 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -1859,9 +1868,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },

View File

@ -11,6 +11,7 @@
"type-check": "vue-tsc --build --force" "type-check": "vue-tsc --build --force"
}, },
"dependencies": { "dependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
"axios": "^1.6.8", "axios": "^1.6.8",
"pinia": "^2.1.7", "pinia": "^2.1.7",

View File

@ -0,0 +1,23 @@
import { client, type ResponseError, handle_error } from "@/api/client";
export interface UserCredentials {
name: string,
password: string,
email?: string
}
export async function signup(body: UserCredentials): Promise<null | ResponseError> {
return await client.post("/auth/signup", JSON.stringify(body))
.catch(handle_error);
}
export async function signin(body: UserCredentials): Promise<null | ResponseError> {
return await client.post("/auth/signin", JSON.stringify(body))
.catch(handle_error);
}
export async function signout(): Promise<null | ResponseError> {
return await client.post("/auth/signout")
.catch(handle_error);
}

View File

@ -13,12 +13,19 @@ export class HttpError extends Error {
} }
export interface ResponseError { export interface ResponseError {
status_code: number, status: number | null,
message: string message: string | null
} }
export function handle_error(error: AxiosError): Promise<ResponseError> { export function handle_error(error: AxiosError): Promise<ResponseError> {
return Promise.reject<ResponseError>({ status_code: error.response?.status, message: error.response?.data }); let message = error.response?.data?.detail || error.response?.data;
console.log(error);
// extract pydantic error message
if (error.response.status == 422) {
message = error.response?.data?.detail[1].ctx.reason;
}
return Promise.reject<ResponseError>({ status: error.response.status, message: message});
} }
const debug = import.meta.hot; const debug = import.meta.hot;

View File

@ -1 +1,2 @@
export * as auth from "@/api/auth";
export * as user from "@/api/user"; export * as user from "@/api/user";

View File

@ -4,24 +4,37 @@
@layer base { @layer base {
body { body {
background-color: rgba(40, 30, 30, 1); /*linear-gradient(rgba(36, 14, 84, 1) 80%, rgba(55, 22, 130, 1)); */ @apply bg-ctp-crust;
/*background-image: url("./background.svg");*/ font-family: Inter,sans-serif;
background-position: left top; font-weight: 400;
background-repeat: repeat-x;
} }
a { a {
@apply text-green-500 hover:text-green-400; @apply text-ctp-green;
} }
h1 { .input {
font-family: BioRhyme,serif; @apply w-full pl-3 pr-3 pt-2 pb-2 rounded border bg-ctp-mantle border-ctp-overlay0 hover:border-ctp-overlay1 focus:border-ctp-green text-ctp-text outline-none;
font-weight: 700;
} }
label { label {
font-family: Space Mono,monospace; @apply text-ctp-text;
font-weight: 500; }
.button {
@apply pt-2 pb-2 pl-5 pr-5 rounded bg-ctp-mantle hover:bg-ctp-base text-ctp-blue;
}
.link-button {
@apply button text-ctp-green;
}
h1 {
@apply text-ctp-text pt-5 pb-5 border-b border-ctp-overlay0 mb-5;
}
label {
@apply text-ctp-text;
} }
} }

View File

@ -0,0 +1,5 @@
<template>
<h1 class="text-center pt-3 pb-3 bg-ctp-red/25 rounded border border-ctp-red text-ctp-red">
<slot></slot>
</h1>
</template>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="relative h-12 border-b border-b-zinc-500"> <div class="relative h-12">
<nav <nav
class="absolute w-full h-[calc(100%-1px)] flex justify-between items-center m-0 pl-3 pr-3 bg-gradient-to-t from-zinc-800 to-zinc-900"> class="absolute w-full h-full flex justify-between items-center m-0 pl-3 pr-3 bg-ctp-mantle">
<div class="items-center m-0 flex"> <div class="items-center m-0 flex">
<slot name="left"></slot> <slot name="left"></slot>
</div> </div>

View File

@ -0,0 +1,5 @@
<template>
<h1 class="text-center pt-3 pb-3 bg-ctp-peach/25 rounded border border-ctp-peach text-ctp-peach">
<slot></slot>
</h1>
</template>

View File

@ -1,5 +0,0 @@
<template>
<h1 class="text-center pt-3 pb-3 bg-orange-900 rounded border border-orange-700">
<slot></slot>
</h1>
</template>

View File

@ -0,0 +1 @@
export * as api from "@/api";

View File

@ -47,12 +47,12 @@ const router = createRouter({
component: () => import("@/views/Home.vue"), component: () => import("@/views/Home.vue"),
}, },
{ {
path: "/user/login", name: "signin", beforeEnter: [bypass_auth], path: "/auth/signin", name: "signin", beforeEnter: [bypass_auth],
component: () => import("@/views/user/SignIn.vue") component: () => import("@/views/auth/SignIn.vue")
}, },
{ {
path: "/user/register", name: "signup", //beforeEnter: [bypass_auth], path: "/auth/signup", name: "signup", //beforeEnter: [bypass_auth],
component: () => import("@/views/user/SignUp.vue") component: () => import("@/views/auth/SignUp.vue")
}, },
{ {
path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth], path: "/user/preferencies", name: "prefs", redirect: { name: "prefs-profile" }, beforeEnter: [required_auth],
@ -78,7 +78,7 @@ const router = createRouter({
}, },
{ {
path: "/:pathMatch(.*)*", name: "not-found", beforeEnter: [bypass_auth], path: "/:pathMatch(.*)*", name: "not-found", beforeEnter: [bypass_auth],
component: () => import("@/views/error/NotFound.vue") component: () => import("@/views/NotFound.vue")
} }
] ]
}); });

View File

@ -26,7 +26,7 @@ async function signout() {
<div class="flex-grow pb-20"> <div class="flex-grow pb-20">
<NavBar> <NavBar>
<template #left> <template #left>
<!-- TODO: logo --> <RouterLink class="link-button" to="/">Home</RouterLink>
</template> </template>
<template #right> <template #right>
<DropdownMenu v-if="userStore.current"> <DropdownMenu v-if="userStore.current">
@ -57,23 +57,23 @@ async function signout() {
</template> </template>
</DropdownMenu> </DropdownMenu>
<RouterLink v-if="!userStore.current" <RouterLink v-if="!userStore.current" class="link-button" to="/user/login">Sign In</RouterLink>
class="flex min-w-9 min-h-9 pt-1 pb-1 pl-3 pr-3 rounded hover:bg-zinc-600" to="/user/login">
Sign In</RouterLink>
</template> </template>
</NavBar> </NavBar>
<main> <main class="w-[1000px] ml-auto mr-auto pt-5 pb-5">
<slot></slot> <slot></slot>
</main> </main>
</div> </div>
<div class="relative overflow-hidden h-full "> <div class="relative overflow-hidden h-full ">
</div> </div>
<footer <footer
class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-gradient-to-b from-zinc-800 to-zinc-900 border-t border-t-zinc-500"> class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
<a href="/">Made with glove</a> <a href="/">Made with glove by Elnafo, 2024</a>
<div>
</div>
<a href="/api/docs">API</a> <a href="/api/docs">API</a>
</footer> </footer>
</template> </template>

View File

@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import Base from '@/views/Base.vue'; import Base from "@/views/Base.vue";
</script> </script>
<template> <template>
<Base> <Base>
Home <h1 class="text-2xl">Easy and secure cloud storage</h1>
<h1 class="text-2xl">Run Materia anywhere</h1>
<h1 class="text-2xl">Manage files with CLI application</h1>
</Base> </Base>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Base from "@/views/Base.vue"; import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue"; import Error from "@/components/Error.vue";
</script> </script>
<template> <template>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/Error.vue";
import { ref, onMounted } from "vue";
import router from "@/router";
import { api } from "@";
import { useUserStore } from "@/stores";
const email_or_username = defineModel("email_or_username");
const password = defineModel("password");
const userStore = useUserStore();
const error = ref(null);
onMounted(async () => {
if (userStore.current) {
router.replace({ path: "/" });
}
});
async function signin() {
if (!email_or_username.value || !password.value) {
return;
}
const body: user.UserCredentials = {
name: null,
password: password.value,
email: null
};
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email_or_username.value)) {
body.name = "";
body.email = email_or_username.value;
} else {
body.name = email_or_username.value;
}
await api.auth.signin(body)
.then(async () => {
//userStore.current = user;
router.push({ path: "/" });
})
.catch(err => { error.value = err.message; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h1 class="text-center text-ctp-text pt-5 pb-5 border-b border-ctp-overlay0 mb-5">Sign In</h1>
<form @submit.prevent>
<div class="mb-5">
<label for="email_or_login">Email or Username</label>
<input v-model="email_or_username" placeholder="" name="email_or_login" required class="input">
</div>
<div class="mb-5">
<label for="password">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required class="input">
</div>
<div class="mb-5 flex justify-between items-center">
<button @click="signin" class="button">Sign In</button>
<button @click="$router.push('/user/register')" class="button">Sign Up</button>
</div>
</form>
<Error v-if="error">{{ error }}</Error>
</div>
</Base>
</template>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/Error.vue";
import { ref } from "vue";
import router from "@/router";
import { api } from "@";
const login = defineModel("login");
const email = defineModel("email");
const password = defineModel("password");
const error = ref(null);
async function signup() {
if (!login.value || !email.value || !password.value) {
return;
}
await api.auth.signup({ name: login.value, password: password.value, email: email.value })
.then(async user => { router.push({ path: "/user/login" }); })
.catch(err => { error.value = err.message; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h1 class="text-center">Sign Up</h1>
<form @submit.prevent>
<div class="mb-5">
<label for="login">Login</label>
<input v-model="login" type="" placeholder="" name="login" required class="input">
</div>
<div class="mb-5">
<label for="email">Email Address</label>
<input v-model="email" type="email" placeholder="" name="email" required class="input">
</div>
<div class="mb-5">
<label for="password">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required class="input">
</div>
<div class="mb-5 flex justify-between items-center">
<button @click="signup" class="button">Sign Up</button>
</div>
</form>
<Error v-if="error">{{ error }}</Error>
</div>
</Base>
</template>

View File

@ -1,76 +0,0 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import { ref, onMounted } from "vue";
import router from "@/router";
import { user } from "@/api";
import { useUserStore } from "@/stores";
const email_or_username = defineModel("email_or_username");
const password = defineModel("password");
const userStore = useUserStore();
const error = ref(null);
onMounted(async () => {
if (userStore.current) {
router.replace({ path: "/" });
}
});
async function signin() {
const body: user.UserCredentials = {
name: null,
password: password.value,
email: null
};
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email_or_username.value)) {
body.name = "";
body.email = email_or_username.value;
} else {
body.name = email_or_username.value;
}
await user.signin(body)
.then(async () => {
//userStore.current = user;
router.push({ path: "/" });
})
.catch(error => { error.value = error; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h1 class="text-center pt-5 pb-5 border-b border-zinc-500">Sign In</h1>
<form @submit.prevent class="m-auto pt-5 pb-5">
<div class="mb-5 ml-auto mr-auto">
<label for="email_or_login" class="text-right w-64 inline-block mr-5">Email or Username</label>
<input v-model="email_or_username" placeholder="" name="email_or_login" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label for="password" class="text-right w-64 inline-block mr-5">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label class="text-right w-64 inline-block mr-5"></label>
<div class="flex justify-between items-center w-1/2 m-auto">
<button @click="signin" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
In</button>
<p>or</p>
<button @click="$router.push('/user/register')"
class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
Up</button>
</div>
</div>
</form>
<Error v-if="error">{{ error }}</Error>
</div>
</Base>
</template>

View File

@ -1,52 +0,0 @@
<script setup lang="ts">
import Base from "@/views/Base.vue";
import Error from "@/components/error/Error.vue";
import { ref } from "vue";
import router from "@/router";
import { user } from "@/api";
const login = defineModel("login");
const email = defineModel("email");
const password = defineModel("password");
const error = ref(null);
async function signup() {
await user.register({ login: login.value, password: password.value, email: email.value })
.then(async user => { router.push({ path: "/user/login" }); })
.catch(error => { error.value = error; });
};
</script>
<template>
<Base>
<div class="ml-auto mr-auto w-1/2 pt-5 pb-5">
<h4 class="text-center pt-5 pb-5 border-b border-zinc-500">Sign Up</h4>
<form @submit.prevent class="m-auto pt-5 pb-5">
<div class="mb-5 ml-auto mr-auto">
<label for="login" class="text-right w-64 inline-block mr-5">Login</label>
<input v-model="login" type="" placeholder="" name="login" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label for="email" class="text-right w-64 inline-block mr-5">Email Address</label>
<input v-model="email" type="email" placeholder="" name="email" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label for="password" class="text-right w-64 inline-block mr-5">Password</label>
<input v-model="password" placeholder="" type="password" name="password" required
class="w-1/2 bg-zinc-800 pl-3 pr-3 pt-2 pb-2 outline-none rounded border border-zinc-500 hover:border-zinc-400 focus:border-green-800">
</div>
<div class="mb-5 ml-auto mr-auto">
<label class="text-right w-64 inline-block mr-5"></label>
<button @click="signup" class="rounded bg-zinc-500 hover:bg-zinc-400 pb-2 pt-2 pl-5 pr-5">Sign
Up</button>
</div>
</form>
<Error v-if="error">{{ error.message }}</Error>
</div>
</Base>
</template>

View File

@ -21,7 +21,12 @@ export default {
} }
}, },
}, },
plugins: [], plugins: [
require("@catppuccin/tailwindcss")({
prefix: "ctp",
defaultFlavour: "macchiato"
})
],
} }