mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: team draft
This commit is contained in:
parent
8159b65605
commit
f5250d5361
@ -32,6 +32,8 @@ const Providers = ({ children }: Props) => {
|
|||||||
colors: {
|
colors: {
|
||||||
...base.colors,
|
...base.colors,
|
||||||
background: theme.background.val,
|
background: theme.background.val,
|
||||||
|
card: theme.background.val,
|
||||||
|
border: theme.borderColor.val,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [theme, colorScheme]);
|
}, [theme, colorScheme]);
|
||||||
|
@ -10,27 +10,25 @@ import {
|
|||||||
useLinkBuilder,
|
useLinkBuilder,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
import Icons from "../ui/icons";
|
import ThemeSwitcher from "./theme-switcher";
|
||||||
import { logout } from "@/stores/auth";
|
import UserMenuButton from "./user-menu-button";
|
||||||
|
|
||||||
const Drawer = (props: DrawerContentComponentProps) => {
|
const Drawer = (props: DrawerContentComponentProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<View p="$4">
|
||||||
|
<UserMenuButton />
|
||||||
|
</View>
|
||||||
|
|
||||||
<DrawerContentScrollView
|
<DrawerContentScrollView
|
||||||
contentContainerStyle={{ padding: 18 }}
|
contentContainerStyle={{ padding: 18, paddingTop: 0 }}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<DrawerItemList {...props} />
|
<DrawerItemList {...props} />
|
||||||
</DrawerContentScrollView>
|
</DrawerContentScrollView>
|
||||||
|
|
||||||
<View p="$4">
|
<View px="$4" py="$2">
|
||||||
<Button
|
<ThemeSwitcher />
|
||||||
justifyContent="flex-start"
|
|
||||||
icon={<Icons name="logout" size={16} />}
|
|
||||||
onPress={() => logout()}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,28 +1,36 @@
|
|||||||
import React from "react";
|
import React, { useId } from "react";
|
||||||
import { Button, GetProps } from "tamagui";
|
import { Switch, GetProps, XStack, Label } from "tamagui";
|
||||||
import Icons from "../ui/icons";
|
|
||||||
import useThemeStore from "@/stores/theme";
|
import useThemeStore from "@/stores/theme";
|
||||||
|
import { Ionicons } from "../ui/icons";
|
||||||
|
|
||||||
type Props = GetProps<typeof Button> & {
|
type Props = GetProps<typeof XStack> & {
|
||||||
iconSize?: number;
|
iconSize?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeSwitcher = ({ iconSize = 24, ...props }: Props) => {
|
const ThemeSwitcher = ({ iconSize = 18, ...props }: Props) => {
|
||||||
const { theme, toggle } = useThemeStore();
|
const { theme, toggle } = useThemeStore();
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<XStack alignItems="center" gap="$2">
|
||||||
icon={
|
<Ionicons
|
||||||
<Icons
|
name={theme === "light" ? "moon-outline" : "sunny-outline"}
|
||||||
name={
|
|
||||||
theme === "light" ? "white-balance-sunny" : "moon-waning-crescent"
|
|
||||||
}
|
|
||||||
size={iconSize}
|
size={iconSize}
|
||||||
/>
|
/>
|
||||||
}
|
<Label htmlFor={id} flex={1} cursor="pointer">
|
||||||
|
{`${theme === "light" ? "Dark" : "Light"} Mode`}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id={id}
|
||||||
onPress={toggle}
|
onPress={toggle}
|
||||||
|
checked={theme === "dark"}
|
||||||
|
size="$2"
|
||||||
|
cursor="pointer"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
<Switch.Thumb animation="quicker" />
|
||||||
|
</Switch>
|
||||||
|
</XStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
102
frontend/components/containers/user-menu-button.tsx
Normal file
102
frontend/components/containers/user-menu-button.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
ListItem,
|
||||||
|
Separator,
|
||||||
|
Text,
|
||||||
|
useMedia,
|
||||||
|
View,
|
||||||
|
YGroup,
|
||||||
|
} from "tamagui";
|
||||||
|
import MenuButton from "../ui/menu-button";
|
||||||
|
import Icons from "../ui/icons";
|
||||||
|
import { logout } from "@/stores/auth";
|
||||||
|
import { useUser } from "@/hooks/useUser";
|
||||||
|
|
||||||
|
const UserMenuButton = () => {
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuButton
|
||||||
|
size="$1"
|
||||||
|
placement="bottom-end"
|
||||||
|
width={213}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
bg="$colorTransparent"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
p={0}
|
||||||
|
gap="$1"
|
||||||
|
>
|
||||||
|
<Avatar circular size="$3">
|
||||||
|
<Avatar.Fallback bg="$blue4" />
|
||||||
|
</Avatar>
|
||||||
|
<View flex={1} style={{ textAlign: "left" }}>
|
||||||
|
<Text numberOfLines={1}>{user?.name}</Text>
|
||||||
|
<Text numberOfLines={1} fontWeight="600" mt="$1.5">
|
||||||
|
Personal
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Icons name="chevron-down" size={16} />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TeamsMenu />
|
||||||
|
<MenuButton.Item
|
||||||
|
onPress={() => console.log("logout")}
|
||||||
|
icon={<Icons name="account" size={16} />}
|
||||||
|
title="Account"
|
||||||
|
/>
|
||||||
|
<Separator w="100%" />
|
||||||
|
<MenuButton.Item
|
||||||
|
onPress={() => logout()}
|
||||||
|
icon={<Icons name="logout" size={16} />}
|
||||||
|
title="Logout"
|
||||||
|
/>
|
||||||
|
</MenuButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeamsMenu = () => {
|
||||||
|
const media = useMedia();
|
||||||
|
const user = useUser();
|
||||||
|
const teams = user?.teams || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuButton
|
||||||
|
size="$1"
|
||||||
|
placement={media.xs ? "bottom" : "right-start"}
|
||||||
|
asChild
|
||||||
|
width={213}
|
||||||
|
trigger={
|
||||||
|
<ListItem
|
||||||
|
hoverTheme
|
||||||
|
pressTheme
|
||||||
|
onPress={() => console.log("logout")}
|
||||||
|
icon={<Icons name="account-group" size={16} />}
|
||||||
|
title="Teams"
|
||||||
|
iconAfter={<Icons name="chevron-right" size={16} />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuButton.Item
|
||||||
|
icon={<Icons name="account" size={16} />}
|
||||||
|
title="Personal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{teams.map((team: any) => (
|
||||||
|
<MenuButton.Item icon={<Text>{team.icon}</Text>} title={team.name} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{teams.length > 0 && <Separator width="100%" />}
|
||||||
|
|
||||||
|
<MenuButton.Item
|
||||||
|
icon={<Icons name="plus" size={16} />}
|
||||||
|
title="Create Team"
|
||||||
|
/>
|
||||||
|
</MenuButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserMenuButton;
|
@ -1,8 +1,12 @@
|
|||||||
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
||||||
|
import IoniconsIcon from "@expo/vector-icons/Ionicons";
|
||||||
import { styled } from "tamagui";
|
import { styled } from "tamagui";
|
||||||
|
|
||||||
export const Icons = styled(MaterialCommunityIcons, {
|
export const Icons = styled(MaterialCommunityIcons, {
|
||||||
color: "$color",
|
color: "$color",
|
||||||
});
|
});
|
||||||
|
export const Ionicons = styled(IoniconsIcon, {
|
||||||
|
color: "$color",
|
||||||
|
});
|
||||||
|
|
||||||
export default Icons;
|
export default Icons;
|
||||||
|
50
frontend/components/ui/menu-button.tsx
Normal file
50
frontend/components/ui/menu-button.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
GetProps,
|
||||||
|
ListItem,
|
||||||
|
Popover,
|
||||||
|
styled,
|
||||||
|
withStaticProperties,
|
||||||
|
} from "tamagui";
|
||||||
|
|
||||||
|
type MenuButtonProps = GetProps<typeof Popover> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
width?: string | number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuButtonFrame = ({
|
||||||
|
asChild,
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
width,
|
||||||
|
...props
|
||||||
|
}: MenuButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Popover {...props}>
|
||||||
|
<Popover.Trigger asChild={asChild}>{trigger}</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content
|
||||||
|
bordered
|
||||||
|
enterStyle={{ y: -10, opacity: 0 }}
|
||||||
|
exitStyle={{ y: -10, opacity: 0 }}
|
||||||
|
animation={["quick", { opacity: { overshootClamping: true } }]}
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuButtonItem = (props: GetProps<typeof ListItem>) => (
|
||||||
|
<Popover.Close asChild>
|
||||||
|
<ListItem hoverTheme pressTheme {...props} />
|
||||||
|
</Popover.Close>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MenuButton = withStaticProperties(MenuButtonFrame, {
|
||||||
|
Item: MenuButtonItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MenuButton;
|
10
frontend/hooks/useUser.ts
Normal file
10
frontend/hooks/useUser.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import authRepo from "@/repositories/auth";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const useUser = () => {
|
||||||
|
const { data: user } = useQuery({
|
||||||
|
queryKey: ["auth", "user"],
|
||||||
|
queryFn: authRepo.getUser,
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
};
|
@ -13,6 +13,7 @@ import { loginResultSchema, loginSchema } from "./schema";
|
|||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import Icons from "@/components/ui/icons";
|
import Icons from "@/components/ui/icons";
|
||||||
import authStore from "@/stores/auth";
|
import authStore from "@/stores/auth";
|
||||||
|
import tamaguiConfig from "@/tamagui.config";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const form = useZForm(loginSchema);
|
const form = useZForm(loginSchema);
|
||||||
@ -42,7 +43,7 @@ export default function LoginPage() {
|
|||||||
options={{
|
options={{
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: 600,
|
maxWidth: tamaguiConfig.media.xs.maxWidth,
|
||||||
marginHorizontal: "auto",
|
marginHorizontal: "auto",
|
||||||
},
|
},
|
||||||
title: "Login",
|
title: "Login",
|
||||||
|
@ -26,6 +26,7 @@ const HostItem = ({ host, onMultiTap, onTap, onEdit }: HostItemProps) => {
|
|||||||
name={host.os}
|
name={host.os}
|
||||||
size={18}
|
size={18}
|
||||||
mr="$2"
|
mr="$2"
|
||||||
|
mt="$1"
|
||||||
fallback="desktop-classic"
|
fallback="desktop-classic"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -39,9 +40,11 @@ const HostItem = ({ host, onMultiTap, onTap, onEdit }: HostItemProps) => {
|
|||||||
{onEdit != null && (
|
{onEdit != null && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
display="none"
|
opacity={0}
|
||||||
$sm={{ display: "block" }}
|
$sm={{ opacity: 1 }}
|
||||||
$group-hover={{ display: "block" }}
|
$group-hover={{ opacity: 1 }}
|
||||||
|
animation="quick"
|
||||||
|
animateOnly={["opacity"]}
|
||||||
onPress={(e) => {
|
onPress={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit();
|
onEdit();
|
||||||
|
@ -13,7 +13,7 @@ type HostsListProps = {
|
|||||||
allowEdit?: boolean;
|
allowEdit?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HostsList = ({ allowEdit = true }: HostsListProps) => {
|
const HostList = ({ allowEdit = true }: HostsListProps) => {
|
||||||
const openSession = useTermSession((i) => i.push);
|
const openSession = useTermSession((i) => i.push);
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@ -98,4 +98,4 @@ const HostsList = ({ allowEdit = true }: HostsListProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(HostsList);
|
export default React.memo(HostList);
|
@ -1,7 +1,7 @@
|
|||||||
import { Button } from "tamagui";
|
import { Button } from "tamagui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Drawer from "expo-router/drawer";
|
import Drawer from "expo-router/drawer";
|
||||||
import HostsList from "./components/hosts-list";
|
import HostList from "./components/host-list";
|
||||||
import HostForm, { hostFormModal } from "./components/form";
|
import HostForm, { hostFormModal } from "./components/form";
|
||||||
import Icons from "@/components/ui/icons";
|
import Icons from "@/components/ui/icons";
|
||||||
import { initialValues } from "./schema/form";
|
import { initialValues } from "./schema/form";
|
||||||
@ -25,7 +25,7 @@ export default function HostsPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HostsList />
|
<HostList />
|
||||||
<HostForm />
|
<HostForm />
|
||||||
<KeyForm />
|
<KeyForm />
|
||||||
</>
|
</>
|
||||||
|
@ -16,7 +16,7 @@ export const UserTypeInputFields = ({ form }: Props) => {
|
|||||||
<InputField f={1} form={form} name="data.username" />
|
<InputField f={1} form={form} name="data.username" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Password">
|
<FormField label="Password">
|
||||||
<InputField f={1} form={form} name="data.password" />
|
<InputField f={1} form={form} name="data.password" secureTextEntry />
|
||||||
</FormField>
|
</FormField>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -32,7 +32,7 @@ export const PVETypeInputFields = ({ form }: Props) => {
|
|||||||
<SelectField form={form} name="data.realm" items={pveRealms} />
|
<SelectField form={form} name="data.realm" items={pveRealms} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Password">
|
<FormField label="Password">
|
||||||
<InputField f={1} form={form} name="data.password" />
|
<InputField f={1} form={form} name="data.password" secureTextEntry />
|
||||||
</FormField>
|
</FormField>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -48,7 +48,7 @@ export const RSATypeInputFields = ({ form }: Props) => {
|
|||||||
<TextAreaField rows={7} f={1} form={form} name="data.private" />
|
<TextAreaField rows={7} f={1} form={form} name="data.private" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Passphrase">
|
<FormField label="Passphrase">
|
||||||
<InputField f={1} form={form} name="data.passphrase" />
|
<InputField f={1} form={form} name="data.passphrase" secureTextEntry />
|
||||||
</FormField>
|
</FormField>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,7 @@ import { ofetch } from "ofetch";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ErrorAlert } from "@/components/ui/alert";
|
import { ErrorAlert } from "@/components/ui/alert";
|
||||||
import { addServer } from "@/stores/app";
|
import { addServer } from "@/stores/app";
|
||||||
|
import tamaguiConfig from "@/tamagui.config";
|
||||||
|
|
||||||
export default function ServerPage() {
|
export default function ServerPage() {
|
||||||
const form = useZForm(serverSchema);
|
const form = useZForm(serverSchema);
|
||||||
@ -41,7 +42,7 @@ export default function ServerPage() {
|
|||||||
options={{
|
options={{
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: 600,
|
maxWidth: tamaguiConfig.media.xs.maxWidth,
|
||||||
marginHorizontal: "auto",
|
marginHorizontal: "auto",
|
||||||
},
|
},
|
||||||
title: "Vaulterm",
|
title: "Vaulterm",
|
||||||
|
@ -4,7 +4,7 @@ import PagerView from "@/components/ui/pager-view";
|
|||||||
import { useTermSession } from "@/stores/terminal-sessions";
|
import { useTermSession } from "@/stores/terminal-sessions";
|
||||||
import { Button, useMedia } from "tamagui";
|
import { Button, useMedia } from "tamagui";
|
||||||
import SessionTabs from "./components/session-tabs";
|
import SessionTabs from "./components/session-tabs";
|
||||||
import HostsList from "../hosts/components/hosts-list";
|
import HostList from "../hosts/components/host-list";
|
||||||
import Drawer from "expo-router/drawer";
|
import Drawer from "expo-router/drawer";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import Icons from "@/components/ui/icons";
|
import Icons from "@/components/ui/icons";
|
||||||
@ -35,7 +35,7 @@ const TerminalPage = () => {
|
|||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
page={curSession}
|
page={curSession}
|
||||||
onChangePage={setSession}
|
onChangePage={setSession}
|
||||||
EmptyComponent={() => <HostsList allowEdit={false} />}
|
EmptyComponent={() => <HostList allowEdit={false} />}
|
||||||
>
|
>
|
||||||
{sessions.map((session) => (
|
{sessions.map((session) => (
|
||||||
<InteractiveSession key={session.id} {...session} />
|
<InteractiveSession key={session.id} {...session} />
|
||||||
|
9
frontend/repositories/auth.ts
Normal file
9
frontend/repositories/auth.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import api from "@/lib/api";
|
||||||
|
|
||||||
|
const authRepo = {
|
||||||
|
getUser() {
|
||||||
|
return api("/auth/user");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authRepo;
|
@ -45,6 +45,7 @@ func Init() {
|
|||||||
|
|
||||||
// Migrate the schema
|
// Migrate the schema
|
||||||
db.AutoMigrate(Models...)
|
db.AutoMigrate(Models...)
|
||||||
|
InitModels(db)
|
||||||
runSeeders(db)
|
runSeeders(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import "rul.sh/vaulterm/models"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"rul.sh/vaulterm/models"
|
||||||
|
)
|
||||||
|
|
||||||
var Models = []interface{}{
|
var Models = []interface{}{
|
||||||
&models.User{},
|
&models.User{},
|
||||||
&models.UserSession{},
|
&models.UserSession{},
|
||||||
&models.Keychain{},
|
&models.Keychain{},
|
||||||
&models.Host{},
|
&models.Host{},
|
||||||
|
&models.Team{},
|
||||||
|
&models.TeamMembers{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitModels(db *gorm.DB) {
|
||||||
|
if err := db.SetupJoinTable(&models.Team{}, "Members", &models.TeamMembers{}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,18 @@ func seedUsers(tx *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
userList := []models.User{
|
teams := []*models.Team{
|
||||||
|
{
|
||||||
|
Name: "My Team",
|
||||||
|
Icon: "☘️",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := tx.Create(&teams); res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
userList := []*models.User{
|
||||||
{
|
{
|
||||||
Name: "Admin",
|
Name: "Admin",
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
@ -42,22 +53,39 @@ func seedUsers(tx *gorm.DB) error {
|
|||||||
Password: testPasswd,
|
Password: testPasswd,
|
||||||
Email: "user@mail.com",
|
Email: "user@mail.com",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Mary Doe",
|
||||||
|
Username: "user2",
|
||||||
|
Password: testPasswd,
|
||||||
|
Email: "user2@mail.com",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if res := tx.Create(&userList); res.Error != nil {
|
if res := tx.Create(&userList); res.Error != nil {
|
||||||
return res.Error
|
return res.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
teamMembers := []models.TeamMembers{
|
||||||
|
{TeamID: teams[0].ID, UserID: userList[0].ID, Role: "owner"},
|
||||||
|
{TeamID: teams[0].ID, UserID: userList[1].ID, Role: "admin"},
|
||||||
|
{TeamID: teams[0].ID, UserID: userList[2].ID, Role: "user"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := tx.Create(&teamMembers); res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSeeders(db *gorm.DB) {
|
func runSeeders(db *gorm.DB) {
|
||||||
db.Transaction(func(tx *gorm.DB) error {
|
db.Transaction(func(tx *gorm.DB) error {
|
||||||
for _, seed := range seeders {
|
for _, seed := range seeders {
|
||||||
if err := seed(db); err != nil {
|
if err := seed(tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
"rul.sh/vaulterm/db"
|
"rul.sh/vaulterm/db"
|
||||||
"rul.sh/vaulterm/models"
|
"rul.sh/vaulterm/models"
|
||||||
)
|
)
|
||||||
@ -32,7 +33,14 @@ func Auth(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func GetUserSession(sessionId string) (*models.UserSession, error) {
|
func GetUserSession(sessionId string) (*models.UserSession, error) {
|
||||||
var session models.UserSession
|
var session models.UserSession
|
||||||
res := db.Get().Joins("User").Where("user_sessions.id = ?", sessionId).First(&session)
|
res := db.Get().
|
||||||
|
Joins("User").
|
||||||
|
Preload("User.Teams", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Select("id", "name", "icon")
|
||||||
|
}).
|
||||||
|
Where("user_sessions.id = ?", sessionId).
|
||||||
|
First(&session)
|
||||||
|
|
||||||
return &session, res.Error
|
return &session, res.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,9 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) BeforeCreate(tx *gorm.DB) error {
|
func (m *Model) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if m.ID == "" {
|
||||||
m.ID = m.GenerateID()
|
m.ID = m.GenerateID()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
server/models/team.go
Normal file
23
server/models/team.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Team struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Name string `json:"name" gorm:"type:varchar(32)"`
|
||||||
|
Icon string `json:"icon" gorm:"type:varchar(2)"`
|
||||||
|
Members []*User `json:"members" gorm:"many2many:team_members"`
|
||||||
|
|
||||||
|
Timestamps
|
||||||
|
SoftDeletes
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamMembers struct {
|
||||||
|
TeamID string `json:"teamId" gorm:"primarykey;type:varchar(26)"`
|
||||||
|
Team Team `json:"team"`
|
||||||
|
UserID string `json:"userId" gorm:"primarykey;type:varchar(26)"`
|
||||||
|
User User `json:"user"`
|
||||||
|
Role string `json:"role" gorm:"type:varchar(16)"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
@ -14,6 +14,8 @@ type User struct {
|
|||||||
Email string `json:"email" gorm:"unique"`
|
Email string `json:"email" gorm:"unique"`
|
||||||
Role string `json:"role" gorm:"default:user;not null;index:users_role_idx;type:varchar(8)"`
|
Role string `json:"role" gorm:"default:user;not null;index:users_role_idx;type:varchar(8)"`
|
||||||
|
|
||||||
|
Teams []*Team `json:"teams" gorm:"many2many:team_members"`
|
||||||
|
|
||||||
Timestamps
|
Timestamps
|
||||||
SoftDeletes
|
SoftDeletes
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
*models.User
|
*models.User
|
||||||
IsAdmin bool
|
IsAdmin bool `json:"isAdmin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserData(user *models.User) *UserContext {
|
func getUserData(user *models.User) *UserContext {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user