validator, i18n, start with auth, locale loader, sequelize7 with ts, announcement

This commit is contained in:
lesion 2024-08-25 09:25:25 +02:00
parent fa60a667ea
commit 1ed0fa4fd4
No known key found for this signature in database
GPG key ID: 352918250B012177
15 changed files with 424 additions and 63 deletions

View file

@ -17,7 +17,7 @@ const config = useRuntimeConfig()
</v-main>
<!-- <CoreDialog /> -->
<!-- <CoreNotification /> -->
<v-footer app height="40"><v-spacer />v.{{config.public.version}} © 2023 Trialsh, made with for a better world</v-footer>
<v-footer app height="40"><v-spacer />v.{{config.public.version}}</v-footer>
</v-app>
</template>

View file

@ -1,4 +1,4 @@
module.exports = {
export default {
ca: 'Català',
cs: 'Czech',
de: 'Deutsch',

View file

@ -1,14 +0,0 @@
export default async (context, locale) => {
try {
if (process.server) {
return context.$axios.$get(`locale/${locale}`)
} else {
// cannot use $axios here as plugins have not yet been loaded
return fetch(`${window.location.origin}/api/locale/${locale}`).then(ret => ret.json())
}
} catch (e) {
console.error(`Error loading locale ${locale}`, e)
}
return {}
}

16
locales/loader.ts Normal file
View file

@ -0,0 +1,16 @@
export default defineI18nLocale(locale => $fetch(`api/locale/${locale}`))
// export default async (context: any, locale: string) => {
// try {
// if (process.server) {
// return context.$axios.$get(`locale/${locale}`)
// } else {
// // cannot use $axios here as plugins have not yet been loaded
// return fetch(`${window.location.origin}/api/locale/${locale}`).then(ret => ret.json())
// }
// } catch (e) {
// console.error(`Error loading locale ${locale}`, e)
// }
// return {}
// }

View file

@ -1,6 +1,39 @@
import { version } from './package.json'
import locales from './locales/index'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
modules: ["vuetify-nuxt-module" ],
modules: ["vuetify-nuxt-module", '@nuxtjs/i18n'],
runtimeConfig: {
public: {
version
}
},
i18n: {
locales: Object.keys(locales).map((key: string) => ({
code: key,
// name: locales[key],
file: 'loader.ts',
iso: key
})),
langDir: 'locales',
lazy: true,
strategy: 'no_prefix',
skipSettingLocaleOnNavigate: true,
},
nitro: {
esbuild: {
options: {
tsconfigRaw: {
// See https://github.com/nuxt/nuxt/issues/14126
// https://github.com/unjs/nitro/issues/273
compilerOptions: {
experimentalDecorators: true,
},
},
},
},
},
})

View file

