Finish localization

- Add user preferences section to settings
- Make settings page available for all users
- Move log out button to settings page
- Make it possible for each user to choose their desired language
This commit is contained in:
viktorstrate
2021-04-11 22:31:42 +02:00
parent a881e0c9df
commit b6a85d0966
22 changed files with 1337 additions and 126 deletions

View File

@@ -6,17 +6,22 @@ import (
"github.com/99designs/gqlgen/graphql"
"github.com/photoview/photoview/api/graphql/auth"
"gorm.io/gorm"
)
func IsAdmin(database *gorm.DB) func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
return func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
user := auth.UserFromContext(ctx)
if user == nil || user.Admin == false {
return nil, errors.New("user must be admin")
}
return next(ctx)
func IsAdmin(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
user := auth.UserFromContext(ctx)
if user == nil || user.Admin == false {
return nil, errors.New("user must be admin")
}
return next(ctx)
}
func IsAuthorized(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
return next(ctx)
}

File diff suppressed because it is too large Load Diff

View File

@@ -68,6 +68,47 @@ type TimelineGroup struct {
Date time.Time `json:"date"`
}
type LanguageTranslation string
const (
LanguageTranslationEn LanguageTranslation = "en"
LanguageTranslationDa LanguageTranslation = "da"
)
var AllLanguageTranslation = []LanguageTranslation{
LanguageTranslationEn,
LanguageTranslationDa,
}
func (e LanguageTranslation) IsValid() bool {
switch e {
case LanguageTranslationEn, LanguageTranslationDa:
return true
}
return false
}
func (e LanguageTranslation) String() string {
return string(e)
}
func (e *LanguageTranslation) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = LanguageTranslation(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid LanguageTranslation", str)
}
return nil
}
func (e LanguageTranslation) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type MediaType string
const (

View File

@@ -36,6 +36,13 @@ type AccessToken struct {
Expire time.Time `gorm:"not null;index"`
}
type UserPreferences struct {
Model
UserID int `gorm:"not null;index"`
User User `gorm:"constraint:OnDelete:CASCADE;"`
Language *LanguageTranslation
}
var ErrorInvalidUserCredentials = errors.New("invalid credentials")
func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error) {

View File

@@ -154,6 +154,49 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri
}, nil
}
func (r *queryResolver) MyUserPreferences(ctx context.Context) (*models.UserPreferences, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
userPref := models.UserPreferences{
UserID: user.ID,
}
if err := r.Database.Where("user_id = ?", user.ID).FirstOrCreate(&userPref).Error; err != nil {
return nil, err
}
return &userPref, nil
}
func (r *mutationResolver) ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
var langTrans *models.LanguageTranslation = nil
if language != nil {
lng := models.LanguageTranslation(*language)
langTrans = &lng
}
var userPref models.UserPreferences
if err := r.Database.Where("user_id = ?", user.ID).FirstOrInit(&userPref).Error; err != nil {
return nil, err
}
userPref.UserID = user.ID
userPref.Language = langTrans
if err := r.Database.Save(&userPref).Error; err != nil {
return nil, err
}
return &userPref, nil
}
// Admin queries
func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) {

View File

@@ -1,3 +1,4 @@
directive @isAuthorized on FIELD_DEFINITION
directive @isAdmin on FIELD_DEFINITION
scalar Time
@@ -30,7 +31,9 @@ type Query {
"List of registered users, must be admin to call"
user(order: Ordering, paginate: Pagination): [User!]! @isAdmin
"Information about the currently logged in user"
myUser: User!
myUser: User! @isAuthorized
myUserPreferences: UserPreferences! @isAuthorized
"List of albums owned by the logged in user."
myAlbums(
@@ -42,7 +45,7 @@ type Query {
showEmpty: Boolean
"Show only albums having favorites"
onlyWithFavorites: Boolean
): [Album!]!
): [Album!]! @isAuthorized
"""
Get album by id, user must own the album or be admin
If valid tokenCredentials are provided, the album may be retrived without further authentication
@@ -50,7 +53,7 @@ type Query {
album(id: ID!, tokenCredentials: ShareTokenCredentials): Album!
"List of media owned by the logged in user"
myMedia(order: Ordering, paginate: Pagination): [Media!]!
myMedia(order: Ordering, paginate: Pagination): [Media!]! @isAuthorized
"""
Get media by id, user must own the media or be admin.
If valid tokenCredentials are provided, the media may be retrived without further authentication
@@ -60,10 +63,10 @@ type Query {
"Get a list of media by their ids, user must own the media or be admin"
mediaList(ids: [ID!]!): [Media!]!
myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]!
myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! @isAuthorized
"Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any!
myMediaGeoJson: Any! @isAuthorized
"Get the mapbox api token, returns null if mapbox is not enabled"
mapboxToken: String
@@ -72,8 +75,8 @@ type Query {
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
myFaceGroups(paginate: Pagination): [FaceGroup!]!
faceGroup(id: ID!): FaceGroup!
myFaceGroups(paginate: Pagination): [FaceGroup!]! @isAuthorized
faceGroup(id: ID!): FaceGroup! @isAuthorized
}
type Mutation {
@@ -89,19 +92,19 @@ type Mutation {
"Scan all users for new media"
scanAll: ScannerResult! @isAdmin
"Scan a single user for new media"
scanUser(userId: ID!): ScannerResult!
scanUser(userId: ID!): ScannerResult! @isAdmin
"Generate share token for album"
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized
"Generate share token for media"
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized
"Delete a share token by it's token value"
deleteShareToken(token: String!): ShareToken
deleteShareToken(token: String!): ShareToken @isAuthorized
"Set a password for a token, if null is passed for the password argument, the password will be cleared"
protectShareToken(token: String!, password: String): ShareToken
protectShareToken(token: String!, password: String): ShareToken @isAuthorized
"Mark or unmark a media as being a favorite"
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized
updateUser(
id: ID!
@@ -124,21 +127,23 @@ type Mutation {
Set how often, in seconds, the server should automatically scan for new media,
a value of 0 will disable periodic scans
"""
setPeriodicScanInterval(interval: Int!): Int!
setPeriodicScanInterval(interval: Int!): Int! @isAdmin
"Set max number of concurrent scanner jobs running at once"
setScannerConcurrentWorkers(workers: Int!): Int!
setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin
changeUserPreferences(language: String): UserPreferences! @isAuthorized
"Assign a label to a face group, set label to null to remove the current one"
setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup!
setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized
"Merge two face groups into a single one, all ImageFaces from source will be moved to destination"
combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup!
combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! @isAuthorized
"Move a list of ImageFaces to another face group"
moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup!
moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! @isAuthorized
"Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match"
recognizeUnlabeledFaces: [ImageFace!]!
recognizeUnlabeledFaces: [ImageFace!]! @isAuthorized
"Move a list of ImageFaces to a new face group"
detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup!
detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized
}
type Subscription {
@@ -216,6 +221,16 @@ type User {
#shareTokens: [ShareToken]
}
enum LanguageTranslation {
en,
da
}
type UserPreferences {
id: ID!
language: LanguageTranslation
}
type Album {
id: ID!
title: String!