materia-web-client: unify style with catppuccin
materia-server: fixing auth
This commit is contained in:
parent
997f37d5ee
commit
d8b19da646
@ -219,7 +219,9 @@
|
||||
};
|
||||
|
||||
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 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -46,8 +46,8 @@ materia-server = "materia_server.main:server"
|
||||
|
||||
[tool.pdm.scripts]
|
||||
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-downgrade.cmd = "alembic downgrade {args:base}"
|
||||
db-upgrade.cmd = "alembic -c ./src/materia_server/alembic.ini upgrade {args:head}"
|
||||
db-downgrade.shell = "alembic -c ./src/materia_server/alembic.ini downgrade {args:base}"
|
||||
db-revision.cmd = "alembic revision {args:--autogenerate}"
|
||||
remove-revisions.shell = "rm -v ./src/materia_server/models/migrations/versions/*.py"
|
||||
|
||||
|
@ -1 +1 @@
|
||||
from materia_server.models.user.user import User, UserCredentials
|
||||
from materia_server.models.user.user import User, UserCredentials, UserIdentity
|
||||
|
@ -3,7 +3,7 @@ from typing import Optional
|
||||
import time
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from pydantic import BaseModel, EmailStr, ConfigDict
|
||||
import pydantic
|
||||
from sqlalchemy import BigInteger, Enum
|
||||
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)
|
||||
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]
|
||||
is_email_private: Mapped[bool] = mapped_column(default = True)
|
||||
hashed_password: Mapped[str]
|
||||
@ -80,5 +80,25 @@ class UserCredentials(BaseModel):
|
||||
password: str
|
||||
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
|
||||
|
@ -1 +1,2 @@
|
||||
from materia_server.routers import api
|
||||
from materia_server.routers import middleware
|
||||
|
@ -1,6 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
from materia_server.routers.api import auth
|
||||
from materia_server.routers.api import user
|
||||
|
||||
router = APIRouter(prefix = "/api")
|
||||
|
||||
router.include_router(auth.router)
|
||||
router.include_router(user.router)
|
||||
|
@ -40,8 +40,9 @@ async def signup(body: user.UserCredentials, ctx: context.Context = Depends()):
|
||||
|
||||
@router.post("/auth/signin")
|
||||
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:
|
||||
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid credentials")
|
||||
if (current_user := await user.User.by_name(body.name, ctx.database)) is None:
|
||||
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):
|
||||
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid password")
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from materia_server.routers.api.user import user
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(user.router)
|
19
materia-server/src/materia_server/routers/api/user/user.py
Normal file
19
materia-server/src/materia_server/routers/api/user/user.py
Normal 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)
|
@ -1,45 +1,79 @@
|
||||
from typing import Optional, Sequence
|
||||
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
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
from enum import StrEnum
|
||||
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.api.token import TokenClaims
|
||||
from materia import db
|
||||
from materia_server import security
|
||||
from materia_server.routers import context
|
||||
from materia_server.models import user
|
||||
|
||||
|
||||
class JwtMiddleware(HTTPBearer):
|
||||
def __init__(self, auto_error: bool = True):
|
||||
super().__init__(auto_error = auto_error)
|
||||
self.claims: Optional[TokenClaims] = None
|
||||
async def get_token_claims(token, ctx: context.Context = Depends()) -> security.TokenClaims:
|
||||
try:
|
||||
secret = ctx.config.oauth2.jwt_secret if ctx.config.oauth2.jwt_signing_algo in ["HS256", "HS384", "HS512"] else ctx.config.oauth2.jwt_signing_key
|
||||
claims = security.validate_token(token, secret)
|
||||
user_id = uuid.UUID(claims.sub) # type: ignore
|
||||
except jwt.PyJWKError as _:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
|
||||
except ValueError as _:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token")
|
||||
|
||||
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":
|
||||
if not (current_user := await user.User.by_id(user_id, ctx.database)):
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing 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
|
||||
|
||||
if not token:
|
||||
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:
|
||||
self.claims = TokenClaims.verify(token, config.jwt.secret)
|
||||
user_id = uuid.UUID(self.claims.sub) # type: ignore
|
||||
except jwt.PyJWKError as _:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
|
||||
except ValueError as _:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid token")
|
||||
refresh_claims = security.validate_token(refresh_token, secret) if refresh_token else None
|
||||
# TODO: check expiration
|
||||
except jwt.PyJWTError:
|
||||
refresh_claims = None
|
||||
|
||||
async with database.session() as session:
|
||||
if not (user := (await session.scalars(select(db.User).where(db.User.id == user_id))).first()):
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing user")
|
||||
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
|
||||
|
||||
request.state.user = user
|
||||
|
||||
WILDCARD = "*"
|
||||
NULL = "null"
|
||||
|
@ -8,6 +8,7 @@
|
||||
"name": "materia-frontend",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^0.1.6",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"axios": "^1.6.8",
|
||||
"pinia": "^2.1.7",
|
||||
@ -484,6 +485,14 @@
|
||||
"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": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
@ -1494,11 +1503,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -1859,9 +1868,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
|
@ -11,6 +11,7 @@
|
||||
"type-check": "vue-tsc --build --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@catppuccin/tailwindcss": "^0.1.6",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"axios": "^1.6.8",
|
||||
"pinia": "^2.1.7",
|
||||
|
23
materia-web-client/src/materia-frontend/src/api/auth.ts
Normal file
23
materia-web-client/src/materia-frontend/src/api/auth.ts
Normal 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);
|
||||
}
|
@ -13,12 +13,19 @@ export class HttpError extends Error {
|
||||
}
|
||||
|
||||
export interface ResponseError {
|
||||
status_code: number,
|
||||
message: string
|
||||
status: number | null,
|
||||
message: string | null
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -1 +1,2 @@
|
||||
export * as auth from "@/api/auth";
|
||||
export * as user from "@/api/user";
|
||||
|
@ -4,24 +4,37 @@
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
background-color: rgba(40, 30, 30, 1); /*linear-gradient(rgba(36, 14, 84, 1) 80%, rgba(55, 22, 130, 1)); */
|
||||
/*background-image: url("./background.svg");*/
|
||||
background-position: left top;
|
||||
background-repeat: repeat-x;
|
||||
@apply bg-ctp-crust;
|
||||
font-family: Inter,sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-green-500 hover:text-green-400;
|
||||
@apply text-ctp-green;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: BioRhyme,serif;
|
||||
font-weight: 700;
|
||||
.input {
|
||||
@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;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: Space Mono,monospace;
|
||||
font-weight: 500;
|
||||
@apply text-ctp-text;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="relative h-12 border-b border-b-zinc-500">
|
||||
<div class="relative h-12">
|
||||
<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">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
|
@ -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>
|
@ -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>
|
1
materia-web-client/src/materia-frontend/src/index.ts
Normal file
1
materia-web-client/src/materia-frontend/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as api from "@/api";
|
@ -47,12 +47,12 @@ const router = createRouter({
|
||||
component: () => import("@/views/Home.vue"),
|
||||
},
|
||||
{
|
||||
path: "/user/login", name: "signin", beforeEnter: [bypass_auth],
|
||||
component: () => import("@/views/user/SignIn.vue")
|
||||
path: "/auth/signin", name: "signin", beforeEnter: [bypass_auth],
|
||||
component: () => import("@/views/auth/SignIn.vue")
|
||||
},
|
||||
{
|
||||
path: "/user/register", name: "signup", //beforeEnter: [bypass_auth],
|
||||
component: () => import("@/views/user/SignUp.vue")
|
||||
path: "/auth/signup", name: "signup", //beforeEnter: [bypass_auth],
|
||||
component: () => import("@/views/auth/SignUp.vue")
|
||||
},
|
||||
{
|
||||
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],
|
||||
component: () => import("@/views/error/NotFound.vue")
|
||||
component: () => import("@/views/NotFound.vue")
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ async function signout() {
|
||||
<div class="flex-grow pb-20">
|
||||
<NavBar>
|
||||
<template #left>
|
||||
<!-- TODO: logo -->
|
||||
<RouterLink class="link-button" to="/">Home</RouterLink>
|
||||
</template>
|
||||
<template #right>
|
||||
<DropdownMenu v-if="userStore.current">
|
||||
@ -57,23 +57,23 @@ async function signout() {
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
|
||||
<RouterLink v-if="!userStore.current"
|
||||
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>
|
||||
<RouterLink v-if="!userStore.current" class="link-button" to="/user/login">Sign In</RouterLink>
|
||||
</template>
|
||||
</NavBar>
|
||||
|
||||
<main>
|
||||
<main class="w-[1000px] ml-auto mr-auto pt-5 pb-5">
|
||||
<slot></slot>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="relative overflow-hidden h-full ">
|
||||
</div>
|
||||
<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">
|
||||
<a href="/">Made with glove</a>
|
||||
class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
|
||||
<a href="/">Made with glove by Elnafo, 2024</a>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<a href="/api/docs">API</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import Base from '@/views/Base.vue';
|
||||
import Base from "@/views/Base.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Base from "@/views/Base.vue";
|
||||
import Error from "@/components/error/Error.vue";
|
||||
import Error from "@/components/Error.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -21,7 +21,12 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require("@catppuccin/tailwindcss")({
|
||||
prefix: "ctp",
|
||||
defaultFlavour: "macchiato"
|
||||
})
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user