@ -1,6 +1,29 @@
{
"name": "nuxt-app",
"private": true,
"name": "gancio",
"version": "2.0.0-alpha",
"description": "A shared agenda for local communities",
"author": "lesion",
"bugs": {
"email": "lesion@autistici.org",
"url": "https://framagit.org/les/gancio/issues"
},
"homepage": "https://gancio.org",
"keywords": [
"AP",
"gancio",
"events",
"federation",
"activitypub",
"event",
"server",
"self-host",
"app"
],
"license": "AGPL-3.0",
"repository": {
"type": "git",
"url": "https://framagit.org/les/gancio"
},
"type": "module",
"scripts": {
"build": "nuxt build",
@ -10,19 +33,22 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxtjs/i18n": "^8.5.0",
"@sequelize/core": "^7.0.0-alpha.37",
"@sequelize/sqlite3": "^7.0.0-alpha.41",
"nuxt": "^3.12.4",
"sequelize": "^6.37.3",
"vue": "latest",
"vuetify-nuxt-module": "^0.16.1"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.5.0",
"@nuxtjs/eslint-module": "^4.1.0",
"bcrypt": "^5.1.1",
"eslint": "^9.9.0",
"eslint-plugin-nuxt": "^4.0.0",
"linkifyjs": "^4.1.3",
"prettier": "^3.3.3",
"sqlite3": "^5.1.7"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

52
pages/Login.vue Normal file
View file

@ -0,0 +1,52 @@
<template>
<v-container class='pa-0 pa-md-3'>
<v-row class="mt-md-5 ma-0" justify='center'>
<v-col class='pa-0 pa-md-3' cols='12' md="6" lg="5" xl="4">
<v-form v-model='valid' ref='form' lazy-validation @submit.prevent='submit'>
<v-card>
<v-card-title>{{$t('common.login')}}</v-card-title>
<v-card-subtitle v-text="$t('login.description')" />
<v-card-text>
<span>{{ email }}</span>
<v-text-field v-model='email' validate-on-blur :rules='$valid.email'
autofocus :label='$t("common.email")' />
<v-text-field v-model='password' :rules='$valid.password'
type='password' :label='$t("common.password")' />
</v-card-text>
<v-card-actions>
<v-btn text
tabindex="1"
@click='forgot' small>{{$t('login.forgot_password')}}</v-btn>
</v-card-actions>
<v-card-actions>
<v-spacer/>
<v-btn v-if='settings?.allow_registration'
to='/register'
text
color='orange'>{{$t('login.not_registered')}}</v-btn>
<v-btn color='success'
type='submit' outlined
:disabled='!valid || loading' :loading='loading'>{{$t('common.login')}}</v-btn>
</v-card-actions>
</v-card>
</v-form>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
const valid = ref('')
const email = ref('')
const password = ref('')
function submit () {
console.error('submit')
}
</script>

28
plugins/validators.ts Normal file
View file

@ -0,0 +1,28 @@
import { test }from "linkifyjs"
export default defineNuxtPlugin((nuxtApp) => {
const $t = nuxtApp.vueApp.$nuxt.$i18n.t.bind(nuxtApp.vueApp.$nuxt.$i18n)
const validators = {
required (fieldName: string) {
return (v: string) => !(v===undefined || v===null || v.length <= 0) || $t('validators.required', { fieldName: $t(fieldName) })
},
email: [
(v: string) => !!v || $t('validators.required', { fieldName: $t('common.email') }),
(v: string) => (v && (v === 'admin' || !!test(v, 'email')) || $t('validators.email'))
],
password: [
(v: string) => !!v || $t('validators.required', { fieldName: $t('common.password') })
],
latitude: [
(v: number) => (v < 90 && v > -90) || $t('validators.latitude')
],
longitude: [
(v: number) => (v < 180 && v > -180) || $t('validators.longitude')
]
}
nuxtApp.provide("valid", validators)
})

View file

@ -1,3 +1,4 @@
// import { Announcement } from "~/server/utils/sequelize"
// return an announcement
export default defineEventHandler((event) => {
return Announcement.findByPk(event.context.params?.id)

View file

@ -1,4 +1,6 @@
import { Event } from "~/server/utils/sequelize"
export default defineEventHandler((event) => {
return Events.findAll()
return Event.findAll()
})

View file

@ -0,0 +1,37 @@
import locales from '~/locales/index'
import fs from 'fs'
export default defineEventHandler(async event => {
// const merge = require('lodash/merge')
// const config = require('../../config')
// const path = require('path')
// const fs = require('fs')
// const log = require('../../log')
const locale = event.context.params?.locale
// check if this locale exists
// if (!locales[locale]) {
// return res.sendStatus(404)
// }
// const defaultLocaleMessages = await import(`../../locales/${locale}.json`, { with: { type: "json" }}) ;
const fileContent = fs.readFileSync(`./locales/${locale}.json`, 'utf-8');
return fileContent
// return defaultLocaleMessages
// // check if we have a user custom messages
// let customLocaleMessages = {}
// const customLocalePath = config.user_locale && path.resolve(config.user_locale, `${locale}.json`)
// if (config.user_locale && fs.existsSync(customLocalePath)) {
// try {
// customLocaleMessages = require(customLocalePath)
// } catch (e) {
// log.error(`Error reading custom locale messages: ${e}`)
// }
// }
// const ret = merge(defaultLocaleMessages, customLocaleMessages)
// return res.json(ret)
// }
// }
})

View file

@ -1,12 +0,0 @@
export default (sequelize: any, DataTypes: any) => {
const Announcement = sequelize.define('announcement', {
title: DataTypes.STRING,
announcement: DataTypes.STRING,
visible: DataTypes.BOOLEAN
})
return Announcement
}

View file

@ -1,3 +1,9 @@
{
"extends": "../.nuxt/tsconfig.server.json"
"extends": "../.nuxt/tsconfig.server.json",
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}

View file

@ -1,37 +1,217 @@
import { DataTypes, Sequelize, Model,
InferAttributes,
InferCreationAttributes } from '@sequelize/core'
import { SqliteDialect } from '@sequelize/sqlite3'
import bcrypt from 'bcrypt'
import {
Sequelize,
type HasManyCreateAssociationMixin,
type InferAttributes,
type InferCreationAttributes,
type Options,
} from "@sequelize/core"
// import AnnouncementModel from '../models/announcement'
// import EventModel from '../models/event'
import {
DataTypes,
Model,
type CreationOptional,
type NonAttribute,
} from "@sequelize/core"
class Announcement extends Model<InferAttributes<Announcement>, InferCreationAttributes<Announcement>> {
declare title: string
declare announcement: string | null
declare visible: boolean
}
import {
Attribute,
HasMany,
NotNull,
Table,
CreatedAt,
UpdatedAt,
PrimaryKey,
AutoIncrement,
BeforeCreate,
BeforeUpsert,
Index,
Unique,
BeforeSave,
} from "@sequelize/core/decorators-legacy"
const Announcement = sequelize.define('announcement', {
title: DataTypes.STRING,
announcement: DataTypes.STRING,
visible: DataTypes.BOOLEAN
})
// import type { SqliteDialect } from "@sequelize/sqlite3";
// import bcrypt from "bcrypt";
const environment = process.env.NODE_ENV || "development";
const db = new Sequelize({
dialect: "sqlite",
storage: "./gancio2.sqlite",
models: [Announcement]
// const options = config[environment as keyof typeof config];
// export const sequelize = new Sequelize({
// ...options,
// } as Options<SqliteDialect>);
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './gancio2.sqlite'
})
// export const Announcement = AnnouncementModel(db, DataTypes)
// export const Events = EventModel(db, DataTypes)
export default sequelize
@Table({
tableName: 'announcements',
})
export class Announcement extends Model<
InferAttributes<Announcement>,
InferCreationAttributes<Announcement>
> {
@Attribute(DataTypes.INTEGER.UNSIGNED)
@PrimaryKey
@AutoIncrement
declare readonly id: CreationOptional<number>;
@Attribute(DataTypes.STRING)
@NotNull
declare title: string;
@Attribute(DataTypes.STRING)
declare announcement: string;
@Attribute(DataTypes.BOOLEAN)
declare visible: string;
try {
db.authenticate()
} catch (e) {
console.error(e)
}
export default db
@Table({
tableName: 'events'
})
export class Event extends Model<
InferAttributes<Event>,
InferCreationAttributes<Event>
> {
@Attribute(DataTypes.INTEGER.UNSIGNED)
@PrimaryKey
@AutoIncrement
declare readonly id: CreationOptional<number>
@Attribute(DataTypes.STRING)
@NotNull
declare title: string
@Attribute(DataTypes.STRING)
@Index
@Unique
declare slug: string
@Attribute(DataTypes.TEXT)
declare description: string
}
type Role = "admin" | "editor" | "user"
@Table({
tableName: "Users",
indexes: [{ fields: ["email"], unique: true }],
})
export class User extends Model<
InferAttributes<User>,
InferCreationAttributes<User>
> {
@Attribute(DataTypes.INTEGER.UNSIGNED)
@PrimaryKey
@AutoIncrement
declare readonly id: CreationOptional<number>;
@Attribute(DataTypes.STRING)
declare display_name: string;
@Attribute(DataTypes.STRING)
@NotNull
declare email: string;
@Attribute(DataTypes.STRING)
@NotNull
declare password: string
@Attribute(DataTypes.STRING)
declare recover_code: string
@Attribute(DataTypes.BOOLEAN)
declare is_active: Boolean
@Attribute(DataTypes.STRING)
declare description: string
@Attribute(DataTypes.ENUM(['admin','editor', 'user']))
declare role: Role
@CreatedAt
declare readonly createdAt: CreationOptional<Date>;
@UpdatedAt
declare readonly updatedAt: CreationOptional<Date>;
async comparePassword (pwd: string) {
if (!this.password) { return false }
return bcrypt.compare(pwd, this.password)
}
// @HasMany(() => Post, {
// foreignKey: "userId",
// inverse: {
// as: "author",
// },
// })
// declare posts?: NonAttribute<Post[]>;
// declare createPost: HasManyCreateAssociationMixin<Post, "userId">;
@BeforeSave
static async hashPassword(instance: User) {
if(instance.changed('password')) {
const password = instance.getDataValue('password')
if (password) {
const hashedPassword = bcrypt.hashSync(password, 10)
instance.setDataValue("password", hashedPassword)
}
}
}
// @BeforeCreate
// static async hashPassword(instance: User) {
// const password = instance.getDataValue("password");
// if (password) {
// const hashedPassword = bcrypt.hashSync(password, 10);
// instance.setDataValue("password", hashedPassword);
// }
// }
}
// @Table({
// tableName: "Posts",
// })
// export class Post extends Model<
// InferAttributes<Post>,
// InferCreationAttributes<Post>
// > {
// @Attribute(DataTypes.INTEGER.UNSIGNED)
// @PrimaryKey
// @AutoIncrement
// declare readonly id: CreationOptional<number>;
// @Attribute(DataTypes.STRING)
// @NotNull
// declare title: string;
// @Attribute(DataTypes.STRING)
// @NotNull
// declare content: string;
// @Attribute({
// type: DataTypes.INTEGER.UNSIGNED,
// references: { table: "Users", key: "id" },
// onUpdate: "CASCADE",
// onDelete: "CASCADE",
// })
// declare userId: number;
// @CreatedAt
// declare readonly createdAt: CreationOptional<Date>;
// @UpdatedAt
// declare readonly updatedAt: CreationOptional<Date>;
// /** Declared by {@link User#posts} */
// declare author?: NonAttribute<User>;
// }
sequelize.addModels([Announcement, Event]);

View file

@ -1,4 +1,10 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}